From 6b242a0f9ae0ce6366da3e9c4cf08f89d2c345aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20B=C3=BCrki?= Date: Tue, 12 May 2026 19:01:34 +0200 Subject: [PATCH 01/66] feat(roles/php): update template for RedHat-based systems, Docs (partially finished) to allow for multiple PHP-FPM pools to be configured individually --- roles/php/README.md | 194 ++++++++++++++---- .../etc/php-fpm.d/RedHat-pool.conf.j2 | 65 ++++-- 2 files changed, 200 insertions(+), 59 deletions(-) diff --git a/roles/php/README.md b/roles/php/README.md index 27c71d2e..63f44bd3 100644 --- a/roles/php/README.md +++ b/roles/php/README.md @@ -84,42 +84,6 @@ This role never exposes to the world that PHP is installed on the server, no mat * Type: Bool. * Default: `true` -`php__fpm_pools__host_var` / `php__fpm_pools__group_var` - -* List of dictionaries containing PHP-FPM pools. -* For the usage in `host_vars` / `group_vars` (can only be used in one group at a time). -* Type: List of dictionaries. -* Default: `[]` -* Subkeys: - - * `name`: - - * Mandatory. The name of the pool. Will also be used as the filename and for logfiles. - * Type: String. - - * `state`: - - * Optional. State of the pool. Possible options: `absent`, `present`. - * Type: String. - * Default: `'present'` - - * `user`: - - * Optional. The Unix user running the pool processes. - * Type: String. - * Default: `'apache'` - - * `group`: - - * Optional. The Unix group running the pool processes. - * Type: String. - * Default: `'apache'` - - * `raw`: - - * Optional. Raw content which will be added to the end of the pool config. - * Type: String. - `php__modules__host_var` / `php__modules__group_var` * List of dictionaries containing additional PHP modules that should be installed via the standard package manager. @@ -341,7 +305,7 @@ php__ini_upload_max_filesize__host_var: '10000M' ### PHP-FPM Pool Config Directives -Variables for `php.ini` directives and their default values, defined and supported by this role. +Variables for PHP-FPM Pool Config directives and their default values, defined and supported by this role. `php__fpm_pool_conf_pm__group_var` / `php__fpm_pool_conf_pm__host_var` @@ -385,32 +349,180 @@ Variables for `php.ini` directives and their default values, defined and support * Type: Number. * Default: `0` -`php__fpm_pools__group_var` / `php__fpm_pools__host_var` +`php__fpm_pools__host_var` / `php__fpm_pools__group_var` -* List defining pool configuration. +* List of dictionaries containing PHP-FPM pools. +* For the usage in `host_vars` / `group_vars` (can only be used in one group at a time). * Type: List of dictionaries. -* Default: `name: 'www'` `user: 'apache'` `group: 'apache'` +* Default: `[]` * Subkeys: * `name`: - * Mandatory. Pool name. + * Mandatory. The name of the pool. Will also be used as the filename and for logfiles. * Type: String. + * `state`: + + * Optional. State of the pool. Possible options: `absent`, `present`. + * Type: String. + * Default: `'present'` + * `user`: * Optional. The Unix user running the pool processes. * Type: String. + * Default: `'apache'` * `group`: * Optional. The Unix group running the pool processes. * Type: String. + * Default: `'apache'` + + * `pm`: + + * Optional. Choose how the process manager will control the number of child processes. + * Type: String. + * Default: `'dynamic'` + + * `pm_max_children`: + + * Optional. The number of child processes to be created when pm is set to `'static'` and the maximum number of child processes when pm is set to `'dynamic'` or `'ondemand'`. + * Type: Number. + * Default: `50` + + * `pm_start_servers`: + + * Optional. The number of child processes created on startup. Must be greater than `pm_min_spare_servers` but less than `pm_max_spare_servers`. Used only when `pm` is set to `'dynamic`'. + * Type: Number. + * Default: `5` + + * `pm_min_spare_servers`: + + * Optional. The desired minimum number of idle server processes. Used only when `pm` is set to `'dynamic'`. + * Type: Number. + * Default: `5` + + * `pm_max_spare_servers`: + + * Optional. The desired maximum number of idle server processes. Used only when `pm` is set to `'dynamic'`. + * Type: Number. + * Default: `35` + + * `pm_process_idle_timeout`: + + * Optional. The number of seconds after which an idle process will be killed. Used only when `pm` is set to `'ondemand'`. Defaults to `'10s'` if unset. Available units: s(econds, default), m(inutes), h(ours), or d(ays). + * Type: String. + * Default: `'10s'` + + * `pm_max_requests`: + + * Optional. The number of requests each child process should execute before respawning. This can be useful to work around memory leaks in 3rd party libraries. For endless request processing specify `0`. + * Type: Number. + * Default: `0` + + * `pm_status_path`: + + * Optional. Path to view FPM status page. + * Type: String. + * Default: `'/{{ item["name"] }}-fpm-status'` + + * `ping_path`: + + * Optional. The ping path to check if FPM is alive and responding. + * Type: String. + * Default: `'/{{ item["name"] }}-fpm-ping'` + + * `request_slowlog_timeout`: + + * Optional. The timeout for serving a single request after which a PHP backtrace will be dumped to the slowlog file. A value of `0` means off. Available units: s(econds, default), m(inutes), h(ours), or d(ays). + * Type: Number. + * Default: `0` + + * `request_slowlog_trace_depth`: + + * Optional. Depth of slow log stack trace. + * Type: Number. + * Default: `20` + + * `request_terminate_timeout`: + + * The timeout for serving a single request after which the worker process will be killed. This option should be used when the `max_execution_time` ini option does not stop script execution for some reason. A value of `0` means off. Available units: s(econds, default), m(inutes), h(ours), or d(ays). + * Type: Number. + * Default: `0` + + * `php_admin_value_session_save_path`: + + * Optional. + * Type: String. + * Default: `'/var/lib/php/session-{{ item["name"] }}'` + + * `php_admin_value_opcache_file_cache`: + + * Optional. + * Type: String. + * Default: `'/var/lib/php/opcache-{{ item["name"] }}'` + + * `php_admin_value_max_execution_time`: + + * Optional. + * Type: Number. + * Default: `{{ php__ini_max_execution_time__combined_var }}` + + * `php_admin_value_max_input_vars`: + + * Optional. + * Type: Number. + * Default: `{{ php__ini_max_input_vars__combined_var }}` + + * `php_admin_value_memory_limit`: + + * Optional. + * Type: String. + * Default: `'{{ php__ini_memory_limit__combined_var }}'` + + * `php_admin_value_opcache_interned_strings_buffer`: + + * Optional. + * Type: Number. + * Default: `{{ php__ini_opcache_interned_strings_buffer__combined_var }}` + + * `php_admin_value_opcache_max_accelerated_files`: + + * Optional. + * Type: Number. + * Default: `{{ php__ini_opcache_max_accelerated_files__combined_var }}` + + * `php_admin_value_opcache_memory_consumption`: + + * Optional. + * Type: Number. + * Default: `{{ php__ini_opcache_memory_consumption__combined_var }}` + + * `php_admin_value_open_basedir`: + + * Optional. + * Type: String. + * Default: unset + + * `php_admin_value_post_max_size`: + + * Optional. + * Type: String. + * Default: `'{{ php__ini_post_max_size__combined_var }}'` + + * `php_admin_value_upload_max_filesize`: + + * Optional. + * Type: String. + * Default: `'{{ php__ini_upload_max_filesize__combined_var }}'` * `raw`: * Optional. Raw content which will be added to the end of the pool config. * Type: String. + * Default: unset Example: ```yaml diff --git a/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 b/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 index a9577b6e..c7bd6948 100644 --- a/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 +++ b/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 @@ -108,7 +108,7 @@ listen.allowed_clients = 127.0.0.1 ; pm.process_idle_timeout - The number of seconds after which ; an idle process will be killed. ; Note: This value is mandatory. -pm = {{ php__fpm_pool_conf_pm__combined_var | d('dynamic') }} +pm = {{ item["pm"] | d('dynamic') }} ; The number of child processes to be created when pm is set to 'static' and the ; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. @@ -119,33 +119,33 @@ pm = {{ php__fpm_pool_conf_pm__combined_var | d('dynamic') }} ; forget to tweak pm.* to fit your needs. ; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' ; Note: This value is mandatory. -pm.max_children = {{ php__fpm_pool_conf_pm_max_children__combined_var | d(50) }} +pm.max_children = {{ item["pm_max_children"] | d(50) }} ; The number of child processes created on startup. ; Note: Used only when pm is set to 'dynamic' ; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2 -pm.start_servers = {{ php__fpm_pool_conf_pm_start_servers__combined_var | d(5) }} +pm.start_servers = {{ item["pm_start_servers"] | d(5) }} ; The desired minimum number of idle server processes. ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' -pm.min_spare_servers = {{ php__fpm_pool_conf_pm_min_spare_servers__combined_var | d(5) }} +pm.min_spare_servers = {{ item["pm_min_spare_servers"] | d(5) }} ; The desired maximum number of idle server processes. ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' -pm.max_spare_servers = {{ php__fpm_pool_conf_pm_max_spare_servers__combined_var | d(35) }} +pm.max_spare_servers = {{ item["pm_max_spare_servers"] | d(35) }} ; The number of seconds after which an idle process will be killed. ; Note: Used only when pm is set to 'ondemand' ; Default Value: 10s -;pm.process_idle_timeout = 10s; +pm.process_idle_timeout = {{ item["pm_process_idle_timeout"] | d("10s") }} ; The number of requests each child process should execute before respawning. ; This can be useful to work around memory leaks in 3rd party libraries. For ; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS. ; Default Value: 0 -;pm.max_requests = 500 +pm.max_requests = {{ item["pm_max_requests"] | d(0) }} ; The URI to view the FPM status page. If this value is not set, no URI will be ; recognized as a status page. It shows the following informations: @@ -244,7 +244,11 @@ pm.max_spare_servers = {{ php__fpm_pool_conf_pm_max_spare_servers__combined_var ; anything, but it may not be a good idea to use the .php extension or it ; may conflict with a real PHP file. ; Default Value: not set -pm.status_path = /fpm-status +[% if item["pm_status_path"] | default() %] +pm.status_path = {{ item["pm_status_path"] }} +[% else %] +pm.status_path = /{{ item["name"] }}-fpm-status +[% endif %] ; The ping URI to call the monitoring page of FPM. If this value is not set, no ; URI will be recognized as a ping page. This could be used to test from outside @@ -256,7 +260,11 @@ pm.status_path = /fpm-status ; anything, but it may not be a good idea to use the .php extension or it ; may conflict with a real PHP file. ; Default Value: not set -ping.path = /fpm-ping +[% if item["ping_path"] | default() %] +ping.path = {{ item["ping_path"] }} +[% else %] +ping.path = /{{ item["name"] }}-fpm-ping +[% endif %] ; This directive may be used to customize the response of a ping request. The ; response is formatted as text/plain with a 200 response code. @@ -335,18 +343,18 @@ slowlog = /var/log/php-fpm/{{ item["name"] }}-slow.log ; dumped to the 'slowlog' file. A value of '0s' means 'off'. ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) ; Default Value: 0 -request_slowlog_timeout = {{ php__fpm_pool_conf_request_slowlog_timeout__combined_var }} +request_slowlog_timeout = {{ item["request_slowlog_timeout"] | d(0) }} ; Depth of slow log stack trace. ; Default Value: 20 -;request_slowlog_trace_depth = 20 +request_slowlog_trace_depth = {{ item["request_slowlog_trace_depth"] | d(20) }} ; The timeout for serving a single request after which the worker process will ; be killed. This option should be used when the 'max_execution_time' ini option ; does not stop script execution for some reason. A value of '0' means 'off'. ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) ; Default Value: 0 -request_terminate_timeout = {{ php__fpm_pool_conf_request_terminate_timeout__combined_var }} +request_terminate_timeout = {{ item["request_terminate_timeout"] | d(0) }} ; Set open file descriptor rlimit. ; Default Value: system defined value @@ -427,9 +435,22 @@ request_terminate_timeout = {{ php__fpm_pool_conf_request_terminate_timeout__com ; specified at startup with the -d argument ;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com ;php_flag[display_errors] = off -php_admin_value[error_log] = /var/log/php-fpm/{{ item["name"] }}-error.log + php_admin_flag[log_errors] = on -;php_admin_value[memory_limit] = 128M +php_admin_value[error_log] = /var/log/php-fpm/{{ item["name"] }}-error.log +php_admin_value[max_execution_time] = {{ item["php_admin_value_max_execution_time"] | d(php__ini_max_execution_time__combined_var) }} +php_admin_value[max_input_vars] = {{ item["php_admin_value_max_input_vars"] | d(php__ini_max_input_vars__combined_var) }} +php_admin_value[memory_limit] = {{ item["php_admin_value_memory_limit"] | d(php__ini_memory_limit__combined_var) }} +php_admin_value[opcache.interned_strings_buffer] = {{ item["php_admin_value_opcache_interned_strings_buffer"] | d(php__ini_opcache_interned_strings_buffer__combined_var) }} +php_admin_value[opcache.max_accelerated_files] = {{ item["php_admin_value_opcache_max_accelerated_files"] | d(php__ini_opcache_max_accelerated_files__combined_var) }} +php_admin_value[opcache.memory_consumption] = {{ item["php_admin_value_opcache_memory_consumption"] | d(php__ini_opcache_memory_consumption__combined_var) }} +[% if item["php_admin_value_open_basedir"] | d() %] +php_admin_value[open_basedir] = {{ item["php_admin_value_open_basedir"] }} +[% else %] +;php_admin_value[open_basedir] = +[% endif %] +php_admin_value[post_max_size] = {{ item["php_admin_value_post_max_size"] | d(php__ini_post_max_size__combined_var) }} +php_admin_value[upload_max_filesize] = {{ item["php_admin_value_upload_max_filesize"] | d(php__ini_upload_max_filesize__combined_var) }} ; Set the following data paths to directories owned by the FPM process user. ; @@ -439,10 +460,18 @@ php_admin_flag[log_errors] = on ; ; See warning about choosing the location of these directories on your system ; at http://php.net/session.save-path -php_value[session.save_handler] = files -php_value[session.save_path] = /var/lib/php/session -php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache -;php_value[opcache.file_cache] = /var/lib/php/opcache +php_admin_value[session.save_handler] = files +[% if item["php_admin_value_opcache_file_cache"] | d() %] +php_admin_value[session.save_path] = {{ item["php_admin_value_opcache_file_cache"] }} +[% else %] +php_admin_value[session.save_path] = /var/lib/php/session-{{ item["name"] }} +[% endif %] +[% if item["php_admin_value_opcache_file_cache"] | d() %] +php_admin_value[opcache.file_cache] = {{ item["php_admin_value_opcache_file_cache"] }} +[% else %] +php_admin_value[opcache.file_cache] = /var/lib/php/opcache-{{ item["name"] }} +[% endif %] +;php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache [% if item["raw"] | default() %] ; raw content From 2aab24ccfd71e99f629c9d9e2d6b00e9b9a940bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20B=C3=BCrki?= Date: Wed, 13 May 2026 09:23:46 +0200 Subject: [PATCH 02/66] fix(roles/php): update timestamp in pool template, use 'd()' in accordance with example role, ensure session.save_path is now set correctly --- .../etc/php-fpm.d/RedHat-pool.conf.j2 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 b/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 index c7bd6948..a53f143f 100644 --- a/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 +++ b/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 @@ -1,7 +1,7 @@ #jinja2:block_start_string:'[%', block_end_string:'%]' ; {{ ansible_managed }} -; 2026012901 -[% if item["by_role"] | default() %] +; 2026051301 +[% if item["by_role"] | d() %] ; Generated by Ansible role: {{ item["by_role"] }} [% endif %] @@ -28,9 +28,9 @@ ; Note: The user is mandatory. If the group is not set, the default user's group ; will be used. ; RPM: apache user chosen to provide access to the same directories as httpd -user = {{ item["user"] | default('apache') }} +user = {{ item["user"] | d('apache') }} ; RPM: Keep a group allowed to write in log dir. -group = {{ item["group"] | default('apache') }} +group = {{ item["group"] | d('apache') }} ; The address on which to accept FastCGI requests. ; Valid syntaxes are: @@ -244,7 +244,7 @@ pm.max_requests = {{ item["pm_max_requests"] | d(0) }} ; anything, but it may not be a good idea to use the .php extension or it ; may conflict with a real PHP file. ; Default Value: not set -[% if item["pm_status_path"] | default() %] +[% if item["pm_status_path"] | d() %] pm.status_path = {{ item["pm_status_path"] }} [% else %] pm.status_path = /{{ item["name"] }}-fpm-status @@ -260,7 +260,7 @@ pm.status_path = /{{ item["name"] }}-fpm-status ; anything, but it may not be a good idea to use the .php extension or it ; may conflict with a real PHP file. ; Default Value: not set -[% if item["ping_path"] | default() %] +[% if item["ping_path"] | d() %] ping.path = {{ item["ping_path"] }} [% else %] ping.path = /{{ item["name"] }}-fpm-ping @@ -461,8 +461,8 @@ php_admin_value[upload_max_filesize] = {{ item["php_admin_value_upload_max_files ; See warning about choosing the location of these directories on your system ; at http://php.net/session.save-path php_admin_value[session.save_handler] = files -[% if item["php_admin_value_opcache_file_cache"] | d() %] -php_admin_value[session.save_path] = {{ item["php_admin_value_opcache_file_cache"] }} +[% if item["php_admin_value_session_save_path"] | d() %] +php_admin_value[session.save_path] = {{ item["php_admin_value_session_save_path"] }} [% else %] php_admin_value[session.save_path] = /var/lib/php/session-{{ item["name"] }} [% endif %] @@ -473,7 +473,7 @@ php_admin_value[opcache.file_cache] = /var/lib/php/opcache-{{ item["name"] }} [% endif %] ;php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache -[% if item["raw"] | default() %] +[% if item["raw"] | d() %] ; raw content {{ item["raw"] }} [% endif %] From c94b7defab5f89b71a1ff225b28e338c67cbcec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20B=C3=BCrki?= Date: Wed, 13 May 2026 18:04:20 +0200 Subject: [PATCH 03/66] feat(roles/php): update template for Debian-based systems, update docs (not complete), update template for RedHat-based systems. --- roles/php/README.md | 2 +- roles/php/tasks/main.yml | 1 + .../etc/php-fpm.d/Debian-pool.conf.j2 | 73 +++++++++------- .../etc/php-fpm.d/RedHat-pool.conf.j2 | 84 ++++++++----------- 4 files changed, 81 insertions(+), 79 deletions(-) diff --git a/roles/php/README.md b/roles/php/README.md index 63f44bd3..af797c03 100644 --- a/roles/php/README.md +++ b/roles/php/README.md @@ -354,7 +354,7 @@ Variables for PHP-FPM Pool Config directives and their default values, defined a * List of dictionaries containing PHP-FPM pools. * For the usage in `host_vars` / `group_vars` (can only be used in one group at a time). * Type: List of dictionaries. -* Default: `[]` +* Default: One pool named `www`. * Subkeys: * `name`: diff --git a/roles/php/tasks/main.yml b/roles/php/tasks/main.yml index 8642121a..b385fb5c 100644 --- a/roles/php/tasks/main.yml +++ b/roles/php/tasks/main.yml @@ -53,6 +53,7 @@ tags: - 'php' + - 'php:fpm' - 'php:ini' - 'php:modules' - 'php:update' diff --git a/roles/php/templates/etc/php-fpm.d/Debian-pool.conf.j2 b/roles/php/templates/etc/php-fpm.d/Debian-pool.conf.j2 index 8c31121d..adf3aec2 100644 --- a/roles/php/templates/etc/php-fpm.d/Debian-pool.conf.j2 +++ b/roles/php/templates/etc/php-fpm.d/Debian-pool.conf.j2 @@ -1,14 +1,14 @@ #jinja2:block_start_string:'[%', block_end_string:'%]' ; {{ ansible_managed }} -; 2026012901 -[% if item["by_role"] | default() %] -; Generated by Ansible role: {{ item["by_role"] }} +; 2026051301 +[% if item['by_role'] | d() %] +; Generated by Ansible role: {{ item['by_role'] }} [% endif %] ; Start a new pool named 'www'. ; the variable $pool can be used in any directive and will be replaced by the ; pool name ('www' here) -[{{ item["name"] }}] +[{{ item['name'] }}] ; Per pool prefix ; It only applies on the following directives: @@ -32,8 +32,8 @@ ; --allow-to-run-as-root option to work. ; Default Values: The user is set to master process running user by default. ; If the group is not set, the user's group is used. -user = {{ item["user"] | default('www-data') }} -group = {{ item["group"] | default('www-data') }} +user = {{ item['user'] | d(php__webserver_user) }} +group = {{ item['group'] | d(php__webserver_group) }} ; The address on which to accept FastCGI requests. ; Valid syntaxes are: @@ -45,7 +45,7 @@ group = {{ item["group"] | default('www-data') }} ; (IPv6 and IPv4-mapped) on a specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. -listen = /run/php/{{ item["name"] }}.sock +listen = /run/php/{{ item['name'] }}.sock ; Set listen(2) backlog. ; Default Value: 511 (-1 on Linux, FreeBSD and OpenBSD) @@ -57,8 +57,8 @@ listen = /run/php/{{ item["name"] }}.sock ; and group can be specified either by name or by their numeric IDs. ; Default Values: Owner is set to the master process running user. If the group ; is not set, the owner's group is used. Mode is set to 0660. -listen.owner = www-data -listen.group = www-data +listen.owner = {{ php__webserver_user }} +listen.group = {{ php__webserver_group }} ;listen.mode = 0660 ; When POSIX Access Control Lists are supported you can set them using @@ -131,22 +131,22 @@ pm = {{ php__fpm_pool_conf_pm__combined_var | d('dynamic') }} ; forget to tweak pm.* to fit your needs. ; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' ; Note: This value is mandatory. -pm.max_children = {{ php__fpm_pool_conf_pm_max_children__combined_var | d(5) }} +pm.max_children = {{ item['pm_max_children'] | d(50) }} ; The number of child processes created on startup. ; Note: Used only when pm is set to 'dynamic' ; Default Value: (min_spare_servers + max_spare_servers) / 2 -pm.start_servers = {{ php__fpm_pool_conf_pm_start_servers__combined_var | d(2) }} +pm.start_servers = {{ item['pm_start_servers'] | d(5) }} ; The desired minimum number of idle server processes. ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' -pm.min_spare_servers = {{ php__fpm_pool_conf_pm_min_spare_servers__combined_var | d(1) }} +pm.min_spare_servers = {{ item['pm_min_spare_servers'] | d(5) }} ; The desired maximum number of idle server processes. ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' -pm.max_spare_servers = {{ php__fpm_pool_conf_pm_max_spare_servers__combined_var | d(3) }} +pm.max_spare_servers = {{ item['pm_max_spare_servers'] | d(35) }} ; The number of rate to spawn child processes at once. ; Note: Used only when pm is set to 'dynamic' @@ -157,13 +157,13 @@ pm.max_spare_servers = {{ php__fpm_pool_conf_pm_max_spare_servers__combined_var ; The number of seconds after which an idle process will be killed. ; Note: Used only when pm is set to 'ondemand' ; Default Value: 10s -;pm.process_idle_timeout = 10s; +pm.process_idle_timeout = {{ item['pm_process_idle_timeout'] | d('10s') }} ; The number of requests each child process should execute before respawning. ; This can be useful to work around memory leaks in 3rd party libraries. For ; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS. ; Default Value: 0 -;pm.max_requests = 500 +pm.max_requests = {{ item['pm_max_requests'] | d(500) }} ; The URI to view the FPM status page. If this value is not set, no URI will be ; recognized as a status page. It shows the following information: @@ -236,8 +236,8 @@ pm.max_spare_servers = {{ php__fpm_pool_conf_pm_max_spare_servers__combined_var ; it's always 0 if the process is not in Idle state ; because memory calculation is done when the request ; processing has terminated; -; If the process is in Idle state, then informations are related to the -; last request the process has served. Otherwise informations are related to +; If the process is in Idle state, then information is related to the +; last request the process has served. Otherwise information is related to ; the current request being served. ; Example output: ; ************************ @@ -256,13 +256,13 @@ pm.max_spare_servers = {{ php__fpm_pool_conf_pm_max_spare_servers__combined_var ; last request memory: 0 ; ; Note: There is a real-time FPM status monitoring sample web page available -; It's available in: /usr/share/php/8.2/fpm/status.html +; It's available in: /usr/share/php/8.4/fpm/status.html ; ; Note: The value must start with a leading slash (/). The value can be ; anything, but it may not be a good idea to use the .php extension or it ; may conflict with a real PHP file. ; Default Value: not set -pm.status_path = /fpm-status +pm.status_path = {{ item['pm_status_path'] | d('/' ~ item['name'] ~ '-fpm-status') }} ; The address on which to accept FastCGI status request. This creates a new ; invisible pool that can handle requests independently. This is useful @@ -290,7 +290,7 @@ pm.status_path = /fpm-status ; anything, but it may not be a good idea to use the .php extension or it ; may conflict with a real PHP file. ; Default Value: not set -ping.path = /fpm-ping +ping.path = {{ item['ping_path'] | d('/' ~ item['name'] ~ '-fpm-ping') }} ; This directive may be used to customize the response of a ping request. The ; response is formatted as text/plain with a 200 response code. @@ -379,24 +379,24 @@ ping.response = pong ; The log file for slow requests ; Default Value: not set ; Note: slowlog is mandatory if request_slowlog_timeout is set -slowlog = log/{{ item["name"] }}-slow.log +slowlog = log/{{ item['name'] }}-slow.log ; The timeout for serving a single request after which a PHP backtrace will be ; dumped to the 'slowlog' file. A value of '0s' means 'off'. ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) ; Default Value: 0 -request_slowlog_timeout = {{ php__fpm_pool_conf_request_slowlog_timeout__combined_var }} +request_slowlog_timeout = {{ item['request_slowlog_timeout'] | d(0) }} ; Depth of slow log stack trace. ; Default Value: 20 -;request_slowlog_trace_depth = 20 +request_slowlog_trace_depth = {{ item['request_slowlog_trace_depth'] | d(20) }} ; The timeout for serving a single request after which the worker process will ; be killed. This option should be used when the 'max_execution_time' ini option ; does not stop script execution for some reason. A value of '0' means 'off'. ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) ; Default Value: 0 -request_terminate_timeout = {{ php__fpm_pool_conf_request_terminate_timeout__combined_var }} +request_terminate_timeout = {{ item['request_terminate_timeout'] | d(0) }} ; The timeout set by 'request_terminate_timeout' ini option is not engaged after ; application calls 'fastcgi_finish_request' or when application has finished and @@ -492,11 +492,28 @@ request_terminate_timeout = {{ php__fpm_pool_conf_request_terminate_timeout__com ; specified at startup with the -d argument ;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com ;php_flag[display_errors] = off -php_admin_value[error_log] = /var/log/php-fpm-{{ item["name"] }}-error.log php_admin_flag[log_errors] = on -;php_admin_value[memory_limit] = 32M +php_admin_value[error_log] = /var/log/php-fpm/{{ item['name'] }}-error.log +php_admin_value[max_execution_time] = {{ item['php_admin_value_max_execution_time'] | d(php__ini_max_execution_time__combined_var) }} +php_admin_value[max_input_vars] = {{ item['php_admin_value_max_input_vars'] | d(php__ini_max_input_vars__combined_var) }} +php_admin_value[memory_limit] = {{ item['php_admin_value_memory_limit'] | d(php__ini_memory_limit__combined_var) }} +php_admin_value[opcache.interned_strings_buffer] = {{ item['php_admin_value_opcache_interned_strings_buffer'] | d(php__ini_opcache_interned_strings_buffer__combined_var) }} +php_admin_value[opcache.max_accelerated_files] = {{ item['php_admin_value_opcache_max_accelerated_files'] | d(php__ini_opcache_max_accelerated_files__combined_var) }} +php_admin_value[opcache.memory_consumption] = {{ item['php_admin_value_opcache_memory_consumption'] | d(php__ini_opcache_memory_consumption__combined_var) }} +[% if item['php_admin_value_open_basedir'] | d() %] +php_admin_value[open_basedir] = {{ item['php_admin_value_open_basedir'] }} +[% else %] +;php_admin_value[open_basedir] = +[% endif %] +php_admin_value[post_max_size] = {{ item['php_admin_value_post_max_size'] | d(php__ini_post_max_size__combined_var) }} +php_admin_value[upload_max_filesize] = {{ item['php_admin_value_upload_max_filesize'] | d(php__ini_upload_max_filesize__combined_var) }} + +php_admin_value[session.save_handler] = files +php_admin_value[session.save_path] = {{ item['php_admin_value_session_save_path'] | d('/var/lib/php/session-' ~ item['name']) }} +php_admin_value[opcache.file_cache] = {{ item['php_admin_value_opcache_file_cache'] | d('/var/lib/php/opcache-' ~ item['name']) }} +;php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache -[% if item["raw"] | default() %] +[% if item['raw'] | () %] ; raw content -{{ item["raw"] }} +{{ item['raw'] }} [% endif %] diff --git a/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 b/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 index a53f143f..d8fe5c16 100644 --- a/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 +++ b/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 @@ -1,14 +1,14 @@ #jinja2:block_start_string:'[%', block_end_string:'%]' ; {{ ansible_managed }} ; 2026051301 -[% if item["by_role"] | d() %] -; Generated by Ansible role: {{ item["by_role"] }} +[% if item['by_role'] | d() %] +; Generated by Ansible role: {{ item['by_role'] }} [% endif %] ; Start a new pool named 'www'. ; the variable $pool can be used in any directive and will be replaced by the ; pool name ('www' here) -[{{ item["name"] }}] +[{{ item['name'] }}] ; Per pool prefix ; It only applies on the following directives: @@ -28,9 +28,9 @@ ; Note: The user is mandatory. If the group is not set, the default user's group ; will be used. ; RPM: apache user chosen to provide access to the same directories as httpd -user = {{ item["user"] | d('apache') }} +user = {{ item['user'] | d(php__webserver_user) }} ; RPM: Keep a group allowed to write in log dir. -group = {{ item["group"] | d('apache') }} +group = {{ item['group'] | d(php__webserver_group) }} ; The address on which to accept FastCGI requests. ; Valid syntaxes are: @@ -42,7 +42,7 @@ group = {{ item["group"] | d('apache') }} ; (IPv6 and IPv4-mapped) on a specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. -listen = /run/php-fpm/{{ item["name"] }}.sock +listen = /run/php-fpm/{{ item['name'] }}.sock ; Set listen(2) backlog. ; Default Value: 511 @@ -108,7 +108,7 @@ listen.allowed_clients = 127.0.0.1 ; pm.process_idle_timeout - The number of seconds after which ; an idle process will be killed. ; Note: This value is mandatory. -pm = {{ item["pm"] | d('dynamic') }} +pm = {{ php__fpm_pool_conf_pm__combined_var | d('dynamic') }} ; The number of child processes to be created when pm is set to 'static' and the ; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. @@ -119,33 +119,33 @@ pm = {{ item["pm"] | d('dynamic') }} ; forget to tweak pm.* to fit your needs. ; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' ; Note: This value is mandatory. -pm.max_children = {{ item["pm_max_children"] | d(50) }} +pm.max_children = {{ item['pm_max_children'] | d(50) }} ; The number of child processes created on startup. ; Note: Used only when pm is set to 'dynamic' ; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2 -pm.start_servers = {{ item["pm_start_servers"] | d(5) }} +pm.start_servers = {{ item['pm_start_servers'] | d(5) }} ; The desired minimum number of idle server processes. ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' -pm.min_spare_servers = {{ item["pm_min_spare_servers"] | d(5) }} +pm.min_spare_servers = {{ item['pm_min_spare_servers'] | d(5) }} ; The desired maximum number of idle server processes. ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' -pm.max_spare_servers = {{ item["pm_max_spare_servers"] | d(35) }} +pm.max_spare_servers = {{ item['pm_max_spare_servers'] | d(35) }} ; The number of seconds after which an idle process will be killed. ; Note: Used only when pm is set to 'ondemand' ; Default Value: 10s -pm.process_idle_timeout = {{ item["pm_process_idle_timeout"] | d("10s") }} +pm.process_idle_timeout = {{ item['pm_process_idle_timeout'] | d('10s') }} ; The number of requests each child process should execute before respawning. ; This can be useful to work around memory leaks in 3rd party libraries. For ; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS. ; Default Value: 0 -pm.max_requests = {{ item["pm_max_requests"] | d(0) }} +pm.max_requests = {{ item['pm_max_requests'] | d(0) }} ; The URI to view the FPM status page. If this value is not set, no URI will be ; recognized as a status page. It shows the following informations: @@ -244,11 +244,7 @@ pm.max_requests = {{ item["pm_max_requests"] | d(0) }} ; anything, but it may not be a good idea to use the .php extension or it ; may conflict with a real PHP file. ; Default Value: not set -[% if item["pm_status_path"] | d() %] -pm.status_path = {{ item["pm_status_path"] }} -[% else %] -pm.status_path = /{{ item["name"] }}-fpm-status -[% endif %] +pm.status_path = {{ item['pm_status_path'] | d('/' ~ item['name'] ~ '-fpm-status') }} ; The ping URI to call the monitoring page of FPM. If this value is not set, no ; URI will be recognized as a ping page. This could be used to test from outside @@ -260,11 +256,7 @@ pm.status_path = /{{ item["name"] }}-fpm-status ; anything, but it may not be a good idea to use the .php extension or it ; may conflict with a real PHP file. ; Default Value: not set -[% if item["ping_path"] | d() %] -ping.path = {{ item["ping_path"] }} -[% else %] -ping.path = /{{ item["name"] }}-fpm-ping -[% endif %] +ping.path = {{ item['ping_path'] | d('/' ~ item['name'] ~ '-fpm-ping') }} ; This directive may be used to customize the response of a ping request. The ; response is formatted as text/plain with a 200 response code. @@ -337,24 +329,24 @@ ping.response = pong ; The log file for slow requests ; Default Value: not set ; Note: slowlog is mandatory if request_slowlog_timeout is set -slowlog = /var/log/php-fpm/{{ item["name"] }}-slow.log +slowlog = /var/log/php-fpm/{{ item['name'] }}-slow.log ; The timeout for serving a single request after which a PHP backtrace will be ; dumped to the 'slowlog' file. A value of '0s' means 'off'. ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) ; Default Value: 0 -request_slowlog_timeout = {{ item["request_slowlog_timeout"] | d(0) }} +request_slowlog_timeout = {{ item['request_slowlog_timeout'] | d(0) }} ; Depth of slow log stack trace. ; Default Value: 20 -request_slowlog_trace_depth = {{ item["request_slowlog_trace_depth"] | d(20) }} +request_slowlog_trace_depth = {{ item['request_slowlog_trace_depth'] | d(20) }} ; The timeout for serving a single request after which the worker process will ; be killed. This option should be used when the 'max_execution_time' ini option ; does not stop script execution for some reason. A value of '0' means 'off'. ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) ; Default Value: 0 -request_terminate_timeout = {{ item["request_terminate_timeout"] | d(0) }} +request_terminate_timeout = {{ item['request_terminate_timeout'] | d(0) }} ; Set open file descriptor rlimit. ; Default Value: system defined value @@ -437,20 +429,20 @@ request_terminate_timeout = {{ item["request_terminate_timeout"] | d(0) }} ;php_flag[display_errors] = off php_admin_flag[log_errors] = on -php_admin_value[error_log] = /var/log/php-fpm/{{ item["name"] }}-error.log -php_admin_value[max_execution_time] = {{ item["php_admin_value_max_execution_time"] | d(php__ini_max_execution_time__combined_var) }} -php_admin_value[max_input_vars] = {{ item["php_admin_value_max_input_vars"] | d(php__ini_max_input_vars__combined_var) }} -php_admin_value[memory_limit] = {{ item["php_admin_value_memory_limit"] | d(php__ini_memory_limit__combined_var) }} -php_admin_value[opcache.interned_strings_buffer] = {{ item["php_admin_value_opcache_interned_strings_buffer"] | d(php__ini_opcache_interned_strings_buffer__combined_var) }} -php_admin_value[opcache.max_accelerated_files] = {{ item["php_admin_value_opcache_max_accelerated_files"] | d(php__ini_opcache_max_accelerated_files__combined_var) }} -php_admin_value[opcache.memory_consumption] = {{ item["php_admin_value_opcache_memory_consumption"] | d(php__ini_opcache_memory_consumption__combined_var) }} -[% if item["php_admin_value_open_basedir"] | d() %] -php_admin_value[open_basedir] = {{ item["php_admin_value_open_basedir"] }} +php_admin_value[error_log] = /var/log/php-fpm/{{ item['name'] }}-error.log +php_admin_value[max_execution_time] = {{ item['php_admin_value_max_execution_time'] | d(php__ini_max_execution_time__combined_var) }} +php_admin_value[max_input_vars] = {{ item['php_admin_value_max_input_vars'] | d(php__ini_max_input_vars__combined_var) }} +php_admin_value[memory_limit] = {{ item['php_admin_value_memory_limit'] | d(php__ini_memory_limit__combined_var) }} +php_admin_value[opcache.interned_strings_buffer] = {{ item['php_admin_value_opcache_interned_strings_buffer'] | d(php__ini_opcache_interned_strings_buffer__combined_var) }} +php_admin_value[opcache.max_accelerated_files] = {{ item['php_admin_value_opcache_max_accelerated_files'] | d(php__ini_opcache_max_accelerated_files__combined_var) }} +php_admin_value[opcache.memory_consumption] = {{ item['php_admin_value_opcache_memory_consumption'] | d(php__ini_opcache_memory_consumption__combined_var) }} +[% if item['php_admin_value_open_basedir'] | d() %] +php_admin_value[open_basedir] = {{ item['php_admin_value_open_basedir'] }} [% else %] ;php_admin_value[open_basedir] = [% endif %] -php_admin_value[post_max_size] = {{ item["php_admin_value_post_max_size"] | d(php__ini_post_max_size__combined_var) }} -php_admin_value[upload_max_filesize] = {{ item["php_admin_value_upload_max_filesize"] | d(php__ini_upload_max_filesize__combined_var) }} +php_admin_value[post_max_size] = {{ item['php_admin_value_post_max_size'] | d(php__ini_post_max_size__combined_var) }} +php_admin_value[upload_max_filesize] = {{ item['php_admin_value_upload_max_filesize'] | d(php__ini_upload_max_filesize__combined_var) }} ; Set the following data paths to directories owned by the FPM process user. ; @@ -461,19 +453,11 @@ php_admin_value[upload_max_filesize] = {{ item["php_admin_value_upload_max_files ; See warning about choosing the location of these directories on your system ; at http://php.net/session.save-path php_admin_value[session.save_handler] = files -[% if item["php_admin_value_session_save_path"] | d() %] -php_admin_value[session.save_path] = {{ item["php_admin_value_session_save_path"] }} -[% else %] -php_admin_value[session.save_path] = /var/lib/php/session-{{ item["name"] }} -[% endif %] -[% if item["php_admin_value_opcache_file_cache"] | d() %] -php_admin_value[opcache.file_cache] = {{ item["php_admin_value_opcache_file_cache"] }} -[% else %] -php_admin_value[opcache.file_cache] = /var/lib/php/opcache-{{ item["name"] }} -[% endif %] +php_admin_value[session.save_path] = {{ item['php_admin_value_session_save_path'] | d('/var/lib/php/session-' ~ item['name']) }} +php_admin_value[opcache.file_cache] = {{ item['php_admin_value_opcache_file_cache'] | d('/var/lib/php/opcache-' ~ item['name']) }} ;php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache -[% if item["raw"] | d() %] +[% if item['raw'] | d() %] ; raw content -{{ item["raw"] }} +{{ item['raw'] }} [% endif %] From e222cbe7b754c53d7f2aa9b05ff9ba8e91e37e29 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 12 May 2026 17:12:31 +0200 Subject: [PATCH 04/66] Add roles/repo_google_chrome --- CHANGELOG.md | 1 + COMPATIBILITY.md | 1 + playbooks/README.md | 7 +++ playbooks/all.yml | 1 + playbooks/repo_google_chrome.yml | 23 +++++++++ roles/repo_google_chrome/README.md | 48 +++++++++++++++++++ roles/repo_google_chrome/defaults/main.yml | 2 + roles/repo_google_chrome/tasks/RedHat.yml | 40 ++++++++++++++++ roles/repo_google_chrome/tasks/main.yml | 18 +++++++ .../etc/yum.repos.d/google-chrome.repo.j2 | 20 ++++++++ 10 files changed, 161 insertions(+) create mode 100644 playbooks/repo_google_chrome.yml create mode 100644 roles/repo_google_chrome/README.md create mode 100644 roles/repo_google_chrome/defaults/main.yml create mode 100644 roles/repo_google_chrome/tasks/RedHat.yml create mode 100644 roles/repo_google_chrome/tasks/main.yml create mode 100644 roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index f4994fb3..925c5f9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **role:mirror**: Document the new per-repository `newest_only` subkey on `mirror__reposync_repos` entries. Defaults to `true` (only the newest version of each package is mirrored). Set to `false` for repositories that publish multiple versions in parallel, such as Icinga, where older versions must remain available. * **role:repo_remi**: Add RHEL 10 / Rocky 10 support (new GPG key, repo templates, and module-stream tasks for EL 10). * **role:repo_remi**: Add `meta/argument_specs.yml` declaring the four user-facing variables (`repo_remi__basic_auth_login`, `repo_remi__enabled_php_version`, `repo_remi__enabled_redis_version`, `repo_remi__mirror_url`) so role-entry validation catches type mismatches and unknown variables. `repo_remi__basic_auth_login` is declared as `type: 'raw'` because its default in `defaults/main.yml` resolves to an empty string when no Bitwarden lookup is configured. +* **role:repo_google_chrome**: New role. Deploys the Google Chrome package repository for RHEL-based distributions, with the same `lfops__repo_mirror_url` / `lfops__repo_basic_auth_login` knobs as the other `repo_*` roles. * **role:monitoring_plugins, role:repo_monitoring_plugins**: Add SLES 15 and SLES 16 support. The roles now install the Linuxfabrik Monitoring Plugins from the SUSE channel of `repo.linuxfabrik.ch` and apply the SUSE-specific package version lock ([#245](https://github.com/Linuxfabrik/lfops/issues/245)). * **role:alternatives, role:elastic_agent, role:elastic_agent_fleet_server, role:icinga_kubernetes_web, role:lvm, role:mailto_root, role:motd, role:proxysql**: (Re-)introduce `meta/argument_specs.yml` so role-entry validation catches type mismatches and missing required variables. The originally proposed specs were correct for these roles (no strict-options login dicts, no `__dependent_var` injections from `setup_*` playbooks), so they are restored unchanged. * **role:apps, role:example, role:kernel_settings**: (Re-)introduce `meta/argument_specs.yml`, with the `__dependent_var` slot declared so `setup_*` playbooks that inject these via `vars:` (e.g. `setup_icinga2_master`, `setup_moodle`, `setup_nextcloud`) pass validation. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 021c3f69..a339aca4 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -128,6 +128,7 @@ Which Ansible role is proven to run on which OS? | repo_epel | | | x | x | x | | | | | | repo_gitlab_ce | | | x | (x) | (x) | | | | | | repo_gitlab_runner | | | x | (x) | (x) | | | | | +| repo_google_chrome | | | x | x | (x) | | | | | | repo_grafana | x | x | x | x | (x) | (x) | (x) | (x) | | | repo_graylog | x | x | x | (x) | (x) | (x) | (x) | (x) | | | repo_icinga | x | x | x | x | x | x | (x) | (x) | | diff --git a/playbooks/README.md b/playbooks/README.md index 3d95e807..72b5cb88 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -840,6 +840,13 @@ Calls the following roles (in order): * [repo_gitlab_runner](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_gitlab_runner) +## repo_google_chrome.yml + +Calls the following roles (in order): + +* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome) + + ## repo_grafana.yml Calls the following roles (in order): diff --git a/playbooks/all.yml b/playbooks/all.yml index 067d14fd..51c30c19 100644 --- a/playbooks/all.yml +++ b/playbooks/all.yml @@ -101,6 +101,7 @@ - import_playbook: 'repo_epel.yml' - import_playbook: 'repo_gitlab_ce.yml' - import_playbook: 'repo_gitlab_runner.yml' +- import_playbook: 'repo_google_chrome.yml' - import_playbook: 'repo_grafana.yml' - import_playbook: 'repo_graylog.yml' - import_playbook: 'repo_icinga.yml' diff --git a/playbooks/repo_google_chrome.yml b/playbooks/repo_google_chrome.yml new file mode 100644 index 00000000..f19a1b89 --- /dev/null +++ b/playbooks/repo_google_chrome.yml @@ -0,0 +1,23 @@ +- name: 'Playbook linuxfabrik.lfops.repo_google_chrome' + hosts: + - 'lfops_repo_google_chrome' + + pre_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-start.yml' + tags: + - 'always' + + + roles: + + - role: 'linuxfabrik.lfops.repo_google_chrome' + + + post_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-end.yml' + tags: + - 'always' diff --git a/roles/repo_google_chrome/README.md b/roles/repo_google_chrome/README.md new file mode 100644 index 00000000..7da86d69 --- /dev/null +++ b/roles/repo_google_chrome/README.md @@ -0,0 +1,48 @@ +# Ansible Role linuxfabrik.lfops.repo_google_chrome + +This role deploys the package repository for [Google Chrome](https://www.google.com/chrome/) on RHEL-based distributions. + + +*Available since LFOps `6.0.2`.* + + +## Tags + +`repo_google_chrome` + +* Deploys the Google Chrome Repository. +* Triggers: none. + + +## Optional Role Variables + +`repo_google_chrome__basic_auth_login` + +* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Type: String. +* Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` + +`repo_google_chrome__mirror_url` + +* Set the URL to a custom mirror server providing the repository. Defaults to `lfops__repo_mirror_url` to allow easily setting the same URL for all `repo_*` roles. If `lfops__repo_mirror_url` is not set, the default mirrors of the repo are used. +* Type: String. +* Default: `'{{ lfops__repo_mirror_url | default("") }}'` + +Example: +```yaml +# optional +repo_google_chrome__basic_auth_login: + username: 'my-username' + password: 'linuxfabrik' +repo_google_chrome__mirror_url: 'https://mirror.example.com' +``` + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/repo_google_chrome/defaults/main.yml b/roles/repo_google_chrome/defaults/main.yml new file mode 100644 index 00000000..69f976e3 --- /dev/null +++ b/roles/repo_google_chrome/defaults/main.yml @@ -0,0 +1,2 @@ +repo_google_chrome__basic_auth_login: '{{ lfops__repo_basic_auth_login | default("") }}' +repo_google_chrome__mirror_url: '{{ lfops__repo_mirror_url | default("") }}' diff --git a/roles/repo_google_chrome/tasks/RedHat.yml b/roles/repo_google_chrome/tasks/RedHat.yml new file mode 100644 index 00000000..83a5f6cf --- /dev/null +++ b/roles/repo_google_chrome/tasks/RedHat.yml @@ -0,0 +1,40 @@ +- block: + + - name: 'curl https://dl-ssl.google.com/linux/linux_signing_key.pub --output /tmp/ansible.google-chrome.key' + ansible.builtin.get_url: + url: 'https://dl-ssl.google.com/linux/linux_signing_key.pub' + dest: '/tmp/ansible.google-chrome.key' + mode: 0o644 + delegate_to: 'localhost' + become: false + run_once: true + changed_when: false # not an actual config change on the server + check_mode: false # run task even if `--check` is specified + + - name: 'copy /tmp/ansible.google-chrome.key to /etc/pki/rpm-gpg/google-chrome.key' + ansible.builtin.copy: + src: '/tmp/ansible.google-chrome.key' + dest: '/etc/pki/rpm-gpg/google-chrome.key' + owner: 'root' + group: 'root' + mode: 0o644 + + # https://www.google.com/linuxrepositories/ + - name: 'deploy the Google Chrome repo (mirror: {{ repo_google_chrome__mirror_url }})' + ansible.builtin.template: + backup: true + src: 'etc/yum.repos.d/google-chrome.repo.j2' + dest: '/etc/yum.repos.d/google-chrome.repo' + owner: 'root' + group: 'root' + mode: 0o644 + + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' + ansible.builtin.include_role: + name: 'shared' + tasks_from: 'remove-rpmnew-rpmsave.yml' + vars: + shared__remove_rpmnew_rpmsave_config_file: '/etc/yum.repos.d/google-chrome.repo' + + tags: + - 'repo_google_chrome' diff --git a/roles/repo_google_chrome/tasks/main.yml b/roles/repo_google_chrome/tasks/main.yml new file mode 100644 index 00000000..4f290d1f --- /dev/null +++ b/roles/repo_google_chrome/tasks/main.yml @@ -0,0 +1,18 @@ +- name: 'Perform platform/version specific tasks' + ansible.builtin.include_tasks: '{{ __task_file }}' + when: '__task_file | length > 0' + vars: + __task_file: '{{ lookup("ansible.builtin.first_found", __first_found_options) }}' + __first_found_options: + files: + - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_version"] }}.yml' + - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_major_version"] }}.yml' + - '{{ ansible_facts["distribution"] }}.yml' + - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_version"] }}.yml' + - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_major_version"] }}.yml' + - '{{ ansible_facts["os_family"] }}.yml' + paths: + - '{{ role_path }}/tasks' + skip: true + tags: + - 'always' diff --git a/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 b/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 new file mode 100644 index 00000000..ff8375bc --- /dev/null +++ b/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 @@ -0,0 +1,20 @@ +# {{ ansible_managed }} +# 2026051201 + +[google-chrome] +name=google-chrome +{% if repo_google_chrome__mirror_url is defined and repo_google_chrome__mirror_url | length %} +baseurl={{ repo_google_chrome__mirror_url }}/linux/chrome/rpm/stable/$basearch +{% else %} +baseurl=https://dl.google.com/linux/chrome/rpm/stable/$basearch +{% endif %} +enabled=1 +gpgcheck=1 +repo_gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/google-chrome.key +sslverify=1 +sslcacert=/etc/pki/tls/certs/ca-bundle.crt +{% if repo_google_chrome__basic_auth_login is defined and repo_google_chrome__basic_auth_login | length %} +username={{ repo_google_chrome__basic_auth_login["username"] }} +password={{ repo_google_chrome__basic_auth_login["password"] }} +{% endif %} From d86d699b640180db4b5caab62627597938b206e1 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 12 May 2026 17:13:31 +0200 Subject: [PATCH 05/66] Add roles/google_chrome --- CHANGELOG.md | 3 +- COMPATIBILITY.md | 1 + playbooks/README.md | 9 + playbooks/all.yml | 1 + playbooks/google_chrome.yml | 31 ++++ roles/google_chrome/README.md | 136 ++++++++++++++ roles/google_chrome/defaults/main.yml | 33 ++++ roles/google_chrome/handlers/main.yml | 20 ++ roles/google_chrome/meta/argument_specs.yml | 71 ++++++++ roles/google_chrome/tasks/main.yml | 171 ++++++++++++++++++ .../system/chrome-headless-proxy.service.j2 | 13 ++ .../system/chrome-headless-proxy.socket.j2 | 11 ++ .../systemd/system/chrome-headless.service.j2 | 54 ++++++ roles/google_chrome/vars/RedHat.yml | 6 + 14 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 playbooks/google_chrome.yml create mode 100644 roles/google_chrome/README.md create mode 100644 roles/google_chrome/defaults/main.yml create mode 100644 roles/google_chrome/handlers/main.yml create mode 100644 roles/google_chrome/meta/argument_specs.yml create mode 100644 roles/google_chrome/tasks/main.yml create mode 100644 roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 create mode 100644 roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 create mode 100644 roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 create mode 100644 roles/google_chrome/vars/RedHat.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 925c5f9f..7737dc63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,11 +34,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +<<<<<<< HEAD * **role:sshd**: Add Debian 13 support. * **role:mirror**: Document the new per-repository `newest_only` subkey on `mirror__reposync_repos` entries. Defaults to `true` (only the newest version of each package is mirrored). Set to `false` for repositories that publish multiple versions in parallel, such as Icinga, where older versions must remain available. * **role:repo_remi**: Add RHEL 10 / Rocky 10 support (new GPG key, repo templates, and module-stream tasks for EL 10). * **role:repo_remi**: Add `meta/argument_specs.yml` declaring the four user-facing variables (`repo_remi__basic_auth_login`, `repo_remi__enabled_php_version`, `repo_remi__enabled_redis_version`, `repo_remi__mirror_url`) so role-entry validation catches type mismatches and unknown variables. `repo_remi__basic_auth_login` is declared as `type: 'raw'` because its default in `defaults/main.yml` resolves to an empty string when no Bitwarden lookup is configured. -* **role:repo_google_chrome**: New role. Deploys the Google Chrome package repository for RHEL-based distributions, with the same `lfops__repo_mirror_url` / `lfops__repo_basic_auth_login` knobs as the other `repo_*` roles. +* **role:google_chrome**: New role. Installs Google Chrome together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chrome-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chrome service, wired with `BindsTo`). Chrome is started on the first incoming connection and stopped again after `google_chrome__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips the `systemd_socket_proxyd_connect_any` SELinux boolean on enforcing hosts so the proxy can reach Chrome on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to. * **role:monitoring_plugins, role:repo_monitoring_plugins**: Add SLES 15 and SLES 16 support. The roles now install the Linuxfabrik Monitoring Plugins from the SUSE channel of `repo.linuxfabrik.ch` and apply the SUSE-specific package version lock ([#245](https://github.com/Linuxfabrik/lfops/issues/245)). * **role:alternatives, role:elastic_agent, role:elastic_agent_fleet_server, role:icinga_kubernetes_web, role:lvm, role:mailto_root, role:motd, role:proxysql**: (Re-)introduce `meta/argument_specs.yml` so role-entry validation catches type mismatches and missing required variables. The originally proposed specs were correct for these roles (no strict-options login dicts, no `__dependent_var` injections from `setup_*` playbooks), so they are restored unchanged. * **role:apps, role:example, role:kernel_settings**: (Re-)introduce `meta/argument_specs.yml`, with the `__dependent_var` slot declared so `setup_*` playbooks that inject these via `vars:` (e.g. `setup_icinga2_master`, `setup_moodle`, `setup_nextcloud`) pass validation. diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index a339aca4..c25a6004 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -42,6 +42,7 @@ Which Ansible role is proven to run on which OS? | gitlab_ce | | | x | (x) | (x) | | | | | | glances | (x) | (x) | x | x | (x) | (x) | (x) | (x) | | | glpi_agent | | | x | x | (x) | | | | | +| google_chrome | | | x | x | (x) | | | | | | grafana | | | x | x | x | | | | | | grafana_grizzly | (x) | (x) | x | x | (x) | (x) | (x) | (x) | | | grav | | | x | (x) | (x) | | | | | diff --git a/playbooks/README.md b/playbooks/README.md index 72b5cb88..388ada67 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -338,6 +338,15 @@ Calls the following roles (in order): * [glpi_agent](https://github.com/Linuxfabrik/lfops/tree/main/roles/glpi_agent) +## google_chrome.yml + +Calls the following roles (in order): + +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `google_chrome__skip_repo_epel` +* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `google_chrome__skip_repo_google_chrome` +* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) + + ## grafana.yml Calls the following roles (in order): diff --git a/playbooks/all.yml b/playbooks/all.yml index 51c30c19..96cda61d 100644 --- a/playbooks/all.yml +++ b/playbooks/all.yml @@ -36,6 +36,7 @@ - import_playbook: 'gitlab_ce.yml' - import_playbook: 'glances.yml' - import_playbook: 'glpi_agent.yml' +- import_playbook: 'google_chrome.yml' - import_playbook: 'grafana.yml' - import_playbook: 'grafana_grizzly.yml' - import_playbook: 'haveged.yml' diff --git a/playbooks/google_chrome.yml b/playbooks/google_chrome.yml new file mode 100644 index 00000000..f959bf4c --- /dev/null +++ b/playbooks/google_chrome.yml @@ -0,0 +1,31 @@ +- name: 'Playbook linuxfabrik.lfops.google_chrome' + hosts: + - 'lfops_google_chrome' + + pre_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-start.yml' + tags: + - 'always' + + + roles: + + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'not google_chrome__skip_repo_epel | d(false) | bool' + + - role: 'linuxfabrik.lfops.repo_google_chrome' + when: + - 'not google_chrome__skip_repo_google_chrome | d(false) | bool' + + - role: 'linuxfabrik.lfops.google_chrome' + + + post_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-end.yml' + tags: + - 'always' diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md new file mode 100644 index 00000000..c4997988 --- /dev/null +++ b/roles/google_chrome/README.md @@ -0,0 +1,136 @@ +# Ansible Role linuxfabrik.lfops.google_chrome + +This role installs [Google Chrome](https://www.google.com/chrome/) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated `chrome-headless` systemd service. Clients connect to a configurable TCP socket; Chrome is started on the first request via `systemd-socket-proxyd` and stopped again after a configurable idle timeout, so no RAM is wasted while the backend is unused. + +The setup is used as a headless browser backend for tools such as the [Icinga Web 2 PDF Export Module](https://github.com/Icinga/icingaweb2-module-pdfexport). + + +*Available since LFOps `6.0.2`.* + + +## How the Role Behaves + +* Three systemd units are deployed: + * `chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). + * `chrome-headless-proxy.service` runs `systemd-socket-proxyd` and forwards traffic to Chrome on `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. + * `chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly — the proxy triggers it via `Requires=`. +* On SELinux-enforcing hosts, the `systemd_socket_proxyd_connect_any` boolean is enabled so the proxy may connect to Chrome's non-standard backend port. +* The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `chrome-headless-proxy.socket` unit, not the Chrome service directly. + + +## Mandatory Requirements + +* Enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. +* Enable the Google Chrome repository. This can be done using the [linuxfabrik.lfops.repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome) role. + +If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/google_chrome.yml), this is automatically done for you. + + +## Tags + +`google_chrome` + +* Creates the `chrome` system user and group. +* Installs Google Chrome along with the required runtime libraries and fonts. +* Sets the `systemd_socket_proxyd_connect_any` SELinux boolean. +* Deploys all three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). +* Ensures the `chrome-headless-proxy.socket` is in the desired state. +* Triggers: daemon-reload, socket restart, Chrome service restart. + +`google_chrome:configure` + +* Deploys the three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). +* Triggers: daemon-reload, socket restart, Chrome service restart. + +`google_chrome:state` + +* Manages the `chrome-headless-proxy.socket` state (start, stop, enable, disable). +* Triggers: none. + + +## Optional Role Variables + +`google_chrome__backend_port` + +* Internal port Chrome itself listens on. The proxy forwards traffic from `listen_port` to this port. Only meaningful to change if `listen_port` and `backend_port` would otherwise collide. +* Type: Number. +* Default: `9223` + +`google_chrome__extra_args__host_var` / `google_chrome__extra_args__group_var` + +* Additional Chrome CLI flags appended to the `ExecStart` line of `chrome-headless.service`, in the order listed. Useful for tuning behavior without overwriting the whole unit. +* Type: List of dictionaries. +* Default: `[]` +* Subkeys: + + * `name`: + + * Mandatory. The CLI flag, including any leading dashes and value (e.g. `--window-size=1920,1080`). + * Type: String. + + * `state`: + + * Optional. `present` or `absent`. + * Type: String. + * Default: `'present'` + +`google_chrome__idle_timeout` + +* Seconds the `systemd-socket-proxyd` waits without active connections before exiting. When it exits, the bound `chrome-headless.service` stops automatically. The next inbound connection re-activates the whole chain, paying ~1–2 seconds of cold-start latency. +* Type: Number. +* Default: `300` + +`google_chrome__listen_address` + +* Address the proxy socket binds to. Keep this on `127.0.0.1` unless you intentionally want to expose it to other hosts; neither the proxy nor Chrome enforces TLS or authentication. +* Type: String. +* Default: `'127.0.0.1'` + +`google_chrome__listen_port` + +* Port the proxy socket listens on. This is the endpoint clients connect to. +* Type: Number. +* Default: `9222` + +`google_chrome__service_enabled` + +* Enables or disables the `chrome-headless-proxy.socket` at boot, analogous to `systemctl enable/disable --now`. +* Type: Bool. +* Default: `true` + +`google_chrome__service_state` + +* Changes the state of the `chrome-headless-proxy.socket`, analogous to `systemctl start/stop/restart/reload`. +* Type: String. One of `reloaded`, `restarted`, `started`, `stopped`. +* Default: `'started'` + +`google_chrome__user_data_dir` + +* Home directory of the `chrome` system user and Chrome user data directory. Used both as the user's `home`, as the `--user-data-dir` value for Chrome, and as the writable path exposed via systemd `ReadWritePaths=`. +* Type: String. +* Default: `'/var/lib/chrome-headless'` + +Example: +```yaml +# optional +google_chrome__backend_port: 9223 +google_chrome__extra_args__host_var: + - name: '--window-size=1920,1080' + - name: '--lang=de-CH' +google_chrome__idle_timeout: 600 +google_chrome__listen_address: '127.0.0.1' +google_chrome__listen_port: 9222 +google_chrome__service_enabled: true +google_chrome__service_state: 'started' +google_chrome__user_data_dir: '/var/lib/chrome-headless' +``` + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/google_chrome/defaults/main.yml b/roles/google_chrome/defaults/main.yml new file mode 100644 index 00000000..7cd77812 --- /dev/null +++ b/roles/google_chrome/defaults/main.yml @@ -0,0 +1,33 @@ +# --- list of dicts injection pattern --- +# Extra Chrome CLI flags appended to the chrome-headless systemd unit. +google_chrome__extra_args__dependent_var: [] +google_chrome__extra_args__group_var: [] +google_chrome__extra_args__host_var: [] +google_chrome__extra_args__role_var: [] +google_chrome__extra_args__combined_var: '{{ ( + google_chrome__extra_args__role_var + + google_chrome__extra_args__dependent_var + + google_chrome__extra_args__group_var + + google_chrome__extra_args__host_var + ) | linuxfabrik.lfops.combine_lod + }}' + +# Chrome's own listening port. The proxy connects to it on demand; clients never +# talk to it directly. +google_chrome__backend_port: 9223 + +# Idle timeout for systemd-socket-proxyd in seconds. After this much time without +# active connections, the proxy exits — and Chrome stops with it via BindsTo. +google_chrome__idle_timeout: 300 + +# External listening endpoint exposed by the chrome-headless-proxy.socket unit. +# This is what clients (Apache, the pdfexport module, ...) connect to. +google_chrome__listen_address: '127.0.0.1' +google_chrome__listen_port: 9222 + +# Lifecycle of the chrome-headless-proxy.socket unit. The Chrome service itself +# is triggered on demand by the proxy and is not managed directly. +google_chrome__service_enabled: true +google_chrome__service_state: 'started' + +google_chrome__user_data_dir: '/var/lib/chrome-headless' diff --git a/roles/google_chrome/handlers/main.yml b/roles/google_chrome/handlers/main.yml new file mode 100644 index 00000000..741892ce --- /dev/null +++ b/roles/google_chrome/handlers/main.yml @@ -0,0 +1,20 @@ +- name: 'google_chrome: systemctl daemon-reload' + ansible.builtin.systemd: + daemon_reload: true + +# Restart chrome before the socket: in a migration from the old, non-socket-activated +# layout the running Chrome still binds the listen port. The socket can only bind +# after Chrome has been re-execed on the backend port. +- name: 'google_chrome: restart chrome-headless' + ansible.builtin.service: + name: 'chrome-headless.service' + state: 'restarted' + when: + - 'google_chrome__service_state != "stopped"' + +- name: 'google_chrome: restart chrome-headless-proxy.socket' + ansible.builtin.service: + name: 'chrome-headless-proxy.socket' + state: 'restarted' + when: + - 'google_chrome__service_state != "stopped"' diff --git a/roles/google_chrome/meta/argument_specs.yml b/roles/google_chrome/meta/argument_specs.yml new file mode 100644 index 00000000..b5fd5ff4 --- /dev/null +++ b/roles/google_chrome/meta/argument_specs.yml @@ -0,0 +1,71 @@ +argument_specs: + main: + options: + + google_chrome__extra_args__dependent_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Google Chrome CLI flags. Dependent-role injection.' + + google_chrome__extra_args__group_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Google Chrome CLI flags. Group-level override.' + + google_chrome__extra_args__host_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Google Chrome CLI flags. Host-level override.' + + google_chrome__backend_port: + type: 'int' + required: false + default: 9223 + description: 'Internal port Chrome itself listens on. The proxy forwards traffic from listen_port to this port.' + + google_chrome__idle_timeout: + type: 'int' + required: false + default: 300 + description: 'Seconds the systemd-socket-proxyd waits without traffic before exiting (and stopping Chrome via BindsTo).' + + google_chrome__listen_address: + type: 'str' + required: false + default: '127.0.0.1' + description: 'Listen address for the Chrome remote debugging interface.' + + google_chrome__listen_port: + type: 'int' + required: false + default: 9222 + description: 'Listen port for the Chrome remote debugging interface.' + + google_chrome__service_enabled: + type: 'bool' + required: false + default: true + description: 'Enables or disables the chrome-headless.service.' + + google_chrome__service_state: + type: 'str' + required: false + default: 'started' + choices: + - 'reloaded' + - 'restarted' + - 'started' + - 'stopped' + description: 'Desired state of the chrome-headless.service.' + + google_chrome__user_data_dir: + type: 'str' + required: false + default: '/var/lib/chrome-headless' + description: 'Home directory of the chrome system user and Chrome user data directory.' diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml new file mode 100644 index 00000000..00d9fa0e --- /dev/null +++ b/roles/google_chrome/tasks/main.yml @@ -0,0 +1,171 @@ +- block: + + - name: 'Set platform/version specific variables' + ansible.builtin.import_role: + name: 'shared' + tasks_from: 'platform-variables.yml' + + tags: + - 'always' + + +- name: 'Perform platform/version specific tasks' + ansible.builtin.include_tasks: '{{ __task_file }}' + when: '__task_file | length > 0' + vars: + __task_file: '{{ lookup("ansible.builtin.first_found", __first_found_options) }}' + __first_found_options: + files: + - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_version"] }}.yml' + - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_major_version"] }}.yml' + - '{{ ansible_facts["distribution"] }}.yml' + - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_version"] }}.yml' + - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_major_version"] }}.yml' + - '{{ ansible_facts["os_family"] }}.yml' + paths: + - '{{ role_path }}/tasks' + skip: true + tags: + - 'always' + + +- block: + + - name: 'groupadd --system chrome' + ansible.builtin.group: + name: 'chrome' + state: 'present' + system: true + + - name: 'useradd --system chrome' + ansible.builtin.user: + name: 'chrome' + comment: 'Headless Google Chrome' + group: 'chrome' + home: '{{ google_chrome__user_data_dir }}' + shell: '/sbin/nologin' + system: true + state: 'present' + + - name: 'install --directory --owner chrome --group chrome --mode 0750 {{ google_chrome__user_data_dir }}' + ansible.builtin.file: + path: '{{ google_chrome__user_data_dir }}' + state: 'directory' + owner: 'chrome' + group: 'chrome' + mode: 0o750 + + - name: 'Install required packages' + ansible.builtin.package: + name: '{{ __google_chrome__packages }}' + state: 'present' + + tags: + - 'google_chrome' + + +- block: + + - name: 'setsebool -P systemd_socket_proxyd_connect_any on' + ansible.posix.seboolean: + name: 'systemd_socket_proxyd_connect_any' + persistent: true + state: true + when: + - 'ansible_facts["selinux"]["status"] != "disabled"' + + - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.socket' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chrome-headless-proxy.socket.j2' + dest: '/etc/systemd/system/chrome-headless-proxy.socket' + owner: 'root' + group: 'root' + mode: 0o644 + notify: + - 'google_chrome: systemctl daemon-reload' + - 'google_chrome: restart chrome-headless-proxy.socket' + + - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chrome-headless-proxy.service.j2' + dest: '/etc/systemd/system/chrome-headless-proxy.service' + owner: 'root' + group: 'root' + mode: 0o644 + notify: + - 'google_chrome: systemctl daemon-reload' + - 'google_chrome: restart chrome-headless-proxy.socket' + + - name: 'Deploy /etc/systemd/system/chrome-headless.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chrome-headless.service.j2' + dest: '/etc/systemd/system/chrome-headless.service' + owner: 'root' + group: 'root' + mode: 0o644 + notify: + - 'google_chrome: systemctl daemon-reload' + - 'google_chrome: restart chrome-headless' + + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' + ansible.builtin.include_role: + name: 'shared' + tasks_from: 'remove-rpmnew-rpmsave.yml' + vars: + shared__remove_rpmnew_rpmsave_config_file: '{{ item }}' + loop: + - '/etc/systemd/system/chrome-headless-proxy.socket' + - '/etc/systemd/system/chrome-headless-proxy.service' + - '/etc/systemd/system/chrome-headless.service' + + tags: + - 'google_chrome' + - 'google_chrome:configure' + + +- block: + + # Force the handlers (daemon-reload, restart chrome on the new backend port, + # restart the socket) to run before the state block tries to enable the socket. + # Without this the socket would fail to load when migrating from the old + # non-socket-activated layout, because Chrome would still be holding the listen + # port at enable-time. + - name: 'Flush handlers' + ansible.builtin.meta: 'flush_handlers' + + tags: + - 'google_chrome' + - 'google_chrome:configure' + - 'google_chrome:state' + + +- block: + + - name: 'systemctl {{ google_chrome__service_enabled | bool | ternary("enable", "disable") }} chrome-headless-proxy.socket' + ansible.builtin.service: + name: 'chrome-headless-proxy.socket' + enabled: '{{ google_chrome__service_enabled | bool }}' + + - name: 'systemctl {{ google_chrome__service_state }} chrome-headless-proxy.socket' + ansible.builtin.service: + name: 'chrome-headless-proxy.socket' + state: '{{ google_chrome__service_state }}' + register: '__google_chrome__service_state_result' + + tags: + - 'google_chrome' + - 'google_chrome:state' + + +- block: + + - name: 'Flush handlers so that the service is ready for dependent roles' + ansible.builtin.meta: 'flush_handlers' + + tags: + - 'google_chrome' + - 'google_chrome:configure' + - 'google_chrome:state' diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 b/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 new file mode 100644 index 00000000..35e1fe33 --- /dev/null +++ b/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 @@ -0,0 +1,13 @@ +# {{ ansible_managed }} +# 2026051201 + +[Unit] +Description=Proxy to on-demand Headless Google Chrome +Requires=chrome-headless.service +After=chrome-headless.service + +[Service] +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ google_chrome__idle_timeout }} {{ google_chrome__listen_address }}:{{ google_chrome__backend_port }} +PrivateTmp=true +Restart=on-failure +RestartSec=5 diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 b/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 new file mode 100644 index 00000000..4bbbb54a --- /dev/null +++ b/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 @@ -0,0 +1,11 @@ +# {{ ansible_managed }} +# 2026051201 + +[Unit] +Description=Socket for on-demand Headless Google Chrome + +[Socket] +ListenStream={{ google_chrome__listen_address }}:{{ google_chrome__listen_port }} + +[Install] +WantedBy=sockets.target diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 b/roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 new file mode 100644 index 00000000..73062de4 --- /dev/null +++ b/roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 @@ -0,0 +1,54 @@ +# {{ ansible_managed }} +# 2026051201 + +[Unit] +Description=Headless Google Chrome +BindsTo=chrome-headless-proxy.service + +[Service] +Type=simple +User=chrome +Group=chrome +ExecStart={{ __google_chrome__binary_path }} \ + --headless=new \ + --disable-gpu \ + --no-first-run \ + --no-default-browser-check \ + --hide-scrollbars \ + --disable-dev-shm-usage \ + --remote-debugging-address={{ google_chrome__listen_address }} \ + --remote-debugging-port={{ google_chrome__backend_port }} \ + --remote-allow-origins=http://{{ google_chrome__listen_address }}:{{ google_chrome__listen_port }} \ +{% for arg in google_chrome__extra_args__combined_var if arg['state'] | d('present') != 'absent' %} + {{ arg['name'] }} \ +{% endfor %} + --user-data-dir={{ google_chrome__user_data_dir }} +Restart=on-failure +RestartSec=5 + +PrivateDevices=true +ProtectClock=true +NoNewPrivileges=true +RemoveIPC=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths={{ google_chrome__user_data_dir }} +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=~cgroup uts ipc +LockPersonality=true +MemoryDenyWriteExecute=false +CapabilityBoundingSet= +AmbientCapabilities= +SystemCallArchitectures=native +ProtectKernelLogs=true +ProtectHostname=true +ProtectClockSetting=true +ProtectProc=invisible +ProcSubset=pid +RestrictSUIDSGID=true +RestrictRealtime=true +UMask=0077 diff --git a/roles/google_chrome/vars/RedHat.yml b/roles/google_chrome/vars/RedHat.yml new file mode 100644 index 00000000..5f780776 --- /dev/null +++ b/roles/google_chrome/vars/RedHat.yml @@ -0,0 +1,6 @@ +__google_chrome__binary_path: '/usr/bin/google-chrome-stable' +__google_chrome__packages: + - 'gnu-free-sans-fonts' + - 'google-chrome-stable' + - 'mesa-libOSMesa' + - 'mesa-libOSMesa-devel' From cc9fbad8ab1a03a7b3dbbae207f5c30803866c16 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Tue, 12 May 2026 17:14:30 +0200 Subject: [PATCH 06/66] fix(roles/icingaweb2_module_pdfexport): wire to chrome-headless service Deploy /etc/icingaweb2/modules/pdfexport/config.ini so the module talks to the chrome-headless service over the Chrome DevTools Protocol by default (host/port), with an optional fall-back to a local Chrome binary. Move the platform-variables import into an always-tagged block so the new icingaweb2_module_pdfexport:configure tag can be run on its own. Wire the repo_epel, repo_google_chrome and google_chrome roles into both the standalone playbook and setup_icinga2_master.yml, with *__skip_* opt-outs tracking the existing pdfexport skip flag. --- CHANGELOG.md | 1 + playbooks/README.md | 5 ++ playbooks/icingaweb2_module_pdfexport.yml | 12 +++++ playbooks/setup_icinga2_master.yml | 10 ++++ roles/icingaweb2_module_pdfexport/README.md | 50 ++++++++++++++++++- .../defaults/main.yml | 9 ++++ .../meta/argument_specs.yml | 30 +++++++++++ .../tasks/main.yml | 37 ++++++++++++++ .../modules/pdfexport/config.ini.j2 | 11 ++++ 9 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 roles/icingaweb2_module_pdfexport/defaults/main.yml create mode 100644 roles/icingaweb2_module_pdfexport/meta/argument_specs.yml create mode 100644 roles/icingaweb2_module_pdfexport/templates/etc/icingaweb2/modules/pdfexport/config.ini.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7737dc63..1404de00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **role:icingaweb2_module_pdfexport, playbooks/icingaweb2_module_pdfexport, playbooks/setup_icinga2_master**: The headless browser backend the module requires was not installed by any role and had to be configured manually, so fresh deployments ended up without working PDF export. The new `google_chrome` and `repo_google_chrome` roles now provide a hardened `chrome-headless.service`, and both `icingaweb2_module_pdfexport.yml` and `setup_icinga2_master.yml` wire them up with `*__skip_*` opt-out variables (in `setup_icinga2_master.yml` the defaults track the existing `icingaweb2_module_pdfexport__skip_role` flag). The role also gained `/etc/icingaweb2/modules/pdfexport/config.ini` deployment with four new variables (`icingaweb2_module_pdfexport__chrome_host`, `__chrome_port`, `__chrome_binary`, `__force_temp_storage`); by default it talks to the `chrome-headless.service` over the Chrome DevTools Protocol, falling back to a local Chrome binary only if `chrome_binary` is set explicitly. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. * **roles**: Set `become: false` on tasks delegated to localhost across the collection. Previously these tasks inherited `become: true` from the playbook level and tried to call `sudo` on the Ansible controller, which fails on controllers without a passwordless sudo setup with `sudo: a password is required`. Affected are all `repo_*` roles, the `*_vm` cloud roles (`exoscale_vm`, `hetzner_vm`, `infomaniak_vm`), all `icingaweb2_module_*` roles that download artefacts, `monitoring_plugins`, `shared`, plus several others. Existing playbooks that were working without playbook-level `become: true` are unaffected ([#242](https://github.com/Linuxfabrik/lfops/issues/242)). diff --git a/playbooks/README.md b/playbooks/README.md index 388ada67..6ff59812 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -454,6 +454,9 @@ Calls the following roles (in order): Calls the following roles (in order): +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `icingaweb2_module_pdfexport__skip_repo_epel` +* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `icingaweb2_module_pdfexport__skip_repo_google_chrome` +* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome): `icingaweb2_module_pdfexport__skip_google_chrome` * [icingaweb2_module_pdfexport](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_pdfexport) @@ -1113,6 +1116,8 @@ Calls the following roles (in order): * [icingaweb2_theme_linuxfabrik](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_theme_linuxfabrik): `setup_icinga2_master__icingaweb2_theme_linuxfabrik__skip_role` * [icingaweb2_module_incubator](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_incubator): `setup_icinga2_master__icingaweb2_module_incubator__skip_role` * [icingaweb2_module_jira](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_jira): `setup_icinga2_master__icingaweb2_module_jira__skip_role` (default: `true`) +* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `setup_icinga2_master__repo_google_chrome__skip_role` (default: tracks `icingaweb2_module_pdfexport__skip_role`) +* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome): `setup_icinga2_master__google_chrome__skip_role` (default: tracks `icingaweb2_module_pdfexport__skip_role`) * [icingaweb2_module_pdfexport](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_pdfexport): `setup_icinga2_master__icingaweb2_module_pdfexport__skip_role` (default: `true`) * [icingaweb2_module_vspheredb](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_vspheredb): `setup_icinga2_master__icingaweb2_module_vspheredb__skip_role` (default: `true`) * [icingaweb2_module_director](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_director): `setup_icinga2_master__icingaweb2_module_director__skip_role` diff --git a/playbooks/icingaweb2_module_pdfexport.yml b/playbooks/icingaweb2_module_pdfexport.yml index 82ab5a9d..ef912403 100644 --- a/playbooks/icingaweb2_module_pdfexport.yml +++ b/playbooks/icingaweb2_module_pdfexport.yml @@ -12,6 +12,18 @@ roles: + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'not icingaweb2_module_pdfexport__skip_repo_epel | d(false) | bool' + + - role: 'linuxfabrik.lfops.repo_google_chrome' + when: + - 'not icingaweb2_module_pdfexport__skip_repo_google_chrome | d(false) | bool' + + - role: 'linuxfabrik.lfops.google_chrome' + when: + - 'not icingaweb2_module_pdfexport__skip_google_chrome | d(false) | bool' + - role: 'linuxfabrik.lfops.icingaweb2_module_pdfexport' diff --git a/playbooks/setup_icinga2_master.yml b/playbooks/setup_icinga2_master.yml index c795bdd4..4b2d6324 100644 --- a/playbooks/setup_icinga2_master.yml +++ b/playbooks/setup_icinga2_master.yml @@ -7,6 +7,7 @@ setup_icinga2_master__apache_httpd__skip_injections__internal_var: '{{ setup_icinga2_master__apache_httpd__skip_injections | d(setup_icinga2_master__apache_httpd__skip_role__internal_var) }}' setup_icinga2_master__apache_httpd__skip_role__internal_var: '{{ setup_icinga2_master__apache_httpd__skip_role | d(false) }}' + setup_icinga2_master__google_chrome__skip_role__internal_var: '{{ setup_icinga2_master__google_chrome__skip_role | d(setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var) }}' setup_icinga2_master__grafana__skip_role__internal_var: '{{ setup_icinga2_master__grafana__skip_role | d(false) }}' setup_icinga2_master__grafana_grizzly__skip_injections__internal_var: '{{ setup_icinga2_master__grafana_grizzly__skip_injections | d(setup_icinga2_master__grafana_grizzly__skip_role__internal_var) }}' setup_icinga2_master__grafana_grizzly__skip_role__internal_var: '{{ setup_icinga2_master__grafana_grizzly__skip_role | d(false) }}' @@ -58,6 +59,7 @@ setup_icinga2_master__redis__skip_injections__internal_var: '{{ setup_icinga2_master__redis__skip_injections | d(setup_icinga2_master__redis__skip_role__internal_var) }}' setup_icinga2_master__redis__skip_role__internal_var: '{{ setup_icinga2_master__redis__skip_role | d(false) }}' setup_icinga2_master__repo_epel__skip_role__internal_var: '{{ setup_icinga2_master__repo_epel__skip_role | d(false) }}' + setup_icinga2_master__repo_google_chrome__skip_role__internal_var: '{{ setup_icinga2_master__repo_google_chrome__skip_role | d(setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var) }}' setup_icinga2_master__repo_grafana__skip_role__internal_var: '{{ setup_icinga2_master__repo_grafana__skip_role | d(false) }}' setup_icinga2_master__repo_icinga__skip_role__internal_var: '{{ setup_icinga2_master__repo_icinga__skip_role | d(false) }}' setup_icinga2_master__repo_influxdb__skip_role__internal_var: '{{ setup_icinga2_master__repo_influxdb__skip_role | d(false) }}' @@ -312,6 +314,14 @@ when: - 'not setup_icinga2_master__icingaweb2_module_jira__skip_role__internal_var' + - role: 'linuxfabrik.lfops.repo_google_chrome' + when: + - 'not setup_icinga2_master__repo_google_chrome__skip_role__internal_var' + + - role: 'linuxfabrik.lfops.google_chrome' + when: + - 'not setup_icinga2_master__google_chrome__skip_role__internal_var' + - role: 'linuxfabrik.lfops.icingaweb2_module_pdfexport' when: - 'not setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var' diff --git a/roles/icingaweb2_module_pdfexport/README.md b/roles/icingaweb2_module_pdfexport/README.md index 2e077d81..fcbf5a78 100644 --- a/roles/icingaweb2_module_pdfexport/README.md +++ b/roles/icingaweb2_module_pdfexport/README.md @@ -15,14 +15,17 @@ This role is tested with the following IcingaWeb2 PDF Export Module versions: * The Tarball for `icingaweb2_module_pdfexport__version` is downloaded on the Ansible controller (`delegate_to: 'localhost'`, `run_once: true`), then copied to the target. The controller therefore needs Internet access to GitHub; the target does not. * On every role run the directory `/usr/share/icingaweb2/modules/pdfexport` is overwritten with the contents of the configured version. To upgrade or downgrade the module, change `icingaweb2_module_pdfexport__version` and re-run the role. * `icingacli module enable pdfexport` is only invoked when `/etc/icingaweb2/enabledModules/pdfexport` does not yet exist (idempotent). -* This role only installs the IcingaWeb2 module itself. Any runtime dependencies of the module (see the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements)) have to be installed and configured separately. +* `/etc/icingaweb2/modules/pdfexport/config.ini` is deployed on every run. By default the module is wired to a running headless Chrome over the Chrome DevTools Protocol (CDP); set `icingaweb2_module_pdfexport__chrome_binary` to fall back to spawning Chrome locally on every export. +* This role only installs and configures the IcingaWeb2 module itself. The headless browser backend it talks to (see the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements)) is provided separately by the [linuxfabrik.lfops.google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) role. ## Mandatory Requirements * A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. * Internet access from the Ansible controller (downloads from `https://github.com/Icinga/icingaweb2-module-pdfexport/archive/`). -* The runtime dependencies listed in the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements) (typically a headless browser binary). Install and configure them separately. +* A running headless Chrome instance providing the remote debugging interface this module talks to. This can be done using the [linuxfabrik.lfops.google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) role. + +If you use the [IcingaWeb2 PDF Export Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2_module_pdfexport.yml), the headless Chrome backend is automatically installed for you. ## Tags @@ -30,6 +33,12 @@ This role is tested with the following IcingaWeb2 PDF Export Module versions: `icingaweb2_module_pdfexport` * Installs and enables the IcingaWeb2 PDF Export Module. +* Deploys `/etc/icingaweb2/modules/pdfexport/config.ini`. +* Triggers: none. + +`icingaweb2_module_pdfexport:configure` + +* Deploys `/etc/icingaweb2/modules/pdfexport/config.ini`. * Triggers: none. @@ -48,6 +57,43 @@ icingaweb2_module_pdfexport__version: 'v0.11.0' ``` +## Optional Role Variables + +`icingaweb2_module_pdfexport__chrome_binary` + +* Path to a local Chrome / Chromium binary. If set, the module spawns Chrome locally on every PDF export and the `chrome_host` / `chrome_port` settings are ignored. Leave empty (the default) to use the remote CDP mode. +* Type: String. +* Default: `''` + +`icingaweb2_module_pdfexport__chrome_host` + +* Address of the headless Chrome instance the module connects to via the Chrome DevTools Protocol. +* Type: String. +* Default: `'{{ google_chrome__listen_address | d("127.0.0.1") }}'` + +`icingaweb2_module_pdfexport__chrome_port` + +* Port of the headless Chrome instance the module connects to via the Chrome DevTools Protocol. +* Type: Number. +* Default: `'{{ google_chrome__listen_port | d(9222) }}'` + +`icingaweb2_module_pdfexport__force_temp_storage` + +* When `true`, the module renders every PDF to a temporary file on disk before sending it to the browser instead of streaming it directly. Useful as a workaround on memory-constrained hosts. +* Type: Bool. +* Default: `false` + +Example: + +```yaml +# optional +icingaweb2_module_pdfexport__chrome_binary: '/usr/bin/google-chrome-stable' +icingaweb2_module_pdfexport__chrome_host: '127.0.0.1' +icingaweb2_module_pdfexport__chrome_port: 9222 +icingaweb2_module_pdfexport__force_temp_storage: false +``` + + ## License [The Unlicense](https://unlicense.org/) diff --git a/roles/icingaweb2_module_pdfexport/defaults/main.yml b/roles/icingaweb2_module_pdfexport/defaults/main.yml new file mode 100644 index 00000000..0995bfed --- /dev/null +++ b/roles/icingaweb2_module_pdfexport/defaults/main.yml @@ -0,0 +1,9 @@ +# If `icingaweb2_module_pdfexport__chrome_binary` is set, the module spawns chrome +# locally on every PDF export. Otherwise it talks to a running headless Chrome via +# the Chrome DevTools Protocol on `chrome_host` / `chrome_port` (default mode). +# Defaults pull from the linuxfabrik.lfops.google_chrome role so a single change +# there propagates here. +icingaweb2_module_pdfexport__chrome_binary: '' +icingaweb2_module_pdfexport__chrome_host: '{{ google_chrome__listen_address | d("127.0.0.1") }}' +icingaweb2_module_pdfexport__chrome_port: '{{ google_chrome__listen_port | d(9222) }}' +icingaweb2_module_pdfexport__force_temp_storage: false diff --git a/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml b/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml new file mode 100644 index 00000000..469ff4a1 --- /dev/null +++ b/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml @@ -0,0 +1,30 @@ +argument_specs: + main: + options: + + icingaweb2_module_pdfexport__chrome_binary: + type: 'str' + required: false + default: '' + description: 'Path to a local Chrome/Chromium binary. If set, the module spawns Chrome locally on every export and the host/port settings are ignored.' + + icingaweb2_module_pdfexport__chrome_host: + type: 'str' + required: false + description: 'Listen address of the headless Chrome instance the module connects to. Defaults to google_chrome__listen_address.' + + icingaweb2_module_pdfexport__chrome_port: + type: 'raw' + required: false + description: 'Listen port of the headless Chrome instance the module connects to. Defaults to google_chrome__listen_port.' + + icingaweb2_module_pdfexport__force_temp_storage: + type: 'bool' + required: false + default: false + description: 'When true, the module renders every PDF to a temporary file on disk before sending it to the browser instead of streaming it directly. Useful as a workaround on memory-constrained hosts.' + + icingaweb2_module_pdfexport__version: + type: 'str' + required: true + description: 'The IcingaWeb2 PDF Export Module version to install. See https://github.com/Icinga/icingaweb2-module-pdfexport/releases.' diff --git a/roles/icingaweb2_module_pdfexport/tasks/main.yml b/roles/icingaweb2_module_pdfexport/tasks/main.yml index e486d0a7..333ce09f 100644 --- a/roles/icingaweb2_module_pdfexport/tasks/main.yml +++ b/roles/icingaweb2_module_pdfexport/tasks/main.yml @@ -5,6 +5,12 @@ name: 'shared' tasks_from: 'platform-variables.yml' + tags: + - 'always' + + +- block: + - name: 'mkdir -p /usr/share/icingaweb2/modules/pdfexport' ansible.builtin.file: path: '/usr/share/icingaweb2/modules/pdfexport' @@ -47,3 +53,34 @@ tags: - 'icingaweb2_module_pdfexport' + + +- block: + + - name: 'mkdir -p /etc/icingaweb2/modules/pdfexport' + ansible.builtin.file: + path: '/etc/icingaweb2/modules/pdfexport' + state: 'directory' + owner: '{{ __icingaweb2_module_pdfexport__icingaweb2_owner }}' + group: 'icingaweb2' + mode: 0o2770 + + - name: 'Deploy /etc/icingaweb2/modules/pdfexport/config.ini' + ansible.builtin.template: + backup: true + src: 'etc/icingaweb2/modules/pdfexport/config.ini.j2' + dest: '/etc/icingaweb2/modules/pdfexport/config.ini' + owner: '{{ __icingaweb2_module_pdfexport__icingaweb2_owner }}' + group: 'icingaweb2' + mode: 0o660 + + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' + ansible.builtin.include_role: + name: 'shared' + tasks_from: 'remove-rpmnew-rpmsave.yml' + vars: + shared__remove_rpmnew_rpmsave_config_file: '/etc/icingaweb2/modules/pdfexport/config.ini' + + tags: + - 'icingaweb2_module_pdfexport' + - 'icingaweb2_module_pdfexport:configure' diff --git a/roles/icingaweb2_module_pdfexport/templates/etc/icingaweb2/modules/pdfexport/config.ini.j2 b/roles/icingaweb2_module_pdfexport/templates/etc/icingaweb2/modules/pdfexport/config.ini.j2 new file mode 100644 index 00000000..d12b6162 --- /dev/null +++ b/roles/icingaweb2_module_pdfexport/templates/etc/icingaweb2/modules/pdfexport/config.ini.j2 @@ -0,0 +1,11 @@ +; {{ ansible_managed }} +; 2026051201 + +[chrome] +{% if icingaweb2_module_pdfexport__chrome_binary | length > 0 %} +binary = "{{ icingaweb2_module_pdfexport__chrome_binary }}" +{% else %} +host = "{{ icingaweb2_module_pdfexport__chrome_host }}" +port = "{{ icingaweb2_module_pdfexport__chrome_port }}" +{% endif %} +force_temp_storage = "{{ icingaweb2_module_pdfexport__force_temp_storage | bool | ternary('1', '0') }}" From 717ebdaab28cca790a550fae8148c5cf3671e624 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Wed, 13 May 2026 10:45:20 +0200 Subject: [PATCH 07/66] docs(roles/google_chrome): explain why systemd-socket-proxyd is needed in front of Chrome --- roles/google_chrome/README.md | 2 +- roles/google_chrome/tasks/main.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md index c4997988..d2417f75 100644 --- a/roles/google_chrome/README.md +++ b/roles/google_chrome/README.md @@ -12,7 +12,7 @@ The setup is used as a headless browser backend for tools such as the [Icinga We * Three systemd units are deployed: * `chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). - * `chrome-headless-proxy.service` runs `systemd-socket-proxyd` and forwards traffic to Chrome on `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. + * `chrome-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. * `chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly — the proxy triggers it via `Requires=`. * On SELinux-enforcing hosts, the `systemd_socket_proxyd_connect_any` boolean is enabled so the proxy may connect to Chrome's non-standard backend port. * The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `chrome-headless-proxy.socket` unit, not the Chrome service directly. diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml index 00d9fa0e..a29b1b52 100644 --- a/roles/google_chrome/tasks/main.yml +++ b/roles/google_chrome/tasks/main.yml @@ -64,6 +64,8 @@ - 'google_chrome' +# Chrome itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chrome: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chrome opens itself). --exit-idle-time on the proxy plus BindsTo= on chrome-headless.service ties Chrome's lifecycle to the proxy, so Chrome stops together with the proxy after the idle timeout. +# The SELinux boolean is required because the proxy connects to Chrome's non-standard backend port, which has no matching SELinux port type. - block: - name: 'setsebool -P systemd_socket_proxyd_connect_any on' From 2a583034fffbb9a2aca796cd054d30ffac19e834 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Wed, 13 May 2026 11:44:37 +0200 Subject: [PATCH 08/66] fix(roles/google_chrome): also set systemd_socket_proxyd_bind_any boolean Without bind_any the chrome-headless-proxy.socket cannot bind the listen port on hosts where the port carries an unexpected SELinux port type (on Rocky/RHEL 9 the default 9222 is registered as hplip_port_t). --- CHANGELOG.md | 4 ++-- roles/google_chrome/README.md | 4 ++-- roles/google_chrome/tasks/main.yml | 10 +++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1404de00..2eba9405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,12 +34,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -<<<<<<< HEAD * **role:sshd**: Add Debian 13 support. * **role:mirror**: Document the new per-repository `newest_only` subkey on `mirror__reposync_repos` entries. Defaults to `true` (only the newest version of each package is mirrored). Set to `false` for repositories that publish multiple versions in parallel, such as Icinga, where older versions must remain available. +* **role:google_chrome**: New role. Installs Google Chrome together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chrome-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chrome service, wired with `BindsTo`). Chrome is started on the first incoming connection and stopped again after `google_chrome__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips two SELinux booleans on enforcing hosts: `systemd_socket_proxyd_bind_any` so the socket unit can bind the listen port (on Rocky/RHEL 9 the default `9222` carries the `hplip_port_t` label, which would otherwise reject the bind), and `systemd_socket_proxyd_connect_any` so the proxy can reach Chrome on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to. +* **role:repo_google_chrome**: New role. Deploys the Google Chrome package repository for RHEL-based distributions, with the same `lfops__repo_mirror_url` / `lfops__repo_basic_auth_login` knobs as the other `repo_*` roles. * **role:repo_remi**: Add RHEL 10 / Rocky 10 support (new GPG key, repo templates, and module-stream tasks for EL 10). * **role:repo_remi**: Add `meta/argument_specs.yml` declaring the four user-facing variables (`repo_remi__basic_auth_login`, `repo_remi__enabled_php_version`, `repo_remi__enabled_redis_version`, `repo_remi__mirror_url`) so role-entry validation catches type mismatches and unknown variables. `repo_remi__basic_auth_login` is declared as `type: 'raw'` because its default in `defaults/main.yml` resolves to an empty string when no Bitwarden lookup is configured. -* **role:google_chrome**: New role. Installs Google Chrome together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chrome-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chrome service, wired with `BindsTo`). Chrome is started on the first incoming connection and stopped again after `google_chrome__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips the `systemd_socket_proxyd_connect_any` SELinux boolean on enforcing hosts so the proxy can reach Chrome on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to. * **role:monitoring_plugins, role:repo_monitoring_plugins**: Add SLES 15 and SLES 16 support. The roles now install the Linuxfabrik Monitoring Plugins from the SUSE channel of `repo.linuxfabrik.ch` and apply the SUSE-specific package version lock ([#245](https://github.com/Linuxfabrik/lfops/issues/245)). * **role:alternatives, role:elastic_agent, role:elastic_agent_fleet_server, role:icinga_kubernetes_web, role:lvm, role:mailto_root, role:motd, role:proxysql**: (Re-)introduce `meta/argument_specs.yml` so role-entry validation catches type mismatches and missing required variables. The originally proposed specs were correct for these roles (no strict-options login dicts, no `__dependent_var` injections from `setup_*` playbooks), so they are restored unchanged. * **role:apps, role:example, role:kernel_settings**: (Re-)introduce `meta/argument_specs.yml`, with the `__dependent_var` slot declared so `setup_*` playbooks that inject these via `vars:` (e.g. `setup_icinga2_master`, `setup_moodle`, `setup_nextcloud`) pass validation. diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md index d2417f75..29d510c7 100644 --- a/roles/google_chrome/README.md +++ b/roles/google_chrome/README.md @@ -14,7 +14,7 @@ The setup is used as a headless browser backend for tools such as the [Icinga We * `chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). * `chrome-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. * `chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly — the proxy triggers it via `Requires=`. -* On SELinux-enforcing hosts, the `systemd_socket_proxyd_connect_any` boolean is enabled so the proxy may connect to Chrome's non-standard backend port. +* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `chrome-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chrome's non-standard backend port. * The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `chrome-headless-proxy.socket` unit, not the Chrome service directly. @@ -32,7 +32,7 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo * Creates the `chrome` system user and group. * Installs Google Chrome along with the required runtime libraries and fonts. -* Sets the `systemd_socket_proxyd_connect_any` SELinux boolean. +* Sets the `systemd_socket_proxyd_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. * Deploys all three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). * Ensures the `chrome-headless-proxy.socket` is in the desired state. * Triggers: daemon-reload, socket restart, Chrome service restart. diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml index a29b1b52..2327269c 100644 --- a/roles/google_chrome/tasks/main.yml +++ b/roles/google_chrome/tasks/main.yml @@ -65,9 +65,17 @@ # Chrome itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chrome: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chrome opens itself). --exit-idle-time on the proxy plus BindsTo= on chrome-headless.service ties Chrome's lifecycle to the proxy, so Chrome stops together with the proxy after the idle timeout. -# The SELinux boolean is required because the proxy connects to Chrome's non-standard backend port, which has no matching SELinux port type. +# Two SELinux booleans are required: bind_any so the .socket can bind the listen port even when it has an unexpected port type (e.g. on Rocky 9, port 9222 maps to hplip_port_t and would otherwise reject the bind); connect_any so the proxy can connect to Chrome's non-standard backend port, which has no matching SELinux port type. - block: + - name: 'setsebool -P systemd_socket_proxyd_bind_any on' + ansible.posix.seboolean: + name: 'systemd_socket_proxyd_bind_any' + persistent: true + state: true + when: + - 'ansible_facts["selinux"]["status"] != "disabled"' + - name: 'setsebool -P systemd_socket_proxyd_connect_any on' ansible.posix.seboolean: name: 'systemd_socket_proxyd_connect_any' From fa0e98eb636b37ad7f7443772701b929ba8dd6fd Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Wed, 13 May 2026 17:23:07 +0200 Subject: [PATCH 09/66] refactor(roles/google_chrome): drop migration-specific handler logic Remove the comments and the chrome-headless-before-socket ordering that only existed to handle the cut-over from a pre-existing, non-socket- activated chrome service. With no such legacy unit in the wild, the regular notify chain (daemon-reload, restart socket, restart chrome on template change) is sufficient. --- roles/google_chrome/handlers/main.yml | 11 ++++------- roles/google_chrome/tasks/main.yml | 7 ++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/roles/google_chrome/handlers/main.yml b/roles/google_chrome/handlers/main.yml index 741892ce..84033af2 100644 --- a/roles/google_chrome/handlers/main.yml +++ b/roles/google_chrome/handlers/main.yml @@ -2,19 +2,16 @@ ansible.builtin.systemd: daemon_reload: true -# Restart chrome before the socket: in a migration from the old, non-socket-activated -# layout the running Chrome still binds the listen port. The socket can only bind -# after Chrome has been re-execed on the backend port. -- name: 'google_chrome: restart chrome-headless' +- name: 'google_chrome: restart chrome-headless-proxy.socket' ansible.builtin.service: - name: 'chrome-headless.service' + name: 'chrome-headless-proxy.socket' state: 'restarted' when: - 'google_chrome__service_state != "stopped"' -- name: 'google_chrome: restart chrome-headless-proxy.socket' +- name: 'google_chrome: restart chrome-headless' ansible.builtin.service: - name: 'chrome-headless-proxy.socket' + name: 'chrome-headless.service' state: 'restarted' when: - 'google_chrome__service_state != "stopped"' diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml index 2327269c..dbd123dc 100644 --- a/roles/google_chrome/tasks/main.yml +++ b/roles/google_chrome/tasks/main.yml @@ -138,11 +138,8 @@ - block: - # Force the handlers (daemon-reload, restart chrome on the new backend port, - # restart the socket) to run before the state block tries to enable the socket. - # Without this the socket would fail to load when migrating from the old - # non-socket-activated layout, because Chrome would still be holding the listen - # port at enable-time. + # Run daemon-reload before the state block, so systemctl enable/start operates + # on the freshly deployed unit definitions. - name: 'Flush handlers' ansible.builtin.meta: 'flush_handlers' From bd20c340e1fb3c4bc433f87aa99457a074108ff3 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Wed, 13 May 2026 17:26:20 +0200 Subject: [PATCH 10/66] feat(roles/repo_google_chrome): add meta/argument_specs.yml Declare the two user-facing variables (basic_auth_login as 'raw', mirror_url as 'str'), matching the pattern repo_remi established. Also sort entries in roles/google_chrome/{meta/argument_specs.yml, defaults/main.yml} alphabetically per CONTRIBUTING.md. --- roles/google_chrome/defaults/main.yml | 8 +++---- roles/google_chrome/meta/argument_specs.yml | 12 +++++----- .../meta/argument_specs.yml | 23 +++++++++++++++++++ 3 files changed, 33 insertions(+), 10 deletions(-) create mode 100644 roles/repo_google_chrome/meta/argument_specs.yml diff --git a/roles/google_chrome/defaults/main.yml b/roles/google_chrome/defaults/main.yml index 7cd77812..dbf1b84b 100644 --- a/roles/google_chrome/defaults/main.yml +++ b/roles/google_chrome/defaults/main.yml @@ -1,3 +1,7 @@ +# Chrome's own listening port. The proxy connects to it on demand; clients never +# talk to it directly. +google_chrome__backend_port: 9223 + # --- list of dicts injection pattern --- # Extra Chrome CLI flags appended to the chrome-headless systemd unit. google_chrome__extra_args__dependent_var: [] @@ -12,10 +16,6 @@ google_chrome__extra_args__combined_var: '{{ ( ) | linuxfabrik.lfops.combine_lod }}' -# Chrome's own listening port. The proxy connects to it on demand; clients never -# talk to it directly. -google_chrome__backend_port: 9223 - # Idle timeout for systemd-socket-proxyd in seconds. After this much time without # active connections, the proxy exits — and Chrome stops with it via BindsTo. google_chrome__idle_timeout: 300 diff --git a/roles/google_chrome/meta/argument_specs.yml b/roles/google_chrome/meta/argument_specs.yml index b5fd5ff4..f2c22db9 100644 --- a/roles/google_chrome/meta/argument_specs.yml +++ b/roles/google_chrome/meta/argument_specs.yml @@ -2,6 +2,12 @@ argument_specs: main: options: + google_chrome__backend_port: + type: 'int' + required: false + default: 9223 + description: 'Internal port Chrome itself listens on. The proxy forwards traffic from listen_port to this port.' + google_chrome__extra_args__dependent_var: type: 'list' elements: 'dict' @@ -23,12 +29,6 @@ argument_specs: default: [] description: 'Extra Google Chrome CLI flags. Host-level override.' - google_chrome__backend_port: - type: 'int' - required: false - default: 9223 - description: 'Internal port Chrome itself listens on. The proxy forwards traffic from listen_port to this port.' - google_chrome__idle_timeout: type: 'int' required: false diff --git a/roles/repo_google_chrome/meta/argument_specs.yml b/roles/repo_google_chrome/meta/argument_specs.yml new file mode 100644 index 00000000..2a67df11 --- /dev/null +++ b/roles/repo_google_chrome/meta/argument_specs.yml @@ -0,0 +1,23 @@ +argument_specs: + main: + options: + + repo_google_chrome__basic_auth_login: + # 'raw' rather than 'dict', because the default in defaults/main.yml + # resolves to '' (empty string) when lfops__repo_basic_auth_login is + # not set; a strict 'dict' spec would reject the empty default. + type: 'raw' + required: false + description: >- + HTTP basic auth credentials for the Google Chrome repository. + Expected as a dict with `username` and `password` keys. Typically + fed by `linuxfabrik.lfops.bitwarden_item`, which returns the full + Bitwarden item with additional keys. + + repo_google_chrome__mirror_url: + type: 'str' + required: false + description: >- + URL of a custom mirror server providing the repository. Defaults + to `lfops__repo_mirror_url`; if that is also unset, the default + upstream mirrors are used. From 95852fc06581c546a38393bd39ca410527f17a4f Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Fri, 15 May 2026 10:43:38 +0200 Subject: [PATCH 11/66] refactor(roles/google_chrome): tighten handler flow and tag boundaries - Split SELinux booleans into their own block, scoped to `google_chrome` only, so `google_chrome:configure` is limited to unit-file deployment as documented in the README. - Move daemon-reload from a handler into a regular task, gated by `is changed` on the three deploy tasks. The state block now runs with the freshly reloaded unit definitions without needing an intermediate `flush_handlers`, and the restart-socket handler can rely on `__google_chrome__service_state_result is not changed` (with an `is not defined` fallback for tag-restricted runs) to skip the redundant restart right after a fresh service start. - Drop the `restart chrome-headless` handler. Changes to the proxy or Chrome service unit only need a daemon-reload now; they take effect on the next socket-activation cycle. Only socket-template changes still trigger an immediate restart, because that unit holds the externally-visible listen port. - Fix descriptions for `google_chrome__service_enabled` and `google_chrome__service_state` in `meta/argument_specs.yml`: both manage the `chrome-headless-proxy.socket` unit, not `chrome-headless.service`. - Drop `mesa-libOSMesa-devel` from the runtime package list; the runtime library `mesa-libOSMesa` stays. --- roles/google_chrome/README.md | 4 +- roles/google_chrome/handlers/main.yml | 22 +++++----- roles/google_chrome/meta/argument_specs.yml | 4 +- roles/google_chrome/tasks/main.yml | 47 ++++++++++----------- roles/google_chrome/vars/RedHat.yml | 1 - 5 files changed, 38 insertions(+), 40 deletions(-) diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md index 29d510c7..b5077002 100644 --- a/roles/google_chrome/README.md +++ b/roles/google_chrome/README.md @@ -35,12 +35,12 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo * Sets the `systemd_socket_proxyd_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. * Deploys all three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). * Ensures the `chrome-headless-proxy.socket` is in the desired state. -* Triggers: daemon-reload, socket restart, Chrome service restart. +* Triggers: daemon-reload on any unit-file change; socket restart only on `chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. `google_chrome:configure` * Deploys the three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). -* Triggers: daemon-reload, socket restart, Chrome service restart. +* Triggers: daemon-reload on any unit-file change; socket restart only on `chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. `google_chrome:state` diff --git a/roles/google_chrome/handlers/main.yml b/roles/google_chrome/handlers/main.yml index 84033af2..f48793dd 100644 --- a/roles/google_chrome/handlers/main.yml +++ b/roles/google_chrome/handlers/main.yml @@ -1,17 +1,17 @@ -- name: 'google_chrome: systemctl daemon-reload' - ansible.builtin.systemd: - daemon_reload: true - +# Only socket-template changes trigger an immediate restart, because the socket unit +# is what binds the externally-visible listen_address:listen_port. Changes to the +# proxy or Chrome service templates only need daemon-reload: the running proxy and +# Chrome process keep going with the old settings until the next idle timeout, and +# the next socket-activation cycle re-spawns them with the updated unit files. +# +# `is not defined` covers the `--tags google_chrome:configure` run, where the state +# block is skipped and __google_chrome__service_state_result is never registered. +# `is not changed` covers the normal flow: skip the restart if the state task just +# (re-)started the socket. - name: 'google_chrome: restart chrome-headless-proxy.socket' ansible.builtin.service: name: 'chrome-headless-proxy.socket' state: 'restarted' when: - - 'google_chrome__service_state != "stopped"' - -- name: 'google_chrome: restart chrome-headless' - ansible.builtin.service: - name: 'chrome-headless.service' - state: 'restarted' - when: + - '__google_chrome__service_state_result is not defined or __google_chrome__service_state_result is not changed' - 'google_chrome__service_state != "stopped"' diff --git a/roles/google_chrome/meta/argument_specs.yml b/roles/google_chrome/meta/argument_specs.yml index f2c22db9..b4d2c15a 100644 --- a/roles/google_chrome/meta/argument_specs.yml +++ b/roles/google_chrome/meta/argument_specs.yml @@ -51,7 +51,7 @@ argument_specs: type: 'bool' required: false default: true - description: 'Enables or disables the chrome-headless.service.' + description: 'Enables or disables the chrome-headless-proxy.socket unit at boot. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' google_chrome__service_state: type: 'str' @@ -62,7 +62,7 @@ argument_specs: - 'restarted' - 'started' - 'stopped' - description: 'Desired state of the chrome-headless.service.' + description: 'Desired state of the chrome-headless-proxy.socket unit. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' google_chrome__user_data_dir: type: 'str' diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml index dbd123dc..2f75df89 100644 --- a/roles/google_chrome/tasks/main.yml +++ b/roles/google_chrome/tasks/main.yml @@ -73,16 +73,21 @@ name: 'systemd_socket_proxyd_bind_any' persistent: true state: true - when: - - 'ansible_facts["selinux"]["status"] != "disabled"' - name: 'setsebool -P systemd_socket_proxyd_connect_any on' ansible.posix.seboolean: name: 'systemd_socket_proxyd_connect_any' persistent: true state: true - when: - - 'ansible_facts["selinux"]["status"] != "disabled"' + + when: + - 'ansible_facts["selinux"]["status"] != "disabled"' + + tags: + - 'google_chrome' + + +- block: - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.socket' ansible.builtin.template: @@ -92,8 +97,8 @@ owner: 'root' group: 'root' mode: 0o644 + register: '__google_chrome__deploy_socket_result' notify: - - 'google_chrome: systemctl daemon-reload' - 'google_chrome: restart chrome-headless-proxy.socket' - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.service' @@ -104,9 +109,7 @@ owner: 'root' group: 'root' mode: 0o644 - notify: - - 'google_chrome: systemctl daemon-reload' - - 'google_chrome: restart chrome-headless-proxy.socket' + register: '__google_chrome__deploy_proxy_result' - name: 'Deploy /etc/systemd/system/chrome-headless.service' ansible.builtin.template: @@ -116,9 +119,18 @@ owner: 'root' group: 'root' mode: 0o644 - notify: - - 'google_chrome: systemctl daemon-reload' - - 'google_chrome: restart chrome-headless' + register: '__google_chrome__deploy_chrome_result' + + # Run daemon-reload as a regular task (not as a handler), so it runs before the + # state block below and so the restart-socket handler can rely on the registered + # __google_chrome__service_state_result to skip redundant restarts. + - name: 'systemctl daemon-reload' + ansible.builtin.systemd: + daemon_reload: true + when: + - '__google_chrome__deploy_socket_result is changed or + __google_chrome__deploy_proxy_result is changed or + __google_chrome__deploy_chrome_result is changed' - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' ansible.builtin.include_role: @@ -136,19 +148,6 @@ - 'google_chrome:configure' -- block: - - # Run daemon-reload before the state block, so systemctl enable/start operates - # on the freshly deployed unit definitions. - - name: 'Flush handlers' - ansible.builtin.meta: 'flush_handlers' - - tags: - - 'google_chrome' - - 'google_chrome:configure' - - 'google_chrome:state' - - - block: - name: 'systemctl {{ google_chrome__service_enabled | bool | ternary("enable", "disable") }} chrome-headless-proxy.socket' diff --git a/roles/google_chrome/vars/RedHat.yml b/roles/google_chrome/vars/RedHat.yml index 5f780776..f79f18d3 100644 --- a/roles/google_chrome/vars/RedHat.yml +++ b/roles/google_chrome/vars/RedHat.yml @@ -3,4 +3,3 @@ __google_chrome__packages: - 'gnu-free-sans-fonts' - 'google-chrome-stable' - 'mesa-libOSMesa' - - 'mesa-libOSMesa-devel' From f0332bc00dd77d876cf9460b4d100b890d518723 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 05:23:19 +0000 Subject: [PATCH 12/66] chore(deps): bump step-security/harden-runner from 2.19.1 to 2.19.3 (#251) Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.19.1 to 2.19.3. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/a5ad31d6a139d249332a2605b85202e8c0b78450...ab7a9404c0f3da075243ca237b5fac12c98deaa5) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-version: 2.19.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/docs.yml | 4 ++-- .github/workflows/lf-build.yml | 2 +- .github/workflows/lf-release.yml | 2 +- .github/workflows/pre-commit-autoupdate.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index be769730..72d19d1c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -31,7 +31,7 @@ jobs: steps: - name: 'Harden Runner' - uses: 'step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450' # v2.19.1 + uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 with: egress-policy: 'audit' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index c5de829f..40c180da 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -11,7 +11,7 @@ jobs: runs-on: 'ubuntu-latest' steps: - name: 'Harden Runner' - uses: 'step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450' # v2.19.1 + uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 with: egress-policy: 'audit' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6ca896c6..b9d69c9f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: runs-on: 'ubuntu-latest' steps: - name: 'Harden the runner (Audit all outbound calls)' - uses: 'step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450' # v2.19.1 + uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 with: egress-policy: 'audit' @@ -53,7 +53,7 @@ jobs: url: '${{ steps.deployment.outputs.page_url }}' steps: - name: 'Harden the runner (Audit all outbound calls)' - uses: 'step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450' # v2.19.1 + uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 with: egress-policy: 'audit' diff --git a/.github/workflows/lf-build.yml b/.github/workflows/lf-build.yml index 0ffc11ab..88d9e549 100644 --- a/.github/workflows/lf-build.yml +++ b/.github/workflows/lf-build.yml @@ -28,7 +28,7 @@ jobs: steps: - name: 'Harden the runner (Audit all outbound calls)' - uses: 'step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450' # v2.19.1 + uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 with: egress-policy: 'audit' diff --git a/.github/workflows/lf-release.yml b/.github/workflows/lf-release.yml index 3b0eaf66..92872f67 100644 --- a/.github/workflows/lf-release.yml +++ b/.github/workflows/lf-release.yml @@ -17,7 +17,7 @@ jobs: steps: - name: 'Harden the runner (Audit all outbound calls)' - uses: 'step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450' # v2.19.1 + uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 with: egress-policy: 'audit' diff --git a/.github/workflows/pre-commit-autoupdate.yml b/.github/workflows/pre-commit-autoupdate.yml index 82c7c72a..92fa589e 100644 --- a/.github/workflows/pre-commit-autoupdate.yml +++ b/.github/workflows/pre-commit-autoupdate.yml @@ -15,7 +15,7 @@ jobs: pull-requests: 'write' steps: - name: 'Harden the runner (Audit all outbound calls)' - uses: 'step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450' # v2.19.1 + uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 with: egress-policy: 'audit' diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3f49570c..8e1a9a09 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -18,7 +18,7 @@ jobs: steps: - name: 'Harden Runner' - uses: 'step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450' # v2.19.1 + uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 with: egress-policy: 'audit' From a75706e1a144c36ce5405c944dccb6ea4ac85a7d Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Wed, 20 May 2026 10:54:45 +0200 Subject: [PATCH 13/66] refactor(roles/google_chrome): rename systemd units and wire CRB repo --- playbooks/README.md | 2 + playbooks/google_chrome.yml | 12 +++ playbooks/icingaweb2_module_pdfexport.yml | 12 +++ roles/google_chrome/README.md | 37 +++++----- roles/google_chrome/defaults/main.yml | 25 ++----- roles/google_chrome/handlers/main.yml | 4 +- roles/google_chrome/meta/argument_specs.yml | 6 +- roles/google_chrome/tasks/main.yml | 74 +++++++------------ ...> google-chrome-headless-proxy.service.j2} | 6 +- ...=> google-chrome-headless-proxy.socket.j2} | 2 +- ...e.j2 => google-chrome-headless.service.j2} | 4 +- .../defaults/main.yml | 5 -- 12 files changed, 88 insertions(+), 101 deletions(-) rename roles/google_chrome/templates/etc/systemd/system/{chrome-headless-proxy.service.j2 => google-chrome-headless-proxy.service.j2} (77%) rename roles/google_chrome/templates/etc/systemd/system/{chrome-headless-proxy.socket.j2 => google-chrome-headless-proxy.socket.j2} (94%) rename roles/google_chrome/templates/etc/systemd/system/{chrome-headless.service.j2 => google-chrome-headless.service.j2} (96%) diff --git a/playbooks/README.md b/playbooks/README.md index 6ff59812..0841b4be 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -342,6 +342,7 @@ Calls the following roles (in order): Calls the following roles (in order): +* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `google_chrome__skip_repo_baseos` * [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `google_chrome__skip_repo_epel` * [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `google_chrome__skip_repo_google_chrome` * [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) @@ -454,6 +455,7 @@ Calls the following roles (in order): Calls the following roles (in order): +* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `icingaweb2_module_pdfexport__skip_repo_baseos` * [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `icingaweb2_module_pdfexport__skip_repo_epel` * [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `icingaweb2_module_pdfexport__skip_repo_google_chrome` * [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome): `icingaweb2_module_pdfexport__skip_google_chrome` diff --git a/playbooks/google_chrome.yml b/playbooks/google_chrome.yml index f959bf4c..c029f420 100644 --- a/playbooks/google_chrome.yml +++ b/playbooks/google_chrome.yml @@ -12,6 +12,18 @@ roles: + # The google_chrome role needs mesa-libOSMesa, which lives in CRB. On EL9 the CRB + # repo is enabled via repo_baseos (Rocky only, like the other playbooks); on EL8 the + # equivalent (PowerTools) ships with the repo_epel repo file. + - role: 'linuxfabrik.lfops.repo_baseos' + repo_baseos__crb_repo_enabled__dependent_var: '{{ + repo_epel__repo_baseos__crb_repo_enabled__dependent_var + }}' + when: + - 'ansible_facts["distribution"] == "Rocky"' + - 'ansible_facts["distribution_major_version"] in ["9", "10"]' + - 'not google_chrome__skip_repo_baseos | d(false) | bool' + - role: 'linuxfabrik.lfops.repo_epel' when: - 'not google_chrome__skip_repo_epel | d(false) | bool' diff --git a/playbooks/icingaweb2_module_pdfexport.yml b/playbooks/icingaweb2_module_pdfexport.yml index ef912403..fea35a6a 100644 --- a/playbooks/icingaweb2_module_pdfexport.yml +++ b/playbooks/icingaweb2_module_pdfexport.yml @@ -12,6 +12,18 @@ roles: + # The google_chrome role needs mesa-libOSMesa, which lives in CRB. On EL9 the CRB + # repo is enabled via repo_baseos (Rocky only, like the other playbooks); on EL8 the + # equivalent (PowerTools) ships with the repo_epel repo file. + - role: 'linuxfabrik.lfops.repo_baseos' + repo_baseos__crb_repo_enabled__dependent_var: '{{ + repo_epel__repo_baseos__crb_repo_enabled__dependent_var + }}' + when: + - 'ansible_facts["distribution"] == "Rocky"' + - 'ansible_facts["distribution_major_version"] in ["9", "10"]' + - 'not icingaweb2_module_pdfexport__skip_repo_baseos | d(false) | bool' + - role: 'linuxfabrik.lfops.repo_epel' when: - 'not icingaweb2_module_pdfexport__skip_repo_epel | d(false) | bool' diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md index b5077002..286dc6ec 100644 --- a/roles/google_chrome/README.md +++ b/roles/google_chrome/README.md @@ -1,6 +1,6 @@ # Ansible Role linuxfabrik.lfops.google_chrome -This role installs [Google Chrome](https://www.google.com/chrome/) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated `chrome-headless` systemd service. Clients connect to a configurable TCP socket; Chrome is started on the first request via `systemd-socket-proxyd` and stopped again after a configurable idle timeout, so no RAM is wasted while the backend is unused. +This role installs [Google Chrome](https://www.google.com/chrome/) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated `google-chrome-headless` systemd service. Clients connect to a configurable TCP socket; Chrome is started on the first request via `systemd-socket-proxyd` and stopped again after a configurable idle timeout, so no RAM is wasted while the backend is unused. The setup is used as a headless browser backend for tools such as the [Icinga Web 2 PDF Export Module](https://github.com/Icinga/icingaweb2-module-pdfexport). @@ -11,15 +11,16 @@ The setup is used as a headless browser backend for tools such as the [Icinga We ## How the Role Behaves * Three systemd units are deployed: - * `chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). - * `chrome-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. - * `chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly — the proxy triggers it via `Requires=`. -* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `chrome-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chrome's non-standard backend port. -* The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `chrome-headless-proxy.socket` unit, not the Chrome service directly. + * `google-chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). + * `google-chrome-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. + * `google-chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly. The proxy triggers it via `Requires=`. +* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `google-chrome-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chrome's non-standard backend port. +* The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `google-chrome-headless-proxy.socket` unit, not the Chrome service directly. ## Mandatory Requirements +* Enable the CRB repository (PowerTools on EL8), which provides `mesa-libOSMesa`. On EL9 this can be done using the [linuxfabrik.lfops.repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos) role; on EL8 it ships with the EPEL repository file. * Enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. * Enable the Google Chrome repository. This can be done using the [linuxfabrik.lfops.repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome) role. @@ -33,18 +34,18 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo * Creates the `chrome` system user and group. * Installs Google Chrome along with the required runtime libraries and fonts. * Sets the `systemd_socket_proxyd_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. -* Deploys all three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). -* Ensures the `chrome-headless-proxy.socket` is in the desired state. -* Triggers: daemon-reload on any unit-file change; socket restart only on `chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. +* Deploys all three systemd units (`google-chrome-headless-proxy.socket`, `google-chrome-headless-proxy.service`, `google-chrome-headless.service`). +* Ensures the `google-chrome-headless-proxy.socket` is in the desired state. +* Triggers: daemon-reload on any unit-file change; socket restart only on `google-chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. `google_chrome:configure` -* Deploys the three systemd units (`chrome-headless-proxy.socket`, `chrome-headless-proxy.service`, `chrome-headless.service`). -* Triggers: daemon-reload on any unit-file change; socket restart only on `chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. +* Deploys the three systemd units (`google-chrome-headless-proxy.socket`, `google-chrome-headless-proxy.service`, `google-chrome-headless.service`). +* Triggers: daemon-reload on any unit-file change; socket restart only on `google-chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. `google_chrome:state` -* Manages the `chrome-headless-proxy.socket` state (start, stop, enable, disable). +* Manages the `google-chrome-headless-proxy.socket` state (start, stop, enable, disable). * Triggers: none. @@ -58,7 +59,7 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo `google_chrome__extra_args__host_var` / `google_chrome__extra_args__group_var` -* Additional Chrome CLI flags appended to the `ExecStart` line of `chrome-headless.service`, in the order listed. Useful for tuning behavior without overwriting the whole unit. +* Additional Chrome CLI flags appended to the `ExecStart` line of `google-chrome-headless.service`, in the order listed. Useful for tuning behavior without overwriting the whole unit. * Type: List of dictionaries. * Default: `[]` * Subkeys: @@ -76,7 +77,7 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo `google_chrome__idle_timeout` -* Seconds the `systemd-socket-proxyd` waits without active connections before exiting. When it exits, the bound `chrome-headless.service` stops automatically. The next inbound connection re-activates the whole chain, paying ~1–2 seconds of cold-start latency. +* Seconds the `systemd-socket-proxyd` waits without active connections before exiting. When it exits, the bound `google-chrome-headless.service` stops automatically. The next inbound connection re-activates the whole chain, paying ~1-2 seconds of cold-start latency. * Type: Number. * Default: `300` @@ -94,13 +95,13 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo `google_chrome__service_enabled` -* Enables or disables the `chrome-headless-proxy.socket` at boot, analogous to `systemctl enable/disable --now`. +* Enables or disables the `google-chrome-headless-proxy.socket` at boot, analogous to `systemctl enable/disable --now`. * Type: Bool. * Default: `true` `google_chrome__service_state` -* Changes the state of the `chrome-headless-proxy.socket`, analogous to `systemctl start/stop/restart/reload`. +* Changes the state of the `google-chrome-headless-proxy.socket`, analogous to `systemctl start/stop/restart/reload`. * Type: String. One of `reloaded`, `restarted`, `started`, `stopped`. * Default: `'started'` @@ -108,7 +109,7 @@ If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blo * Home directory of the `chrome` system user and Chrome user data directory. Used both as the user's `home`, as the `--user-data-dir` value for Chrome, and as the writable path exposed via systemd `ReadWritePaths=`. * Type: String. -* Default: `'/var/lib/chrome-headless'` +* Default: `'/var/lib/google-chrome-headless'` Example: ```yaml @@ -122,7 +123,7 @@ google_chrome__listen_address: '127.0.0.1' google_chrome__listen_port: 9222 google_chrome__service_enabled: true google_chrome__service_state: 'started' -google_chrome__user_data_dir: '/var/lib/chrome-headless' +google_chrome__user_data_dir: '/var/lib/google-chrome-headless' ``` diff --git a/roles/google_chrome/defaults/main.yml b/roles/google_chrome/defaults/main.yml index dbf1b84b..f8c539e7 100644 --- a/roles/google_chrome/defaults/main.yml +++ b/roles/google_chrome/defaults/main.yml @@ -1,13 +1,4 @@ -# Chrome's own listening port. The proxy connects to it on demand; clients never -# talk to it directly. google_chrome__backend_port: 9223 - -# --- list of dicts injection pattern --- -# Extra Chrome CLI flags appended to the chrome-headless systemd unit. -google_chrome__extra_args__dependent_var: [] -google_chrome__extra_args__group_var: [] -google_chrome__extra_args__host_var: [] -google_chrome__extra_args__role_var: [] google_chrome__extra_args__combined_var: '{{ ( google_chrome__extra_args__role_var + google_chrome__extra_args__dependent_var + @@ -15,19 +6,13 @@ google_chrome__extra_args__combined_var: '{{ ( google_chrome__extra_args__host_var ) | linuxfabrik.lfops.combine_lod }}' - -# Idle timeout for systemd-socket-proxyd in seconds. After this much time without -# active connections, the proxy exits — and Chrome stops with it via BindsTo. +google_chrome__extra_args__dependent_var: [] +google_chrome__extra_args__group_var: [] +google_chrome__extra_args__host_var: [] +google_chrome__extra_args__role_var: [] google_chrome__idle_timeout: 300 - -# External listening endpoint exposed by the chrome-headless-proxy.socket unit. -# This is what clients (Apache, the pdfexport module, ...) connect to. google_chrome__listen_address: '127.0.0.1' google_chrome__listen_port: 9222 - -# Lifecycle of the chrome-headless-proxy.socket unit. The Chrome service itself -# is triggered on demand by the proxy and is not managed directly. google_chrome__service_enabled: true google_chrome__service_state: 'started' - -google_chrome__user_data_dir: '/var/lib/chrome-headless' +google_chrome__user_data_dir: '/var/lib/google-chrome-headless' diff --git a/roles/google_chrome/handlers/main.yml b/roles/google_chrome/handlers/main.yml index f48793dd..11272b6b 100644 --- a/roles/google_chrome/handlers/main.yml +++ b/roles/google_chrome/handlers/main.yml @@ -8,9 +8,9 @@ # block is skipped and __google_chrome__service_state_result is never registered. # `is not changed` covers the normal flow: skip the restart if the state task just # (re-)started the socket. -- name: 'google_chrome: restart chrome-headless-proxy.socket' +- name: 'google_chrome: restart google-chrome-headless-proxy.socket' ansible.builtin.service: - name: 'chrome-headless-proxy.socket' + name: 'google-chrome-headless-proxy.socket' state: 'restarted' when: - '__google_chrome__service_state_result is not defined or __google_chrome__service_state_result is not changed' diff --git a/roles/google_chrome/meta/argument_specs.yml b/roles/google_chrome/meta/argument_specs.yml index b4d2c15a..cccab91a 100644 --- a/roles/google_chrome/meta/argument_specs.yml +++ b/roles/google_chrome/meta/argument_specs.yml @@ -51,7 +51,7 @@ argument_specs: type: 'bool' required: false default: true - description: 'Enables or disables the chrome-headless-proxy.socket unit at boot. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' + description: 'Enables or disables the google-chrome-headless-proxy.socket unit at boot. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' google_chrome__service_state: type: 'str' @@ -62,10 +62,10 @@ argument_specs: - 'restarted' - 'started' - 'stopped' - description: 'Desired state of the chrome-headless-proxy.socket unit. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' + description: 'Desired state of the google-chrome-headless-proxy.socket unit. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' google_chrome__user_data_dir: type: 'str' required: false - default: '/var/lib/chrome-headless' + default: '/var/lib/google-chrome-headless' description: 'Home directory of the chrome system user and Chrome user data directory.' diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml index 2f75df89..21373f9d 100644 --- a/roles/google_chrome/tasks/main.yml +++ b/roles/google_chrome/tasks/main.yml @@ -9,26 +9,6 @@ - 'always' -- name: 'Perform platform/version specific tasks' - ansible.builtin.include_tasks: '{{ __task_file }}' - when: '__task_file | length > 0' - vars: - __task_file: '{{ lookup("ansible.builtin.first_found", __first_found_options) }}' - __first_found_options: - files: - - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_version"] }}.yml' - - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_major_version"] }}.yml' - - '{{ ansible_facts["distribution"] }}.yml' - - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_version"] }}.yml' - - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_major_version"] }}.yml' - - '{{ ansible_facts["os_family"] }}.yml' - paths: - - '{{ role_path }}/tasks' - skip: true - tags: - - 'always' - - - block: - name: 'groupadd --system chrome' @@ -64,7 +44,7 @@ - 'google_chrome' -# Chrome itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chrome: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chrome opens itself). --exit-idle-time on the proxy plus BindsTo= on chrome-headless.service ties Chrome's lifecycle to the proxy, so Chrome stops together with the proxy after the idle timeout. +# Chrome itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chrome: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chrome opens itself). --exit-idle-time on the proxy plus BindsTo= on google-chrome-headless.service ties Chrome's lifecycle to the proxy, so Chrome stops together with the proxy after the idle timeout. # Two SELinux booleans are required: bind_any so the .socket can bind the listen port even when it has an unexpected port type (e.g. on Rocky 9, port 9222 maps to hplip_port_t and would otherwise reject the bind); connect_any so the proxy can connect to Chrome's non-standard backend port, which has no matching SELinux port type. - block: @@ -80,47 +60,58 @@ persistent: true state: true + # block when: - 'ansible_facts["selinux"]["status"] != "disabled"' - tags: - 'google_chrome' - block: - - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.socket' + - name: 'Deploy /etc/systemd/system/google-chrome-headless-proxy.socket' ansible.builtin.template: backup: true - src: 'etc/systemd/system/chrome-headless-proxy.socket.j2' - dest: '/etc/systemd/system/chrome-headless-proxy.socket' + src: 'etc/systemd/system/google-chrome-headless-proxy.socket.j2' + dest: '/etc/systemd/system/google-chrome-headless-proxy.socket' owner: 'root' group: 'root' mode: 0o644 register: '__google_chrome__deploy_socket_result' notify: - - 'google_chrome: restart chrome-headless-proxy.socket' + - 'google_chrome: restart google-chrome-headless-proxy.socket' - - name: 'Deploy /etc/systemd/system/chrome-headless-proxy.service' + - name: 'Deploy /etc/systemd/system/google-chrome-headless-proxy.service' ansible.builtin.template: backup: true - src: 'etc/systemd/system/chrome-headless-proxy.service.j2' - dest: '/etc/systemd/system/chrome-headless-proxy.service' + src: 'etc/systemd/system/google-chrome-headless-proxy.service.j2' + dest: '/etc/systemd/system/google-chrome-headless-proxy.service' owner: 'root' group: 'root' mode: 0o644 register: '__google_chrome__deploy_proxy_result' - - name: 'Deploy /etc/systemd/system/chrome-headless.service' + - name: 'Deploy /etc/systemd/system/google-chrome-headless.service' ansible.builtin.template: backup: true - src: 'etc/systemd/system/chrome-headless.service.j2' - dest: '/etc/systemd/system/chrome-headless.service' + src: 'etc/systemd/system/google-chrome-headless.service.j2' + dest: '/etc/systemd/system/google-chrome-headless.service' owner: 'root' group: 'root' mode: 0o644 register: '__google_chrome__deploy_chrome_result' + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' + ansible.builtin.include_role: + name: 'shared' + tasks_from: 'remove-rpmnew-rpmsave.yml' + vars: + shared__remove_rpmnew_rpmsave_config_file: '{{ item }}' + loop: + - '/etc/systemd/system/google-chrome-headless-proxy.socket' + - '/etc/systemd/system/google-chrome-headless-proxy.service' + - '/etc/systemd/system/google-chrome-headless.service' + # Run daemon-reload as a regular task (not as a handler), so it runs before the # state block below and so the restart-socket handler can rely on the registered # __google_chrome__service_state_result to skip redundant restarts. @@ -132,17 +123,6 @@ __google_chrome__deploy_proxy_result is changed or __google_chrome__deploy_chrome_result is changed' - - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' - ansible.builtin.include_role: - name: 'shared' - tasks_from: 'remove-rpmnew-rpmsave.yml' - vars: - shared__remove_rpmnew_rpmsave_config_file: '{{ item }}' - loop: - - '/etc/systemd/system/chrome-headless-proxy.socket' - - '/etc/systemd/system/chrome-headless-proxy.service' - - '/etc/systemd/system/chrome-headless.service' - tags: - 'google_chrome' - 'google_chrome:configure' @@ -150,14 +130,14 @@ - block: - - name: 'systemctl {{ google_chrome__service_enabled | bool | ternary("enable", "disable") }} chrome-headless-proxy.socket' + - name: 'systemctl {{ google_chrome__service_enabled | bool | ternary("enable", "disable") }} google-chrome-headless-proxy.socket' ansible.builtin.service: - name: 'chrome-headless-proxy.socket' + name: 'google-chrome-headless-proxy.socket' enabled: '{{ google_chrome__service_enabled | bool }}' - - name: 'systemctl {{ google_chrome__service_state }} chrome-headless-proxy.socket' + - name: 'systemctl {{ google_chrome__service_state }} google-chrome-headless-proxy.socket' ansible.builtin.service: - name: 'chrome-headless-proxy.socket' + name: 'google-chrome-headless-proxy.socket' state: '{{ google_chrome__service_state }}' register: '__google_chrome__service_state_result' diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 similarity index 77% rename from roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 rename to roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 index 35e1fe33..a51e3b47 100644 --- a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.service.j2 +++ b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 @@ -1,10 +1,10 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052001 [Unit] Description=Proxy to on-demand Headless Google Chrome -Requires=chrome-headless.service -After=chrome-headless.service +Requires=google-chrome-headless.service +After=google-chrome-headless.service [Service] ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ google_chrome__idle_timeout }} {{ google_chrome__listen_address }}:{{ google_chrome__backend_port }} diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 similarity index 94% rename from roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 rename to roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 index 4bbbb54a..eb7e5c64 100644 --- a/roles/google_chrome/templates/etc/systemd/system/chrome-headless-proxy.socket.j2 +++ b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052001 [Unit] Description=Socket for on-demand Headless Google Chrome diff --git a/roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 similarity index 96% rename from roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 rename to roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 index 73062de4..cfff2f55 100644 --- a/roles/google_chrome/templates/etc/systemd/system/chrome-headless.service.j2 +++ b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 @@ -1,9 +1,9 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052001 [Unit] Description=Headless Google Chrome -BindsTo=chrome-headless-proxy.service +BindsTo=google-chrome-headless-proxy.service [Service] Type=simple diff --git a/roles/icingaweb2_module_pdfexport/defaults/main.yml b/roles/icingaweb2_module_pdfexport/defaults/main.yml index 0995bfed..1bd3a3ef 100644 --- a/roles/icingaweb2_module_pdfexport/defaults/main.yml +++ b/roles/icingaweb2_module_pdfexport/defaults/main.yml @@ -1,8 +1,3 @@ -# If `icingaweb2_module_pdfexport__chrome_binary` is set, the module spawns chrome -# locally on every PDF export. Otherwise it talks to a running headless Chrome via -# the Chrome DevTools Protocol on `chrome_host` / `chrome_port` (default mode). -# Defaults pull from the linuxfabrik.lfops.google_chrome role so a single change -# there propagates here. icingaweb2_module_pdfexport__chrome_binary: '' icingaweb2_module_pdfexport__chrome_host: '{{ google_chrome__listen_address | d("127.0.0.1") }}' icingaweb2_module_pdfexport__chrome_port: '{{ google_chrome__listen_port | d(9222) }}' From 8f25b4b235762a24b0775d7a8ddab4381bc57afc Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 16:26:33 +0200 Subject: [PATCH 14/66] refactor(roles/chromium_headless): replace google_chrome with EPEL chromium-headless --- CHANGELOG.md | 5 +- COMPATIBILITY.md | 3 +- playbooks/README.md | 32 ++-- playbooks/all.yml | 3 +- ...oogle_chrome.yml => chromium_headless.yml} | 10 +- playbooks/google_chrome.yml | 43 ----- playbooks/icingaweb2_module_pdfexport.yml | 20 +-- playbooks/setup_icinga2_master.yml | 11 +- roles/chromium_headless/README.md | 135 +++++++++++++++ roles/chromium_headless/defaults/main.yml | 18 ++ roles/chromium_headless/handlers/main.yml | 17 ++ .../chromium_headless/meta/argument_specs.yml | 71 ++++++++ roles/chromium_headless/tasks/main.yml | 157 ++++++++++++++++++ .../system/chromium-headless-proxy.service.j2 | 18 ++ .../system/chromium-headless-proxy.socket.j2 | 11 ++ .../system/chromium-headless.service.j2 | 57 +++++++ roles/chromium_headless/vars/RedHat.yml | 4 + roles/google_chrome/README.md | 137 --------------- roles/google_chrome/defaults/main.yml | 18 -- roles/google_chrome/handlers/main.yml | 17 -- roles/google_chrome/meta/argument_specs.yml | 71 -------- roles/google_chrome/tasks/main.yml | 157 ------------------ .../google-chrome-headless-proxy.service.j2 | 13 -- .../google-chrome-headless-proxy.socket.j2 | 11 -- .../system/google-chrome-headless.service.j2 | 54 ------ roles/google_chrome/vars/RedHat.yml | 5 - roles/icingaweb2_module_pdfexport/README.md | 18 +- .../defaults/main.yml | 4 +- .../meta/argument_specs.yml | 6 +- roles/repo_google_chrome/README.md | 48 ------ roles/repo_google_chrome/defaults/main.yml | 2 - .../meta/argument_specs.yml | 23 --- roles/repo_google_chrome/tasks/RedHat.yml | 40 ----- roles/repo_google_chrome/tasks/main.yml | 18 -- .../etc/yum.repos.d/google-chrome.repo.j2 | 20 --- 35 files changed, 528 insertions(+), 749 deletions(-) rename playbooks/{repo_google_chrome.yml => chromium_headless.yml} (53%) delete mode 100644 playbooks/google_chrome.yml create mode 100644 roles/chromium_headless/README.md create mode 100644 roles/chromium_headless/defaults/main.yml create mode 100644 roles/chromium_headless/handlers/main.yml create mode 100644 roles/chromium_headless/meta/argument_specs.yml create mode 100644 roles/chromium_headless/tasks/main.yml create mode 100644 roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 create mode 100644 roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.socket.j2 create mode 100644 roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 create mode 100644 roles/chromium_headless/vars/RedHat.yml delete mode 100644 roles/google_chrome/README.md delete mode 100644 roles/google_chrome/defaults/main.yml delete mode 100644 roles/google_chrome/handlers/main.yml delete mode 100644 roles/google_chrome/meta/argument_specs.yml delete mode 100644 roles/google_chrome/tasks/main.yml delete mode 100644 roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 delete mode 100644 roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 delete mode 100644 roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 delete mode 100644 roles/google_chrome/vars/RedHat.yml delete mode 100644 roles/repo_google_chrome/README.md delete mode 100644 roles/repo_google_chrome/defaults/main.yml delete mode 100644 roles/repo_google_chrome/meta/argument_specs.yml delete mode 100644 roles/repo_google_chrome/tasks/RedHat.yml delete mode 100644 roles/repo_google_chrome/tasks/main.yml delete mode 100644 roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eba9405..553fb093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,10 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:chromium_headless**: New role. Installs the headless Chromium shell (`chromium-headless` from EPEL) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chromium-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chromium service, wired with `BindsTo`). Chromium is started on the first incoming connection and stopped again after `chromium_headless__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips two SELinux booleans on enforcing hosts: `systemd_socket_proxyd_bind_any` so the socket unit can bind the listen port (on Rocky/RHEL 9 the default `9222` carries the `hplip_port_t` label, which would otherwise reject the bind), and `systemd_socket_proxyd_connect_any` so the proxy can reach Chromium on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to, without pulling in Google's proprietary repository. * **role:sshd**: Add Debian 13 support. * **role:mirror**: Document the new per-repository `newest_only` subkey on `mirror__reposync_repos` entries. Defaults to `true` (only the newest version of each package is mirrored). Set to `false` for repositories that publish multiple versions in parallel, such as Icinga, where older versions must remain available. -* **role:google_chrome**: New role. Installs Google Chrome together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chrome-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chrome service, wired with `BindsTo`). Chrome is started on the first incoming connection and stopped again after `google_chrome__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips two SELinux booleans on enforcing hosts: `systemd_socket_proxyd_bind_any` so the socket unit can bind the listen port (on Rocky/RHEL 9 the default `9222` carries the `hplip_port_t` label, which would otherwise reject the bind), and `systemd_socket_proxyd_connect_any` so the proxy can reach Chrome on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to. -* **role:repo_google_chrome**: New role. Deploys the Google Chrome package repository for RHEL-based distributions, with the same `lfops__repo_mirror_url` / `lfops__repo_basic_auth_login` knobs as the other `repo_*` roles. * **role:repo_remi**: Add RHEL 10 / Rocky 10 support (new GPG key, repo templates, and module-stream tasks for EL 10). * **role:repo_remi**: Add `meta/argument_specs.yml` declaring the four user-facing variables (`repo_remi__basic_auth_login`, `repo_remi__enabled_php_version`, `repo_remi__enabled_redis_version`, `repo_remi__mirror_url`) so role-entry validation catches type mismatches and unknown variables. `repo_remi__basic_auth_login` is declared as `type: 'raw'` because its default in `defaults/main.yml` resolves to an empty string when no Bitwarden lookup is configured. * **role:monitoring_plugins, role:repo_monitoring_plugins**: Add SLES 15 and SLES 16 support. The roles now install the Linuxfabrik Monitoring Plugins from the SUSE channel of `repo.linuxfabrik.ch` and apply the SUSE-specific package version lock ([#245](https://github.com/Linuxfabrik/lfops/issues/245)). @@ -65,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -* **role:icingaweb2_module_pdfexport, playbooks/icingaweb2_module_pdfexport, playbooks/setup_icinga2_master**: The headless browser backend the module requires was not installed by any role and had to be configured manually, so fresh deployments ended up without working PDF export. The new `google_chrome` and `repo_google_chrome` roles now provide a hardened `chrome-headless.service`, and both `icingaweb2_module_pdfexport.yml` and `setup_icinga2_master.yml` wire them up with `*__skip_*` opt-out variables (in `setup_icinga2_master.yml` the defaults track the existing `icingaweb2_module_pdfexport__skip_role` flag). The role also gained `/etc/icingaweb2/modules/pdfexport/config.ini` deployment with four new variables (`icingaweb2_module_pdfexport__chrome_host`, `__chrome_port`, `__chrome_binary`, `__force_temp_storage`); by default it talks to the `chrome-headless.service` over the Chrome DevTools Protocol, falling back to a local Chrome binary only if `chrome_binary` is set explicitly. +* **role:icingaweb2_module_pdfexport, playbooks/icingaweb2_module_pdfexport, playbooks/setup_icinga2_master**: The headless browser backend the module requires was not installed by any role and had to be configured manually, so fresh deployments ended up without working PDF export. The new `chromium_headless` role now provides a hardened `chromium-headless.service`, and both `icingaweb2_module_pdfexport.yml` and `setup_icinga2_master.yml` wire it up with `*__skip_*` opt-out variables (in `setup_icinga2_master.yml` the defaults track the existing `icingaweb2_module_pdfexport__skip_role` flag). The role also gained `/etc/icingaweb2/modules/pdfexport/config.ini` deployment with four new variables (`icingaweb2_module_pdfexport__chrome_host`, `__chrome_port`, `__chrome_binary`, `__force_temp_storage`); by default it talks to the `chromium-headless.service` over the Chrome DevTools Protocol, falling back to a local Chromium binary only if `chrome_binary` is set explicitly. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. * **roles**: Set `become: false` on tasks delegated to localhost across the collection. Previously these tasks inherited `become: true` from the playbook level and tried to call `sudo` on the Ansible controller, which fails on controllers without a passwordless sudo setup with `sudo: a password is required`. Affected are all `repo_*` roles, the `*_vm` cloud roles (`exoscale_vm`, `hetzner_vm`, `infomaniak_vm`), all `icingaweb2_module_*` roles that download artefacts, `monitoring_plugins`, `shared`, plus several others. Existing playbooks that were working without playbook-level `become: true` are unaffected ([#242](https://github.com/Linuxfabrik/lfops/issues/242)). diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index c25a6004..99413fbd 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -16,6 +16,7 @@ Which Ansible role is proven to run on which OS? | bind | | | x | x | x | | | | | | blocky | | | x | x | (x) | | | | | | borg_local | | | x | (x) | (x) | | | | | +| chromium_headless | | | x | x | (x) | | | | | | chrony | | | x | x | x | | | | | | clamav | | | x | x | (x) | | | | | | cloud_init | (x) | (x) | x | x | x | (x) | (x) | (x) | | @@ -42,7 +43,6 @@ Which Ansible role is proven to run on which OS? | gitlab_ce | | | x | (x) | (x) | | | | | | glances | (x) | (x) | x | x | (x) | (x) | (x) | (x) | | | glpi_agent | | | x | x | (x) | | | | | -| google_chrome | | | x | x | (x) | | | | | | grafana | | | x | x | x | | | | | | grafana_grizzly | (x) | (x) | x | x | (x) | (x) | (x) | (x) | | | grav | | | x | (x) | (x) | | | | | @@ -129,7 +129,6 @@ Which Ansible role is proven to run on which OS? | repo_epel | | | x | x | x | | | | | | repo_gitlab_ce | | | x | (x) | (x) | | | | | | repo_gitlab_runner | | | x | (x) | (x) | | | | | -| repo_google_chrome | | | x | x | (x) | | | | | | repo_grafana | x | x | x | x | (x) | (x) | (x) | (x) | | | repo_graylog | x | x | x | (x) | (x) | (x) | (x) | (x) | | | repo_icinga | x | x | x | x | x | x | (x) | (x) | | diff --git a/playbooks/README.md b/playbooks/README.md index 0841b4be..810a660f 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -109,6 +109,14 @@ Calls the following roles (in order): * [borg_local](https://github.com/Linuxfabrik/lfops/tree/main/roles/borg_local) +## chromium_headless.yml + +Calls the following roles (in order): + +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `chromium_headless__skip_repo_epel` +* [chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless) + + ## chrony.yml Calls the following roles (in order): @@ -338,16 +346,6 @@ Calls the following roles (in order): * [glpi_agent](https://github.com/Linuxfabrik/lfops/tree/main/roles/glpi_agent) -## google_chrome.yml - -Calls the following roles (in order): - -* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `google_chrome__skip_repo_baseos` -* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `google_chrome__skip_repo_epel` -* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `google_chrome__skip_repo_google_chrome` -* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) - - ## grafana.yml Calls the following roles (in order): @@ -455,10 +453,8 @@ Calls the following roles (in order): Calls the following roles (in order): -* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `icingaweb2_module_pdfexport__skip_repo_baseos` * [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `icingaweb2_module_pdfexport__skip_repo_epel` -* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `icingaweb2_module_pdfexport__skip_repo_google_chrome` -* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome): `icingaweb2_module_pdfexport__skip_google_chrome` +* [chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless): `icingaweb2_module_pdfexport__skip_chromium_headless` * [icingaweb2_module_pdfexport](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_pdfexport) @@ -854,13 +850,6 @@ Calls the following roles (in order): * [repo_gitlab_runner](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_gitlab_runner) -## repo_google_chrome.yml - -Calls the following roles (in order): - -* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome) - - ## repo_grafana.yml Calls the following roles (in order): @@ -1118,8 +1107,7 @@ Calls the following roles (in order): * [icingaweb2_theme_linuxfabrik](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_theme_linuxfabrik): `setup_icinga2_master__icingaweb2_theme_linuxfabrik__skip_role` * [icingaweb2_module_incubator](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_incubator): `setup_icinga2_master__icingaweb2_module_incubator__skip_role` * [icingaweb2_module_jira](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_jira): `setup_icinga2_master__icingaweb2_module_jira__skip_role` (default: `true`) -* [repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome): `setup_icinga2_master__repo_google_chrome__skip_role` (default: tracks `icingaweb2_module_pdfexport__skip_role`) -* [google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome): `setup_icinga2_master__google_chrome__skip_role` (default: tracks `icingaweb2_module_pdfexport__skip_role`) +* [chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless): `setup_icinga2_master__chromium_headless__skip_role` (default: tracks `icingaweb2_module_pdfexport__skip_role`) * [icingaweb2_module_pdfexport](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_pdfexport): `setup_icinga2_master__icingaweb2_module_pdfexport__skip_role` (default: `true`) * [icingaweb2_module_vspheredb](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_vspheredb): `setup_icinga2_master__icingaweb2_module_vspheredb__skip_role` (default: `true`) * [icingaweb2_module_director](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_director): `setup_icinga2_master__icingaweb2_module_director__skip_role` diff --git a/playbooks/all.yml b/playbooks/all.yml index 96cda61d..5d66638c 100644 --- a/playbooks/all.yml +++ b/playbooks/all.yml @@ -10,6 +10,7 @@ - import_playbook: 'bind.yml' - import_playbook: 'blocky.yml' - import_playbook: 'borg_local.yml' +- import_playbook: 'chromium_headless.yml' - import_playbook: 'chrony.yml' - import_playbook: 'clamav.yml' - import_playbook: 'cloud_init.yml' @@ -36,7 +37,6 @@ - import_playbook: 'gitlab_ce.yml' - import_playbook: 'glances.yml' - import_playbook: 'glpi_agent.yml' -- import_playbook: 'google_chrome.yml' - import_playbook: 'grafana.yml' - import_playbook: 'grafana_grizzly.yml' - import_playbook: 'haveged.yml' @@ -102,7 +102,6 @@ - import_playbook: 'repo_epel.yml' - import_playbook: 'repo_gitlab_ce.yml' - import_playbook: 'repo_gitlab_runner.yml' -- import_playbook: 'repo_google_chrome.yml' - import_playbook: 'repo_grafana.yml' - import_playbook: 'repo_graylog.yml' - import_playbook: 'repo_icinga.yml' diff --git a/playbooks/repo_google_chrome.yml b/playbooks/chromium_headless.yml similarity index 53% rename from playbooks/repo_google_chrome.yml rename to playbooks/chromium_headless.yml index f19a1b89..0500b191 100644 --- a/playbooks/repo_google_chrome.yml +++ b/playbooks/chromium_headless.yml @@ -1,6 +1,6 @@ -- name: 'Playbook linuxfabrik.lfops.repo_google_chrome' +- name: 'Playbook linuxfabrik.lfops.chromium_headless' hosts: - - 'lfops_repo_google_chrome' + - 'lfops_chromium_headless' pre_tasks: - ansible.builtin.import_role: @@ -12,7 +12,11 @@ roles: - - role: 'linuxfabrik.lfops.repo_google_chrome' + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'not chromium_headless__skip_repo_epel | d(false) | bool' + + - role: 'linuxfabrik.lfops.chromium_headless' post_tasks: diff --git a/playbooks/google_chrome.yml b/playbooks/google_chrome.yml deleted file mode 100644 index c029f420..00000000 --- a/playbooks/google_chrome.yml +++ /dev/null @@ -1,43 +0,0 @@ -- name: 'Playbook linuxfabrik.lfops.google_chrome' - hosts: - - 'lfops_google_chrome' - - pre_tasks: - - ansible.builtin.import_role: - name: 'shared' - tasks_from: 'log-start.yml' - tags: - - 'always' - - - roles: - - # The google_chrome role needs mesa-libOSMesa, which lives in CRB. On EL9 the CRB - # repo is enabled via repo_baseos (Rocky only, like the other playbooks); on EL8 the - # equivalent (PowerTools) ships with the repo_epel repo file. - - role: 'linuxfabrik.lfops.repo_baseos' - repo_baseos__crb_repo_enabled__dependent_var: '{{ - repo_epel__repo_baseos__crb_repo_enabled__dependent_var - }}' - when: - - 'ansible_facts["distribution"] == "Rocky"' - - 'ansible_facts["distribution_major_version"] in ["9", "10"]' - - 'not google_chrome__skip_repo_baseos | d(false) | bool' - - - role: 'linuxfabrik.lfops.repo_epel' - when: - - 'not google_chrome__skip_repo_epel | d(false) | bool' - - - role: 'linuxfabrik.lfops.repo_google_chrome' - when: - - 'not google_chrome__skip_repo_google_chrome | d(false) | bool' - - - role: 'linuxfabrik.lfops.google_chrome' - - - post_tasks: - - ansible.builtin.import_role: - name: 'shared' - tasks_from: 'log-end.yml' - tags: - - 'always' diff --git a/playbooks/icingaweb2_module_pdfexport.yml b/playbooks/icingaweb2_module_pdfexport.yml index fea35a6a..57ce3f4e 100644 --- a/playbooks/icingaweb2_module_pdfexport.yml +++ b/playbooks/icingaweb2_module_pdfexport.yml @@ -12,29 +12,13 @@ roles: - # The google_chrome role needs mesa-libOSMesa, which lives in CRB. On EL9 the CRB - # repo is enabled via repo_baseos (Rocky only, like the other playbooks); on EL8 the - # equivalent (PowerTools) ships with the repo_epel repo file. - - role: 'linuxfabrik.lfops.repo_baseos' - repo_baseos__crb_repo_enabled__dependent_var: '{{ - repo_epel__repo_baseos__crb_repo_enabled__dependent_var - }}' - when: - - 'ansible_facts["distribution"] == "Rocky"' - - 'ansible_facts["distribution_major_version"] in ["9", "10"]' - - 'not icingaweb2_module_pdfexport__skip_repo_baseos | d(false) | bool' - - role: 'linuxfabrik.lfops.repo_epel' when: - 'not icingaweb2_module_pdfexport__skip_repo_epel | d(false) | bool' - - role: 'linuxfabrik.lfops.repo_google_chrome' - when: - - 'not icingaweb2_module_pdfexport__skip_repo_google_chrome | d(false) | bool' - - - role: 'linuxfabrik.lfops.google_chrome' + - role: 'linuxfabrik.lfops.chromium_headless' when: - - 'not icingaweb2_module_pdfexport__skip_google_chrome | d(false) | bool' + - 'not icingaweb2_module_pdfexport__skip_chromium_headless | d(false) | bool' - role: 'linuxfabrik.lfops.icingaweb2_module_pdfexport' diff --git a/playbooks/setup_icinga2_master.yml b/playbooks/setup_icinga2_master.yml index 4b2d6324..68a66a9a 100644 --- a/playbooks/setup_icinga2_master.yml +++ b/playbooks/setup_icinga2_master.yml @@ -7,7 +7,7 @@ setup_icinga2_master__apache_httpd__skip_injections__internal_var: '{{ setup_icinga2_master__apache_httpd__skip_injections | d(setup_icinga2_master__apache_httpd__skip_role__internal_var) }}' setup_icinga2_master__apache_httpd__skip_role__internal_var: '{{ setup_icinga2_master__apache_httpd__skip_role | d(false) }}' - setup_icinga2_master__google_chrome__skip_role__internal_var: '{{ setup_icinga2_master__google_chrome__skip_role | d(setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var) }}' + setup_icinga2_master__chromium_headless__skip_role__internal_var: '{{ setup_icinga2_master__chromium_headless__skip_role | d(setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var) }}' setup_icinga2_master__grafana__skip_role__internal_var: '{{ setup_icinga2_master__grafana__skip_role | d(false) }}' setup_icinga2_master__grafana_grizzly__skip_injections__internal_var: '{{ setup_icinga2_master__grafana_grizzly__skip_injections | d(setup_icinga2_master__grafana_grizzly__skip_role__internal_var) }}' setup_icinga2_master__grafana_grizzly__skip_role__internal_var: '{{ setup_icinga2_master__grafana_grizzly__skip_role | d(false) }}' @@ -59,7 +59,6 @@ setup_icinga2_master__redis__skip_injections__internal_var: '{{ setup_icinga2_master__redis__skip_injections | d(setup_icinga2_master__redis__skip_role__internal_var) }}' setup_icinga2_master__redis__skip_role__internal_var: '{{ setup_icinga2_master__redis__skip_role | d(false) }}' setup_icinga2_master__repo_epel__skip_role__internal_var: '{{ setup_icinga2_master__repo_epel__skip_role | d(false) }}' - setup_icinga2_master__repo_google_chrome__skip_role__internal_var: '{{ setup_icinga2_master__repo_google_chrome__skip_role | d(setup_icinga2_master__icingaweb2_module_pdfexport__skip_role__internal_var) }}' setup_icinga2_master__repo_grafana__skip_role__internal_var: '{{ setup_icinga2_master__repo_grafana__skip_role | d(false) }}' setup_icinga2_master__repo_icinga__skip_role__internal_var: '{{ setup_icinga2_master__repo_icinga__skip_role | d(false) }}' setup_icinga2_master__repo_influxdb__skip_role__internal_var: '{{ setup_icinga2_master__repo_influxdb__skip_role | d(false) }}' @@ -314,13 +313,9 @@ when: - 'not setup_icinga2_master__icingaweb2_module_jira__skip_role__internal_var' - - role: 'linuxfabrik.lfops.repo_google_chrome' + - role: 'linuxfabrik.lfops.chromium_headless' when: - - 'not setup_icinga2_master__repo_google_chrome__skip_role__internal_var' - - - role: 'linuxfabrik.lfops.google_chrome' - when: - - 'not setup_icinga2_master__google_chrome__skip_role__internal_var' + - 'not setup_icinga2_master__chromium_headless__skip_role__internal_var' - role: 'linuxfabrik.lfops.icingaweb2_module_pdfexport' when: diff --git a/roles/chromium_headless/README.md b/roles/chromium_headless/README.md new file mode 100644 index 00000000..f83f377e --- /dev/null +++ b/roles/chromium_headless/README.md @@ -0,0 +1,135 @@ +# Ansible Role linuxfabrik.lfops.chromium_headless + +This role installs the headless [Chromium](https://www.chromium.org/) shell together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated `chromium-headless` systemd service. Clients connect to a configurable TCP socket; Chromium is started on the first request via `systemd-socket-proxyd` and stopped again after a configurable idle timeout, so no RAM is wasted while the backend is unused. + +The setup is used as a headless browser backend for tools such as the [Icinga Web 2 PDF Export Module](https://github.com/Icinga/icingaweb2-module-pdfexport). + + +*Available since LFOps `6.0.2`.* + + +## How the Role Behaves + +* Three systemd units are deployed: + * `chromium-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). + * `chromium-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chromium (Chromium itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. On EL8 (systemd 239) the underlying `--exit-idle-time` option does not exist, so the idle shutdown is skipped there and the backend stays resident once activated; on-demand start still works. + * `chromium-headless.service` runs the actual Chromium process under the `chromium` system user. Its start job is held until Chromium's debugging port actually accepts connections, so the proxy (ordered after it) never races ahead and fails the first request. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chromium stops too; the SIGTERM-triggered exit is treated as clean so this does not mark the unit failed. It is **not** enabled on boot and must not be started directly. The proxy triggers it via `Requires=`. +* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `chromium-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chromium's non-standard backend port. +* The service-lifecycle variables (`chromium_headless__service_enabled`, `__service_state`) manage the `chromium-headless-proxy.socket` unit, not the Chromium service directly. + + +## Mandatory Requirements + +* Enable the EPEL repository, which provides `chromium-headless`. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. + +If you use the [Chromium Headless Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/chromium_headless.yml), this is automatically done for you. + + +## Tags + +`chromium_headless` + +* Creates the `chromium` system user and group. +* Installs Chromium along with the required runtime libraries and fonts. +* Sets the `systemd_socket_proxyd_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. +* Deploys all three systemd units (`chromium-headless-proxy.socket`, `chromium-headless-proxy.service`, `chromium-headless.service`). +* Ensures the `chromium-headless-proxy.socket` is in the desired state. +* Triggers: daemon-reload on any unit-file change; socket restart only on `chromium-headless-proxy.socket` changes. Changes to the proxy or Chromium service unit file take effect on the next socket-activation cycle. + +`chromium_headless:configure` + +* Deploys the three systemd units (`chromium-headless-proxy.socket`, `chromium-headless-proxy.service`, `chromium-headless.service`). +* Triggers: daemon-reload on any unit-file change; socket restart only on `chromium-headless-proxy.socket` changes. Changes to the proxy or Chromium service unit file take effect on the next socket-activation cycle. + +`chromium_headless:state` + +* Manages the `chromium-headless-proxy.socket` state (start, stop, enable, disable). +* Triggers: none. + + +## Optional Role Variables + +`chromium_headless__backend_port` + +* Internal port Chromium itself listens on. The proxy forwards traffic from `listen_port` to this port. Only meaningful to change if `listen_port` and `backend_port` would otherwise collide. +* Type: Number. +* Default: `9223` + +`chromium_headless__extra_args__host_var` / `chromium_headless__extra_args__group_var` + +* Additional Chromium CLI flags appended to the `ExecStart` line of `chromium-headless.service`, in the order listed. Useful for tuning behavior without overwriting the whole unit. +* Type: List of dictionaries. +* Default: `[]` +* Subkeys: + + * `name`: + + * Mandatory. The CLI flag, including any leading dashes and value (e.g. `--window-size=1920,1080`). + * Type: String. + + * `state`: + + * Optional. `present` or `absent`. + * Type: String. + * Default: `'present'` + +`chromium_headless__idle_timeout` + +* Seconds the `systemd-socket-proxyd` waits without active connections before exiting. When it exits, the bound `chromium-headless.service` stops automatically. The next inbound connection re-activates the whole chain, paying ~1-2 seconds of cold-start latency. Has no effect on EL8, whose systemd (239) lacks the `--exit-idle-time` option; the backend stays resident there. +* Type: Number. +* Default: `300` + +`chromium_headless__listen_address` + +* Address the proxy socket binds to. Keep this on `127.0.0.1` unless you intentionally want to expose it to other hosts; neither the proxy nor Chromium enforces TLS or authentication. +* Type: String. +* Default: `'127.0.0.1'` + +`chromium_headless__listen_port` + +* Port the proxy socket listens on. This is the endpoint clients connect to. +* Type: Number. +* Default: `9222` + +`chromium_headless__service_enabled` + +* Enables or disables the `chromium-headless-proxy.socket` at boot, analogous to `systemctl enable/disable --now`. +* Type: Bool. +* Default: `true` + +`chromium_headless__service_state` + +* Changes the state of the `chromium-headless-proxy.socket`, analogous to `systemctl start/stop/restart/reload`. +* Type: String. One of `reloaded`, `restarted`, `started`, `stopped`. +* Default: `'started'` + +`chromium_headless__user_data_dir` + +* Home directory of the `chromium` system user and Chromium user data directory. Used both as the user's `home`, as the `--user-data-dir` value for Chromium, and as the writable path exposed via systemd `ReadWritePaths=`. +* Type: String. +* Default: `'/var/lib/chromium-headless'` + +Example: +```yaml +# optional +chromium_headless__backend_port: 9223 +chromium_headless__extra_args__host_var: + - name: '--window-size=1920,1080' + - name: '--lang=de-CH' +chromium_headless__idle_timeout: 600 +chromium_headless__listen_address: '127.0.0.1' +chromium_headless__listen_port: 9222 +chromium_headless__service_enabled: true +chromium_headless__service_state: 'started' +chromium_headless__user_data_dir: '/var/lib/chromium-headless' +``` + + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/chromium_headless/defaults/main.yml b/roles/chromium_headless/defaults/main.yml new file mode 100644 index 00000000..7073336b --- /dev/null +++ b/roles/chromium_headless/defaults/main.yml @@ -0,0 +1,18 @@ +chromium_headless__backend_port: 9223 +chromium_headless__extra_args__combined_var: '{{ ( + chromium_headless__extra_args__role_var + + chromium_headless__extra_args__dependent_var + + chromium_headless__extra_args__group_var + + chromium_headless__extra_args__host_var + ) | linuxfabrik.lfops.combine_lod + }}' +chromium_headless__extra_args__dependent_var: [] +chromium_headless__extra_args__group_var: [] +chromium_headless__extra_args__host_var: [] +chromium_headless__extra_args__role_var: [] +chromium_headless__idle_timeout: 300 +chromium_headless__listen_address: '127.0.0.1' +chromium_headless__listen_port: 9222 +chromium_headless__service_enabled: true +chromium_headless__service_state: 'started' +chromium_headless__user_data_dir: '/var/lib/chromium-headless' diff --git a/roles/chromium_headless/handlers/main.yml b/roles/chromium_headless/handlers/main.yml new file mode 100644 index 00000000..ef8cb21b --- /dev/null +++ b/roles/chromium_headless/handlers/main.yml @@ -0,0 +1,17 @@ +# Only socket-template changes trigger an immediate restart, because the socket unit +# is what binds the externally-visible listen_address:listen_port. Changes to the +# proxy or Chromium service templates only need daemon-reload: the running proxy and +# Chromium process keep going with the old settings until the next idle timeout, and +# the next socket-activation cycle re-spawns them with the updated unit files. +# +# `is not defined` covers the `--tags chromium_headless:configure` run, where the state +# block is skipped and __chromium_headless__service_state_result is never registered. +# `is not changed` covers the normal flow: skip the restart if the state task just +# (re-)started the socket. +- name: 'chromium_headless: restart chromium-headless-proxy.socket' + ansible.builtin.service: + name: 'chromium-headless-proxy.socket' + state: 'restarted' + when: + - '__chromium_headless__service_state_result is not defined or __chromium_headless__service_state_result is not changed' + - 'chromium_headless__service_state != "stopped"' diff --git a/roles/chromium_headless/meta/argument_specs.yml b/roles/chromium_headless/meta/argument_specs.yml new file mode 100644 index 00000000..f77d9561 --- /dev/null +++ b/roles/chromium_headless/meta/argument_specs.yml @@ -0,0 +1,71 @@ +argument_specs: + main: + options: + + chromium_headless__backend_port: + type: 'int' + required: false + default: 9223 + description: 'Internal port Chromium itself listens on. The proxy forwards traffic from listen_port to this port.' + + chromium_headless__extra_args__dependent_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Chromium CLI flags. Dependent-role injection.' + + chromium_headless__extra_args__group_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Chromium CLI flags. Group-level override.' + + chromium_headless__extra_args__host_var: + type: 'list' + elements: 'dict' + required: false + default: [] + description: 'Extra Chromium CLI flags. Host-level override.' + + chromium_headless__idle_timeout: + type: 'int' + required: false + default: 300 + description: 'Seconds the systemd-socket-proxyd waits without traffic before exiting (and stopping Chromium via BindsTo).' + + chromium_headless__listen_address: + type: 'str' + required: false + default: '127.0.0.1' + description: 'Listen address for the Chromium remote debugging interface.' + + chromium_headless__listen_port: + type: 'int' + required: false + default: 9222 + description: 'Listen port for the Chromium remote debugging interface.' + + chromium_headless__service_enabled: + type: 'bool' + required: false + default: true + description: 'Enables or disables the chromium-headless-proxy.socket unit at boot. The Chromium service itself is triggered on demand by the proxy and is not managed directly.' + + chromium_headless__service_state: + type: 'str' + required: false + default: 'started' + choices: + - 'reloaded' + - 'restarted' + - 'started' + - 'stopped' + description: 'Desired state of the chromium-headless-proxy.socket unit. The Chromium service itself is triggered on demand by the proxy and is not managed directly.' + + chromium_headless__user_data_dir: + type: 'str' + required: false + default: '/var/lib/chromium-headless' + description: 'Home directory of the chromium system user and Chromium user data directory.' diff --git a/roles/chromium_headless/tasks/main.yml b/roles/chromium_headless/tasks/main.yml new file mode 100644 index 00000000..7cd20d98 --- /dev/null +++ b/roles/chromium_headless/tasks/main.yml @@ -0,0 +1,157 @@ +- block: + + - name: 'Set platform/version specific variables' + ansible.builtin.import_role: + name: 'shared' + tasks_from: 'platform-variables.yml' + + tags: + - 'always' + + +- block: + + - name: 'groupadd --system chromium' + ansible.builtin.group: + name: 'chromium' + state: 'present' + system: true + + - name: 'useradd --system chromium' + ansible.builtin.user: + name: 'chromium' + comment: 'Headless Chromium' + group: 'chromium' + home: '{{ chromium_headless__user_data_dir }}' + shell: '/sbin/nologin' + system: true + state: 'present' + + - name: 'install --directory --owner chromium --group chromium --mode 0750 {{ chromium_headless__user_data_dir }}' + ansible.builtin.file: + path: '{{ chromium_headless__user_data_dir }}' + state: 'directory' + owner: 'chromium' + group: 'chromium' + mode: 0o750 + + - name: 'Install required packages' + ansible.builtin.package: + name: '{{ __chromium_headless__packages }}' + state: 'present' + + tags: + - 'chromium_headless' + + +# Chromium itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chromium: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chromium opens itself). --exit-idle-time on the proxy plus BindsTo= on chromium-headless.service ties Chromium's lifecycle to the proxy, so Chromium stops together with the proxy after the idle timeout. +# Two SELinux booleans are required: bind_any so the .socket can bind the listen port even when it has an unexpected port type (e.g. on Rocky 9, port 9222 maps to hplip_port_t and would otherwise reject the bind); connect_any so the proxy can connect to Chromium's non-standard backend port, which has no matching SELinux port type. +- block: + + - name: 'setsebool -P systemd_socket_proxyd_bind_any on' + ansible.posix.seboolean: + name: 'systemd_socket_proxyd_bind_any' + persistent: true + state: true + + - name: 'setsebool -P systemd_socket_proxyd_connect_any on' + ansible.posix.seboolean: + name: 'systemd_socket_proxyd_connect_any' + persistent: true + state: true + + # block + when: + - 'ansible_facts["selinux"]["status"] != "disabled"' + tags: + - 'chromium_headless' + + +- block: + + - name: 'Deploy /etc/systemd/system/chromium-headless-proxy.socket' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chromium-headless-proxy.socket.j2' + dest: '/etc/systemd/system/chromium-headless-proxy.socket' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__chromium_headless__deploy_socket_result' + notify: + - 'chromium_headless: restart chromium-headless-proxy.socket' + + - name: 'Deploy /etc/systemd/system/chromium-headless-proxy.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chromium-headless-proxy.service.j2' + dest: '/etc/systemd/system/chromium-headless-proxy.service' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__chromium_headless__deploy_proxy_result' + + - name: 'Deploy /etc/systemd/system/chromium-headless.service' + ansible.builtin.template: + backup: true + src: 'etc/systemd/system/chromium-headless.service.j2' + dest: '/etc/systemd/system/chromium-headless.service' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__chromium_headless__deploy_chrome_result' + + - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' + ansible.builtin.include_role: + name: 'shared' + tasks_from: 'remove-rpmnew-rpmsave.yml' + vars: + shared__remove_rpmnew_rpmsave_config_file: '{{ item }}' + loop: + - '/etc/systemd/system/chromium-headless-proxy.socket' + - '/etc/systemd/system/chromium-headless-proxy.service' + - '/etc/systemd/system/chromium-headless.service' + + # Run daemon-reload as a regular task (not as a handler), so it runs before the + # state block below and so the restart-socket handler can rely on the registered + # __chromium_headless__service_state_result to skip redundant restarts. + - name: 'systemctl daemon-reload' + ansible.builtin.systemd: + daemon_reload: true + when: + - '__chromium_headless__deploy_socket_result is changed or + __chromium_headless__deploy_proxy_result is changed or + __chromium_headless__deploy_chrome_result is changed' + + tags: + - 'chromium_headless' + - 'chromium_headless:configure' + + +- block: + + - name: 'systemctl {{ chromium_headless__service_enabled | bool | ternary("enable", "disable") }} chromium-headless-proxy.socket' + ansible.builtin.service: + name: 'chromium-headless-proxy.socket' + enabled: '{{ chromium_headless__service_enabled | bool }}' + + - name: 'systemctl {{ chromium_headless__service_state }} chromium-headless-proxy.socket' + ansible.builtin.service: + name: 'chromium-headless-proxy.socket' + state: '{{ chromium_headless__service_state }}' + register: '__chromium_headless__service_state_result' + + tags: + - 'chromium_headless' + - 'chromium_headless:state' + + +- block: + + - name: 'Flush handlers so that the service is ready for dependent roles' + ansible.builtin.meta: 'flush_handlers' + + tags: + - 'chromium_headless' + - 'chromium_headless:configure' + - 'chromium_headless:state' diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 new file mode 100644 index 00000000..3a7343b5 --- /dev/null +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 @@ -0,0 +1,18 @@ +# {{ ansible_managed }} +# 2026052101 + +[Unit] +Description=Proxy to on-demand Headless Chromium +Requires=chromium-headless.service +After=chromium-headless.service + +[Service] +{# --exit-idle-time was added in systemd 246. EL8 ships systemd 239 and rejects the option, so we omit it there; the proxy (and the Chromium service bound to it) then stays resident once activated. EL9 (systemd 252) and newer support it. #} +{% if ansible_facts['distribution_major_version'] | int >= 9 %} +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ chromium_headless__idle_timeout }} {{ chromium_headless__listen_address }}:{{ chromium_headless__backend_port }} +{% else %} +ExecStart=/usr/lib/systemd/systemd-socket-proxyd {{ chromium_headless__listen_address }}:{{ chromium_headless__backend_port }} +{% endif %} +PrivateTmp=true +Restart=on-failure +RestartSec=5 diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.socket.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.socket.j2 new file mode 100644 index 00000000..f20198f2 --- /dev/null +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.socket.j2 @@ -0,0 +1,11 @@ +# {{ ansible_managed }} +# 2026052002 + +[Unit] +Description=Socket for on-demand Headless Chromium + +[Socket] +ListenStream={{ chromium_headless__listen_address }}:{{ chromium_headless__listen_port }} + +[Install] +WantedBy=sockets.target diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 new file mode 100644 index 00000000..837df4ef --- /dev/null +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 @@ -0,0 +1,57 @@ +# {{ ansible_managed }} +# 2026052101 + +[Unit] +Description=Headless Chromium +BindsTo=chromium-headless-proxy.service + +[Service] +Type=simple +User=chromium +Group=chromium +ExecStart={{ __chromium_headless__binary_path }} \ + --headless=new \ + --disable-gpu \ + --no-first-run \ + --no-default-browser-check \ + --hide-scrollbars \ + --disable-dev-shm-usage \ + --remote-debugging-address={{ chromium_headless__listen_address }} \ + --remote-debugging-port={{ chromium_headless__backend_port }} \ + --remote-allow-origins=http://{{ chromium_headless__listen_address }}:{{ chromium_headless__listen_port }} \ +{% for arg in chromium_headless__extra_args__combined_var if arg['state'] | d('present') != 'absent' %} + {{ arg['name'] }} \ +{% endfor %} + --user-data-dir={{ chromium_headless__user_data_dir }} +# Chromium binds its debugging port a moment after the process starts and does not implement systemd socket activation. Block the start job until the port accepts connections, otherwise systemd-socket-proxyd (ordered After= this unit) connects too early and the first request fails with "Connection refused". +ExecStartPost=/bin/bash -c 'until (echo > /dev/tcp/{{ chromium_headless__listen_address }}/{{ chromium_headless__backend_port }}) 2>/dev/null; do sleep 0.1; done' +Restart=on-failure +RestartSec=5 +# Chromium traps SIGTERM and exits 143 (128 + SIGTERM); treat that as a clean shutdown so stopping the bound proxy on idle does not mark this unit failed. +SuccessExitStatus=143 + +PrivateDevices=true +ProtectClock=true +NoNewPrivileges=true +RemoveIPC=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths={{ chromium_headless__user_data_dir }} +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +RestrictNamespaces=~cgroup uts ipc +LockPersonality=true +MemoryDenyWriteExecute=false +CapabilityBoundingSet= +AmbientCapabilities= +SystemCallArchitectures=native +ProtectKernelLogs=true +ProtectHostname=true +ProtectProc=invisible +ProcSubset=pid +RestrictSUIDSGID=true +RestrictRealtime=true +UMask=0077 diff --git a/roles/chromium_headless/vars/RedHat.yml b/roles/chromium_headless/vars/RedHat.yml new file mode 100644 index 00000000..d31bf926 --- /dev/null +++ b/roles/chromium_headless/vars/RedHat.yml @@ -0,0 +1,4 @@ +__chromium_headless__binary_path: '/usr/lib64/chromium-browser/headless_shell' +__chromium_headless__packages: + - 'chromium-headless' + - 'gnu-free-sans-fonts' diff --git a/roles/google_chrome/README.md b/roles/google_chrome/README.md deleted file mode 100644 index 286dc6ec..00000000 --- a/roles/google_chrome/README.md +++ /dev/null @@ -1,137 +0,0 @@ -# Ansible Role linuxfabrik.lfops.google_chrome - -This role installs [Google Chrome](https://www.google.com/chrome/) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated `google-chrome-headless` systemd service. Clients connect to a configurable TCP socket; Chrome is started on the first request via `systemd-socket-proxyd` and stopped again after a configurable idle timeout, so no RAM is wasted while the backend is unused. - -The setup is used as a headless browser backend for tools such as the [Icinga Web 2 PDF Export Module](https://github.com/Icinga/icingaweb2-module-pdfexport). - - -*Available since LFOps `6.0.2`.* - - -## How the Role Behaves - -* Three systemd units are deployed: - * `google-chrome-headless-proxy.socket` listens on `listen_address:listen_port` (default `127.0.0.1:9222`). - * `google-chrome-headless-proxy.service` runs `systemd-socket-proxyd`, which bridges the activated socket to Chrome (Chrome itself does not implement the systemd socket-activation protocol), forwarding traffic to `backend_port` (default `9223`). On `idle_timeout` seconds without traffic it exits. - * `google-chrome-headless.service` runs the actual Chrome process under the `chrome` system user. It is bound to the proxy via `BindsTo=`, so when the proxy exits on idle, Chrome stops too. It is **not** enabled on boot and must not be started directly. The proxy triggers it via `Requires=`. -* On SELinux-enforcing hosts, two booleans are enabled: `systemd_socket_proxyd_bind_any` so the `google-chrome-headless-proxy.socket` unit may bind the listen port even when it carries an unexpected SELinux port type (on Rocky/RHEL 9 the default `9222` is registered as `hplip_port_t`), and `systemd_socket_proxyd_connect_any` so the proxy may connect to Chrome's non-standard backend port. -* The service-lifecycle variables (`google_chrome__service_enabled`, `__service_state`) manage the `google-chrome-headless-proxy.socket` unit, not the Chrome service directly. - - -## Mandatory Requirements - -* Enable the CRB repository (PowerTools on EL8), which provides `mesa-libOSMesa`. On EL9 this can be done using the [linuxfabrik.lfops.repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos) role; on EL8 it ships with the EPEL repository file. -* Enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* Enable the Google Chrome repository. This can be done using the [linuxfabrik.lfops.repo_google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_google_chrome) role. - -If you use the [Google Chrome Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/google_chrome.yml), this is automatically done for you. - - -## Tags - -`google_chrome` - -* Creates the `chrome` system user and group. -* Installs Google Chrome along with the required runtime libraries and fonts. -* Sets the `systemd_socket_proxyd_bind_any` and `systemd_socket_proxyd_connect_any` SELinux booleans. -* Deploys all three systemd units (`google-chrome-headless-proxy.socket`, `google-chrome-headless-proxy.service`, `google-chrome-headless.service`). -* Ensures the `google-chrome-headless-proxy.socket` is in the desired state. -* Triggers: daemon-reload on any unit-file change; socket restart only on `google-chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. - -`google_chrome:configure` - -* Deploys the three systemd units (`google-chrome-headless-proxy.socket`, `google-chrome-headless-proxy.service`, `google-chrome-headless.service`). -* Triggers: daemon-reload on any unit-file change; socket restart only on `google-chrome-headless-proxy.socket` changes. Changes to the proxy or Chrome service unit file take effect on the next socket-activation cycle. - -`google_chrome:state` - -* Manages the `google-chrome-headless-proxy.socket` state (start, stop, enable, disable). -* Triggers: none. - - -## Optional Role Variables - -`google_chrome__backend_port` - -* Internal port Chrome itself listens on. The proxy forwards traffic from `listen_port` to this port. Only meaningful to change if `listen_port` and `backend_port` would otherwise collide. -* Type: Number. -* Default: `9223` - -`google_chrome__extra_args__host_var` / `google_chrome__extra_args__group_var` - -* Additional Chrome CLI flags appended to the `ExecStart` line of `google-chrome-headless.service`, in the order listed. Useful for tuning behavior without overwriting the whole unit. -* Type: List of dictionaries. -* Default: `[]` -* Subkeys: - - * `name`: - - * Mandatory. The CLI flag, including any leading dashes and value (e.g. `--window-size=1920,1080`). - * Type: String. - - * `state`: - - * Optional. `present` or `absent`. - * Type: String. - * Default: `'present'` - -`google_chrome__idle_timeout` - -* Seconds the `systemd-socket-proxyd` waits without active connections before exiting. When it exits, the bound `google-chrome-headless.service` stops automatically. The next inbound connection re-activates the whole chain, paying ~1-2 seconds of cold-start latency. -* Type: Number. -* Default: `300` - -`google_chrome__listen_address` - -* Address the proxy socket binds to. Keep this on `127.0.0.1` unless you intentionally want to expose it to other hosts; neither the proxy nor Chrome enforces TLS or authentication. -* Type: String. -* Default: `'127.0.0.1'` - -`google_chrome__listen_port` - -* Port the proxy socket listens on. This is the endpoint clients connect to. -* Type: Number. -* Default: `9222` - -`google_chrome__service_enabled` - -* Enables or disables the `google-chrome-headless-proxy.socket` at boot, analogous to `systemctl enable/disable --now`. -* Type: Bool. -* Default: `true` - -`google_chrome__service_state` - -* Changes the state of the `google-chrome-headless-proxy.socket`, analogous to `systemctl start/stop/restart/reload`. -* Type: String. One of `reloaded`, `restarted`, `started`, `stopped`. -* Default: `'started'` - -`google_chrome__user_data_dir` - -* Home directory of the `chrome` system user and Chrome user data directory. Used both as the user's `home`, as the `--user-data-dir` value for Chrome, and as the writable path exposed via systemd `ReadWritePaths=`. -* Type: String. -* Default: `'/var/lib/google-chrome-headless'` - -Example: -```yaml -# optional -google_chrome__backend_port: 9223 -google_chrome__extra_args__host_var: - - name: '--window-size=1920,1080' - - name: '--lang=de-CH' -google_chrome__idle_timeout: 600 -google_chrome__listen_address: '127.0.0.1' -google_chrome__listen_port: 9222 -google_chrome__service_enabled: true -google_chrome__service_state: 'started' -google_chrome__user_data_dir: '/var/lib/google-chrome-headless' -``` - - -## License - -[The Unlicense](https://unlicense.org/) - - -## Author Information - -[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/google_chrome/defaults/main.yml b/roles/google_chrome/defaults/main.yml deleted file mode 100644 index f8c539e7..00000000 --- a/roles/google_chrome/defaults/main.yml +++ /dev/null @@ -1,18 +0,0 @@ -google_chrome__backend_port: 9223 -google_chrome__extra_args__combined_var: '{{ ( - google_chrome__extra_args__role_var + - google_chrome__extra_args__dependent_var + - google_chrome__extra_args__group_var + - google_chrome__extra_args__host_var - ) | linuxfabrik.lfops.combine_lod - }}' -google_chrome__extra_args__dependent_var: [] -google_chrome__extra_args__group_var: [] -google_chrome__extra_args__host_var: [] -google_chrome__extra_args__role_var: [] -google_chrome__idle_timeout: 300 -google_chrome__listen_address: '127.0.0.1' -google_chrome__listen_port: 9222 -google_chrome__service_enabled: true -google_chrome__service_state: 'started' -google_chrome__user_data_dir: '/var/lib/google-chrome-headless' diff --git a/roles/google_chrome/handlers/main.yml b/roles/google_chrome/handlers/main.yml deleted file mode 100644 index 11272b6b..00000000 --- a/roles/google_chrome/handlers/main.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Only socket-template changes trigger an immediate restart, because the socket unit -# is what binds the externally-visible listen_address:listen_port. Changes to the -# proxy or Chrome service templates only need daemon-reload: the running proxy and -# Chrome process keep going with the old settings until the next idle timeout, and -# the next socket-activation cycle re-spawns them with the updated unit files. -# -# `is not defined` covers the `--tags google_chrome:configure` run, where the state -# block is skipped and __google_chrome__service_state_result is never registered. -# `is not changed` covers the normal flow: skip the restart if the state task just -# (re-)started the socket. -- name: 'google_chrome: restart google-chrome-headless-proxy.socket' - ansible.builtin.service: - name: 'google-chrome-headless-proxy.socket' - state: 'restarted' - when: - - '__google_chrome__service_state_result is not defined or __google_chrome__service_state_result is not changed' - - 'google_chrome__service_state != "stopped"' diff --git a/roles/google_chrome/meta/argument_specs.yml b/roles/google_chrome/meta/argument_specs.yml deleted file mode 100644 index cccab91a..00000000 --- a/roles/google_chrome/meta/argument_specs.yml +++ /dev/null @@ -1,71 +0,0 @@ -argument_specs: - main: - options: - - google_chrome__backend_port: - type: 'int' - required: false - default: 9223 - description: 'Internal port Chrome itself listens on. The proxy forwards traffic from listen_port to this port.' - - google_chrome__extra_args__dependent_var: - type: 'list' - elements: 'dict' - required: false - default: [] - description: 'Extra Google Chrome CLI flags. Dependent-role injection.' - - google_chrome__extra_args__group_var: - type: 'list' - elements: 'dict' - required: false - default: [] - description: 'Extra Google Chrome CLI flags. Group-level override.' - - google_chrome__extra_args__host_var: - type: 'list' - elements: 'dict' - required: false - default: [] - description: 'Extra Google Chrome CLI flags. Host-level override.' - - google_chrome__idle_timeout: - type: 'int' - required: false - default: 300 - description: 'Seconds the systemd-socket-proxyd waits without traffic before exiting (and stopping Chrome via BindsTo).' - - google_chrome__listen_address: - type: 'str' - required: false - default: '127.0.0.1' - description: 'Listen address for the Chrome remote debugging interface.' - - google_chrome__listen_port: - type: 'int' - required: false - default: 9222 - description: 'Listen port for the Chrome remote debugging interface.' - - google_chrome__service_enabled: - type: 'bool' - required: false - default: true - description: 'Enables or disables the google-chrome-headless-proxy.socket unit at boot. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' - - google_chrome__service_state: - type: 'str' - required: false - default: 'started' - choices: - - 'reloaded' - - 'restarted' - - 'started' - - 'stopped' - description: 'Desired state of the google-chrome-headless-proxy.socket unit. The Chrome service itself is triggered on demand by the proxy and is not managed directly.' - - google_chrome__user_data_dir: - type: 'str' - required: false - default: '/var/lib/google-chrome-headless' - description: 'Home directory of the chrome system user and Chrome user data directory.' diff --git a/roles/google_chrome/tasks/main.yml b/roles/google_chrome/tasks/main.yml deleted file mode 100644 index 21373f9d..00000000 --- a/roles/google_chrome/tasks/main.yml +++ /dev/null @@ -1,157 +0,0 @@ -- block: - - - name: 'Set platform/version specific variables' - ansible.builtin.import_role: - name: 'shared' - tasks_from: 'platform-variables.yml' - - tags: - - 'always' - - -- block: - - - name: 'groupadd --system chrome' - ansible.builtin.group: - name: 'chrome' - state: 'present' - system: true - - - name: 'useradd --system chrome' - ansible.builtin.user: - name: 'chrome' - comment: 'Headless Google Chrome' - group: 'chrome' - home: '{{ google_chrome__user_data_dir }}' - shell: '/sbin/nologin' - system: true - state: 'present' - - - name: 'install --directory --owner chrome --group chrome --mode 0750 {{ google_chrome__user_data_dir }}' - ansible.builtin.file: - path: '{{ google_chrome__user_data_dir }}' - state: 'directory' - owner: 'chrome' - group: 'chrome' - mode: 0o750 - - - name: 'Install required packages' - ansible.builtin.package: - name: '{{ __google_chrome__packages }}' - state: 'present' - - tags: - - 'google_chrome' - - -# Chrome itself does not implement the systemd socket-activation protocol (sd_listen_fds() / LISTEN_FDS); it always binds its own --remote-debugging-port. Direct socket activation is therefore not possible, so we deploy a systemd-socket-proxyd in front of Chrome: the .socket unit binds listen_port, the proxy accepts on that activated fd and forwards to backend_port (the port Chrome opens itself). --exit-idle-time on the proxy plus BindsTo= on google-chrome-headless.service ties Chrome's lifecycle to the proxy, so Chrome stops together with the proxy after the idle timeout. -# Two SELinux booleans are required: bind_any so the .socket can bind the listen port even when it has an unexpected port type (e.g. on Rocky 9, port 9222 maps to hplip_port_t and would otherwise reject the bind); connect_any so the proxy can connect to Chrome's non-standard backend port, which has no matching SELinux port type. -- block: - - - name: 'setsebool -P systemd_socket_proxyd_bind_any on' - ansible.posix.seboolean: - name: 'systemd_socket_proxyd_bind_any' - persistent: true - state: true - - - name: 'setsebool -P systemd_socket_proxyd_connect_any on' - ansible.posix.seboolean: - name: 'systemd_socket_proxyd_connect_any' - persistent: true - state: true - - # block - when: - - 'ansible_facts["selinux"]["status"] != "disabled"' - tags: - - 'google_chrome' - - -- block: - - - name: 'Deploy /etc/systemd/system/google-chrome-headless-proxy.socket' - ansible.builtin.template: - backup: true - src: 'etc/systemd/system/google-chrome-headless-proxy.socket.j2' - dest: '/etc/systemd/system/google-chrome-headless-proxy.socket' - owner: 'root' - group: 'root' - mode: 0o644 - register: '__google_chrome__deploy_socket_result' - notify: - - 'google_chrome: restart google-chrome-headless-proxy.socket' - - - name: 'Deploy /etc/systemd/system/google-chrome-headless-proxy.service' - ansible.builtin.template: - backup: true - src: 'etc/systemd/system/google-chrome-headless-proxy.service.j2' - dest: '/etc/systemd/system/google-chrome-headless-proxy.service' - owner: 'root' - group: 'root' - mode: 0o644 - register: '__google_chrome__deploy_proxy_result' - - - name: 'Deploy /etc/systemd/system/google-chrome-headless.service' - ansible.builtin.template: - backup: true - src: 'etc/systemd/system/google-chrome-headless.service.j2' - dest: '/etc/systemd/system/google-chrome-headless.service' - owner: 'root' - group: 'root' - mode: 0o644 - register: '__google_chrome__deploy_chrome_result' - - - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' - ansible.builtin.include_role: - name: 'shared' - tasks_from: 'remove-rpmnew-rpmsave.yml' - vars: - shared__remove_rpmnew_rpmsave_config_file: '{{ item }}' - loop: - - '/etc/systemd/system/google-chrome-headless-proxy.socket' - - '/etc/systemd/system/google-chrome-headless-proxy.service' - - '/etc/systemd/system/google-chrome-headless.service' - - # Run daemon-reload as a regular task (not as a handler), so it runs before the - # state block below and so the restart-socket handler can rely on the registered - # __google_chrome__service_state_result to skip redundant restarts. - - name: 'systemctl daemon-reload' - ansible.builtin.systemd: - daemon_reload: true - when: - - '__google_chrome__deploy_socket_result is changed or - __google_chrome__deploy_proxy_result is changed or - __google_chrome__deploy_chrome_result is changed' - - tags: - - 'google_chrome' - - 'google_chrome:configure' - - -- block: - - - name: 'systemctl {{ google_chrome__service_enabled | bool | ternary("enable", "disable") }} google-chrome-headless-proxy.socket' - ansible.builtin.service: - name: 'google-chrome-headless-proxy.socket' - enabled: '{{ google_chrome__service_enabled | bool }}' - - - name: 'systemctl {{ google_chrome__service_state }} google-chrome-headless-proxy.socket' - ansible.builtin.service: - name: 'google-chrome-headless-proxy.socket' - state: '{{ google_chrome__service_state }}' - register: '__google_chrome__service_state_result' - - tags: - - 'google_chrome' - - 'google_chrome:state' - - -- block: - - - name: 'Flush handlers so that the service is ready for dependent roles' - ansible.builtin.meta: 'flush_handlers' - - tags: - - 'google_chrome' - - 'google_chrome:configure' - - 'google_chrome:state' diff --git a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 deleted file mode 100644 index a51e3b47..00000000 --- a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.service.j2 +++ /dev/null @@ -1,13 +0,0 @@ -# {{ ansible_managed }} -# 2026052001 - -[Unit] -Description=Proxy to on-demand Headless Google Chrome -Requires=google-chrome-headless.service -After=google-chrome-headless.service - -[Service] -ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ google_chrome__idle_timeout }} {{ google_chrome__listen_address }}:{{ google_chrome__backend_port }} -PrivateTmp=true -Restart=on-failure -RestartSec=5 diff --git a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 deleted file mode 100644 index eb7e5c64..00000000 --- a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless-proxy.socket.j2 +++ /dev/null @@ -1,11 +0,0 @@ -# {{ ansible_managed }} -# 2026052001 - -[Unit] -Description=Socket for on-demand Headless Google Chrome - -[Socket] -ListenStream={{ google_chrome__listen_address }}:{{ google_chrome__listen_port }} - -[Install] -WantedBy=sockets.target diff --git a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 b/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 deleted file mode 100644 index cfff2f55..00000000 --- a/roles/google_chrome/templates/etc/systemd/system/google-chrome-headless.service.j2 +++ /dev/null @@ -1,54 +0,0 @@ -# {{ ansible_managed }} -# 2026052001 - -[Unit] -Description=Headless Google Chrome -BindsTo=google-chrome-headless-proxy.service - -[Service] -Type=simple -User=chrome -Group=chrome -ExecStart={{ __google_chrome__binary_path }} \ - --headless=new \ - --disable-gpu \ - --no-first-run \ - --no-default-browser-check \ - --hide-scrollbars \ - --disable-dev-shm-usage \ - --remote-debugging-address={{ google_chrome__listen_address }} \ - --remote-debugging-port={{ google_chrome__backend_port }} \ - --remote-allow-origins=http://{{ google_chrome__listen_address }}:{{ google_chrome__listen_port }} \ -{% for arg in google_chrome__extra_args__combined_var if arg['state'] | d('present') != 'absent' %} - {{ arg['name'] }} \ -{% endfor %} - --user-data-dir={{ google_chrome__user_data_dir }} -Restart=on-failure -RestartSec=5 - -PrivateDevices=true -ProtectClock=true -NoNewPrivileges=true -RemoveIPC=true -PrivateTmp=true -ProtectSystem=strict -ProtectHome=true -ReadWritePaths={{ google_chrome__user_data_dir }} -ProtectKernelTunables=true -ProtectKernelModules=true -ProtectControlGroups=true -RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX -RestrictNamespaces=~cgroup uts ipc -LockPersonality=true -MemoryDenyWriteExecute=false -CapabilityBoundingSet= -AmbientCapabilities= -SystemCallArchitectures=native -ProtectKernelLogs=true -ProtectHostname=true -ProtectClockSetting=true -ProtectProc=invisible -ProcSubset=pid -RestrictSUIDSGID=true -RestrictRealtime=true -UMask=0077 diff --git a/roles/google_chrome/vars/RedHat.yml b/roles/google_chrome/vars/RedHat.yml deleted file mode 100644 index f79f18d3..00000000 --- a/roles/google_chrome/vars/RedHat.yml +++ /dev/null @@ -1,5 +0,0 @@ -__google_chrome__binary_path: '/usr/bin/google-chrome-stable' -__google_chrome__packages: - - 'gnu-free-sans-fonts' - - 'google-chrome-stable' - - 'mesa-libOSMesa' diff --git a/roles/icingaweb2_module_pdfexport/README.md b/roles/icingaweb2_module_pdfexport/README.md index fcbf5a78..51a028bc 100644 --- a/roles/icingaweb2_module_pdfexport/README.md +++ b/roles/icingaweb2_module_pdfexport/README.md @@ -15,17 +15,17 @@ This role is tested with the following IcingaWeb2 PDF Export Module versions: * The Tarball for `icingaweb2_module_pdfexport__version` is downloaded on the Ansible controller (`delegate_to: 'localhost'`, `run_once: true`), then copied to the target. The controller therefore needs Internet access to GitHub; the target does not. * On every role run the directory `/usr/share/icingaweb2/modules/pdfexport` is overwritten with the contents of the configured version. To upgrade or downgrade the module, change `icingaweb2_module_pdfexport__version` and re-run the role. * `icingacli module enable pdfexport` is only invoked when `/etc/icingaweb2/enabledModules/pdfexport` does not yet exist (idempotent). -* `/etc/icingaweb2/modules/pdfexport/config.ini` is deployed on every run. By default the module is wired to a running headless Chrome over the Chrome DevTools Protocol (CDP); set `icingaweb2_module_pdfexport__chrome_binary` to fall back to spawning Chrome locally on every export. -* This role only installs and configures the IcingaWeb2 module itself. The headless browser backend it talks to (see the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements)) is provided separately by the [linuxfabrik.lfops.google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) role. +* `/etc/icingaweb2/modules/pdfexport/config.ini` is deployed on every run. By default the module is wired to a running headless Chromium over the Chrome DevTools Protocol (CDP); set `icingaweb2_module_pdfexport__chrome_binary` to fall back to spawning Chromium locally on every export. +* This role only installs and configures the IcingaWeb2 module itself. The headless browser backend it talks to (see the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements)) is provided separately by the [linuxfabrik.lfops.chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless) role. ## Mandatory Requirements * A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. * Internet access from the Ansible controller (downloads from `https://github.com/Icinga/icingaweb2-module-pdfexport/archive/`). -* A running headless Chrome instance providing the remote debugging interface this module talks to. This can be done using the [linuxfabrik.lfops.google_chrome](https://github.com/Linuxfabrik/lfops/tree/main/roles/google_chrome) role. +* A running headless Chromium instance providing the remote debugging interface this module talks to. This can be done using the [linuxfabrik.lfops.chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless) role. -If you use the [IcingaWeb2 PDF Export Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2_module_pdfexport.yml), the headless Chrome backend is automatically installed for you. +If you use the [IcingaWeb2 PDF Export Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2_module_pdfexport.yml), the headless Chromium backend is automatically installed for you. ## Tags @@ -67,15 +67,15 @@ icingaweb2_module_pdfexport__version: 'v0.11.0' `icingaweb2_module_pdfexport__chrome_host` -* Address of the headless Chrome instance the module connects to via the Chrome DevTools Protocol. +* Address of the headless Chromium instance the module connects to via the Chrome DevTools Protocol. * Type: String. -* Default: `'{{ google_chrome__listen_address | d("127.0.0.1") }}'` +* Default: `'{{ chromium_headless__listen_address | d("127.0.0.1") }}'` `icingaweb2_module_pdfexport__chrome_port` -* Port of the headless Chrome instance the module connects to via the Chrome DevTools Protocol. +* Port of the headless Chromium instance the module connects to via the Chrome DevTools Protocol. * Type: Number. -* Default: `'{{ google_chrome__listen_port | d(9222) }}'` +* Default: `'{{ chromium_headless__listen_port | d(9222) }}'` `icingaweb2_module_pdfexport__force_temp_storage` @@ -87,7 +87,7 @@ Example: ```yaml # optional -icingaweb2_module_pdfexport__chrome_binary: '/usr/bin/google-chrome-stable' +icingaweb2_module_pdfexport__chrome_binary: '/usr/lib64/chromium-browser/headless_shell' icingaweb2_module_pdfexport__chrome_host: '127.0.0.1' icingaweb2_module_pdfexport__chrome_port: 9222 icingaweb2_module_pdfexport__force_temp_storage: false diff --git a/roles/icingaweb2_module_pdfexport/defaults/main.yml b/roles/icingaweb2_module_pdfexport/defaults/main.yml index 1bd3a3ef..bc0b8ba7 100644 --- a/roles/icingaweb2_module_pdfexport/defaults/main.yml +++ b/roles/icingaweb2_module_pdfexport/defaults/main.yml @@ -1,4 +1,4 @@ icingaweb2_module_pdfexport__chrome_binary: '' -icingaweb2_module_pdfexport__chrome_host: '{{ google_chrome__listen_address | d("127.0.0.1") }}' -icingaweb2_module_pdfexport__chrome_port: '{{ google_chrome__listen_port | d(9222) }}' +icingaweb2_module_pdfexport__chrome_host: '{{ chromium_headless__listen_address | d("127.0.0.1") }}' +icingaweb2_module_pdfexport__chrome_port: '{{ chromium_headless__listen_port | d(9222) }}' icingaweb2_module_pdfexport__force_temp_storage: false diff --git a/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml b/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml index 469ff4a1..593bfc3e 100644 --- a/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml +++ b/roles/icingaweb2_module_pdfexport/meta/argument_specs.yml @@ -6,17 +6,17 @@ argument_specs: type: 'str' required: false default: '' - description: 'Path to a local Chrome/Chromium binary. If set, the module spawns Chrome locally on every export and the host/port settings are ignored.' + description: 'Path to a local Chrome/Chromium binary. If set, the module spawns Chromium locally on every export and the host/port settings are ignored.' icingaweb2_module_pdfexport__chrome_host: type: 'str' required: false - description: 'Listen address of the headless Chrome instance the module connects to. Defaults to google_chrome__listen_address.' + description: 'Listen address of the headless Chromium instance the module connects to. Defaults to chromium_headless__listen_address.' icingaweb2_module_pdfexport__chrome_port: type: 'raw' required: false - description: 'Listen port of the headless Chrome instance the module connects to. Defaults to google_chrome__listen_port.' + description: 'Listen port of the headless Chromium instance the module connects to. Defaults to chromium_headless__listen_port.' icingaweb2_module_pdfexport__force_temp_storage: type: 'bool' diff --git a/roles/repo_google_chrome/README.md b/roles/repo_google_chrome/README.md deleted file mode 100644 index 7da86d69..00000000 --- a/roles/repo_google_chrome/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Ansible Role linuxfabrik.lfops.repo_google_chrome - -This role deploys the package repository for [Google Chrome](https://www.google.com/chrome/) on RHEL-based distributions. - - -*Available since LFOps `6.0.2`.* - - -## Tags - -`repo_google_chrome` - -* Deploys the Google Chrome Repository. -* Triggers: none. - - -## Optional Role Variables - -`repo_google_chrome__basic_auth_login` - -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. -* Type: String. -* Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` - -`repo_google_chrome__mirror_url` - -* Set the URL to a custom mirror server providing the repository. Defaults to `lfops__repo_mirror_url` to allow easily setting the same URL for all `repo_*` roles. If `lfops__repo_mirror_url` is not set, the default mirrors of the repo are used. -* Type: String. -* Default: `'{{ lfops__repo_mirror_url | default("") }}'` - -Example: -```yaml -# optional -repo_google_chrome__basic_auth_login: - username: 'my-username' - password: 'linuxfabrik' -repo_google_chrome__mirror_url: 'https://mirror.example.com' -``` - - -## License - -[The Unlicense](https://unlicense.org/) - - -## Author Information - -[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/repo_google_chrome/defaults/main.yml b/roles/repo_google_chrome/defaults/main.yml deleted file mode 100644 index 69f976e3..00000000 --- a/roles/repo_google_chrome/defaults/main.yml +++ /dev/null @@ -1,2 +0,0 @@ -repo_google_chrome__basic_auth_login: '{{ lfops__repo_basic_auth_login | default("") }}' -repo_google_chrome__mirror_url: '{{ lfops__repo_mirror_url | default("") }}' diff --git a/roles/repo_google_chrome/meta/argument_specs.yml b/roles/repo_google_chrome/meta/argument_specs.yml deleted file mode 100644 index 2a67df11..00000000 --- a/roles/repo_google_chrome/meta/argument_specs.yml +++ /dev/null @@ -1,23 +0,0 @@ -argument_specs: - main: - options: - - repo_google_chrome__basic_auth_login: - # 'raw' rather than 'dict', because the default in defaults/main.yml - # resolves to '' (empty string) when lfops__repo_basic_auth_login is - # not set; a strict 'dict' spec would reject the empty default. - type: 'raw' - required: false - description: >- - HTTP basic auth credentials for the Google Chrome repository. - Expected as a dict with `username` and `password` keys. Typically - fed by `linuxfabrik.lfops.bitwarden_item`, which returns the full - Bitwarden item with additional keys. - - repo_google_chrome__mirror_url: - type: 'str' - required: false - description: >- - URL of a custom mirror server providing the repository. Defaults - to `lfops__repo_mirror_url`; if that is also unset, the default - upstream mirrors are used. diff --git a/roles/repo_google_chrome/tasks/RedHat.yml b/roles/repo_google_chrome/tasks/RedHat.yml deleted file mode 100644 index 83a5f6cf..00000000 --- a/roles/repo_google_chrome/tasks/RedHat.yml +++ /dev/null @@ -1,40 +0,0 @@ -- block: - - - name: 'curl https://dl-ssl.google.com/linux/linux_signing_key.pub --output /tmp/ansible.google-chrome.key' - ansible.builtin.get_url: - url: 'https://dl-ssl.google.com/linux/linux_signing_key.pub' - dest: '/tmp/ansible.google-chrome.key' - mode: 0o644 - delegate_to: 'localhost' - become: false - run_once: true - changed_when: false # not an actual config change on the server - check_mode: false # run task even if `--check` is specified - - - name: 'copy /tmp/ansible.google-chrome.key to /etc/pki/rpm-gpg/google-chrome.key' - ansible.builtin.copy: - src: '/tmp/ansible.google-chrome.key' - dest: '/etc/pki/rpm-gpg/google-chrome.key' - owner: 'root' - group: 'root' - mode: 0o644 - - # https://www.google.com/linuxrepositories/ - - name: 'deploy the Google Chrome repo (mirror: {{ repo_google_chrome__mirror_url }})' - ansible.builtin.template: - backup: true - src: 'etc/yum.repos.d/google-chrome.repo.j2' - dest: '/etc/yum.repos.d/google-chrome.repo' - owner: 'root' - group: 'root' - mode: 0o644 - - - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' - ansible.builtin.include_role: - name: 'shared' - tasks_from: 'remove-rpmnew-rpmsave.yml' - vars: - shared__remove_rpmnew_rpmsave_config_file: '/etc/yum.repos.d/google-chrome.repo' - - tags: - - 'repo_google_chrome' diff --git a/roles/repo_google_chrome/tasks/main.yml b/roles/repo_google_chrome/tasks/main.yml deleted file mode 100644 index 4f290d1f..00000000 --- a/roles/repo_google_chrome/tasks/main.yml +++ /dev/null @@ -1,18 +0,0 @@ -- name: 'Perform platform/version specific tasks' - ansible.builtin.include_tasks: '{{ __task_file }}' - when: '__task_file | length > 0' - vars: - __task_file: '{{ lookup("ansible.builtin.first_found", __first_found_options) }}' - __first_found_options: - files: - - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_version"] }}.yml' - - '{{ ansible_facts["distribution"] }}{{ ansible_facts["distribution_major_version"] }}.yml' - - '{{ ansible_facts["distribution"] }}.yml' - - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_version"] }}.yml' - - '{{ ansible_facts["os_family"] }}{{ ansible_facts["distribution_major_version"] }}.yml' - - '{{ ansible_facts["os_family"] }}.yml' - paths: - - '{{ role_path }}/tasks' - skip: true - tags: - - 'always' diff --git a/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 b/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 deleted file mode 100644 index ff8375bc..00000000 --- a/roles/repo_google_chrome/templates/etc/yum.repos.d/google-chrome.repo.j2 +++ /dev/null @@ -1,20 +0,0 @@ -# {{ ansible_managed }} -# 2026051201 - -[google-chrome] -name=google-chrome -{% if repo_google_chrome__mirror_url is defined and repo_google_chrome__mirror_url | length %} -baseurl={{ repo_google_chrome__mirror_url }}/linux/chrome/rpm/stable/$basearch -{% else %} -baseurl=https://dl.google.com/linux/chrome/rpm/stable/$basearch -{% endif %} -enabled=1 -gpgcheck=1 -repo_gpgcheck=1 -gpgkey=file:///etc/pki/rpm-gpg/google-chrome.key -sslverify=1 -sslcacert=/etc/pki/tls/certs/ca-bundle.crt -{% if repo_google_chrome__basic_auth_login is defined and repo_google_chrome__basic_auth_login | length %} -username={{ repo_google_chrome__basic_auth_login["username"] }} -password={{ repo_google_chrome__basic_auth_login["password"] }} -{% endif %} From b7dd3c8f52c9671f06c42c1ccca3208ddc753c4c Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:08:22 +0200 Subject: [PATCH 15/66] fix(roles/chromium_headless): keep Chromium debugging port bound to localhost Chromium's --remote-debugging-address now always binds 127.0.0.1 instead of chromium_headless__listen_address. Only the proxy socket is meant to be the public endpoint; binding the backend to a routable listen_address exposed Chromium's unauthenticated CDP port off-host and let clients bypass the idle-managed proxy. The proxy and the ExecStartPost health check connect to 127.0.0.1 accordingly. Also documents why MemoryDenyWriteExecute must stay false (V8 JIT) and bumps the two unit-template timestamps. --- roles/chromium_headless/README.md | 2 +- .../etc/systemd/system/chromium-headless-proxy.service.j2 | 7 ++++--- .../etc/systemd/system/chromium-headless.service.j2 | 8 +++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/roles/chromium_headless/README.md b/roles/chromium_headless/README.md index f83f377e..b2935150 100644 --- a/roles/chromium_headless/README.md +++ b/roles/chromium_headless/README.md @@ -81,7 +81,7 @@ If you use the [Chromium Headless Playbook](https://github.com/Linuxfabrik/lfops `chromium_headless__listen_address` -* Address the proxy socket binds to. Keep this on `127.0.0.1` unless you intentionally want to expose it to other hosts; neither the proxy nor Chromium enforces TLS or authentication. +* Address the `chromium-headless-proxy.socket` binds to. Chromium's own debugging port always stays on `127.0.0.1` regardless of this setting, so only the proxy is reachable here. Keep this on `127.0.0.1` unless you intentionally want to expose the proxy to other hosts; it enforces neither TLS nor authentication. * Type: String. * Default: `'127.0.0.1'` diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 index 3a7343b5..cf7c5238 100644 --- a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026052101 +# 2026052102 [Unit] Description=Proxy to on-demand Headless Chromium @@ -7,11 +7,12 @@ Requires=chromium-headless.service After=chromium-headless.service [Service] +{# The proxy always connects to the backend on 127.0.0.1, never chromium_headless__listen_address: Chromium binds its unauthenticated debugging port locally only (see chromium-headless.service). #} {# --exit-idle-time was added in systemd 246. EL8 ships systemd 239 and rejects the option, so we omit it there; the proxy (and the Chromium service bound to it) then stays resident once activated. EL9 (systemd 252) and newer support it. #} {% if ansible_facts['distribution_major_version'] | int >= 9 %} -ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ chromium_headless__idle_timeout }} {{ chromium_headless__listen_address }}:{{ chromium_headless__backend_port }} +ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ chromium_headless__idle_timeout }} 127.0.0.1:{{ chromium_headless__backend_port }} {% else %} -ExecStart=/usr/lib/systemd/systemd-socket-proxyd {{ chromium_headless__listen_address }}:{{ chromium_headless__backend_port }} +ExecStart=/usr/lib/systemd/systemd-socket-proxyd 127.0.0.1:{{ chromium_headless__backend_port }} {% endif %} PrivateTmp=true Restart=on-failure diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 index 837df4ef..92b06af0 100644 --- a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless.service.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026052101 +# 2026052102 [Unit] Description=Headless Chromium @@ -9,6 +9,7 @@ BindsTo=chromium-headless-proxy.service Type=simple User=chromium Group=chromium +# The backend always binds 127.0.0.1, never chromium_headless__listen_address. Only the proxy socket is the public endpoint (it may bind a routable listen_address); Chromium's raw remote-debugging port has no authentication, so it must never be reachable off-host. The proxy connects to it locally. ExecStart={{ __chromium_headless__binary_path }} \ --headless=new \ --disable-gpu \ @@ -16,7 +17,7 @@ ExecStart={{ __chromium_headless__binary_path }} \ --no-default-browser-check \ --hide-scrollbars \ --disable-dev-shm-usage \ - --remote-debugging-address={{ chromium_headless__listen_address }} \ + --remote-debugging-address=127.0.0.1 \ --remote-debugging-port={{ chromium_headless__backend_port }} \ --remote-allow-origins=http://{{ chromium_headless__listen_address }}:{{ chromium_headless__listen_port }} \ {% for arg in chromium_headless__extra_args__combined_var if arg['state'] | d('present') != 'absent' %} @@ -24,7 +25,7 @@ ExecStart={{ __chromium_headless__binary_path }} \ {% endfor %} --user-data-dir={{ chromium_headless__user_data_dir }} # Chromium binds its debugging port a moment after the process starts and does not implement systemd socket activation. Block the start job until the port accepts connections, otherwise systemd-socket-proxyd (ordered After= this unit) connects too early and the first request fails with "Connection refused". -ExecStartPost=/bin/bash -c 'until (echo > /dev/tcp/{{ chromium_headless__listen_address }}/{{ chromium_headless__backend_port }}) 2>/dev/null; do sleep 0.1; done' +ExecStartPost=/bin/bash -c 'until (echo > /dev/tcp/127.0.0.1/{{ chromium_headless__backend_port }}) 2>/dev/null; do sleep 0.1; done' Restart=on-failure RestartSec=5 # Chromium traps SIGTERM and exits 143 (128 + SIGTERM); treat that as a clean shutdown so stopping the bound proxy on idle does not mark this unit failed. @@ -44,6 +45,7 @@ ProtectControlGroups=true RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX RestrictNamespaces=~cgroup uts ipc LockPersonality=true +# Must stay false: Chromium's V8 JIT maps writable+executable pages, which MemoryDenyWriteExecute=true would kill at startup. MemoryDenyWriteExecute=false CapabilityBoundingSet= AmbientCapabilities= From 22aa56465db09dfedb99b05b3aff228903d00b73 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:08:53 +0200 Subject: [PATCH 16/66] style(roles/chromium_headless): align defaults order and internal naming with example --- roles/chromium_headless/defaults/main.yml | 8 ++++---- roles/chromium_headless/tasks/main.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/roles/chromium_headless/defaults/main.yml b/roles/chromium_headless/defaults/main.yml index 7073336b..52fa488d 100644 --- a/roles/chromium_headless/defaults/main.yml +++ b/roles/chromium_headless/defaults/main.yml @@ -1,4 +1,8 @@ chromium_headless__backend_port: 9223 +chromium_headless__extra_args__dependent_var: [] +chromium_headless__extra_args__group_var: [] +chromium_headless__extra_args__host_var: [] +chromium_headless__extra_args__role_var: [] chromium_headless__extra_args__combined_var: '{{ ( chromium_headless__extra_args__role_var + chromium_headless__extra_args__dependent_var + @@ -6,10 +10,6 @@ chromium_headless__extra_args__combined_var: '{{ ( chromium_headless__extra_args__host_var ) | linuxfabrik.lfops.combine_lod }}' -chromium_headless__extra_args__dependent_var: [] -chromium_headless__extra_args__group_var: [] -chromium_headless__extra_args__host_var: [] -chromium_headless__extra_args__role_var: [] chromium_headless__idle_timeout: 300 chromium_headless__listen_address: '127.0.0.1' chromium_headless__listen_port: 9222 diff --git a/roles/chromium_headless/tasks/main.yml b/roles/chromium_headless/tasks/main.yml index 7cd20d98..c7294aaa 100644 --- a/roles/chromium_headless/tasks/main.yml +++ b/roles/chromium_headless/tasks/main.yml @@ -99,7 +99,7 @@ owner: 'root' group: 'root' mode: 0o644 - register: '__chromium_headless__deploy_chrome_result' + register: '__chromium_headless__deploy_service_result' - name: 'Remove rpmnew / rpmsave (and Debian equivalents)' ansible.builtin.include_role: @@ -121,7 +121,7 @@ when: - '__chromium_headless__deploy_socket_result is changed or __chromium_headless__deploy_proxy_result is changed or - __chromium_headless__deploy_chrome_result is changed' + __chromium_headless__deploy_service_result is changed' tags: - 'chromium_headless' From 04b92516fdc6b3b34c615ecb275e504b9d0d7649 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:09:20 +0200 Subject: [PATCH 17/66] docs(changelog): condense chromium_headless and pdfexport entries --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 553fb093..8a93e9a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* **role:chromium_headless**: New role. Installs the headless Chromium shell (`chromium-headless` from EPEL) together with the runtime libraries and fonts required for headless rendering, and sets up a socket-activated, hardened `chromium-headless` systemd stack (socket + `systemd-socket-proxyd` + the actual Chromium service, wired with `BindsTo`). Chromium is started on the first incoming connection and stopped again after `chromium_headless__idle_timeout` seconds of inactivity, so no RAM is wasted while the backend is unused. The role also flips two SELinux booleans on enforcing hosts: `systemd_socket_proxyd_bind_any` so the socket unit can bind the listen port (on Rocky/RHEL 9 the default `9222` carries the `hplip_port_t` label, which would otherwise reject the bind), and `systemd_socket_proxyd_connect_any` so the proxy can reach Chromium on its non-standard backend port. Provides the headless browser backend that the Icinga Web 2 PDF Export Module talks to, without pulling in Google's proprietary repository. +* **role:chromium_headless**: New role. Provides a hardened, socket-activated headless Chromium backend (started on the first request, stopped again after an idle timeout, so it uses no RAM while unused) for tools such as the Icinga Web 2 PDF Export Module. Installs `chromium-headless` from EPEL instead of Google's proprietary repository. * **role:sshd**: Add Debian 13 support. * **role:mirror**: Document the new per-repository `newest_only` subkey on `mirror__reposync_repos` entries. Defaults to `true` (only the newest version of each package is mirrored). Set to `false` for repositories that publish multiple versions in parallel, such as Icinga, where older versions must remain available. * **role:repo_remi**: Add RHEL 10 / Rocky 10 support (new GPG key, repo templates, and module-stream tasks for EL 10). @@ -64,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -* **role:icingaweb2_module_pdfexport, playbooks/icingaweb2_module_pdfexport, playbooks/setup_icinga2_master**: The headless browser backend the module requires was not installed by any role and had to be configured manually, so fresh deployments ended up without working PDF export. The new `chromium_headless` role now provides a hardened `chromium-headless.service`, and both `icingaweb2_module_pdfexport.yml` and `setup_icinga2_master.yml` wire it up with `*__skip_*` opt-out variables (in `setup_icinga2_master.yml` the defaults track the existing `icingaweb2_module_pdfexport__skip_role` flag). The role also gained `/etc/icingaweb2/modules/pdfexport/config.ini` deployment with four new variables (`icingaweb2_module_pdfexport__chrome_host`, `__chrome_port`, `__chrome_binary`, `__force_temp_storage`); by default it talks to the `chromium-headless.service` over the Chrome DevTools Protocol, falling back to a local Chromium binary only if `chrome_binary` is set explicitly. +* **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. * **roles**: Set `become: false` on tasks delegated to localhost across the collection. Previously these tasks inherited `become: true` from the playbook level and tried to call `sudo` on the Ansible controller, which fails on controllers without a passwordless sudo setup with `sudo: a password is required`. Affected are all `repo_*` roles, the `*_vm` cloud roles (`exoscale_vm`, `hetzner_vm`, `infomaniak_vm`), all `icingaweb2_module_*` roles that download artefacts, `monitoring_plugins`, `shared`, plus several others. Existing playbooks that were working without playbook-level `become: true` are unaffected ([#242](https://github.com/Linuxfabrik/lfops/issues/242)). From fb859d4f975af6260b42f4fb5630292d684f5d82 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:10:22 +0200 Subject: [PATCH 18/66] docs(contributing): list chromium_headless under roles with special features --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 71196ae8..a76985b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -672,6 +672,11 @@ The following roles use techniques that are unusual within LFOps. Roles not in t * [grav](https://github.com/Linuxfabrik/lfops/tree/main/roles/grav): Four separate `chmod` passes (files `664`, `bin/` `775`, directories `775`, plus a setgid pass on directories), each registered with `changed_when` based on the `--changes` output for idempotency. +#### systemd socket activation with an on-demand backend + +* [chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless): Fronts a long-running daemon (Chromium, which does not implement the systemd socket-activation protocol) with a `systemd-socket-proxyd`. A `.socket` unit binds the public port, the proxy forwards to the backend on `127.0.0.1` and exits after an idle timeout, and `BindsTo=` ties the backend's lifecycle to the proxy so it starts on the first request and stops when idle. + + #### Other * [apache_solr](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_solr): Picks the matching OpenJDK package for the configured Solr major version (Solr 9 → OpenJDK 17, Solr 8 → OpenJDK 8) via a per-major-version lookup in `vars/main.yml`. From dfad123b92100742cc368eba29e16ce348768f45 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:21:53 +0200 Subject: [PATCH 19/66] style(roles/icingaweb2_module_pdfexport): silence risky-file-permissions on temp-file tasks --- roles/icingaweb2_module_pdfexport/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/icingaweb2_module_pdfexport/tasks/main.yml b/roles/icingaweb2_module_pdfexport/tasks/main.yml index 333ce09f..60b519e2 100644 --- a/roles/icingaweb2_module_pdfexport/tasks/main.yml +++ b/roles/icingaweb2_module_pdfexport/tasks/main.yml @@ -19,7 +19,7 @@ group: 'icingaweb2' mode: 0o755 - - name: 'curl https://github.com/Icinga/icingaweb2-module-pdfexport/archive/{{ icingaweb2_module_pdfexport__version }}.tar.gz --output /tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz' + - name: 'curl https://github.com/Icinga/icingaweb2-module-pdfexport/archive/{{ icingaweb2_module_pdfexport__version }}.tar.gz --output /tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz' # noqa risky-file-permissions (temporary file) ansible.builtin.get_url: url: 'https://github.com/Icinga/icingaweb2-module-pdfexport/archive/{{ icingaweb2_module_pdfexport__version }}.tar.gz' dest: '/tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz' @@ -28,7 +28,7 @@ run_once: true check_mode: false # run task even if `--check` is specified - - name: 'copy /tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz to the server' + - name: 'copy /tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz to the server' # noqa risky-file-permissions (temporary file) ansible.builtin.copy: src: '/tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz' dest: '/tmp/ansible.icingaweb2-module-pdfexport-{{ icingaweb2_module_pdfexport__version }}.tar.gz' From 09becaccd8f8cbdcd82c6dabf57b745eae2f048b Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 18:32:53 +0200 Subject: [PATCH 20/66] feat(roles/chromium_headless): add Debian support --- COMPATIBILITY.md | 2 +- playbooks/chromium_headless.yml | 1 + playbooks/icingaweb2_module_pdfexport.yml | 1 + roles/chromium_headless/README.md | 4 ++-- .../etc/systemd/system/chromium-headless-proxy.service.j2 | 6 +++--- roles/chromium_headless/vars/Debian.yml | 6 ++++++ roles/chromium_headless/vars/RedHat.yml | 2 ++ roles/chromium_headless/vars/RedHat8.yml | 2 ++ 8 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 roles/chromium_headless/vars/Debian.yml create mode 100644 roles/chromium_headless/vars/RedHat8.yml diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 99413fbd..de868c1f 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -16,7 +16,7 @@ Which Ansible role is proven to run on which OS? | bind | | | x | x | x | | | | | | blocky | | | x | x | (x) | | | | | | borg_local | | | x | (x) | (x) | | | | | -| chromium_headless | | | x | x | (x) | | | | | +| chromium_headless | x | x | x | x | (x) | | | | | | chrony | | | x | x | x | | | | | | clamav | | | x | x | (x) | | | | | | cloud_init | (x) | (x) | x | x | x | (x) | (x) | (x) | | diff --git a/playbooks/chromium_headless.yml b/playbooks/chromium_headless.yml index 0500b191..ce4ca69a 100644 --- a/playbooks/chromium_headless.yml +++ b/playbooks/chromium_headless.yml @@ -14,6 +14,7 @@ - role: 'linuxfabrik.lfops.repo_epel' when: + - 'ansible_facts["os_family"] == "RedHat"' - 'not chromium_headless__skip_repo_epel | d(false) | bool' - role: 'linuxfabrik.lfops.chromium_headless' diff --git a/playbooks/icingaweb2_module_pdfexport.yml b/playbooks/icingaweb2_module_pdfexport.yml index 57ce3f4e..4724eee3 100644 --- a/playbooks/icingaweb2_module_pdfexport.yml +++ b/playbooks/icingaweb2_module_pdfexport.yml @@ -14,6 +14,7 @@ - role: 'linuxfabrik.lfops.repo_epel' when: + - 'ansible_facts["os_family"] == "RedHat"' - 'not icingaweb2_module_pdfexport__skip_repo_epel | d(false) | bool' - role: 'linuxfabrik.lfops.chromium_headless' diff --git a/roles/chromium_headless/README.md b/roles/chromium_headless/README.md index b2935150..4c292e95 100644 --- a/roles/chromium_headless/README.md +++ b/roles/chromium_headless/README.md @@ -20,9 +20,9 @@ The setup is used as a headless browser backend for tools such as the [Icinga We ## Mandatory Requirements -* Enable the EPEL repository, which provides `chromium-headless`. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. +* On Red Hat-family systems, enable the EPEL repository, which provides `chromium-headless`. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. On Debian, the `chromium-headless-shell` package ships in the default repositories, so no extra repository is required. -If you use the [Chromium Headless Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/chromium_headless.yml), this is automatically done for you. +If you use the [Chromium Headless Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/chromium_headless.yml), the EPEL repository is enabled for you on Red Hat-family systems. ## Tags diff --git a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 index cf7c5238..8e773f99 100644 --- a/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 +++ b/roles/chromium_headless/templates/etc/systemd/system/chromium-headless-proxy.service.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026052102 +# 2026052103 [Unit] Description=Proxy to on-demand Headless Chromium @@ -8,8 +8,8 @@ After=chromium-headless.service [Service] {# The proxy always connects to the backend on 127.0.0.1, never chromium_headless__listen_address: Chromium binds its unauthenticated debugging port locally only (see chromium-headless.service). #} -{# --exit-idle-time was added in systemd 246. EL8 ships systemd 239 and rejects the option, so we omit it there; the proxy (and the Chromium service bound to it) then stays resident once activated. EL9 (systemd 252) and newer support it. #} -{% if ansible_facts['distribution_major_version'] | int >= 9 %} +{# --exit-idle-time was added in systemd 246. Platforms whose systemd predates it (EL8 ships 239) omit the option via __chromium_headless__proxy_exit_idle_time_supported; the proxy (and the Chromium service bound to it) then stays resident once activated. #} +{% if __chromium_headless__proxy_exit_idle_time_supported | bool %} ExecStart=/usr/lib/systemd/systemd-socket-proxyd --exit-idle-time={{ chromium_headless__idle_timeout }} 127.0.0.1:{{ chromium_headless__backend_port }} {% else %} ExecStart=/usr/lib/systemd/systemd-socket-proxyd 127.0.0.1:{{ chromium_headless__backend_port }} diff --git a/roles/chromium_headless/vars/Debian.yml b/roles/chromium_headless/vars/Debian.yml new file mode 100644 index 00000000..4f995b7a --- /dev/null +++ b/roles/chromium_headless/vars/Debian.yml @@ -0,0 +1,6 @@ +__chromium_headless__binary_path: '/usr/bin/chromium-headless-shell' +__chromium_headless__packages: + - 'chromium-headless-shell' + - 'fonts-freefont-ttf' +# Debian 12 (systemd 252) and newer support systemd-socket-proxyd --exit-idle-time (added in systemd 246). +__chromium_headless__proxy_exit_idle_time_supported: true diff --git a/roles/chromium_headless/vars/RedHat.yml b/roles/chromium_headless/vars/RedHat.yml index d31bf926..f045ac30 100644 --- a/roles/chromium_headless/vars/RedHat.yml +++ b/roles/chromium_headless/vars/RedHat.yml @@ -2,3 +2,5 @@ __chromium_headless__binary_path: '/usr/lib64/chromium-browser/headless_shell' __chromium_headless__packages: - 'chromium-headless' - 'gnu-free-sans-fonts' +# systemd-socket-proxyd --exit-idle-time (systemd 246+) is available on EL9 and newer; EL8 overrides this to false in RedHat8.yml. +__chromium_headless__proxy_exit_idle_time_supported: true diff --git a/roles/chromium_headless/vars/RedHat8.yml b/roles/chromium_headless/vars/RedHat8.yml new file mode 100644 index 00000000..24046187 --- /dev/null +++ b/roles/chromium_headless/vars/RedHat8.yml @@ -0,0 +1,2 @@ +# EL8 ships systemd 239, which lacks systemd-socket-proxyd --exit-idle-time (added in systemd 246). +__chromium_headless__proxy_exit_idle_time_supported: false From 987b9d40945d0cd7ffa338a75b6b7eed3c652f62 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 18:41:21 +0200 Subject: [PATCH 21/66] fix(roles/redis): add missing vars for Debian --- CHANGELOG.md | 1 + roles/redis/vars/Debian.yml | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a93e9a8..5f08bff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **role:redis**: Added missing paths for running against Debian. * **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. diff --git a/roles/redis/vars/Debian.yml b/roles/redis/vars/Debian.yml index dcbd8acc..86f76a62 100644 --- a/roles/redis/vars/Debian.yml +++ b/roles/redis/vars/Debian.yml @@ -1,2 +1,9 @@ +__redis__config_dir: '/etc/redis' +__redis__config_file: 'redis.conf' +__redis__data_dir: '/var/lib/redis' +__redis__module_dir: '/etc/redis/modules' +__redis__package: 'redis-server' +__redis__runtime_dir: '/var/run/redis' + redis__conf_logfile: '/var/log/redis/redis-server.log' redis__service_name: 'redis-server.service' From 8f2f227e9a6b857c87bda14bc7b9f33c944d5206 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 22 May 2026 11:22:44 +0200 Subject: [PATCH 22/66] docs(compatibility): correct chromium_headless tested platforms --- COMPATIBILITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index de868c1f..132f6645 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -16,7 +16,7 @@ Which Ansible role is proven to run on which OS? | bind | | | x | x | x | | | | | | blocky | | | x | x | (x) | | | | | | borg_local | | | x | (x) | (x) | | | | | -| chromium_headless | x | x | x | x | (x) | | | | | +| chromium_headless | x | (x) | x | x | x | | | | | | chrony | | | x | x | x | | | | | | clamav | | | x | x | (x) | | | | | | cloud_init | (x) | (x) | x | x | x | (x) | (x) | (x) | | From 5ca8fe9c399cbccbcbad6ae4cef5a76442c16de5 Mon Sep 17 00:00:00 2001 From: Ali Bhatti Date: Sat, 16 May 2026 19:02:34 +0200 Subject: [PATCH 23/66] feat(roles/graylog_datanode, roles/graylog_server): add template for Graylog 7.1 --- CHANGELOG.md | 1 + .../etc/graylog/datanode/7.1-datanode.conf.j2 | 175 ++++ .../etc/graylog/server/7.1-server.conf.j2 | 827 ++++++++++++++++++ 3 files changed, 1003 insertions(+) create mode 100644 roles/graylog_datanode/templates/etc/graylog/datanode/7.1-datanode.conf.j2 create mode 100644 roles/graylog_server/templates/etc/graylog/server/7.1-server.conf.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f08bff9..d7bc270b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * **role:chromium_headless**: New role. Provides a hardened, socket-activated headless Chromium backend (started on the first request, stopped again after an idle timeout, so it uses no RAM while unused) for tools such as the Icinga Web 2 PDF Export Module. Installs `chromium-headless` from EPEL instead of Google's proprietary repository. +* **role:graylog_datanode, role:graylog_server**: Add template for Graylog 7.1. * **role:sshd**: Add Debian 13 support. * **role:mirror**: Document the new per-repository `newest_only` subkey on `mirror__reposync_repos` entries. Defaults to `true` (only the newest version of each package is mirrored). Set to `false` for repositories that publish multiple versions in parallel, such as Icinga, where older versions must remain available. * **role:repo_remi**: Add RHEL 10 / Rocky 10 support (new GPG key, repo templates, and module-stream tasks for EL 10). diff --git a/roles/graylog_datanode/templates/etc/graylog/datanode/7.1-datanode.conf.j2 b/roles/graylog_datanode/templates/etc/graylog/datanode/7.1-datanode.conf.j2 new file mode 100644 index 00000000..512ec500 --- /dev/null +++ b/roles/graylog_datanode/templates/etc/graylog/datanode/7.1-datanode.conf.j2 @@ -0,0 +1,175 @@ +# {{ ansible_managed }} +# 2026051601 +# 7.1 +# see https://go2docs.graylog.org/7-1/setting_up_graylog/data_node_configuration_file.htm +##################################### +# GRAYLOG DATANODE CONFIGURATION FILE +##################################### +# +# This is the Graylog DataNode configuration file. The file has to use ISO 8859-1/Latin-1 character encoding. +# Characters that cannot be directly represented in this encoding can be written using Unicode escapes +# as defined in https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.3, using the \u prefix. +# For example, \u002c. +# +# * Entries are generally expected to be a single line of the form, one of the following: +# +# propertyName=propertyValue +# propertyName:propertyValue +# +# * White space that appears between the property name and property value is ignored, +# so the following are equivalent: +# +# name=Stephen +# name = Stephen +# +# * White space at the beginning of the line is also ignored. +# +# * Lines that start with the comment characters ! or # are ignored. Blank lines are also ignored. +# +# * The property value is generally terminated by the end of the line. White space following the +# property value is not ignored, and is treated as part of the property value. +# +# * A property value can span several lines if each line is terminated by a backslash (‘\’) character. +# For example: +# +# targetCities=\ +# Detroit,\ +# Chicago,\ +# Los Angeles +# +# This is equivalent to targetCities=Detroit,Chicago,Los Angeles (white space at the beginning of lines is ignored). +# +# * The characters newline, carriage return, and tab can be inserted with characters \n, \r, and \t, respectively. +# +# * The backslash character must be escaped as a double backslash. For example: +# +# path=c:\\docs\\doc1 +# + +# The auto-generated node ID will be stored in this file and read after restarts. It is a good idea +# to use an absolute file path here if you are starting Graylog DataNode from init scripts or similar. +node_id_file = /etc/graylog/datanode/node-id + +# location of your data-node configuration files - put additional files like manually created certificates etc. here +config_location = /etc/graylog/datanode + +# You MUST set a secret to secure/pepper the stored user passwords here. Use at least 64 characters. +# Generate one by using for example: pwgen -N 1 -s 96 +# ATTENTION: This value must be the same on all Graylog and Datanode nodes in the cluster. +# Changing this value after installation will render all user sessions and encrypted values in the database invalid. (e.g. encrypted access tokens) +password_secret = {{ graylog_datanode__password_secret }} + +# The default root user is named 'admin' +#root_username = admin + +# You MUST specify a hash password for the root user (which you only need to initially set up the +# system and in case you lose connectivity to your authentication backend) +# This password cannot be changed using the API or via the web interface. If you need to change it, +# modify it in this file. +# Create one by using for example: echo -n yourpassword | sha256sum +# and put the resulting hash value into the following line +root_password_sha2 = + +# connection to MongoDB, shared with the Graylog server +# See https://docs.mongodb.com/manual/reference/connection-string/ for details +mongodb_uri = {{ graylog_datanode__mongodb_uri }} + +#### HTTP bind address +# +# The network interface used by the Graylog DataNode to bind all services. +# +bind_address = {{ graylog_datanode__bind_address }} + +#### Hostname +# +# if you need to specify the hostname to use (because looking it up programmatically gives wrong results) +# hostname = + +#### HTTP port +# +# The port where the DataNode REST api is listening +# +datanode_http_port = {{ graylog_datanode__datanode_http_port }} + +#### HTTP publish URI +# +# This configuration should be used if you want to connect to this Graylog DataNode's REST API and it is available on +# another network interface than $http_bind_address, +# for example if the machine has multiple network interfaces or is behind a NAT gateway. +# http_publish_uri = + +#### OpenSearch HTTP port +# +# The port where OpenSearch HTTP is listening on +# +# opensearch_http_port = 9200 + +#### OpenSearch transport port +# +# The port where OpenSearch transports is listening on +# +# opensearch_transport_port = 9300 + +#### OpenSearch node name config option +# +# use this, if your node name should be different from the hostname that's found by programmatically looking it up +# +# node_name = + +#### OpenSearch discovery_seed_hosts config option +# +# if you're not using the automatic data node setup and want to create a cluster, you have to setup the discovery seed hosts +# +# opensearch_discovery_seed_hosts = + +#### OpenSearch initial_manager_nodes config option +# +# if you're not using the automatic data node setup and want to create a cluster, you have to setup the initial manager nodes +# make sure to remove this setting after the cluster has formed +# +# initial_cluster_manager_nodes = + +#### OpenSearch folders +# +# set these if you need OpenSearch to be located in a special place or want to include an existing version +# +# Root directory of the used opensearch distribution +opensearch_location = /usr/share/graylog-datanode/dist + +opensearch_config_location = /var/lib/graylog-datanode/opensearch/config +opensearch_data_location = {{ graylog_datanode__opensearch_data_location }} +opensearch_logs_location = /var/log/graylog-datanode/opensearch + +#### OpenSearch Certificate bundles for transport and http layer security +# +# if you're not using the automatic data node setup, you can manually configure your SSL certificates +# transport_certificate = datanode-transport-certificates.p12 +# transport_certificate_password = password +# http_certificate = datanode-http-certificates.p12 +# http_certificate_password = password + +#### OpenSearch log buffers size +# +# the number of lines from stderr and stdout of the OpenSearch process that are buffered inside the DataNode for logging etc. +# +# process_logs_buffer_size = 500 + +#### OpenSearch JWT token usage +# +# communication between Graylog and OpenSearch is secured by JWT. These are the defaults used for the token usage +# adjust them, if you have special needs. +# +# indexer_jwt_auth_token_caching_duration = 60s +# indexer_jwt_auth_token_expiration_duration = 180s + +opensearch_heap = {{ graylog_datanode__opensearch_heap }} + +{% if graylog_datanode__path_repos | length %} +#### Data Tiering Properties +node_search_cache_size = {{ graylog_datanode__node_search_cache_size }} +path_repo = {{ graylog_datanode__path_repos | join(',') }} +{% endif %} +{% if graylog_datanode__raw is defined and graylog_datanode__raw | length %} +#### Raw +{{ graylog_datanode__raw }} +{% endif %} diff --git a/roles/graylog_server/templates/etc/graylog/server/7.1-server.conf.j2 b/roles/graylog_server/templates/etc/graylog/server/7.1-server.conf.j2 new file mode 100644 index 00000000..29b0642f --- /dev/null +++ b/roles/graylog_server/templates/etc/graylog/server/7.1-server.conf.j2 @@ -0,0 +1,827 @@ +# {{ ansible_managed }} +# 2026051601 +# 7.1 +############################ +# GRAYLOG CONFIGURATION FILE +############################ +# +# This is the Graylog configuration file. The file has to use ISO 8859-1/Latin-1 character encoding. +# Characters that cannot be directly represented in this encoding can be written using Unicode escapes +# as defined in https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.3, using the \u prefix. +# For example, \u002c. +# +# * Entries are generally expected to be a single line of the form, one of the following: +# +# propertyName=propertyValue +# propertyName:propertyValue +# +# * White space that appears between the property name and property value is ignored, +# so the following are equivalent: +# +# name=Stephen +# name = Stephen +# +# * White space at the beginning of the line is also ignored. +# +# * Lines that start with the comment characters ! or # are ignored. Blank lines are also ignored. +# +# * The property value is generally terminated by the end of the line. White space following the +# property value is not ignored, and is treated as part of the property value. +# +# * A property value can span several lines if each line is terminated by a backslash (‘\’) character. +# For example: +# +# targetCities=\ +# Detroit,\ +# Chicago,\ +# Los Angeles +# +# This is equivalent to targetCities=Detroit,Chicago,Los Angeles (white space at the beginning of lines is ignored). +# +# * The characters newline, carriage return, and tab can be inserted with characters \n, \r, and \t, respectively. +# +# * The backslash character must be escaped as a double backslash. For example: +# +# path=c:\\docs\\doc1 +# + +# If you are running more than one instances of Graylog server you have to select one of these +# instances as leader. The leader will perform some periodical tasks that non-leaders won't perform. +is_leader = {{ graylog_server__is_leader | lower }} + +# The auto-generated node ID will be stored in this file and read after restarts. It is a good idea +# to use an absolute file path here if you are starting Graylog server from init scripts or similar. +node_id_file = /etc/graylog/server/node-id + +# You MUST set a secret to secure/pepper the stored user passwords here. Use at least 64 characters. +# Generate one by using for example: pwgen -N 1 -s 96 +# ATTENTION: This value must be the same on all Graylog nodes in the cluster. +# Changing this value after installation will render all user sessions and encrypted values in the database invalid. (e.g. encrypted access tokens) +password_secret = {{ graylog_server__password_secret }} + +# The default root user is named 'admin' +root_username = {{ graylog_server__root_user["username"] }} + +# You MUST specify a hash password for the root user (which you only need to initially set up the +# system and in case you lose connectivity to your authentication backend) +# This password cannot be changed using the API or via the web interface. If you need to change it, +# modify it in this file. +# Create one by using for example: echo -n yourpassword | sha256sum +# and put the resulting hash value into the following line +root_password_sha2 = {{ graylog_server__root_user["password"] | hash('sha256') }} + +# The email address of the root user. +# Default is empty +root_email = {{ graylog_server__root_user["email"] | default('') }} + +# The time zone setting of the root user. See http://www.joda.org/joda-time/timezones.html for a list of valid time zones. +# Default is UTC +root_timezone = {{ graylog_server__timezone | default('UTC') }} + +# Set the bin directory here (relative or absolute) +# This directory contains binaries that are used by the Graylog server. +# Default: bin +bin_dir = /usr/share/graylog-server/bin + +# Set the data directory here (relative or absolute) +# This directory is used to store Graylog server state. +data_dir = /var/lib/graylog-server + +# Set plugin directory here (relative or absolute) +plugin_dir = /usr/share/graylog-server/plugin + +############### +# HTTP settings +############### + +#### HTTP bind address +# +# The network interface used by the Graylog HTTP interface. +# +# This network interface must be accessible by all Graylog nodes in the cluster and by all clients +# using the Graylog web interface. +# +# If the port is omitted, Graylog will use port 9000 by default. +# +# Default: 127.0.0.1:9000 +#http_bind_address = 127.0.0.1:9000 +#http_bind_address = [2001:db8::1]:9000 +http_bind_address = {{ graylog_server__http_bind_address | default('127.0.0.1') }}:{{ graylog_server__http_bind_port | default('9000') }} + +#### HTTP publish URI +# +# The HTTP URI of this Graylog node which is used to communicate with the other Graylog nodes in the cluster and by all +# clients using the Graylog web interface. +# +# The URI will be published in the cluster discovery APIs, so that other Graylog nodes will be able to find and connect to this Graylog node. +# +# This configuration setting has to be used if this Graylog node is available on another network interface than $http_bind_address, +# for example if the machine has multiple network interfaces or is behind a NAT gateway. +# +# If $http_bind_address contains a wildcard IPv4 address (0.0.0.0), the first non-loopback IPv4 address of this machine will be used. +# This configuration setting *must not* contain a wildcard address! +# +# Default: http://$http_bind_address/ +#http_publish_uri = http://192.168.1.1:9000/ +{% if graylog_server__http_publish_uri | length %} +http_publish_uri = {{ graylog_server__http_publish_uri }} +{% endif %} + +#### External Graylog URI +# +# The public URI of Graylog which will be used by the Graylog web interface to communicate with the Graylog REST API. +# +# The external Graylog URI usually has to be specified, if Graylog is running behind a reverse proxy or load-balancer +# and it will be used to generate URLs addressing entities in the Graylog REST API (see $http_bind_address). +# +# When using Graylog Collector, this URI will be used to receive heartbeat messages and must be accessible for all collectors. +# +# This setting can be overridden on a per-request basis with the "X-Graylog-Server-URL" HTTP request header. +# +# Default: $http_publish_uri +#http_external_uri = + +#### Enable CORS headers for HTTP interface +# +# This allows browsers to make Cross-Origin requests from any origin. +# This is disabled for security reasons and typically only needed if running graylog +# with a separate server for frontend development. +# +# Default: false +#http_enable_cors = false + +#### Enable GZIP support for HTTP interface +# +# This compresses API responses and therefore helps to reduce +# overall round trip times. This is enabled by default. Uncomment the next line to disable it. +#http_enable_gzip = false + +# The maximum size of the HTTP request headers in bytes. +#http_max_header_size = 8192 + +# The size of the thread pool used exclusively for serving the HTTP interface. +#http_thread_pool_size = 64 + +################ +# HTTPS settings +################ + +#### Enable HTTPS support for the HTTP interface +# +# This secures the communication with the HTTP interface with TLS to prevent request forgery and eavesdropping. +# +# Default: false +#http_enable_tls = true + +# The X.509 certificate chain file in PEM format to use for securing the HTTP interface. +#http_tls_cert_file = /path/to/graylog.crt + +# The PKCS#8 private key file in PEM format to use for securing the HTTP interface. +#http_tls_key_file = /path/to/graylog.key + +# The password to unlock the private key used for securing the HTTP interface. +#http_tls_key_password = secret + +# If set to "true", Graylog will periodically investigate indices to figure out which fields are used in which streams. +# It will make field list in Graylog interface show only fields used in selected streams, but can decrease system performance, +# especially on systems with great number of streams and fields. +stream_aware_field_types=false + +# Comma separated list of trusted proxies that are allowed to set the client address with X-Forwarded-For +# header. May be subnets, or hosts. +#trusted_proxies = 127.0.0.1/32, 0:0:0:0:0:0:0:1/128 +{% if graylog_server__trusted_proxies | length %} +trusted_proxies = {{ graylog_server__trusted_proxies | join(', ') }} +{% endif %} + +# List of Elasticsearch hosts Graylog should connect to. +# Need to be specified as a comma-separated list of valid URIs for the http ports of your elasticsearch nodes. +# If one or more of your elasticsearch hosts require authentication, include the credentials in each node URI that +# requires authentication. +# +# Default: http://127.0.0.1:9200 +#elasticsearch_hosts = http://node1:9200,http://user:password@node2:9200 +{% if graylog_server__elasticsearch_hosts is defined and graylog_server__elasticsearch_hosts | length %} +elasticsearch_hosts = {{ graylog_server__elasticsearch_hosts | join(',') }} +{% endif %} + +# Maximum number of attempts to connect to elasticsearch on boot for the version probe. +# +# Default: 0, retry indefinitely with the given delay until a connection could be established +#elasticsearch_version_probe_attempts = 5 + +# Waiting time in between connection attempts for elasticsearch_version_probe_attempts +# +# Default: 5s +#elasticsearch_version_probe_delay = 5s + +# Maximum amount of time to wait for successful connection to Elasticsearch HTTP port. +# +# Default: 10 Seconds +#elasticsearch_connect_timeout = 10s + +# Maximum amount of time to wait for reading back a response from an Elasticsearch server. +# (e. g. during search, index creation, or index time-range calculations) +# +# Default: 60 seconds +#elasticsearch_socket_timeout = 60s + +# Maximum idle time for an Elasticsearch connection. If this is exceeded, this connection will +# be tore down. +# +# Default: inf +#elasticsearch_idle_timeout = -1s + +# Maximum number of total connections to Elasticsearch. +# +# Default: 200 +#elasticsearch_max_total_connections = 200 + +# Maximum number of total connections per Elasticsearch route (normally this means per +# elasticsearch server). +# +# Default: 20 +#elasticsearch_max_total_connections_per_route = 20 + +# Maximum number of times Graylog will retry failed requests to Elasticsearch. +# +# Default: 2 +#elasticsearch_max_retries = 2 + +# Enable automatic Elasticsearch node discovery through Nodes Info, +# see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/cluster-nodes-info.html +# +# WARNING: Automatic node discovery does not work if Elasticsearch requires authentication, e. g. with Shield. +# +# Default: false +#elasticsearch_discovery_enabled = true + +# Filter for including/excluding Elasticsearch nodes in discovery according to their custom attributes, +# see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/cluster.html#cluster-nodes +# +# Default: empty +#elasticsearch_discovery_filter = rack:42 + +# Frequency of the Elasticsearch node discovery. +# +# Default: 30s +# elasticsearch_discovery_frequency = 30s + +# Set the default scheme when connecting to Elasticsearch discovered nodes +# +# Default: http (available options: http, https) +#elasticsearch_discovery_default_scheme = http + +# Enable payload compression for Elasticsearch requests. +# +# Default: false +#elasticsearch_compression_enabled = true + +# Enable use of "Expect: 100-continue" Header for Elasticsearch index requests. +# If this is disabled, Graylog cannot properly handle HTTP 413 Request Entity Too Large errors. +# +# Default: true +#elasticsearch_use_expect_continue = true + +# Graylog uses Index Sets to manage settings for groups of indices. The default options for index sets are configurable +# for each index set in Graylog under System > Configuration > Index Set Defaults. +# The following settings are used to initialize in-database defaults on the first Graylog server startup. +# Specify these values if you want the Graylog server and indices to start with specific settings. + +# The prefix for the Default Graylog index set. +# +#elasticsearch_index_prefix = graylog + +# The name of the index template for the Default Graylog index set. +# +#elasticsearch_template_name = graylog-internal + +# The prefix for the for graylog event indices. +# +#default_events_index_prefix = gl-events + +# The prefix for graylog system event indices. +# +#default_system_events_index_prefix = gl-system-events + +# Analyzer (tokenizer) to use for message and full_message field. The "standard" filter usually is a good idea. +# All supported analyzers are: standard, simple, whitespace, stop, keyword, pattern, language, snowball, custom +# Elasticsearch documentation: https://www.elastic.co/guide/en/elasticsearch/reference/2.3/analysis.html +# Note that this setting only takes effect on newly created indices. +# +#elasticsearch_analyzer = standard + +# How many Elasticsearch shards and replicas should be used per index? +# +#elasticsearch_shards = 1 +#elasticsearch_replicas = 0 + +# Maximum number of attempts to connect to datanode on boot. +# Default: 0, retry indefinitely with the given delay until a connection could be established +#datanode_startup_connection_attempts = 5 + +# Waiting time in between connection attempts for datanode_startup_connection_attempts +# +# Default: 5s +# datanode_startup_connection_delay = 5s + +# Disable the optimization of Elasticsearch indices after index cycling. This may take some load from Elasticsearch +# on heavily used systems with large indices, but it will decrease search performance. The default is to optimize +# cycled indices. +# +#disable_index_optimization = true + +# Optimize the index down to <= index_optimization_max_num_segments. A higher number may take some load from Elasticsearch +# on heavily used systems with large indices, but it will decrease search performance. The default is 1. +# +#index_optimization_max_num_segments = 1 + +# Time interval to refresh index field types. +# Default: 5s +#index_field_type_refresh_interval = 5s + +# Time interval to trigger a full refresh of the index field types for all indexes. This will query ES for all indexes +# and populate any missing field type information to the database. +# +#index_field_type_periodical_full_refresh_interval = 5m + +# You can configure the default strategy used to determine when to rotate the currently active write index. +# Multiple rotation strategies are supported, the default being "time-size-optimizing": +# - "time-size-optimizing" tries to rotate daily, while focussing on optimal sized shards. +# The global default values can be configured with +# "time_size_optimizing_retention_min_lifetime" and "time_size_optimizing_retention_max_lifetime". +# - "count" of messages per index, use elasticsearch_max_docs_per_index below to configure +# - "size" per index, use elasticsearch_max_size_per_index below to configure +# - "time" interval between index rotations, use elasticsearch_max_time_per_index to configure +# A strategy may be disabled by specifying the optional enabled_index_rotation_strategies list and excluding that strategy. +# +#enabled_index_rotation_strategies = count,size,time,time-size-optimizing + +# The default index rotation strategy to use. +#rotation_strategy = time-size-optimizing + +# (Approximate) maximum number of documents in an Elasticsearch index before a new index +# is being created, also see no_retention and elasticsearch_max_number_of_indices. +# Configure this if you used 'rotation_strategy = count' above. +# +#elasticsearch_max_docs_per_index = 20000000 + +# (Approximate) maximum size in bytes per Elasticsearch index on disk before a new index is being created, also see +# no_retention and elasticsearch_max_number_of_indices. Default is 30GB. +# Configure this if you used 'rotation_strategy = size' above. +# +#elasticsearch_max_size_per_index = 32212254720 + +# (Approximate) maximum time before a new Elasticsearch index is being created, also see +# no_retention and elasticsearch_max_number_of_indices. Default is 1 day. +# Configure this if you used 'rotation_strategy = time' above. +# Please note that this rotation period does not look at the time specified in the received messages, but is +# using the real clock value to decide when to rotate the index! +# Specify the time using a duration and a suffix indicating which unit you want: +# 1w = 1 week +# 1d = 1 day +# 12h = 12 hours +# Permitted suffixes are: d for day, h for hour, m for minute, s for second. +# +#elasticsearch_max_time_per_index = 1d + +# Controls whether empty indices are rotated. Only applies to the "time" rotation_strategy. +# +#elasticsearch_rotate_empty_index_set=false + +# Provides a hard upper limit for the retention period of any index set at configuration time. +# +# This setting is used to validate the value a user chooses for the maximum number of retained indexes, when configuring +# an index set. However, it is only in effect, when a time-based rotation strategy is chosen. +# +# If a rotation strategy other than time-based is selected and/or no value is provided for this setting, no upper limit +# for index retention will be enforced. This is also the default. + +# Default: none +#max_index_retention_period = P90d + +# Optional upper bound on elasticsearch_max_time_per_index +# +#elasticsearch_max_write_index_age = 1d + +# Disable message retention on this node, i. e. disable Elasticsearch index rotation. +#no_retention = false + +# Decide what happens with the oldest indices when the maximum number of indices is reached. +# The following strategies are available: +# - delete # Deletes the index completely (Default) +# - close # Closes the index and hides it from the system. Can be re-opened later. +# +#retention_strategy = delete + +# This configuration list limits the retention strategies available for user configuration via the UI +# The following strategies can be disabled: +# - delete # Deletes the index completely (Default) +# - close # Closes the index and hides it from the system. Can be re-opened later. +# - none # No operation is performed. The index stays open. (Not recommended) +# WARNING: At least one strategy must be enabled. Be careful when extending this list on existing installations! +disabled_retention_strategies = none,close + +# How many indices do you want to keep for the delete and close retention types? +# +#elasticsearch_max_number_of_indices = 20 + +# Disable checking the version of Elasticsearch for being compatible with this Graylog release. +# WARNING: Using Graylog with unsupported and untested versions of Elasticsearch may lead to data loss! +# +#elasticsearch_disable_version_check = true + +# Do you want to allow searches with leading wildcards? This can be extremely resource hungry and should only +# be enabled with care. See also: https://docs.graylog.org/docs/query-language +allow_leading_wildcard_searches = false + +# Do you want to allow searches to be highlighted? Depending on the size of your messages this can be memory hungry and +# should only be enabled after making sure your Elasticsearch cluster has enough memory. +allow_highlighting = false + +# Sets field value suggestion mode. The possible values are: +# 1. "off" - field value suggestions are turned off +# 2. "textual_only" - field values are suggested only for textual fields +# 3. "on" (default) - field values are suggested for all field types, even the types where suggestions are inefficient performance-wise +field_value_suggestion_mode = on + +# Global timeout for index optimization (force merge) requests. +# Default: 1h +#elasticsearch_index_optimization_timeout = 1h + +# Maximum number of concurrently running index optimization (force merge) jobs. +# If you are using lots of different index sets, you might want to increase that number. +# This value should be set lower than elasticsearch_max_total_connections_per_route, otherwise index optimization +# could deplete all the client connections to the search server and block new messages ingestion for prolonged +# periods of time. +# Default: 10 +#elasticsearch_index_optimization_jobs = 10 + +# Mute the logging-output of ES deprecation warnings during REST calls in the ES RestClient +#elasticsearch_mute_deprecation_warnings = true + +# Time interval for index range information cleanups. This setting defines how often stale index range information +# is being purged from the database. +# Default: 1h +#index_ranges_cleanup_interval = 1h + +# Batch size for the Elasticsearch output. This is the maximum accumulated size of messages that are written to +# Elasticsearch in a batch call. If the configured batch size has not been reached within output_flush_interval seconds, +# everything that is available will be flushed at once. +# Each output buffer processor has to keep an entire batch of messages in memory until it has been sent to +# Elasticsearch, so increasing this value will also increase the memory requirements of the Graylog server. +# Batch sizes can be specified in data units (e.g. bytes, kilobytes, megabytes) or as an absolute number of messages. +# Example: output_batch_size = 10mb +output_batch_size = 500 + +# Flush interval (in seconds) for the Elasticsearch output. This is the maximum amount of time between two +# batches of messages written to Elasticsearch. It is only effective at all if your minimum number of messages +# for this time period is less than output_batch_size * outputbuffer_processors. +output_flush_interval = 1 + +# As stream outputs are loaded only on demand, an output which is failing to initialize will be tried over and +# over again. To prevent this, the following configuration options define after how many faults an output will +# not be tried again for an also configurable amount of seconds. +output_fault_count_threshold = 5 +output_fault_penalty_seconds = 30 + +# Number of process buffer processors running in parallel. +# By default, the value will be determined automatically based on the number of CPU cores available to the JVM, using +# the formula (<#cores> * 0.36 + 0.625) rounded to the nearest integer. +# Set this value explicitly to override the dynamically calculated value. Try raising the number if your buffers are +# filling up. +#processbuffer_processors = 5 + +# Number of output buffer processors running in parallel. +# By default, the value will be determined automatically based on the number of CPU cores available to the JVM, using +# the formula (<#cores> * 0.162 + 0.625) rounded to the nearest integer. +# Set this value explicitly to override the dynamically calculated value. Try raising the number if your buffers are +# filling up. +#outputbuffer_processors = 3 + +# The size of the thread pool in the output buffer processor. +# Default: 3 +#outputbuffer_processor_threads_core_pool_size = 3 + +# UDP receive buffer size for all message inputs (e. g. SyslogUDPInput). +#udp_recvbuffer_sizes = 1048576 + +# Wait strategy describing how buffer processors wait on a cursor sequence. (default: sleeping) +# Possible types: +# - yielding +# Compromise between performance and CPU usage. +# - sleeping +# Compromise between performance and CPU usage. Latency spikes can occur after quiet periods. +# - blocking +# High throughput, low latency, higher CPU usage. +# - busy_spinning +# Avoids syscalls which could introduce latency jitter. Best when threads can be bound to specific CPU cores. +processor_wait_strategy = blocking + +# Size of internal ring buffers. Raise this if raising outputbuffer_processors does not help anymore. +# For optimum performance your LogMessage objects in the ring buffer should fit in your CPU L3 cache. +# Must be a power of 2. (512, 1024, 2048, ...) +ring_size = 65536 + +inputbuffer_ring_size = 65536 +inputbuffer_wait_strategy = blocking + +# Number of input buffer processors running in parallel. +#inputbuffer_processors = 2 + +# Manually stopped inputs are no longer auto-restarted. To re-enable the previous behavior, set auto_restart_inputs to true. +#auto_restart_inputs = true + +# Enable the message journal. +message_journal_enabled = true + +# The directory which will be used to store the message journal. The directory must be exclusively used by Graylog and +# must not contain any other files than the ones created by Graylog itself. +# +# ATTENTION: +# If you create a separate partition for the journal files and use a file system creating directories like 'lost+found' +# in the root directory, you need to create a sub directory for your journal. +# Otherwise Graylog will log an error message that the journal is corrupt and Graylog will not start. +# Default: /journal +message_journal_dir = {{ graylog_server__message_journal_dir }} + +# Journal hold messages before they could be written to Elasticsearch. +# For a maximum of 12 hours or 5 GB whichever happens first. +# During normal operation the journal will be smaller. +#message_journal_max_age = 12h +#message_journal_max_size = 5gb + +#message_journal_flush_age = 1m +#message_journal_flush_interval = 1000000 +#message_journal_segment_age = 1h +#message_journal_segment_size = 100mb + +# Number of threads used exclusively for dispatching internal events. Default is 2. +#async_eventbus_processors = 2 + +# How many seconds to wait between marking node as DEAD for possible load balancers and starting the actual +# shutdown process. Set to 0 if you have no status checking load balancers in front. +lb_recognition_period_seconds = 3 + +# Journal usage percentage that triggers requesting throttling for this server node from load balancers. The feature is +# disabled if not set. +#lb_throttle_threshold_percentage = 95 + +# Every message is matched against the configured streams and it can happen that a stream contains rules which +# take an unusual amount of time to run, for example if its using regular expressions that perform excessive backtracking. +# This will impact the processing of the entire server. To keep such misbehaving stream rules from impacting other +# streams, Graylog limits the execution time for each stream. +# The default values are noted below, the timeout is in milliseconds. +# If the stream matching for one stream took longer than the timeout value, and this happened more than "max_faults" times +# that stream is disabled and a notification is shown in the web interface. +#stream_processing_timeout = 2000 +#stream_processing_max_faults = 3 + +# Since 0.21 the Graylog server supports pluggable output modules. This means a single message can be written to multiple +# outputs. The next setting defines the timeout for a single output module, including the default output module where all +# messages end up. +# +# Time in milliseconds to wait for all message outputs to finish writing a single message. +#output_module_timeout = 10000 + +# Time in milliseconds after which a detected stale leader node is being rechecked on startup. +stale_leader_timeout = {{ graylog_server__stale_leader_timeout_ms }} + +# Time in milliseconds which Graylog is waiting for all threads to stop on shutdown. +#shutdown_timeout = 30000 + +# MongoDB connection string +# See https://docs.mongodb.com/manual/reference/connection-string/ for details +mongodb_uri = {{ graylog_server__mongodb_uri }} + +# Authenticate against the MongoDB server +# '+'-signs in the username or password need to be replaced by '%2B' +#mongodb_uri = mongodb://grayloguser:secret@localhost:27017/graylog + +# Use a replica set instead of a single host +#mongodb_uri = mongodb://grayloguser:secret@localhost:27017,localhost:27018,localhost:27019/graylog?replicaSet=rs01 + +# DNS Seedlist https://docs.mongodb.com/manual/reference/connection-string/#dns-seedlist-connection-format +#mongodb_uri = mongodb+srv://server.example.org/graylog + +# Increase this value according to the maximum connections your MongoDB server can handle from a single client +# if you encounter MongoDB connection problems. +mongodb_max_connections = 1000 + +# Maximum number of attempts to connect to MongoDB on boot for the version probe. +# +# Default: 0, retry indefinitely until a connection can be established +#mongodb_version_probe_attempts = 5 + +# Email transport +#transport_email_enabled = false +#transport_email_hostname = mail.example.com +#transport_email_port = 587 +#transport_email_use_auth = true +#transport_email_auth_username = you@example.com +#transport_email_auth_password = secret +#transport_email_from_email = graylog@example.com +#transport_email_socket_connection_timeout = 10s +#transport_email_socket_timeout = 10s + +# Encryption settings +# +# ATTENTION: +# Using SMTP with STARTTLS *and* SMTPS at the same time is *not* possible. + +# Use SMTP with STARTTLS, see https://en.wikipedia.org/wiki/Opportunistic_TLS +#transport_email_use_tls = true + +# Use SMTP over SSL (SMTPS), see https://en.wikipedia.org/wiki/SMTPS +# This is deprecated on most SMTP services! +#transport_email_use_ssl = false + + +# Specify and uncomment this if you want to include links to the stream in your stream alert mails. +# This should define the fully qualified base url to your web interface exactly the same way as it is accessed by your users. +#transport_email_web_interface_url = https://graylog.example.com + +# The User-Agent header for outgoing HTTP connections. +# Default: Graylog +#http_user_agent = Graylog + +# The default connect timeout for outgoing HTTP connections. +# Values must be a positive duration (and between 1 and 2147483647 when converted to milliseconds). +# Default: 5s +#http_connect_timeout = 5s + +# The default read timeout for outgoing HTTP connections. +# Values must be a positive duration (and between 1 and 2147483647 when converted to milliseconds). +# Default: 10s +#http_read_timeout = 10s + +# The default write timeout for outgoing HTTP connections. +# Values must be a positive duration (and between 1 and 2147483647 when converted to milliseconds). +# Default: 10s +#http_write_timeout = 10s + +# HTTP proxy for outgoing HTTP connections +# ATTENTION: If you configure a proxy, make sure to also configure the "http_non_proxy_hosts" option so internal +# HTTP connections with other nodes does not go through the proxy. +# Examples: +# - http://proxy.example.com:8123 +# - http://username:password@proxy.example.com:8123 +#http_proxy_uri = + +# A list of hosts that should be reached directly, bypassing the configured proxy server. +# This is a list of patterns separated by ",". The patterns may start or end with a "*" for wildcards. +# Any host matching one of these patterns will be reached through a direct connection instead of through a proxy. +# Examples: +# - localhost,127.0.0.1 +# - 10.0.*,*.example.com +#http_non_proxy_hosts = + +# Connection timeout for a configured LDAP server (e. g. ActiveDirectory) in milliseconds. +#ldap_connection_timeout = 2000 + +# Disable the use of a native system stats collector (currently OSHI) +#disable_native_system_stats_collector = false + +# The default cache time for dashboard widgets. (Default: 10 seconds, minimum: 1 second) +#dashboard_widget_default_cache_time = 10s + +# For some cluster-related REST requests, the node must query all other nodes in the cluster. This is the maximum number +# of threads available for this. Increase it, if '/cluster/*' requests take long to complete. +# Should be http_thread_pool_size * average_cluster_size if you have a high number of concurrent users. +#proxied_requests_thread_pool_size = 64 + +# The default HTTP call timeout for cluster-related REST requests. This timeout might be overriden for some +# resources in code or other configuration values. (some cluster metrics resources use a lower timeout) +#proxied_requests_default_call_timeout = 5s + +# The server is writing processing status information to the database on a regular basis. This setting controls how +# often the data is written to the database. +# Default: 1s (cannot be less than 1s) +#processing_status_persist_interval = 1s + +# Configures the threshold for detecting outdated processing status records. Any records that haven't been updated +# in the configured threshold will be ignored. +# Default: 1m (one minute) +#processing_status_update_threshold = 1m + +# Configures the journal write rate threshold for selecting processing status records. Any records that have a lower +# one minute rate than the configured value might be ignored. (dependent on number of messages in the journal) +# Default: 1 +#processing_status_journal_write_rate_threshold = 1 + +# Automatically load content packs in "content_packs_dir" on the first start of Graylog. +#content_packs_loader_enabled = false + +# The directory which contains content packs which should be loaded on the first start of Graylog. +# Default: /contentpacks +#content_packs_dir = data/contentpacks + +# A comma-separated list of content packs (files in "content_packs_dir") which should be applied on +# the first start of Graylog. +# Default: empty +#content_packs_auto_install = grok-patterns.json + +# The allowed TLS protocols for system wide TLS enabled servers. (e.g. message inputs, http interface) +# Setting this to an empty value, leaves it up to system libraries and the used JDK to chose a default. +# Default: TLSv1.2,TLSv1.3 (might be automatically adjusted to protocols supported by the JDK) +#enabled_tls_protocols = TLSv1.2,TLSv1.3 + +# Enable Prometheus exporter HTTP server. +# Default: false +#prometheus_exporter_enabled = false + +# IP address and port for the Prometheus exporter HTTP server. +# Default: 127.0.0.1:9833 +#prometheus_exporter_bind_address = 127.0.0.1:9833 + +# Path to the Prometheus exporter core mapping file. If this option is enabled, the full built-in core mapping is +# replaced with the mappings in this file. +# This file is monitored for changes and updates will be applied at runtime. +# Default: none +#prometheus_exporter_mapping_file_path_core = prometheus-exporter-mapping-core.yml + +# Path to the Prometheus exporter custom mapping file. If this option is enabled, the mappings in this file are +# configured in addition to the built-in core mappings. The mappings in this file cannot overwrite any core mappings. +# This file is monitored for changes and updates will be applied at runtime. +# Default: none +#prometheus_exporter_mapping_file_path_custom = prometheus-exporter-mapping-custom.yml + +# Configures the refresh interval for the monitored Prometheus exporter mapping files. +# Default: 60s +#prometheus_exporter_mapping_file_refresh_interval = 60s + +# An absolute path where scripts are permitted to be executed from. +integrations_scripts_dir = /usr/share/graylog-server/scripts + +# Optional allowed paths for Graylog data files. If provided, certain operations in Graylog will only be permitted +# if the data file(s) are located in the specified paths (for example, with the CSV File lookup adapter). +# All subdirectories of indicated paths are allowed by default. This Provides an additional layer of security, +# and allows administrators to control where in the file system Graylog users can select files from. +#allowed_auxiliary_paths = /etc/graylog/data-files,/etc/custom-allowed-path + +# Do not perform any preflight checks when starting Graylog +# Default: false +#skip_preflight_checks = false + +# Ignore any exceptions encountered when running migrations +# Use with caution - skipping failing migrations may result in an inconsistent DB state. +# Default: false +#ignore_migration_failures = false + +# Comma-separated list of notification types which should not emit a system event. +# Default: SIDECAR_STATUS_UNKNOWN which would create a new event whenever the status of a sidecar becomes "Unknown" +#system_event_excluded_types = SIDECAR_STATUS_UNKNOWN + +# RSS settings for content stream +#content_stream_rss_url = https://www.graylog.org/post +#content_stream_refresh_interval = 7d + +# Maximum value that can be set for an event limit. +# Default: 1000 +#event_definition_max_event_limit = 1000 + +# Optional limits on scheduling concurrency by job type. No more than the specified number of worker +# threads will be executing jobs of the specified type across the entire cluster. +# Default: no limitation +# Note: Monitor job queue metrics to avoid excessive backlog of unprocessed jobs when using this setting! +# Available job types in Graylog Open: +# check-for-cert-renewal-execution-v1 +# event-processor-execution-v1 +# notification-execution-v1 +#job_scheduler_concurrency_limits = event-processor-execution-v1:2,notification-execution-v1:2 + +# The size of the thread pool that executes search jobs for indexed data. (Data Node/OpenSearch) +# WARNING: This configuration setting should only be changed if you are certain of what you are doing. +# Modifying this setting without proper knowledge may lead to unexpected behavior or system +# instability. Proceed with caution. +# Default: 4 +#search_query_engine_indexer_jobs_pool_size = 4 + +# The queue size for the thread pool that executes search jobs for indexed data. (Data Node/OpenSearch) +# A value of "0" means that the queue is unbounded. +# WARNING: This configuration setting should only be changed if you are certain of what you are doing. +# Modifying this setting without proper knowledge may lead to unexpected behavior or system +# instability. Proceed with caution. +# Default: 0 +#search_query_engine_indexer_jobs_queue_size = 0 + +# The size of the thread pool that executes search jobs for data in Data Lake. +# WARNING: This configuration setting should only be changed if you are certain of what you are doing. +# Modifying this setting without proper knowledge may lead to unexpected behavior or system +# instability. Proceed with caution. +# Default: 4 +#search_query_engine_data_lake_jobs_pool_size = 4 + +# The queue size for the thread pool that executes search jobs for data in Data Lake. +# A value of "0" means that the queue is unbounded. +# WARNING: This configuration setting should only be changed if you are certain of what you are doing. +# Modifying this setting without proper knowledge may lead to unexpected behavior or system +# instability. Proceed with caution. +# Default: 0 +#search_query_engine_data_lake_jobs_queue_size = 0 + +################## +# Privacy settings +################## + +telemetry_enabled = false From 50d00ef9b1b62917e80c8ded4ed88f21a884ef30 Mon Sep 17 00:00:00 2001 From: Jihan El Karz Date: Mon, 18 May 2026 13:44:16 +0200 Subject: [PATCH 24/66] fix(roles/keycloak): run kc.sh build as keycloak user --- CHANGELOG.md | 1 + roles/keycloak/tasks/main.yml | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7bc270b..6e6a243a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **role:redis**: Added missing paths for running against Debian. * **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. +* **role:keycloak**: Fix ownership under `/opt/keycloak/data/`. Previously the post-install build step ran as `root` and left `/opt/keycloak/data/` and `/opt/keycloak/data/tmp/` owned by `root:root`, which the `keycloak` service user could not write into (no `data/cache/` was ever created). The build now runs as the `keycloak` service user, and existing installations get the ownership corrected on the next role run. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. * **roles**: Set `become: false` on tasks delegated to localhost across the collection. Previously these tasks inherited `become: true` from the playbook level and tried to call `sudo` on the Ansible controller, which fails on controllers without a passwordless sudo setup with `sudo: a password is required`. Affected are all `repo_*` roles, the `*_vm` cloud roles (`exoscale_vm`, `hetzner_vm`, `infomaniak_vm`), all `icingaweb2_module_*` roles that download artefacts, `monitoring_plugins`, `shared`, plus several others. Existing playbooks that were working without playbook-level `become: true` are unaffected ([#242](https://github.com/Linuxfabrik/lfops/issues/242)). diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml index 13de4453..84b5ff2b 100644 --- a/roles/keycloak/tasks/main.yml +++ b/roles/keycloak/tasks/main.yml @@ -118,10 +118,14 @@ - block: - - name: 'Change the working directory to /opt/keycloak and bin/kc.sh build --db {{ keycloak__db_vendor }}' + # kc.sh build always rebuilds and gives no reliable idempotency signal in its stdout, hence changed_when: true. + - name: 'cd /opt/keycloak && bin/kc.sh build --db {{ keycloak__db_vendor }}' ansible.builtin.command: 'bin/kc.sh build --db {{ keycloak__db_vendor }}' args: chdir: '/opt/keycloak' + become: true + become_user: 'keycloak' + changed_when: true when: 'keycloak__mode == "production"' tags: From 6b85f0666dcb99edfde991f28c4ff12ff24d27d8 Mon Sep 17 00:00:00 2001 From: Jihan El Karz Date: Mon, 18 May 2026 13:45:09 +0200 Subject: [PATCH 25/66] feat(roles/keycloak): auto-remove bootstrap admin credentials after first run --- CHANGELOG.md | 1 + roles/keycloak/README.md | 10 ++-- roles/keycloak/tasks/main.yml | 52 ++++++++++++++++++- .../etc/sysconfig/keycloak-sysconfig.j2 | 12 ++++- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6a243a..58221f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* **role:keycloak**: The role no longer leaves the bootstrap admin credentials lying around in `/etc/sysconfig/keycloak` after the first run. It now writes the credentials, waits for Keycloak to consume them on startup (provisioning the bootstrap admin in the `master` realm), re-renders the sysconfig file with the credentials removed, and stores a state marker at `/opt/keycloak/.bootstrap_admin_done` so subsequent runs skip the credential render entirely. After the first run, `keycloak__admin_login` can be removed from the inventory. Disaster recovery: delete the marker file, re-add the variable, re-run. Also recommend a `-temp` suffix for the initial admin username (example: `keycloak-admin-temp`) so it is visually obvious in the Keycloak UI which account must be deleted once a permanent admin exists. * **role:monitoring_plugins**: `install_method: 'source'` now reads the per-Python-LTS lockfile under `lockfiles/pyXX/requirements.txt` (`py39` ... `py314`) from both the `monitoring-plugins` and `lib` repos, picking the directory that matches the target host's Python. The previous root-level `requirements.txt` no longer exists upstream. No variable changes; rsync sources updated. * **CONTRIBUTING**: `meta/argument_specs.yml` must declare the `__dependent_var` slot for any variable that `setup_*` playbooks inject into the role via `vars:`. Dict variables fed by external lookups like `linuxfabrik.lfops.bitwarden_item` should use `type: 'dict'` without strict sub-options, since the lookup returns the full item with additional keys. * **role:example**: Demonstrate the `delegate_to: 'localhost'` + `become: false` pattern (download on the controller, copy to the target) so role authors can copy it consistently. diff --git a/roles/keycloak/README.md b/roles/keycloak/README.md index d65446c9..0527d346 100644 --- a/roles/keycloak/README.md +++ b/roles/keycloak/README.md @@ -54,13 +54,17 @@ All Keycloak config settings are described here: https://www.keycloak.org/server `keycloak__admin_login` -* The *temporary* Keycloak Admin login credentials. To harden security, create a permanent admin account after logging in as a temporary admin user, and delete the temporary one. +* The *temporary* Keycloak bootstrap admin login credentials. Keycloak only honors `KC_BOOTSTRAP_ADMIN_USERNAME` / `KC_BOOTSTRAP_ADMIN_PASSWORD` on the very first start, when no admin user exists in the `master` realm yet. Subsequent restarts ignore these variables. +* Mandatory only on the first role run (and during disaster recovery, see below). The role writes the credentials to `/etc/sysconfig/keycloak`, restarts Keycloak so it consumes them and provisions the bootstrap admin in the `master` realm, then immediately re-renders the sysconfig file with the credentials removed and marks the bootstrap as done via `/opt/keycloak/.bootstrap_admin_done`. The cleartext password no longer lingers on disk after the role finishes. +* On subsequent runs the role detects the marker file and renders the sysconfig file without credentials right away. `keycloak__admin_login` can be removed from the inventory at that point. +* Use a username that visibly marks the account as throwaway (suffix `-temp`), so it is obvious in the Keycloak UI which account must be deleted once a permanent admin has been created. +* Disaster recovery (e.g. lost database, need to re-bootstrap an admin): remove `/opt/keycloak/.bootstrap_admin_done`, re-add `keycloak__admin_login` to the inventory, and re-run the role. * Type: Dictionary. * Subkeys: * `username`: - * Mandatory. Username. + * Mandatory. Username. By convention, end with `-temp` (e.g. `keycloak-admin-temp`) to flag the account as the bootstrap user that must be deleted after the permanent admin is in place. * Type: String. * `password`: @@ -99,7 +103,7 @@ Example: # mandatory keycloak__admin_login: password: 'password' - username: 'keycloak-admin' + username: 'keycloak-admin-temp' keycloak__db_login: password: 'password' username: 'keycloak' diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml index 84b5ff2b..481bf562 100644 --- a/roles/keycloak/tasks/main.yml +++ b/roles/keycloak/tasks/main.yml @@ -1,9 +1,14 @@ - block: - - name: 'Debug Variables:' + - name: 'stat /opt/keycloak/.bootstrap_admin_done' + ansible.builtin.stat: + path: '/opt/keycloak/.bootstrap_admin_done' + register: '__keycloak__bootstrap_marker' + + - name: 'Debug Variables' ansible.builtin.debug: msg: | - keycloak__admin_login.username: {{ keycloak__admin_login.username }} + keycloak__admin_login.username: {{ (keycloak__admin_login | d({}))["username"] | d("(bootstrap already done)") }} keycloak__db_login.username: {{ keycloak__db_login.username }} keycloak__db_url_database: {{ keycloak__db_url_database }} keycloak__db_url_host: {{ keycloak__db_url_host }} @@ -95,6 +100,8 @@ owner: 'root' group: 'root' mode: 0o640 + vars: + __keycloak__render_bootstrap_creds: '{{ not __keycloak__bootstrap_marker["stat"]["exists"] }}' notify: 'keycloak: systemctl restart keycloak' - name: 'Create keycloak.service' @@ -148,3 +155,44 @@ - 'keycloak' - 'keycloak:configure' - 'keycloak:state' + + +- block: + + - name: 'Flush handlers so Keycloak restarts with the bootstrap credentials loaded' + ansible.builtin.meta: 'flush_handlers' + + - name: 'Wait for Keycloak listener on port {{ __keycloak__listen_port }} (bootstrap admin gets provisioned during startup)' + ansible.builtin.wait_for: + port: '{{ __keycloak__listen_port }}' + host: '127.0.0.1' + timeout: 180 + vars: + __keycloak__listen_port: '{{ (keycloak__https_certificate_file | d("") | length > 0) | ternary(8443, 8080) }}' + + - name: 'Deploy /etc/sysconfig/keycloak (without bootstrap credentials)' + ansible.builtin.template: + backup: true + src: 'etc/sysconfig/keycloak-sysconfig.j2' + dest: '/etc/sysconfig/keycloak' + owner: 'root' + group: 'root' + mode: 0o640 + vars: + __keycloak__render_bootstrap_creds: false + + - name: 'touch /opt/keycloak/.bootstrap_admin_done' + ansible.builtin.file: + path: '/opt/keycloak/.bootstrap_admin_done' + state: 'touch' + owner: 'keycloak' + group: 'keycloak' + mode: 0o644 + + when: + - 'not __keycloak__bootstrap_marker["stat"]["exists"]' + - 'keycloak__state == "started"' + + tags: + - 'keycloak' + - 'keycloak:configure' diff --git a/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 b/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 index 1e7db746..d1f70e04 100644 --- a/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 +++ b/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 @@ -1,7 +1,17 @@ # {{ ansible_managed }} -# 2025022701 +# 2026051801 +{% if __keycloak__render_bootstrap_creds | d(false) | bool %} # WARN: Environment variable 'KEYCLOAK_ADMIN' is deprecated, use 'KC_BOOTSTRAP_ADMIN_USERNAME' instead # WARN: Environment variable 'KEYCLOAK_ADMIN_PASSWORD' is deprecated, use 'KC_BOOTSTRAP_ADMIN_PASSWORD' instead KC_BOOTSTRAP_ADMIN_USERNAME={{ keycloak__admin_login.username }} KC_BOOTSTRAP_ADMIN_PASSWORD={{ keycloak__admin_login.password }} +{% else %} +# Bootstrap admin credentials have been removed by the role after they served +# their purpose on the first Keycloak start. Re-bootstrap (e.g. after a DB +# loss) by removing the state marker /opt/keycloak/.bootstrap_admin_done and +# re-running the role with `keycloak__admin_login` defined. +# +# This file is intentionally kept on disk (empty of credentials) because +# keycloak.service references it as a mandatory `EnvironmentFile=`. +{% endif %} From 7fd7c415cd4b75e131a83a1ad9951ca4f80e555e Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 19 May 2026 17:14:12 +0200 Subject: [PATCH 26/66] style(roles/keycloak): improve state file handling and variable naming and some other minor improvements --- CHANGELOG.md | 2 +- roles/keycloak/README.md | 9 +- roles/keycloak/tasks/main.yml | 130 +++++++++++++----- .../etc/sysconfig/keycloak-sysconfig.j2 | 14 +- 4 files changed, 105 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58221f00..8e79972d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -* **role:keycloak**: The role no longer leaves the bootstrap admin credentials lying around in `/etc/sysconfig/keycloak` after the first run. It now writes the credentials, waits for Keycloak to consume them on startup (provisioning the bootstrap admin in the `master` realm), re-renders the sysconfig file with the credentials removed, and stores a state marker at `/opt/keycloak/.bootstrap_admin_done` so subsequent runs skip the credential render entirely. After the first run, `keycloak__admin_login` can be removed from the inventory. Disaster recovery: delete the marker file, re-add the variable, re-run. Also recommend a `-temp` suffix for the initial admin username (example: `keycloak-admin-temp`) so it is visually obvious in the Keycloak UI which account must be deleted once a permanent admin exists. +* **role:keycloak**: The role no longer leaves the bootstrap admin credentials lying around in `/etc/sysconfig/keycloak` after the first run. It now writes the credentials, waits for Keycloak to consume them on startup (provisioning the bootstrap admin in the `master` realm), re-renders the sysconfig file with the credentials removed, and stores a state marker at `/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state` so subsequent runs skip the credential render entirely. After the first run, `keycloak__admin_login` can be removed from the inventory. Disaster recovery: delete the marker file, re-add the variable, re-run. Also recommend a `-temp` suffix for the initial admin username (example: `keycloak-admin-temp`) so it is visually obvious in the Keycloak UI which account must be deleted once a permanent admin exists. * **role:monitoring_plugins**: `install_method: 'source'` now reads the per-Python-LTS lockfile under `lockfiles/pyXX/requirements.txt` (`py39` ... `py314`) from both the `monitoring-plugins` and `lib` repos, picking the directory that matches the target host's Python. The previous root-level `requirements.txt` no longer exists upstream. No variable changes; rsync sources updated. * **CONTRIBUTING**: `meta/argument_specs.yml` must declare the `__dependent_var` slot for any variable that `setup_*` playbooks inject into the role via `vars:`. Dict variables fed by external lookups like `linuxfabrik.lfops.bitwarden_item` should use `type: 'dict'` without strict sub-options, since the lookup returns the full item with additional keys. * **role:example**: Demonstrate the `delegate_to: 'localhost'` + `become: false` pattern (download on the controller, copy to the target) so role authors can copy it consistently. diff --git a/roles/keycloak/README.md b/roles/keycloak/README.md index 0527d346..1a6e8f15 100644 --- a/roles/keycloak/README.md +++ b/roles/keycloak/README.md @@ -55,10 +55,10 @@ All Keycloak config settings are described here: https://www.keycloak.org/server `keycloak__admin_login` * The *temporary* Keycloak bootstrap admin login credentials. Keycloak only honors `KC_BOOTSTRAP_ADMIN_USERNAME` / `KC_BOOTSTRAP_ADMIN_PASSWORD` on the very first start, when no admin user exists in the `master` realm yet. Subsequent restarts ignore these variables. -* Mandatory only on the first role run (and during disaster recovery, see below). The role writes the credentials to `/etc/sysconfig/keycloak`, restarts Keycloak so it consumes them and provisions the bootstrap admin in the `master` realm, then immediately re-renders the sysconfig file with the credentials removed and marks the bootstrap as done via `/opt/keycloak/.bootstrap_admin_done`. The cleartext password no longer lingers on disk after the role finishes. +* Mandatory only on the first role run. The role writes the credentials to `/etc/sysconfig/keycloak`, restarts Keycloak so it consumes them and provisions the bootstrap admin in the `master` realm, then immediately re-renders the sysconfig file with the credentials removed and marks the bootstrap as done via `/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state`. The cleartext password no longer lingers on disk after the role finishes. * On subsequent runs the role detects the marker file and renders the sysconfig file without credentials right away. `keycloak__admin_login` can be removed from the inventory at that point. * Use a username that visibly marks the account as throwaway (suffix `-temp`), so it is obvious in the Keycloak UI which account must be deleted once a permanent admin has been created. -* Disaster recovery (e.g. lost database, need to re-bootstrap an admin): remove `/opt/keycloak/.bootstrap_admin_done`, re-add `keycloak__admin_login` to the inventory, and re-run the role. +* For disaster recovery (e.g. lost database, need to re-bootstrap an admin): remove `/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state`, re-add `keycloak__admin_login` to the inventory, and re-run the role. * Type: Dictionary. * Subkeys: @@ -96,17 +96,18 @@ All Keycloak config settings are described here: https://www.keycloak.org/server `keycloak__version` * The version of Keycloak that should be installed. +* Possible options: . * Type: String. Example: ```yaml # mandatory keycloak__admin_login: - password: 'password' username: 'keycloak-admin-temp' + password: 'linuxfabrik' keycloak__db_login: - password: 'password' username: 'keycloak' + password: 'linuxfabrik' keycloak__hostname: 'keycloak.local' keycloak__version: '26.1.2' ``` diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml index 481bf562..2cb1cfc1 100644 --- a/roles/keycloak/tasks/main.yml +++ b/roles/keycloak/tasks/main.yml @@ -1,33 +1,42 @@ - block: - - name: 'stat /opt/keycloak/.bootstrap_admin_done' + - name: 'stat /etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state' ansible.builtin.stat: - path: '/opt/keycloak/.bootstrap_admin_done' - register: '__keycloak__bootstrap_marker' + path: '/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state' + register: '__keycloak__admin_login_bootstrapped_stat' + + # Read-only fact used in the debug output and as the default value of the per-render + # `__keycloak__include_bootstrap_creds` flag below. Do NOT reuse this fact directly in the + # sysconfig template: set_fact has higher Ansible variable precedence than task-level `vars:`, + # so a task trying to override this fact would be silently ignored. + - name: 'Set __keycloak__admin_login_bootstrapped' + ansible.builtin.set_fact: + __keycloak__admin_login_bootstrapped: '{{ __keycloak__admin_login_bootstrapped_stat["stat"]["exists"] }}' - name: 'Debug Variables' ansible.builtin.debug: - msg: | - keycloak__admin_login.username: {{ (keycloak__admin_login | d({}))["username"] | d("(bootstrap already done)") }} - keycloak__db_login.username: {{ keycloak__db_login.username }} - keycloak__db_url_database: {{ keycloak__db_url_database }} - keycloak__db_url_host: {{ keycloak__db_url_host }} - keycloak__db_vendor: {{ keycloak__db_vendor }} - keycloak__expose_healthcheck_endpoints: {{ keycloak__expose_healthcheck_endpoints }} - keycloak__expose_metrics_endpoints: {{ keycloak__expose_metrics_endpoints }} - keycloak__hostname: {{ keycloak__hostname }} - keycloak__hostname_backchannel_dynamic: {{ keycloak__hostname_backchannel_dynamic }} - keycloak__https_certificate_file: {{ keycloak__https_certificate_file }} - keycloak__https_certificate_key_file: {{ keycloak__https_certificate_key_file }} - keycloak__https_protocols: {{ keycloak__https_protocols }} - keycloak__log: {{ keycloak__log }} - keycloak__log_file: {{ keycloak__log_file }} - keycloak__mode: {{ keycloak__mode }} - keycloak__proxy_headers: {{ keycloak__proxy_headers }} - keycloak__service_enabled: {{ keycloak__service_enabled }} - keycloak__spi_sticky_session_encoder_infinispan_should_attach_route: {{ keycloak__spi_sticky_session_encoder_infinispan_should_attach_route }} - keycloak__state: {{ keycloak__state }} - keycloak__version: {{ keycloak__version }} + msg: + - 'keycloak__admin_login.username: {{ (keycloak__admin_login | d({}))["username"] | d("(unset)") }}' + - '/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state exists: {{ __keycloak__admin_login_bootstrapped }}' + - 'keycloak__db_login.username: {{ keycloak__db_login["username"] }}' + - 'keycloak__db_url_database: {{ keycloak__db_url_database }}' + - 'keycloak__db_url_host: {{ keycloak__db_url_host }}' + - 'keycloak__db_vendor: {{ keycloak__db_vendor }}' + - 'keycloak__expose_healthcheck_endpoints: {{ keycloak__expose_healthcheck_endpoints }}' + - 'keycloak__expose_metrics_endpoints: {{ keycloak__expose_metrics_endpoints }}' + - 'keycloak__hostname: {{ keycloak__hostname }}' + - 'keycloak__hostname_backchannel_dynamic: {{ keycloak__hostname_backchannel_dynamic }}' + - 'keycloak__https_certificate_file: {{ keycloak__https_certificate_file }}' + - 'keycloak__https_certificate_key_file: {{ keycloak__https_certificate_key_file }}' + - 'keycloak__https_protocols: {{ keycloak__https_protocols }}' + - 'keycloak__log: {{ keycloak__log }}' + - 'keycloak__log_file: {{ keycloak__log_file }}' + - 'keycloak__mode: {{ keycloak__mode }}' + - 'keycloak__proxy_headers: {{ keycloak__proxy_headers }}' + - 'keycloak__service_enabled: {{ keycloak__service_enabled }}' + - 'keycloak__spi_sticky_session_encoder_infinispan_should_attach_route: {{ keycloak__spi_sticky_session_encoder_infinispan_should_attach_route }}' + - 'keycloak__state: {{ keycloak__state }}' + - 'keycloak__version: {{ keycloak__version }}' tags: - 'always' @@ -101,7 +110,7 @@ group: 'root' mode: 0o640 vars: - __keycloak__render_bootstrap_creds: '{{ not __keycloak__bootstrap_marker["stat"]["exists"] }}' + __keycloak__include_bootstrap_creds: '{{ not __keycloak__admin_login_bootstrapped }}' notify: 'keycloak: systemctl restart keycloak' - name: 'Create keycloak.service' @@ -162,37 +171,82 @@ - name: 'Flush handlers so Keycloak restarts with the bootstrap credentials loaded' ansible.builtin.meta: 'flush_handlers' - - name: 'Wait for Keycloak listener on port {{ __keycloak__listen_port }} (bootstrap admin gets provisioned during startup)' + # Port-up only proves Quarkus is listening, not that the bootstrap admin was provisioned. + # The admin-cli token request below is the actual readiness check; this wait_for just + # avoids spamming "connection refused" before Keycloak has bound the socket. + - name: 'Wait for Keycloak listener on port {{ __keycloak__listen_port }}' ansible.builtin.wait_for: port: '{{ __keycloak__listen_port }}' host: '127.0.0.1' - timeout: 180 + delay: 5 + sleep: 2 + timeout: 300 + vars: + __keycloak__listen_port: '{{ (keycloak__https_certificate_file | length > 0) | ternary(8443, 8080) }}' + + # Verify the bootstrap admin actually exists with the configured password before touching + # the marker. If this fails, the marker stays absent and a re-run with corrected variables + # will re-render the credentials and retry. + - name: 'curl --data grant_type=password --data client_id=admin-cli {{ __keycloak__base_url }}/realms/master/protocol/openid-connect/token' + ansible.builtin.uri: + url: '{{ __keycloak__base_url }}/realms/master/protocol/openid-connect/token' + method: 'POST' + body_format: 'form-urlencoded' + body: + client_id: 'admin-cli' + grant_type: 'password' + password: '{{ keycloak__admin_login["password"] }}' + username: '{{ keycloak__admin_login["username"] }}' + status_code: 200 + validate_certs: false + register: '__keycloak__admin_token_result' + until: '__keycloak__admin_token_result["status"] | d(0) == 200' + retries: 30 + delay: 5 + no_log: true + changed_when: false + check_mode: false vars: - __keycloak__listen_port: '{{ (keycloak__https_certificate_file | d("") | length > 0) | ternary(8443, 8080) }}' + __keycloak__listen_port: '{{ (keycloak__https_certificate_file | length > 0) | ternary(8443, 8080) }}' + __keycloak__scheme: '{{ (keycloak__https_certificate_file | length > 0) | ternary("https", "http") }}' + __keycloak__base_url: '{{ __keycloak__scheme }}://127.0.0.1:{{ __keycloak__listen_port }}' + # `backup: false` is on purpose. The previous file contained the cleartext bootstrap password; + # writing a `.~` backup would leave those credentials on disk and defeat the + # whole point of this cleanup pass. - name: 'Deploy /etc/sysconfig/keycloak (without bootstrap credentials)' ansible.builtin.template: - backup: true + backup: false src: 'etc/sysconfig/keycloak-sysconfig.j2' dest: '/etc/sysconfig/keycloak' owner: 'root' group: 'root' mode: 0o640 vars: - __keycloak__render_bootstrap_creds: false + __keycloak__include_bootstrap_creds: false - - name: 'touch /opt/keycloak/.bootstrap_admin_done' + - name: 'mkdir -p /etc/ansible/facts.d' ansible.builtin.file: - path: '/opt/keycloak/.bootstrap_admin_done' - state: 'touch' - owner: 'keycloak' - group: 'keycloak' - mode: 0o644 + path: '/etc/ansible/facts.d' + state: 'directory' + owner: 'root' + group: 'root' + mode: 0o755 + + # `copy` with `content: ''` instead of `file` with `state: touch` so the task does not + # report changed on every run after the bootstrap is complete. + - name: 'Create state marker /etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state' + ansible.builtin.copy: + content: '' + dest: '/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state' + owner: 'root' + group: 'root' + mode: 0o600 + # block when: - - 'not __keycloak__bootstrap_marker["stat"]["exists"]' + - 'not (__keycloak__admin_login_bootstrapped | bool)' - 'keycloak__state == "started"' - tags: - 'keycloak' - 'keycloak:configure' diff --git a/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 b/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 index d1f70e04..de3ab542 100644 --- a/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 +++ b/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 @@ -1,15 +1,15 @@ # {{ ansible_managed }} -# 2026051801 +# 2026051808 -{% if __keycloak__render_bootstrap_creds | d(false) | bool %} -# WARN: Environment variable 'KEYCLOAK_ADMIN' is deprecated, use 'KC_BOOTSTRAP_ADMIN_USERNAME' instead -# WARN: Environment variable 'KEYCLOAK_ADMIN_PASSWORD' is deprecated, use 'KC_BOOTSTRAP_ADMIN_PASSWORD' instead -KC_BOOTSTRAP_ADMIN_USERNAME={{ keycloak__admin_login.username }} -KC_BOOTSTRAP_ADMIN_PASSWORD={{ keycloak__admin_login.password }} +{% if __keycloak__include_bootstrap_creds | d(false) | bool %} +{# WARN: Environment variable 'KEYCLOAK_ADMIN' is deprecated, use 'KC_BOOTSTRAP_ADMIN_USERNAME' instead #} +{# WARN: Environment variable 'KEYCLOAK_ADMIN_PASSWORD' is deprecated, use 'KC_BOOTSTRAP_ADMIN_PASSWORD' instead #} +KC_BOOTSTRAP_ADMIN_USERNAME={{ keycloak__admin_login["username"] }} +KC_BOOTSTRAP_ADMIN_PASSWORD={{ keycloak__admin_login["password"] }} {% else %} # Bootstrap admin credentials have been removed by the role after they served # their purpose on the first Keycloak start. Re-bootstrap (e.g. after a DB -# loss) by removing the state marker /opt/keycloak/.bootstrap_admin_done and +# loss) by removing /etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state and # re-running the role with `keycloak__admin_login` defined. # # This file is intentionally kept on disk (empty of credentials) because From 638b1b3007601677132238856d3bac40d7e75471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20B=C3=BCrki?= Date: Mon, 18 May 2026 15:21:40 +0200 Subject: [PATCH 27/66] docs(roles/motd): update default value of motd__legal_notice --- CHANGELOG.md | 1 + roles/motd/README.md | 2 +- roles/motd/defaults/main.yml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e79972d..48b8b7e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* **role:motd**: Updated default value of `motd__legal_notice`. * **role:dnf_versionlock**: Rename internal OS-specific variables `dnf_versionlock__list_path` and `dnf_versionlock__packages` to `__dnf_versionlock__list_path` and `__dnf_versionlock__packages`. They are set in `vars/RedHat{7,8,9}.yml` and `vars/Fedora{40,41}.yml` and were never meant to be overridden from inventory; the `__` prefix makes that visible (LFOps convention). If you set either of these in your inventory, switch to the new names. * **role:icingaweb2_module_businessprocess**: Rename internal variable `icingaweb2_module_businessprocess__icingaweb2_owner` (set in `vars/{Debian,RedHat}.yml`) to `__icingaweb2_module_businessprocess__icingaweb2_owner`. Inventory overrides need to be renamed; the value (`www-data` on Debian, `apache` on Red Hat) stays the same. * **role:icingaweb2_module_company**: Rename internal variable `icingaweb2_module_company__icingaweb2_owner` (set in `vars/{Debian,RedHat}.yml`) to `__icingaweb2_module_company__icingaweb2_owner`. Inventory overrides need to be renamed; the value (`www-data` on Debian, `apache` on Red Hat) stays the same. diff --git a/roles/motd/README.md b/roles/motd/README.md index d600be4b..e713884a 100644 --- a/roles/motd/README.md +++ b/roles/motd/README.md @@ -64,7 +64,7 @@ motd__legal_notice: |+ * monitored and recorded by system personnel. Anyone using this * * system expressly consents to such monitoring and is advised that * * if such monitoring reveals possible evidence of criminal activity * - * system personal may provide the evidence of such monitoring to law * + * system personnel may provide the evidence of such monitoring to law * * enforcement officials. * * * ************************************************************************ diff --git a/roles/motd/defaults/main.yml b/roles/motd/defaults/main.yml index c087d9ab..10e653e3 100644 --- a/roles/motd/defaults/main.yml +++ b/roles/motd/defaults/main.yml @@ -31,7 +31,7 @@ motd__legal_notice: |+ * monitored and recorded by system personnel. Anyone using this * * system expressly consents to such monitoring and is advised that * * if such monitoring reveals possible evidence of criminal activity * - * system personal may provide the evidence of such monitoring to law * + * system personnel may provide the evidence of such monitoring to law * * enforcement officials. * * * ************************************************************************ From f5a1dcca162aeb1709293edfed0a5345befde6b2 Mon Sep 17 00:00:00 2001 From: Ali Bhatti Date: Tue, 19 May 2026 10:08:34 +0200 Subject: [PATCH 28/66] fix(roles/nodejs): support switching module stream --- CHANGELOG.md | 2 ++ roles/nodejs/tasks/main.yml | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b8b7e3..1fff2d5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,9 +66,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +<<<<<<< HEAD * **role:redis**: Added missing paths for running against Debian. * **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. * **role:keycloak**: Fix ownership under `/opt/keycloak/data/`. Previously the post-install build step ran as `root` and left `/opt/keycloak/data/` and `/opt/keycloak/data/tmp/` owned by `root:root`, which the `keycloak` service user could not write into (no `data/cache/` was ever created). The build now runs as the `keycloak` service user, and existing installations get the ownership corrected on the next role run. +* **role:nodejs**: Fix `@nodejs:` install failing with `broken groups or modules: nodejs:`. Two issues compounded: DNF refuses to silently switch an already-enabled module stream, and some modules ship without a `[d]efault` profile, so `@nodejs:` (no profile specified) cannot be resolved. The role now runs `dnf -y module reset nodejs` first when `nodejs__dnf_module_stream` is set, and installs the explicit `/common` profile. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. * **roles**: Set `become: false` on tasks delegated to localhost across the collection. Previously these tasks inherited `become: true` from the playbook level and tried to call `sudo` on the Ansible controller, which fails on controllers without a passwordless sudo setup with `sudo: a password is required`. Affected are all `repo_*` roles, the `*_vm` cloud roles (`exoscale_vm`, `hetzner_vm`, `infomaniak_vm`), all `icingaweb2_module_*` roles that download artefacts, `monitoring_plugins`, `shared`, plus several others. Existing playbooks that were working without playbook-level `become: true` are unaffected ([#242](https://github.com/Linuxfabrik/lfops/issues/242)). diff --git a/roles/nodejs/tasks/main.yml b/roles/nodejs/tasks/main.yml index f31e88c1..8a5cecef 100644 --- a/roles/nodejs/tasks/main.yml +++ b/roles/nodejs/tasks/main.yml @@ -7,10 +7,19 @@ state: 'present' when: 'nodejs__dnf_module_stream is not defined or not (nodejs__dnf_module_stream | string | length > 0)' - - name: 'Install @nodejs:{{ nodejs__dnf_module_stream }} -y' + - name: 'dnf -y module reset nodejs' + ansible.builtin.command: 'dnf -y module reset nodejs' + register: '__nodejs__dnf_module_reset_result' + changed_when: '"Nothing to do" not in __nodejs__dnf_module_reset_result["stdout"]' + when: + - 'ansible_facts["os_family"] == "RedHat"' + - 'nodejs__dnf_module_stream is defined' + - 'nodejs__dnf_module_stream | string | length > 0' + + - name: 'Install @nodejs:{{ nodejs__dnf_module_stream }}/common -y' ansible.builtin.package: name: - - '@nodejs:{{ nodejs__dnf_module_stream }}' + - '@nodejs:{{ nodejs__dnf_module_stream }}/common' state: 'present' when: - 'nodejs__dnf_module_stream is defined' From 429eb22abd2aea9962203f8d05db528fb5dff226 Mon Sep 17 00:00:00 2001 From: Ali Bhatti Date: Tue, 19 May 2026 10:28:29 +0200 Subject: [PATCH 29/66] docs(roles/network): hint towards checking connection name --- roles/network/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/roles/network/README.md b/roles/network/README.md index 3cd7aa97..6e0a1954 100644 --- a/roles/network/README.md +++ b/roles/network/README.md @@ -51,12 +51,13 @@ network_connections: state: 'up' # remove the default connections + # check with `nmcli connection show` + - name: 'cloud-init eth0' + persistent_state: 'absent' - name: 'System eth0' persistent_state: 'absent' - name: 'System eth1' persistent_state: 'absent' - - name: 'System eth2' - persistent_state: 'absent' ``` From 8ac42d2ae1555472de04e43c8d79d416713e0406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20B=C3=BCrki?= Date: Tue, 19 May 2026 12:03:30 +0200 Subject: [PATCH 30/66] fix(roles/blocky): ensure blocky service is restarted after updating the binary --- CHANGELOG.md | 2 +- roles/blocky/tasks/main.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fff2d5f..00543de1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,11 +66,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -<<<<<<< HEAD * **role:redis**: Added missing paths for running against Debian. * **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. * **role:keycloak**: Fix ownership under `/opt/keycloak/data/`. Previously the post-install build step ran as `root` and left `/opt/keycloak/data/` and `/opt/keycloak/data/tmp/` owned by `root:root`, which the `keycloak` service user could not write into (no `data/cache/` was ever created). The build now runs as the `keycloak` service user, and existing installations get the ownership corrected on the next role run. * **role:nodejs**: Fix `@nodejs:` install failing with `broken groups or modules: nodejs:`. Two issues compounded: DNF refuses to silently switch an already-enabled module stream, and some modules ship without a `[d]efault` profile, so `@nodejs:` (no profile specified) cannot be resolved. The role now runs `dnf -y module reset nodejs` first when `nodejs__dnf_module_stream` is set, and installs the explicit `/common` profile. +* **role:blocky**: The handler `blocky: validate config & restart blocky.service` is now notified if the blocky binary is changed on the host to ensure that the blocky service is restarted after an update (as it was already documented for the `blocky` tag) * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. * **roles**: Set `become: false` on tasks delegated to localhost across the collection. Previously these tasks inherited `become: true` from the playbook level and tried to call `sudo` on the Ansible controller, which fails on controllers without a passwordless sudo setup with `sudo: a password is required`. Affected are all `repo_*` roles, the `*_vm` cloud roles (`exoscale_vm`, `hetzner_vm`, `infomaniak_vm`), all `icingaweb2_module_*` roles that download artefacts, `monitoring_plugins`, `shared`, plus several others. Existing playbooks that were working without playbook-level `become: true` are unaffected ([#242](https://github.com/Linuxfabrik/lfops/issues/242)). diff --git a/roles/blocky/tasks/main.yml b/roles/blocky/tasks/main.yml index 1816e5a1..67436751 100644 --- a/roles/blocky/tasks/main.yml +++ b/roles/blocky/tasks/main.yml @@ -65,6 +65,7 @@ owner: 'root' group: 'root' mode: 0o755 + notify: 'blocky: validate config & restart blocky.service' # if you want to use port 53 or 953 on Linux you should add CAP_NET_BIND_SERVICE capability to # the binary From 66255c172700332e46a91681aad6b42ff24226bd Mon Sep 17 00:00:00 2001 From: Ali Bhatti Date: Wed, 20 May 2026 09:57:15 +0200 Subject: [PATCH 31/66] fix(roles/graylog_server): fix input creation failure by removing a deprecated property --- CHANGELOG.md | 1 + roles/graylog_server/README.md | 6 ------ roles/graylog_server/defaults/main.yml | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00543de1..c20b3d5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **role:redis**: Added missing paths for running against Debian. * **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. +* **role:graylog_server**: Fix the `graylog_server:configure_defaults` run aborting on Graylog 7.0+ with `Status code was 400 and not [200]` / `Unable to map property can_be_default` while creating the default index set, by removing the property from the role. Graylog 7.x dropped it and 6.x ignored it. * **role:keycloak**: Fix ownership under `/opt/keycloak/data/`. Previously the post-install build step ran as `root` and left `/opt/keycloak/data/` and `/opt/keycloak/data/tmp/` owned by `root:root`, which the `keycloak` service user could not write into (no `data/cache/` was ever created). The build now runs as the `keycloak` service user, and existing installations get the ownership corrected on the next role run. * **role:nodejs**: Fix `@nodejs:` install failing with `broken groups or modules: nodejs:`. Two issues compounded: DNF refuses to silently switch an already-enabled module stream, and some modules ship without a `[d]efault` profile, so `@nodejs:` (no profile specified) cannot be resolved. The role now runs `dnf -y module reset nodejs` first when `nodejs__dnf_module_stream` is set, and installs the explicit `/common` profile. * **role:blocky**: The handler `blocky: validate config & restart blocky.service` is now notified if the blocky binary is changed on the host to ensure that the blocky service is restarted after an update (as it was already documented for the `blocky` tag) diff --git a/roles/graylog_server/README.md b/roles/graylog_server/README.md index 44a0c81f..0d0d7286 100644 --- a/roles/graylog_server/README.md +++ b/roles/graylog_server/README.md @@ -178,11 +178,6 @@ graylog_server__root_user: * Default: See [defaults/main.yml](https://github.com/Linuxfabrik/lfops/blob/main/roles/graylog_server/defaults/main.yml) * Subkeys: - * `can_be_default`: - - * Mandatory. Whether this index set can be default. - * Type: Bool. - * `creation_date`: * Mandatory. Date in ISO 8601 format. @@ -341,7 +336,6 @@ graylog_server__opts: '-Xms2g -Xmx2g -server -XX:+UseG1GC -XX:-OmitStackTraceInF graylog_server__service_enabled: false graylog_server__stale_leader_timeout_ms: 10000 graylog_server__system_default_index_set: - can_be_default: true creation_date: '{{ ansible_date_time.iso8601 }}' description: 'One index per day; 365 indices max' field_type_refresh_interval: 5000 diff --git a/roles/graylog_server/defaults/main.yml b/roles/graylog_server/defaults/main.yml index d6a0d45c..16413a08 100644 --- a/roles/graylog_server/defaults/main.yml +++ b/roles/graylog_server/defaults/main.yml @@ -11,7 +11,6 @@ graylog_server__timezone: 'Europe/Zurich' graylog_server__trusted_proxies: [] graylog_server__system_default_index_set: - can_be_default: true creation_date: '{{ ansible_date_time.iso8601 }}' description: 'One index per day; 365 indices max' field_type_refresh_interval: 5000 From de31e97b558b9447f72e1c2a566b462fbff83b9b Mon Sep 17 00:00:00 2001 From: Ali Bhatti Date: Wed, 20 May 2026 09:58:57 +0200 Subject: [PATCH 32/66] fix(roles/graylog_server): enforce a key marked as mandatory in the role readme --- CHANGELOG.md | 1 + roles/graylog_server/tasks/main.yml | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c20b3d5f..9880ad24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **role:redis**: Added missing paths for running against Debian. * **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. +* **role:graylog_server**: Validate that each `graylog_server__system_inputs` entry sets `global: true` or assigns a `node`. Key was marked as mandatory but not enforced. The role now aborts the `graylog_server:configure_defaults` run with a clear message. * **role:graylog_server**: Fix the `graylog_server:configure_defaults` run aborting on Graylog 7.0+ with `Status code was 400 and not [200]` / `Unable to map property can_be_default` while creating the default index set, by removing the property from the role. Graylog 7.x dropped it and 6.x ignored it. * **role:keycloak**: Fix ownership under `/opt/keycloak/data/`. Previously the post-install build step ran as `root` and left `/opt/keycloak/data/` and `/opt/keycloak/data/tmp/` owned by `root:root`, which the `keycloak` service user could not write into (no `data/cache/` was ever created). The build now runs as the `keycloak` service user, and existing installations get the ownership corrected on the next role run. * **role:nodejs**: Fix `@nodejs:` install failing with `broken groups or modules: nodejs:`. Two issues compounded: DNF refuses to silently switch an already-enabled module stream, and some modules ship without a `[d]efault` profile, so `@nodejs:` (no profile specified) cannot be resolved. The role now runs `dnf -y module reset nodejs` first when `nodejs__dnf_module_stream` is set, and installs the explicit `/common` profile. diff --git a/roles/graylog_server/tasks/main.yml b/roles/graylog_server/tasks/main.yml index 5fdc21cd..ed1dd66e 100644 --- a/roles/graylog_server/tasks/main.yml +++ b/roles/graylog_server/tasks/main.yml @@ -116,6 +116,16 @@ - block: + - name: 'Validate that each graylog_server__system_inputs entry is global or assigned to a node' + ansible.builtin.assert: + that: + - '(item["global"] | default(false) | bool) or (item["node"] | default("") | length > 0)' + fail_msg: 'Each graylog_server__system_inputs entry must set "global: true" or assign a "node", otherwise Graylog creates the input but never starts it' + quiet: true + loop: '{{ graylog_server__system_inputs }}' + loop_control: + label: '{{ item["title"] | default(item["type"]) }}' + - name: 'Get all inputs' ansible.builtin.uri: url: 'http://{{ graylog_server__http_bind_address }}:{{ graylog_server__http_bind_port }}/api/system/inputs' From 8e485aae204879ecb0959ee19cf805defef0118b Mon Sep 17 00:00:00 2001 From: Ali Bhatti Date: Wed, 20 May 2026 11:58:34 +0200 Subject: [PATCH 33/66] docs(roles/graylog_server): specify where to get input types from --- roles/graylog_server/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/graylog_server/README.md b/roles/graylog_server/README.md index 0d0d7286..1ac5be31 100644 --- a/roles/graylog_server/README.md +++ b/roles/graylog_server/README.md @@ -306,6 +306,7 @@ graylog_server__root_user: * Mandatory. The type of the input. * Type: String. + * To list the input types available on your Graylog node, append `/api-browser#?route=get-/system/inputs/types` to your Graylog web interface URL (for example `https://graylog.example.com/api-browser#?route=get-/system/inputs/types`) and click `Execute`. The `/system/inputs/types/all` route additionally returns each type's available `configuration` fields. `graylog_server__timezone` From cb0fd10120454fb9793dd97cce28f747db6bd37f Mon Sep 17 00:00:00 2001 From: Ali Bhatti Date: Wed, 20 May 2026 18:06:32 +0200 Subject: [PATCH 34/66] fix(roles/graylog_server): fix "conditional result was of type str" deprecation warning --- CHANGELOG.md | 1 + roles/graylog_datanode/tasks/main.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9880ad24..278162b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **role:redis**: Added missing paths for running against Debian. * **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. +* **role:graylog_datanode**: Fix the `Conditional result ... was of type 'str'` deprecation warning. * **role:graylog_server**: Validate that each `graylog_server__system_inputs` entry sets `global: true` or assigns a `node`. Key was marked as mandatory but not enforced. The role now aborts the `graylog_server:configure_defaults` run with a clear message. * **role:graylog_server**: Fix the `graylog_server:configure_defaults` run aborting on Graylog 7.0+ with `Status code was 400 and not [200]` / `Unable to map property can_be_default` while creating the default index set, by removing the property from the role. Graylog 7.x dropped it and 6.x ignored it. * **role:keycloak**: Fix ownership under `/opt/keycloak/data/`. Previously the post-install build step ran as `root` and left `/opt/keycloak/data/` and `/opt/keycloak/data/tmp/` owned by `root:root`, which the `keycloak` service user could not write into (no `data/cache/` was ever created). The build now runs as the `keycloak` service user, and existing installations get the ownership corrected on the next role run. diff --git a/roles/graylog_datanode/tasks/main.yml b/roles/graylog_datanode/tasks/main.yml index 8a34f3a8..b56d9d8a 100644 --- a/roles/graylog_datanode/tasks/main.yml +++ b/roles/graylog_datanode/tasks/main.yml @@ -10,7 +10,7 @@ - name: 'Validate that graylog_datanode__node_search_cache_size follows OpenSearch Bytes format' ansible.builtin.assert: that: - - 'graylog_datanode__node_search_cache_size | ansible.builtin.regex_search("^[0-9]+(b|kb|mb|gb|tb|pb)$")' + - 'graylog_datanode__node_search_cache_size is search("^[0-9]+(b|kb|mb|gb|tb|pb)$")' fail_msg: '`graylog_datanode__node_search_cache_size: {{ graylog_datanode__node_search_cache_size }}` does not follow OpenSearch Bytes format' quiet: true From 33a657929276af21fe0a42f9f326f9307314f857 Mon Sep 17 00:00:00 2001 From: Markus Frei Date: Mon, 18 May 2026 13:42:32 +0200 Subject: [PATCH 35/66] feat(roles/redis): raise net.core.somaxconn default to 4096 --- CHANGELOG.md | 1 + roles/redis/defaults/main.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 278162b3..3fe899a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed * **role:keycloak**: The role no longer leaves the bootstrap admin credentials lying around in `/etc/sysconfig/keycloak` after the first run. It now writes the credentials, waits for Keycloak to consume them on startup (provisioning the bootstrap admin in the `master` realm), re-renders the sysconfig file with the credentials removed, and stores a state marker at `/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state` so subsequent runs skip the credential render entirely. After the first run, `keycloak__admin_login` can be removed from the inventory. Disaster recovery: delete the marker file, re-add the variable, re-run. Also recommend a `-temp` suffix for the initial admin username (example: `keycloak-admin-temp`) so it is visually obvious in the Keycloak UI which account must be deleted once a permanent admin exists. +* **role:redis**: Bump default for `net.core.somaxconn` from `1024` to `4096` to match the RHEL 9 / RHEL 10 kernel default and the current Redis upstream recommendation. Hosts on RHEL 9 or 10 see no effective change (the override was already below the kernel default); RHEL 8 hosts now get `4096` instead of `1024`. * **role:monitoring_plugins**: `install_method: 'source'` now reads the per-Python-LTS lockfile under `lockfiles/pyXX/requirements.txt` (`py39` ... `py314`) from both the `monitoring-plugins` and `lib` repos, picking the directory that matches the target host's Python. The previous root-level `requirements.txt` no longer exists upstream. No variable changes; rsync sources updated. * **CONTRIBUTING**: `meta/argument_specs.yml` must declare the `__dependent_var` slot for any variable that `setup_*` playbooks inject into the role via `vars:`. Dict variables fed by external lookups like `linuxfabrik.lfops.bitwarden_item` should use `type: 'dict'` without strict sub-options, since the lookup returns the full item with additional keys. * **role:example**: Demonstrate the `delegate_to: 'localhost'` + `become: false` pattern (download on the controller, copy to the target) so role authors can copy it consistently. diff --git a/roles/redis/defaults/main.yml b/roles/redis/defaults/main.yml index 52533489..87c5342e 100644 --- a/roles/redis/defaults/main.yml +++ b/roles/redis/defaults/main.yml @@ -27,5 +27,5 @@ redis__kernel_settings__sysctl__dependent_var: # WARNING: The TCP backlog setting of 511 cannot be enforced because # /proc/sys/net/core/somaxconn is set to the lower value of 128. - name: 'net.core.somaxconn' - value: 1024 + value: 4096 redis__kernel_settings__transparent_hugepages__dependent_var: 'madvise' From f71a94c2a7af0976d10d618e10a345d8343bb761 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:29:00 +0200 Subject: [PATCH 36/66] docs(roles): standardize role README structure across the fleet --- CHANGELOG.md | 1 + CONTRIBUTING.md | 38 ++++- README.md | 9 +- roles/acme_sh/README.md | 32 ++-- roles/apache_httpd/README.md | 56 ++----- roles/apache_solr/README.md | 6 +- roles/apache_tomcat/README.md | 22 ++- roles/bind/README.md | 16 +- roles/clamav/README.md | 8 +- roles/collabora/README.md | 10 +- roles/coturn/README.md | 6 +- roles/docker/README.md | 6 +- roles/duplicity/README.md | 19 ++- roles/elastic_agent/README.md | 36 ++--- roles/elastic_agent_fleet_server/README.md | 61 ++++---- roles/elasticsearch/README.md | 96 ++++++------ roles/example/README.md | 147 +++++++++++++++++- roles/exoscale_vm/README.md | 4 +- roles/fail2ban/README.md | 10 +- roles/fangfrisch/README.md | 6 +- roles/files/README.md | 6 +- roles/firewall/README.md | 10 +- roles/freeipa_client/README.md | 10 +- roles/freeipa_server/README.md | 15 +- roles/github_project_createrepo/README.md | 10 +- roles/gitlab_ce/README.md | 6 +- roles/glances/README.md | 29 ++-- roles/glpi_agent/README.md | 6 +- roles/grafana/README.md | 6 +- roles/grafana_grizzly/README.md | 8 +- roles/grav/README.md | 22 +-- roles/graylog_datanode/README.md | 22 ++- roles/graylog_server/README.md | 30 ++-- roles/hetzner_vm/README.md | 26 ++-- roles/icinga2_agent/README.md | 14 +- roles/icinga2_master/README.md | 13 +- roles/icinga_kubernetes/README.md | 6 +- roles/icinga_kubernetes_web/README.md | 6 +- roles/icingadb/README.md | 6 +- roles/icingadb_web/README.md | 6 +- roles/icingaweb2/README.md | 19 ++- .../README.md | 9 +- roles/icingaweb2_module_company/README.md | 9 +- roles/icingaweb2_module_cube/README.md | 9 +- roles/icingaweb2_module_director/README.md | 8 +- roles/icingaweb2_module_doc/README.md | 6 +- roles/icingaweb2_module_fileshipper/README.md | 11 +- roles/icingaweb2_module_generictts/README.md | 9 +- roles/icingaweb2_module_grafana/README.md | 8 +- roles/icingaweb2_module_incubator/README.md | 9 +- roles/icingaweb2_module_jira/README.md | 8 +- roles/icingaweb2_module_pdfexport/README.md | 11 +- roles/icingaweb2_module_reporting/README.md | 11 +- roles/icingaweb2_module_vspheredb/README.md | 8 +- roles/icingaweb2_module_x509/README.md | 8 +- roles/icingaweb2_theme_linuxfabrik/README.md | 9 +- roles/influxdb/README.md | 8 +- roles/infomaniak_vm/README.md | 13 +- roles/keepalived/README.md | 2 +- roles/kernel_settings/README.md | 4 +- roles/keycloak/README.md | 13 +- roles/kibana/README.md | 40 ++--- roles/kvm_host/README.md | 6 +- roles/kvm_vm/README.md | 8 +- roles/librenms/README.md | 16 +- roles/libreoffice/README.md | 6 +- roles/login/README.md | 5 +- roles/logstash/README.md | 6 +- roles/mailto_root/README.md | 8 +- roles/mailx/README.md | 10 -- roles/mariadb_server/README.md | 24 +-- roles/mastodon/README.md | 50 +++--- roles/maxmind_geoip/README.md | 15 +- roles/mirror/README.md | 12 +- roles/mod_maxminddb/README.md | 10 +- roles/mongodb/README.md | 37 +++-- roles/monitoring_plugins/README.md | 2 +- .../README.md | 8 +- roles/moodle/README.md | 16 +- roles/network/README.md | 6 +- roles/nextcloud/README.md | 32 ++-- roles/opensearch/README.md | 126 +++++++-------- roles/openvpn_server/README.md | 12 +- roles/php/README.md | 10 +- roles/podman_containers/README.md | 6 +- roles/policycoreutils/README.md | 24 --- roles/postgresql_server/README.md | 14 +- roles/python_venv/README.md | 14 +- roles/redis/README.md | 2 +- roles/repo_icinga/README.md | 2 +- roles/repo_rpmfusion/README.md | 6 +- roles/rocketchat/README.md | 25 +-- roles/selinux/README.md | 6 +- roles/sshd/README.md | 6 +- roles/system_update/README.md | 18 ++- roles/telegraf/README.md | 6 +- roles/tools/README.md | 6 +- roles/uptimerobot/README.md | 125 ++++++++------- roles/wordpress/README.md | 10 +- roles/yum_utils/README.md | 16 -- 100 files changed, 1009 insertions(+), 780 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe899a4..60c6256c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* **docs**: All role READMEs now follow a consistent structure that separates the dependencies a playbook sets up for you from what you must provide yourself. Documentation only, no behavior changes. * **role:keycloak**: The role no longer leaves the bootstrap admin credentials lying around in `/etc/sysconfig/keycloak` after the first run. It now writes the credentials, waits for Keycloak to consume them on startup (provisioning the bootstrap admin in the `master` realm), re-renders the sysconfig file with the credentials removed, and stores a state marker at `/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state` so subsequent runs skip the credential render entirely. After the first run, `keycloak__admin_login` can be removed from the inventory. Disaster recovery: delete the marker file, re-add the variable, re-run. Also recommend a `-temp` suffix for the initial admin username (example: `keycloak-admin-temp`) so it is visually obvious in the Keycloak UI which account must be deleted once a permanent admin exists. * **role:redis**: Bump default for `net.core.somaxconn` from `1024` to `4096` to match the RHEL 9 / RHEL 10 kernel default and the current Redis upstream recommendation. Hosts on RHEL 9 or 10 see no effective change (the override was already below the kernel default); RHEL 8 hosts now get `4096` instead of `1024`. * **role:monitoring_plugins**: `install_method: 'source'` now reads the per-Python-LTS lockfile under `lockfiles/pyXX/requirements.txt` (`py39` ... `py314`) from both the `monitoring-plugins` and `lib` repos, picking the directory that matches the target host's Python. The previous root-level `requirements.txt` no longer exists upstream. No variable changes; rsync sources updated. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a76985b2..ba0e7a6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -182,7 +182,7 @@ Commit scopes: When creating a new role, make sure to deliver: * The role itself. -* `roles//README.md`, following `roles/example/README.md` as a template. +* `roles//README.md`, following `roles/example/README.md` as a template and the section menu under "README" below. * `roles//meta/argument_specs.yml` declaring all user-facing variables. * Update `playbooks/README.md`. * Update `playbooks/all.yml`. @@ -248,6 +248,42 @@ When creating a new role, make sure to deliver: * Document all changes in the [CHANGELOG.md](https://github.com/Linuxfabrik/lfops/blob/main/CHANGELOG.md) file. +#### README + +`roles/example/README.md` is the canonical template. Keep the following sections in this order, drop the optional ones that do not apply, and do not invent new top-level sections: + +``` +# Ansible Role linuxfabrik.lfops. + intro paragraph(s) (mandatory) +[ "This role is compatible with the following versions:" + list ] (optional) +*Available since LFOps `X.X.X`.* (mandatory) +## How the Role Behaves (optional) +## Known Limitations (optional) +## Dependent Roles (optional) +## Requirements (optional) +## Single-Node Setup / Cluster Setup / Adding a Node ... / (optional walkthrough) +## Post-Installation Steps (optional) +## Tags (mandatory) +## Mandatory Role Variables (optional) +## Recommended Role Variables (optional) +## Optional Role Variables (optional) +## Optional Role Variables - (optional, repeatable) +## Troubleshooting (optional) +## License (mandatory) +## Author Information (mandatory) +``` + +* **`*Available since LFOps`**: marks the LFOps release in which the role first shipped. Set it once and never change it afterwards. When you add a new role you do not know the next version tag yet, so write the literal line `*Available in the next LFOps release.*` instead; it is rewritten to the real version with `sed` when the next release is cut. +* **How the Role Behaves**: proactive, non-obvious design/runtime notes (controller-vs-target download split and who needs network access, idempotency / overwrite-on-rerun, the upgrade path, what the role does NOT do, security caveats). Distinct from Troubleshooting (reactive error->fix) and Known Limitations (hard constraints the operator cannot work around). +* **Dependent Roles vs Requirements**: these answer "which other LFOps roles does the playbook wire in" vs "what must the operator provide themselves". Decide where an item goes by what it is: + * Another LFOps role that **this role's own playbook** (`X.yml`, or a bundling `setup_X.yml`) runs goes under `## Dependent Roles`. Write each bullet declaratively as a state ("The X repository must be enabled (role: ...)") and name the role. Roles run by default form the first list, right under the lead-in ("Any LFOps playbook that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables."); mark feature-optional ones with "Optional:". Roles the playbook ships but leaves off by default go under a "These roles are not enabled by default; enable them via the playbook's skip variables if needed:" list-title. Do not list skip-variable names or the exact play order - those live solely in `playbooks/README.md`. + * A value the user supplies is a variable: document it under `## ... Role Variables` only, never as a dependency or requirement. + * Everything else the operator must provide goes under `## Requirements`: host resources, an external account or subscription, or credentials as plain bullets; hands-on procedures (run a SEPARATE playbook for a dependency, mint a token in a web console, install on the Ansible controller, configure DNS) under a "Manual steps:" list-title, written imperatively. A dependency that needs a separate playbook is a manual step here, not a dependent role. Mark feature-optional items with "Optional:". +* **Variable subgroups**: large roles MAY split optional (or mandatory) variables into `## Optional Role Variables - ` sections, using the upstream module name, the variable prefix as a code span, or a functional label. These are the same canonical section repeated; give each its own `Example:` block. A subgroup MAY keep its Mandatory and Optional sections paired together (group-by-module, as in `apache_httpd`) instead of forcing all mandatory subgroups before all optional ones. A variable grouping must always be its own `##` section, never a `###` subheading inside `## Optional Role Variables`. +* **Subheadings and walkthroughs**: `###` subheadings are allowed only as sub-structure inside a section (e.g. procedural steps within a walkthrough), never as a stand-in for a variable subgroup. The walkthrough slot accepts a descriptive role-specific title when none of `Single-Node Setup` / `Cluster Setup` / `Adding a Node to an Existing Cluster` fits (e.g. `bind` `## Primary-Secondary Example`). Sections are separated by two blank lines, including above and below the `*Available since*` marker. +* **Special roles**: utility/meta roles (e.g. `shared`) MAY replace `## Tags` and the `## *Role Variables` sections with `## Available Tasks` and `## Usage Example`. Controller-side API roles (e.g. `uptimerobot`) MAY add `## Running the Role`, `## Example Inventory` and `## Read-Only Inspection`. Both MUST keep the mandatory frame (title, marker, License, Author Information). +* **Reference-grade sections**: a role MAY add a focused role-specific section where no canonical section fits (e.g. `monitoring_plugins` `## Installation Methods`, `keycloak` `## Using a reverse proxy`). Keep these to a minimum; prefer folding behaviour notes into `## How the Role Behaves` and error/fix notes into `## Troubleshooting`. + + #### Tasks * Always use the FQCN of the module. diff --git a/README.md b/README.md index 36a97f55..c1974938 100644 --- a/README.md +++ b/README.md @@ -284,12 +284,19 @@ ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.all --tags mari ### Skipping Roles in a Playbook -The playbooks support skipping individual roles using inventory variables. For example, to skip the firewall role in `setup_basic`: +Each playbook wires in the roles it needs, gated by per-role skip variables. These let you tailor what a playbook runs without editing it: + +* Set a skip variable to `true` to **skip** a role the playbook runs by default. +* Set it to `false` to **enable** a role the playbook ships but leaves off by default. + +For example, to skip the firewall role in `setup_basic`: ```yaml setup_basic__skip_firewall: true ``` +Every playbook, the roles it runs, and their skip variables (including the defaults) are documented in [playbooks/README.md](playbooks/README.md). + In playbooks that support role injections (like `setup_icinga2_master`), there are two variables: * `playbook_name__role_name__skip_role`: Skips the role and disables the role's injections. Have a look at the playbook for the default value. diff --git a/roles/acme_sh/README.md b/roles/acme_sh/README.md index cc07dd60..2963f263 100644 --- a/roles/acme_sh/README.md +++ b/roles/acme_sh/README.md @@ -14,22 +14,28 @@ SSLCertificateChainFile /etc/pki/tls/certs/www.example.com-chain.crt *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install `openssl`. This can be done using the [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps) role. -* Install `tar`. This can be done using the [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps) role. -* Have a configured web server. If you are using LFOps to manage an Apache reverse proxy, a virtual host working for acme might be defined like this: +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -``` -apache_httpd__vhosts__host_var: - - conf_server_name: 'other.example.com' - enabled: true - state: 'present' - template: 'redirect' - virtualhost_port: 80 -``` +* `openssl` must be installed (role: [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps)). +* `tar` must be installed (role: [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps)). + + +## Requirements + +Manual steps: + +* Configure a web server. The playbook does not set this up. If you are using LFOps to manage an Apache reverse proxy, a virtual host working for acme might be defined like this: -If you use the [acme.sh Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/acme_sh.yml), this is automatically done for you (except configuring the webserver). + ```yaml + apache_httpd__vhosts__host_var: + - conf_server_name: 'www.example.com' + enabled: true + state: 'present' + template: 'redirect' + virtualhost_port: 80 + ``` ## Tags diff --git a/roles/apache_httpd/README.md b/roles/apache_httpd/README.md index a6504fbb..9ff21aeb 100644 --- a/roles/apache_httpd/README.md +++ b/roles/apache_httpd/README.md @@ -6,7 +6,7 @@ This role installs and configures a CIS-compliant [Apache httpd](https://httpd.a *Available since LFOps `2.0.0`.* -## What this Role does +## How the Role Behaves This role configures Apache using a Debian-style layout with `conf-available/conf-enabled`, `mods-available/mods-enabled`, and `sites-available/sites-enabled` directories. On Red Hat-based systems, this means a significant restructuring of the default Apache configuration. The goal is to make adding and removing mods, virtual hosts, and extra configuration directives as flexible as possible, regardless of the underlying platform. @@ -25,53 +25,38 @@ The config is split into several files forming the configuration hierarchy outli We avoid using `` in vHost definitions and in the global `httpd.conf` to facilitate debugging. Without ``, a missing module causes a clear startup error instead of silently dropping configuration. `` is only used in `mods-available/` and `conf-available/` where it is necessary to guard module-specific configuration. -For flexibility, use the `raw` variable to configure the following topics (have a look at the "Apache vHost Configs" section for some examples): +For flexibility, use the `raw` variable to configure the following topics (see [EXAMPLES.md](https://github.com/Linuxfabrik/lfops/blob/main/roles/apache_httpd/EXAMPLES.md) for vHost configuration examples): * SSL/TLS Certificates. * Quality of Service (`mod_qos` directives). * Proxy passing rules. * Any other configuration instructions not covered in the "Role Variables" chapters. -If you want to check Apache with [our STIG audit script](https://github.com/Linuxfabrik/stig), run it like this: - -* Apache Application Server:
`./audit.py --lengthy --profile-name='CIS Apache HTTP Server 2.4' --profile-version='v2.0.0' --hostname=web --control-name-exclude='2\.4|2\.6|2\.8|5\.7|6\.6|6\.7` -* Apache Reverse Proxy Server:
`./audit.py --lengthy --profile-name='CIS Apache HTTP Server 2.4' --profile-version='v2.0.0' --hostname=proxy --control-name-exclude='2\.4|2\.6|2\.8|5\.7` - - -## What this Role doesn't do - -* PHP: This role prefers the use of PHP-FPM over PHP, but it does not install either. -* SELinux: Use specialized roles to set specific SELinux Booleans, Policies etc. - - -## Platform-Specific Behavior - -This role supports both Red Hat and Debian-based systems. Paths and service names differ between platforms: +This role supports both Red Hat and Debian-based systems. The following paths and service names differ between platforms; the differences are handled automatically and all documentation below uses Red Hat paths: * Config path: Red Hat `/etc/httpd`, Debian `/etc/apache2` * Service name: Red Hat `httpd`, Debian `apache2` * User/Group: Red Hat `apache`/`apache`, Debian `www-data`/`www-data` * PHP-FPM socket: Red Hat `/run/php-fpm/www.sock`, Debian `/run/php/www.sock` -These differences are handled automatically. All documentation below uses Red Hat paths. - +This role does NOT: -## Config Examples for vHosts +* install PHP or PHP-FPM. It prefers PHP-FPM over mod_php, but installs neither. -See [EXAMPLES.md](https://github.com/Linuxfabrik/lfops/blob/main/roles/apache_httpd/EXAMPLES.md). +## Dependent Roles +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -## Mandatory Requirements +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). +* The `python3-passlib` library must be installed (role: [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python)). -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* Install the `python3-passlib` library. This can be done using the [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python) role. +## Requirements -## Optional Requirements - -* Install PHP and configure PHP-FPM. This can be done using the [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php) role. +Manual steps: +* Optional: deploy PHP and configure PHP-FPM by running the [php](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/php.yml) playbook (role: [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php)). ## Tags @@ -136,21 +121,6 @@ Tip: * To deploy a single vHost only, supplement the `apache_httpd:vhosts` tag with the extra variable `--extra-vars='apache_httpd__limit_vhosts=["www.example.com"]'`. See [Optional Role Variables - Specific to this role](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd#optional-role-variables---specific-to-this-role). - -## Skip Variables - -This role is used in several playbooks that provide skip variables to disable specific dependencies. See the playbooks documentation for details: - -* [apache_httpd.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#apache_httpdyml) -* [setup_grav.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_gravyml) -* [setup_icinga2_master.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_icinga2_masteryml) -* [setup_librenms.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_librenmsyml) -* [setup_mastodon.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_mastodonyml) -* [setup_moodle.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_moodleyml) -* [setup_nextcloud.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_nextcloudyml) -* [setup_wordpress.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_wordpressyml) - - ## Mandatory Role Variables - Global Apache Config (core) `apache_httpd__conf_server_admin` @@ -728,7 +698,6 @@ This role creates a vHost named `localhost` by default. See [defaults/main.yml]( Example: See [EXAMPLES.md](https://github.com/Linuxfabrik/lfops/blob/main/roles/apache_httpd/EXAMPLES.md). - ## Optional Role Variables - mod_dir `apache_httpd__mod_dir_directory_index` @@ -1050,7 +1019,6 @@ apache_httpd__wsgi_script_alias: '/ /var/www/html/python/index.py' ``` - ## License [The Unlicense](https://unlicense.org/) diff --git a/roles/apache_solr/README.md b/roles/apache_solr/README.md index 4f4c0eae..82b8e1b8 100644 --- a/roles/apache_solr/README.md +++ b/roles/apache_solr/README.md @@ -12,11 +12,11 @@ This Ansible role *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install Java 11+. This can be done using the [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps) role. Java OpenJDK latest is recommended. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [Apache Solr Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/apache_solr.yml), this is automatically done for you. +* Java 11+ must be installed (role: [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps)). Java OpenJDK latest is recommended. ## Tags diff --git a/roles/apache_tomcat/README.md b/roles/apache_tomcat/README.md index be46f895..97c632a8 100644 --- a/roles/apache_tomcat/README.md +++ b/roles/apache_tomcat/README.md @@ -22,6 +22,20 @@ Notes: *Available since LFOps `2.0.0`.* +## Dependent Roles + +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). + + +## Requirements + +Manual steps: + +* Set the required SELinux booleans and policies by running the [selinux](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/selinux.yml) playbook (role: [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux)). + + ## Multiple Tomcat Instances How to deploy multiple Tomcat instances on a single server using this and other roles? Imagine you want to run an 'author' and a 'public' instance. Place your config files in `host_files` (for example `host_files/{{ inventory_hostname }}/var/lib/tomcats/{author,public}/conf/{context,server}.xml` and deploy like this: @@ -120,14 +134,6 @@ ansible-playbook --inventory=myinv linuxfabrik.lfops.shell ``` -## Mandatory Requirements - -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* Set SELinux Booleans and Policies properly. This can be done using the [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux) role. - -If you use the [Apache Tomcat Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/apache_tomcat.yml), this is automatically done for you. - - ## Tags `apache_tomcat` diff --git a/roles/bind/README.md b/roles/bind/README.md index 828e4c49..12c8f0c1 100644 --- a/roles/bind/README.md +++ b/roles/bind/README.md @@ -2,6 +2,7 @@ This role installs and configures [bind](https://www.isc.org/bind/) as a DNS server, either as a primary or secondary. + *Available since LFOps `2.0.0`.* @@ -254,16 +255,15 @@ bind__zones: internal-website.example.com A 192.0.2.3 ``` +Primary-Secondary Example: -## Primary-Secondary Example - -With this configuration the primary actively notifies the secondary for any zone changes (i.e. changes to the serial). -The secondary actively checks the serial for changes every 1 hour (`TIME-TO-REFRESH`). -The secondary caches the zone file locally, and uses the cached version during startup. +* With this configuration the primary actively notifies the secondary for any zone changes (i.e. changes to the serial). +* The secondary actively checks the serial for changes every 1 hour (`TIME-TO-REFRESH`). +* The secondary caches the zone file locally, and uses the cached version during startup. -Note: BIND 9.11 (RHEL8) does not yet support `primary` and `secondary`, use `master` and `slave` instead. +* Note: BIND 9.11 (RHEL8) does not yet support `primary` and `secondary`, use `master` and `slave` instead. -Primary: +* Primary: ```yaml # either set `bind__allow_transfer` for all zones, or the `allow_transfer` subkey per zone to allow access bind__allow_transfer: @@ -311,7 +311,7 @@ bind__zones: 3 IN PTR secondary.example.com. ``` -Secondary: +* Secondary: ```yaml bind__zones: - name: 'example.com' diff --git a/roles/clamav/README.md b/roles/clamav/README.md index 604adf15..352b95df 100644 --- a/roles/clamav/README.md +++ b/roles/clamav/README.md @@ -16,12 +16,12 @@ wget http://www.eicar.org/download/eicarcom2.zip *Available since LFOps `3.0.0`.* -## Optional Requirements +## Dependent Roles -* Enable the `antivirus_can_scan_system` and `antivirus_use_jit` SELinux Booleans. This can be done using the [linuxfabrik.lfops.selinux](https://github.com/linuxfabrik/lfops/tree/main/roles/selinux) role. -* Fangfrisch to download unofficial signatures. This can be done using the [linuxfabrik.lfops.fangfrisch](https://github.com/linuxfabrik/lfops/tree/main/roles/fangfrisch) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [ClamAV Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/clamav.yml), this is automatically done for you. +* On RHEL-compatible systems, the `antivirus_can_scan_system` and `antivirus_use_jit` SELinux booleans must be enabled (role: [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux)) so ClamAV can scan the whole system. +* Optional: Fangfrisch must be installed to download unofficial ClamAV signatures (role: [linuxfabrik.lfops.fangfrisch](https://github.com/Linuxfabrik/lfops/tree/main/roles/fangfrisch)). ## Tags diff --git a/roles/collabora/README.md b/roles/collabora/README.md index 1d57584d..20c1e895 100644 --- a/roles/collabora/README.md +++ b/roles/collabora/README.md @@ -6,11 +6,15 @@ This role installs and configures either [Collabora Online Development Edition]( *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable either the official [Collabora CODE Repository](https://docs.fedoraproject.org/en-US/collabora_code/) or your Collabora Enterprise Repository. This can be done using the [linuxfabrik.lfops.repo_collabora_code](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_collabora_code) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["Collabora" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/collabora.yml), this is automatically done for you. +* The official [Collabora CODE Repository](https://docs.fedoraproject.org/en-US/collabora_code/) must be enabled (role: [linuxfabrik.lfops.repo_collabora_code](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_collabora_code)). + +These roles are not enabled by default; enable them via the playbook's skip variables if needed: + +* The Collabora Enterprise repository can be enabled instead (role: [linuxfabrik.lfops.repo_collabora](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_collabora)). ## Tags diff --git a/roles/coturn/README.md b/roles/coturn/README.md index d60b09b7..a2a8b1e2 100644 --- a/roles/coturn/README.md +++ b/roles/coturn/README.md @@ -6,9 +6,11 @@ This role installs and configures [coturn](https://github.com/coturn/coturn). *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). ## Tags diff --git a/roles/docker/README.md b/roles/docker/README.md index fa665449..afa77cec 100644 --- a/roles/docker/README.md +++ b/roles/docker/README.md @@ -6,11 +6,11 @@ This role installs and configures [docker](https://www.docker.com/). *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the official [docker repository](https://docs.docker.com/engine/install/centos/#install-using-the-repository). This can be done using the [linuxfabrik.lfops.repo_docker](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_docker) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["docker" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/docker.yml), this is automatically done for you. +* The official [docker repository](https://docs.docker.com/engine/install/centos/#install-using-the-repository) must be enabled (role: [linuxfabrik.lfops.repo_docker](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_docker)). ## Tags diff --git a/roles/duplicity/README.md b/roles/duplicity/README.md index 8931fd6e..8bb65c2f 100644 --- a/roles/duplicity/README.md +++ b/roles/duplicity/README.md @@ -8,26 +8,29 @@ Note that this role does not support running with `--check`, as it first creates *Available since LFOps `1.0.0`.* -## duba (Duplicity Backup) +## How the Role Behaves -The role comes with the special Python wrapper script `duba` for duplicity, implemented by Linuxfabrik. The script currently does a massive parallel backup to a Swift storage backend with duplicity, where the number of duplicity processes is ``min(processor count, 6) + 1``. The script's configuration file is located at `/etc/duba/duba.json`. +* The role ships `duba`, a Linuxfabrik Python wrapper for duplicity. It runs a massively parallel backup to a Swift storage backend, using `min(processor count, 6) + 1` duplicity processes. Its configuration file is `/etc/duba/duba.json`. +* To start a backup, call `duba` (or `duba --config=/etc/duba/duba.json --command=backup`). See `duba --help` for details. -To start a backup, simply call `duba` (or `duba --config=/etc/duba/duba.json --command=backup`). Have a look at `duba --help` for details. +## Dependent Roles -## Mandatory Requirements +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* Install `duplicity`, `python-swiftclient` and `python-keystoneclient` into a Python 3 virtual environment in `/opt/python-venv/duplicity`. This can be done using the [linuxfabrik.lfops.python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv) role. +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). +* `duplicity`, `python-swiftclient` and `python-keystoneclient` must be installed into a Python 3 virtual environment in `/opt/python-venv/duplicity` (role: [linuxfabrik.lfops.python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv)). **Attention** > Make sure the virtual environment is not writable by other users to prevent privilege escalation. This is also done by the [linuxfabrik.lfops.python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv) role. -## Optional Requirements +## Requirements -* Create a symbolic link from `/opt/python-venv/duplicity/bin/duplicity` to `/usr/local/bin/duplicity` for easier usage on the command line. +Manual steps: + +* Optionally, create a symbolic link from `/opt/python-venv/duplicity/bin/duplicity` to `/usr/local/bin/duplicity` for easier usage on the command line. * Either configure journald to persist your logs and do the rotating, or use logrotated. diff --git a/roles/elastic_agent/README.md b/roles/elastic_agent/README.md index ae7d959e..c5f2b2d4 100644 --- a/roles/elastic_agent/README.md +++ b/roles/elastic_agent/README.md @@ -6,16 +6,26 @@ This role installs and configures [Elastic Agent](https://www.elastic.co/elastic *Available since LFOps `6.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the Elasticsearch Package Repository. This can be done using the [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch) role. -* A running Fleet Server. This can be set up using the [linuxfabrik.lfops.elastic_agent_fleet_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/elastic_agent_fleet_server) role. -* An enrollment token from Kibana Fleet. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. +* The Elasticsearch package repository must be enabled (role: [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch)). The elastic-agent package is served from it. -## Optional Requirements -* CA certificate for verifying the Fleet Server TLS certificate. This is the CA used for Fleet Server, typically the same as Elasticsearch. +## Requirements + +* A Fleet Server must be running and reachable. + +Manual steps: + +* Deploy a Fleet Server by running the [elastic_agent_fleet_server](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/elastic_agent_fleet_server.yml) playbook (role: [linuxfabrik.lfops.elastic_agent_fleet_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/elastic_agent_fleet_server)). +* Get an enrollment token from Kibana and store it as `elastic_agent__enrollment_token`: + + 1. In Kibana, go to Fleet → Enrollment tokens + 2. Click "Create enrollment token" + 3. Select the agent policy + 4. Copy the token ## Tags @@ -41,18 +51,6 @@ This role installs and configures [Elastic Agent](https://www.elastic.co/elastic * Triggers: none. -## Pre-Installation Steps - -### Get Enrollment Token - -Get an enrollment token from Kibana: - -1. In Kibana, go to Fleet → Enrollment tokens -2. Click "Create enrollment token" -3. Select the agent policy -4. Copy the token - - ## Mandatory Role Variables `elastic_agent__enrollment_token` @@ -79,7 +77,7 @@ elastic_agent__fleet_url: 'https://fleet1.example.com:8220' `elastic_agent__fleet_ca` -* ASCII-armored PEM CA certificate for verifying the Fleet Server TLS certificate. +* ASCII-armored PEM CA certificate for verifying the Fleet Server TLS certificate, typically the same CA as Elasticsearch. * Type: String. * Default: unset diff --git a/roles/elastic_agent_fleet_server/README.md b/roles/elastic_agent_fleet_server/README.md index 164e0f72..8a40f8a0 100644 --- a/roles/elastic_agent_fleet_server/README.md +++ b/roles/elastic_agent_fleet_server/README.md @@ -6,42 +6,22 @@ This role installs and configures [Elastic Agent](https://www.elastic.co/elastic *Available since LFOps `6.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the Elasticsearch Package Repository. This can be done using the [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch) role. -* A running Elasticsearch cluster. -* A Fleet Server service token. Generate one using the Elasticsearch API or Kibana (Fleet -> Add Fleet Server). +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. +* The Elasticsearch package repository must be enabled (role: [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch)). -## Optional Requirements -* TLS certificates for the Fleet Server. Generate them using the Elasticsearch `certutil` tool (see below). +## Requirements +* A running Elasticsearch cluster must be reachable. -## Tags - -`elastic_agent_fleet_server` - -* Installs and configures elastic-agent as Fleet Server. -* Triggers: none. +Manual steps: -`elastic_agent_fleet_server:certs` - -* Deploys TLS certificates. -* Triggers: none. - -`elastic_agent_fleet_server:enroll` - -* Enrolls the agent as Fleet Server. -* Triggers: none. - -`elastic_agent_fleet_server:state` - -* Manages the state of the elastic-agent service. -* Triggers: none. - - -## Pre-Installation Steps +* Deploy Elasticsearch by running the [elasticsearch](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/elasticsearch.yml) playbook (role: [linuxfabrik.lfops.elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/elasticsearch)). +* Generate a Fleet Server service token using the Elasticsearch API or Kibana (Fleet -> Add Fleet Server). See below. +* Optionally, generate TLS certificates for the Fleet Server using the Elasticsearch `certutil` tool. See below. ### Generate Service Token @@ -95,6 +75,29 @@ Copy the generated certificates to the Ansible inventory. The certificates are u * `elastic_agent_fleet_server__ssl_key` - The Fleet Server private key +## Tags + +`elastic_agent_fleet_server` + +* Installs and configures elastic-agent as Fleet Server. +* Triggers: none. + +`elastic_agent_fleet_server:certs` + +* Deploys TLS certificates. +* Triggers: none. + +`elastic_agent_fleet_server:enroll` + +* Enrolls the agent as Fleet Server. +* Triggers: none. + +`elastic_agent_fleet_server:state` + +* Manages the state of the elastic-agent service. +* Triggers: none. + + ## Mandatory Role Variables `elastic_agent_fleet_server__elasticsearch_host` diff --git a/roles/elasticsearch/README.md b/roles/elasticsearch/README.md index 7d399de5..decbc0ba 100644 --- a/roles/elasticsearch/README.md +++ b/roles/elasticsearch/README.md @@ -8,18 +8,12 @@ Note that this role does NOT let you specify a particular Elasticsearch server v *Available since LFOps `5.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the official Elasticsearch repository. This can be done using the [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [elasticsearch playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/elasticsearch.yml), this is automatically done for you. - - -## Optional Requirements - -* Set `vm.swappiness` to 1. This can be done using the [linuxfabrik.lfops.kernel_settings](https://github.com/Linuxfabrik/lfops/tree/main/roles/kernel_settings) role. - -If you use the [elasticsearch playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/elasticsearch.yml), this is automatically done for you. +* The official Elasticsearch repository must be enabled (role: [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch)). +* Optional: `vm.swappiness` set to 1 (role: [linuxfabrik.lfops.kernel_settings](https://github.com/Linuxfabrik/lfops/tree/main/roles/kernel_settings)). ## Single-Node Setup @@ -27,46 +21,7 @@ If you use the [elasticsearch playbook](https://github.com/Linuxfabrik/lfops/blo For a single-node setup, no special configuration is needed beyond the mandatory requirements. When `elasticsearch__discovery_seed_hosts` is not set, the role automatically configures `discovery.type: single-node`. After installation, generate the initial password for the `elastic` user (see Post-Installation Steps below). -## Tags - -`elasticsearch` - -* Installs Elasticsearch and unzip. -* Creates the data directory and tmp directory. -* Deploys all configuration files. -* Deploys TLS certificates (if `elasticsearch__ca_cert` is set). -* Manages the state of the Elasticsearch service. -* Triggers: elasticsearch.service restart. - -`elasticsearch:certs` - -* Deploys TLS certificates (CA, HTTP, transport). -* Triggers: elasticsearch.service restart. - -`elasticsearch:configure` - -* Deploys `/etc/elasticsearch/elasticsearch.yml`. -* Deploys `/etc/elasticsearch/log4j2.properties`. -* Deploys the sysconfig file. -* Deploys `/tmp/certutil.yml` (if `elasticsearch__discovery_seed_hosts` is set). -* Triggers: elasticsearch.service restart. - -`elasticsearch:state` - -* Manages the state of the Elasticsearch service (`systemctl enable/disable`, `start/stop/restart`). -* Triggers: none. - - -## Post-Installation Steps - -After setting up a single node or cluster, generate the initial password for the `elastic` user: - -```bash -/usr/share/elasticsearch/bin/elasticsearch-reset-password --username elastic -``` - - -## Setting Up an Elasticsearch Cluster +## Cluster Setup This role supports creating a multi-node Elasticsearch cluster using manual certificate distribution. Elasticsearch 8.x ships with an enrollment token mechanism for adding nodes to a cluster. However, enrollment tokens expire after 30 minutes, and the process requires interactive commands on the command line. This makes it unsuitable for automation with Ansible. Instead, this role generates TLS certificates manually using `elasticsearch-certutil` and distributes them to all nodes via Ansible. @@ -182,7 +137,7 @@ curl --cacert "$elastic_cacert" \ The status should be `green` with all nodes listed. -## Adding a New Node to an Existing Cluster +## Adding a Node to an Existing Cluster 1. Generate certificates for the new node using the existing CA (**manual**). On the node where the CA is stored: ```bash @@ -218,6 +173,45 @@ ansible-playbook --inventory inventory linuxfabrik.lfops.elasticsearch --limit n 6. Roll out the `elasticsearch__discovery_seed_hosts` to all cluster nodes (**automated**) +## Post-Installation Steps + +After setting up a single node or cluster, generate the initial password for the `elastic` user: + +```bash +/usr/share/elasticsearch/bin/elasticsearch-reset-password --username elastic +``` + + +## Tags + +`elasticsearch` + +* Installs Elasticsearch and unzip. +* Creates the data directory and tmp directory. +* Deploys all configuration files. +* Deploys TLS certificates (if `elasticsearch__ca_cert` is set). +* Manages the state of the Elasticsearch service. +* Triggers: elasticsearch.service restart. + +`elasticsearch:certs` + +* Deploys TLS certificates (CA, HTTP, transport). +* Triggers: elasticsearch.service restart. + +`elasticsearch:configure` + +* Deploys `/etc/elasticsearch/elasticsearch.yml`. +* Deploys `/etc/elasticsearch/log4j2.properties`. +* Deploys the sysconfig file. +* Deploys `/tmp/certutil.yml` (if `elasticsearch__discovery_seed_hosts` is set). +* Triggers: elasticsearch.service restart. + +`elasticsearch:state` + +* Manages the state of the Elasticsearch service (`systemctl enable/disable`, `start/stop/restart`). +* Triggers: none. + + ## Optional Role Variables `elasticsearch__action_auto_create_index__host_var` / `elasticsearch__action_auto_create_index__group_var` diff --git a/roles/example/README.md b/roles/example/README.md index 6089b1b0..b2d30ed7 100644 --- a/roles/example/README.md +++ b/roles/example/README.md @@ -2,23 +2,115 @@ This role installs and configures [Example](https://example.com/). Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -This role also serves as a reference for consistent role development across LFOps. All `ansible.builtin.*` modules used in `tasks/main.yml` are documented with their most common parameters. +This role also serves as a reference for consistent role development across LFOps. All `ansible.builtin.*` modules used in `tasks/main.yml` are documented with their most common parameters. The section structure below is the canonical menu for every role README: keep the sections in this order, drop the optional ones that do not apply, and never invent new top-level sections. This role is compatible with the following example versions: * 1.0.0 * 2.0.0 -## Mandatory Requirements -* Enable the example repository. This can be done using the [linuxfabrik.lfops.repo_example](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_example) role. + +*Available since LFOps `2.0.0`.* -If you use the [Example Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/example.yml), this is automatically done for you. + +## How the Role Behaves -## Optional Requirements +* The release tarball is downloaded on the Ansible controller (`delegate_to: 'localhost'`, `run_once: true`) and copied to the target, so targets without Internet access can still be provisioned. The controller needs outbound access to `example.com`; the target does not. +* Configuration is fully templated. On every run the files under `/etc/example/` are re-rendered from the role's templates (a timestamped backup is kept), so out-of-band manual edits are overwritten. Manage all settings through the role variables below. +* A configuration change notifies a chained handler that first runs `example --validate-config` and then restarts `example.service`. The restart is skipped when the service was just started in the same run or when `example__service_state` is `stopped`. +* Version-specific defaults are loaded from the *installed* package version (`vars/.yml`), not from `example__version`. +* On Red Hat-family hosts the role manages the SELinux port type and the `httpd_can_network_connect` boolean, but only when SELinux is not disabled. +* This role does not manage the firewall or TLS certificates. Open the listener ports and provide certificates separately. -* Install the optional dependency. This can be done using the [linuxfabrik.lfops.optional_dependency](https://github.com/Linuxfabrik/lfops/tree/main/roles/optional_dependency) role. + + +## Known Limitations + +* The role configures a single standalone node; clustering is not supported. +* Downgrades are not tested. Lowering `example__version` after installation may leave stale files under `/opt/example`. + + + +## Dependent Roles + +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* The example repository must be enabled (role: [linuxfabrik.lfops.repo_example](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_example)). The example packages are served from it. +* Optional: the optional dependency (role: [linuxfabrik.lfops.optional_dependency](https://github.com/Linuxfabrik/lfops/tree/main/roles/optional_dependency)) enables the optional feature. + +These roles are not enabled by default; enable them via the playbook's skip variables if needed: + +* An alternative cache backend (role: [linuxfabrik.lfops.example_cache](https://github.com/Linuxfabrik/lfops/tree/main/roles/example_cache)) replaces the built-in cache. + + + +## Requirements + +* At least 2 GB RAM on the target host. +* Outbound HTTPS access from the target to `example.com` for license validation. + +Manual steps: + +* Deploy a database server first by running the [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server) role. The example role expects an existing database. +* Obtain an API token from the Example web console and store it in your inventory as `example__api_token`. +* Optional: install the Example CLI on the Ansible controller (`pip install example-cli`) to use the `example:inspect` helper tasks. + + + + + + +## Post-Installation Steps + +* Retrieve the initial admin password from `/var/lib/example/initial-admin-password` and change it on first login. ## Tags @@ -66,6 +158,16 @@ example__version: '3.2.1' ``` + +## Recommended Role Variables + +`example__backup_target` + +* Where nightly dumps are written. Strongly recommended in production; without it no backups are taken. +* Type: String. +* Default: `''` (no backups) + + ## Optional Role Variables `example__conf_log_level__host_var` / `example__conf_log_level__group_var` @@ -230,6 +332,39 @@ example__users__host_var: ``` + +## Optional Role Variables - `example__conf_tls_*` Config Directives + +`example__conf_tls_protocols` + +* Allowed TLS protocol versions. +* Type: String. +* Default: `'TLSv1.2 TLSv1.3'` + +Example: +```yaml +# optional +example__conf_tls_protocols: 'TLSv1.3' +``` + + + +## Troubleshooting + +**`example.service` fails to start with `bind: address already in use`** + +* Another process occupies a configured port. Change the affected `example__listeners` entry or stop the conflicting service. + + ## License [The Unlicense](https://unlicense.org/) diff --git a/roles/exoscale_vm/README.md b/roles/exoscale_vm/README.md index dfc5c312..807cc885 100644 --- a/roles/exoscale_vm/README.md +++ b/roles/exoscale_vm/README.md @@ -11,7 +11,9 @@ This role creates and manages instances (virtual machines) on [Exoscale](https:/ * Resizing / scaling of instances is currently not supported -## Mandatory Requirements +## Requirements + +Manual steps: * Install the [exo command line tool](https://github.com/exoscale/cli/releases) and configure your Exoscale account using `exo config` on the Ansible control node. * Install the `python3-cs` library on the Ansible control node. diff --git a/roles/fail2ban/README.md b/roles/fail2ban/README.md index f42b79ce..ea709c89 100644 --- a/roles/fail2ban/README.md +++ b/roles/fail2ban/README.md @@ -11,13 +11,13 @@ This role provides two additional filters: *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install the `python3-policycoreutils` module (required for the SELinux Ansible tasks). This can be done using the [linuxfabrik.lfops.policycoreutils](https://github.com/Linuxfabrik/lfops/tree/main/roles/policycoreutils) role. -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* On RHEL-compatible systems, enable the `nis_enabled` SELinux boolean. This can be done using the [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["Fail2Ban" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/fail2ban.yml), this is automatically done for you. +* The `python3-policycoreutils` module must be installed (required for the SELinux Ansible tasks) (role: [linuxfabrik.lfops.policycoreutils](https://github.com/Linuxfabrik/lfops/tree/main/roles/policycoreutils)). +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). +* On RHEL-compatible systems, the `nis_enabled` SELinux boolean must be enabled (role: [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux)). ## Tags diff --git a/roles/fangfrisch/README.md b/roles/fangfrisch/README.md index ee5254e4..f6cf2d2e 100644 --- a/roles/fangfrisch/README.md +++ b/roles/fangfrisch/README.md @@ -6,11 +6,11 @@ This role installs and configures [Fangfrisch](https://rseichter.github.io/fangf *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* A Python virtual environment `/opt/python-venv/clamav-fangfrisch/` with `fangfrisch` installed. This can be done using the [linuxfabrik.lfops.python_venv](https://github.com/linuxfabrik/lfops/tree/main/roles/python_venv) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [Fangfrisch Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/fangfrisch.yml), this is automatically done for you. +* A Python virtual environment `/opt/python-venv/clamav-fangfrisch/` with `fangfrisch` installed must be present (role: [linuxfabrik.lfops.python_venv](https://github.com/linuxfabrik/lfops/tree/main/roles/python_venv)). ## Tags diff --git a/roles/files/README.md b/roles/files/README.md index 20e2b41a..10df38b9 100644 --- a/roles/files/README.md +++ b/roles/files/README.md @@ -6,9 +6,11 @@ This role manages file system entities such as files, directories and symlinks. *Available since LFOps `3.0.0`.* -## Optional Requirements +## Requirements -* It is recommeded to set `inventory_ignore_patterns = '(host|group)_files'` in your `ansible.cfg` on the Ansible Controller to ignore files in `inventory_dir/host_files`. +Manual steps: + +* Optional: set `inventory_ignore_patterns = '(host|group)_files'` in your `ansible.cfg` on the Ansible controller to ignore files in `inventory_dir/host_files`. ## Tags diff --git a/roles/firewall/README.md b/roles/firewall/README.md index 25e94827..350769e0 100644 --- a/roles/firewall/README.md +++ b/roles/firewall/README.md @@ -6,14 +6,12 @@ This role configures a firewall on the system. For the currently supported firew *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Requirements -* When using `firewall__firewall == fwbuilder`, you either need to manually deploy a Firewall Builder file to `/etc/fwb.sh` or use the ``firewall__fwbuilder_repo_url`` variable to clone the Firewall Builder files automatically. +Manual steps: - -## Optional Requirements - -* When using `firewall__firewall == iptables`, you can place an iptables config file in your inventory, which will be deployed to the system. The file has to be placed into `{{ inventory_dir }}/host_files/{{ inventory_hostname }}/etc/sysconfig/iptables`. +* When using `firewall__firewall == fwbuilder`, either manually deploy a Firewall Builder file to `/etc/fwb.sh` or use the ``firewall__fwbuilder_repo_url`` variable to clone the Firewall Builder files automatically. +* When using `firewall__firewall == iptables`, optionally place an iptables config file in your inventory, which will be deployed to the system. The file has to be placed into `{{ inventory_dir }}/host_files/{{ inventory_hostname }}/etc/sysconfig/iptables`. ## Tags diff --git a/roles/freeipa_client/README.md b/roles/freeipa_client/README.md index f3cc875d..d5e7c3be 100644 --- a/roles/freeipa_client/README.md +++ b/roles/freeipa_client/README.md @@ -6,12 +6,18 @@ This role installs and configures [FreeIPA](https://www.freeipa.org/) as a clien *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Known Limitations -* Install the [ansible-freeipa Ansible Collection](https://github.com/freeipa/ansible-freeipa) on the Ansible control node. This can be done by calling `ansible-galaxy collection install freeipa.ansible_freeipa`. * The role must be run with Ansible's `linear` strategy (the default). It is incompatible with strategies that reuse the target Python interpreter, such as Mitogen's `mitogen_linear`, because the underlying ansible-freeipa modules use `ipalib`'s global API singleton and fail with `API.bootstrap() already called` on the second module call. The bundled `playbooks/freeipa_client.yml` sets `strategy: 'linear'` explicitly. +## Requirements + +Manual steps: + +* Install the [ansible-freeipa Ansible Collection](https://github.com/freeipa/ansible-freeipa) on the Ansible control node by calling `ansible-galaxy collection install freeipa.ansible_freeipa`. + + ## Tags `freeipa_client` diff --git a/roles/freeipa_server/README.md b/roles/freeipa_server/README.md index 30ddd638..60994f23 100644 --- a/roles/freeipa_server/README.md +++ b/roles/freeipa_server/README.md @@ -8,10 +8,15 @@ Ideally, the FreeIPA should be installed on a separate server. If that is not po *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Known Limitations + +* The role must be run with Ansible's `linear` strategy (the default). It is incompatible with strategies that reuse the target Python interpreter, such as Mitogen's `mitogen_linear`, because the underlying ansible-freeipa modules use `ipalib`'s global API singleton and fail with `API.bootstrap() already called` on the second module call. The bundled `playbooks/freeipa_server.yml` sets `strategy: 'linear'` explicitly. + + +## Requirements * At least 2 GB RAM are required. -* The IPA installer is quite picky about the DNS configuration. The following checks are done by installer: +* The IPA installer is quite picky about the DNS configuration. The following checks are done by the installer: * The hostname cannot be `localhost` or `localhost6`. * The hostname must be fully-qualified (`server.ipa.test`). Use two-level domain names. Otherwise you'll get error messages like `Invalid realm name: single label realms are not supported`. @@ -20,8 +25,10 @@ Ideally, the FreeIPA should be installed on a separate server. If that is not po * If neither the domain nor the realm being set, you'll get error messages like `In unattended mode you need to provide at least -r, -p and -a options`. * Do not use an existing domain or hostname unless you own the domain. It's a common mistake to use `example.com`. We recommend to use a reserved top level domain from RFC2606 for private test installations, e.g. `ipa.test`. -* Install the [ansible-freeipa Ansible Collection](https://github.com/freeipa/ansible-freeipa) on the Ansible control node. This can be done by calling `ansible-galaxy collection install freeipa.ansible_freeipa`. -* The role must be run with Ansible's `linear` strategy (the default). It is incompatible with strategies that reuse the target Python interpreter, such as Mitogen's `mitogen_linear`, because the underlying ansible-freeipa modules use `ipalib`'s global API singleton and fail with `API.bootstrap() already called` on the second module call. The bundled `playbooks/freeipa_server.yml` sets `strategy: 'linear'` explicitly. + +Manual steps: + +* Install the [ansible-freeipa Ansible Collection](https://github.com/freeipa/ansible-freeipa) on the Ansible control node by calling `ansible-galaxy collection install freeipa.ansible_freeipa`. ## Tags diff --git a/roles/github_project_createrepo/README.md b/roles/github_project_createrepo/README.md index 8b5710da..c15e3281 100644 --- a/roles/github_project_createrepo/README.md +++ b/roles/github_project_createrepo/README.md @@ -6,13 +6,13 @@ This role installs and configures [github_project_createrepo](https://github.com *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install Python 3. This can be done using the [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python) role. -* Install `createrepo`. This can be done using the [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps) role. -* Install `git`. This can be done using the [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [`github_project_createrepo` Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/github_project_createrepo.yml), this is automatically done for you. +* Python 3 must be installed (role: [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python)). +* `createrepo` must be installed (role: [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps)). +* `git` must be installed (role: [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps)). ## Tags diff --git a/roles/gitlab_ce/README.md b/roles/gitlab_ce/README.md index c14b16f4..da6bc02c 100644 --- a/roles/gitlab_ce/README.md +++ b/roles/gitlab_ce/README.md @@ -9,11 +9,11 @@ This role installs and configures [GitLab CE](https://about.gitlab.com/), includ *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the official GitLab CE Repository. This can be done using the [linuxfabrik.lfops.repo_gitlab_ce](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_gitlab_ce) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [gitlab_ce Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/gitlab_ce.yml), this is automatically done for you. +* The official GitLab CE Repository must be enabled (role: [linuxfabrik.lfops.repo_gitlab_ce](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_gitlab_ce)). ## Tags diff --git a/roles/glances/README.md b/roles/glances/README.md index f5e1b5bc..85eecd80 100644 --- a/roles/glances/README.md +++ b/roles/glances/README.md @@ -6,34 +6,27 @@ This role installs [glances](https://nicolargo.github.io/glances/) and drops a s *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* On RHEL-compatible systems, the EPEL repository (provides the `glances` package). The companion playbook (`playbooks/glances.yml`) takes care of this automatically by also running [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) on RHEL 7/8/9 hosts. -* On Rocky Linux 9, the CRB repository (moved from EPEL into the base repo on Rocky 9). The playbook also runs [linuxfabrik.lfops.repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos) for that distribution / version unless skipped (see below). -* `glances` is currently **not packaged in EPEL 10**, so this role fails with `No package glances available.` on RHEL 10 and clones (Rocky / Alma 10). Install glances manually (e.g. via `pip install glances` in a venv, or from a third-party repo) on those hosts and skip this role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). It provides the `glances` package. +* On Rocky Linux 9, the CRB repository must be enabled (role: [linuxfabrik.lfops.repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos)). The `glances` package moved from EPEL into the base repository on Rocky 9. -## Tags -`glances` +## Requirements -* Installs glances and configures the `top` / `glances` aliases. -* Triggers: none. +Manual steps: +* On RHEL 10 and clones (Rocky / Alma 10), `glances` is not packaged in EPEL 10, so this role fails with `No package glances available.`. Install glances manually there (for example via `pip install glances` in a venv, or from a third-party repo) and skip this role. -## Optional Playbook Variables -`glances__skip_repo_baseos` +## Tags -* Skip the implicit `linuxfabrik.lfops.repo_baseos` invocation on Rocky Linux 9. Set this if you manage the CRB repository yourself or if the host has no Internet access to the Rocky mirrors. -* Type: Bool. -* Default: `false` +`glances` -Example: -```yaml -# optional -glances__skip_repo_baseos: true -``` +* Installs glances and configures the `top` / `glances` aliases. +* Triggers: none. ## License diff --git a/roles/glpi_agent/README.md b/roles/glpi_agent/README.md index 18749d36..c9f52c24 100644 --- a/roles/glpi_agent/README.md +++ b/roles/glpi_agent/README.md @@ -6,11 +6,11 @@ This role installs and configures the [GLPI Agent](https://glpi-agent.readthedoc *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["GLPI Agent" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/glpi_agent.yml), this is automatically done for you. +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). ## Tags diff --git a/roles/grafana/README.md b/roles/grafana/README.md index f920e928..a1806c96 100644 --- a/roles/grafana/README.md +++ b/roles/grafana/README.md @@ -6,9 +6,11 @@ This role installs and configures [Grafana](https://grafana.com/). *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the official [Grafana OSS Repository](https://grafana.com/docs/grafana/latest/installation/rpm/). This can be done using the [linuxfabrik.lfops.repo_grafana](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_grafana) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* The official [Grafana OSS Repository](https://grafana.com/docs/grafana/latest/installation/rpm/) must be enabled (role: [linuxfabrik.lfops.repo_grafana](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_grafana)). ## Tags diff --git a/roles/grafana_grizzly/README.md b/roles/grafana_grizzly/README.md index ff9a1cc5..008bd2be 100644 --- a/roles/grafana_grizzly/README.md +++ b/roles/grafana_grizzly/README.md @@ -8,12 +8,12 @@ Additionally, this role allows you to apply Grafana resources which are saved as *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* A Grafana Server. This can be done using the [linuxfabrik.lfops.grafana](https://github.com/linuxfabrik/lfops/tree/main/roles/grafana) role. -* A Grafana [service account](https://grafana.com/docs/grafana/latest/administration/service-accounts/) with an Admin token. This can be done using the [linuxfabrik.lfops.grafana](https://github.com/linuxfabrik/lfops/tree/main/roles/grafana) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [Grafana Grizzly Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/grafana_grizzly.yml) this is automatically done for you. +* A Grafana Server must be available (role: [linuxfabrik.lfops.grafana](https://github.com/linuxfabrik/lfops/tree/main/roles/grafana)). +* A Grafana [service account](https://grafana.com/docs/grafana/latest/administration/service-accounts/) with an Admin token must exist (role: [linuxfabrik.lfops.grafana](https://github.com/linuxfabrik/lfops/tree/main/roles/grafana)). ## Tags diff --git a/roles/grav/README.md b/roles/grav/README.md index 2bc1f728..3411ea37 100644 --- a/roles/grav/README.md +++ b/roles/grav/README.md @@ -8,12 +8,19 @@ It is possible to configure whether the Grav Admin Panel should be installed (it *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Known Limitations + +There might be more to implement: + +* https://learn.getgrav.org/17/security/configuration + + +## Dependent Roles -* Install a web server (for example Apache httpd), and configure a virtual host for Grav. This can be done using the [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd) role. -* Install PHP 7.3.6+ (**PHP 8.1 recommended** (20220930)). This can be done using the [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) and [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["Grav" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/grav.yml), this is automatically done for you. +* A web server (for example Apache httpd) with a virtual host configured for Grav must be available (role: [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd)). +* PHP 7.3.6+ (**PHP 8.1 recommended** (20220930)) must be installed (roles: [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) and [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php)). ## Tags @@ -125,13 +132,6 @@ grav__users: * Default: `true` -## Known Limitations - -There might be more to implement: - -* https://learn.getgrav.org/17/security/configuration - - ## License [The Unlicense](https://unlicense.org/) diff --git a/roles/graylog_datanode/README.md b/roles/graylog_datanode/README.md index 5a6abff7..c774e7a2 100644 --- a/roles/graylog_datanode/README.md +++ b/roles/graylog_datanode/README.md @@ -14,18 +14,24 @@ Note that this role does NOT let you specify a particular Graylog Data Node vers * This role does not currently support more than one data node. -## Mandatory Requirements +## Dependent Roles -Sizing of disks: +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -* `/`: at least 4 GB free disk space (create a 8+ GB partition). -* `/var`: at least 15 GB free disk space (create a 20+ GB partition). +* MongoDB must be installed (role: [linuxfabrik.lfops.mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/mongodb)). +* The official [Graylog repository](https://go2docs.graylog.org/current/downloading_and_installing_graylog/red_hat_installation.htm) must be enabled (role: [linuxfabrik.lfops.repo_graylog](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_graylog)). -If you use the ["Setup Graylog Data Node" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_graylog_datanode.yml), the following is automatically done for you: -* Install MongoDB. This can be done using the [linuxfabrik.lfops.mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/mongodb) role. -* If you're not using a versioned MongoDB repository, don't forget to protect MongoDB from being updated with newer minor and major versions. This can be done using the [linuxfabrik.lfops.dnf_versionlock](https://github.com/Linuxfabrik/lfops/tree/main/roles/dnf_versionlock) role. -* Enable the official [Graylog repository](https://go2docs.graylog.org/current/downloading_and_installing_graylog/red_hat_installation.htm). This can be done using the [linuxfabrik.lfops.repo_graylog](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_graylog) role. +## Requirements + +* Size the disks before running the role: + + * `/`: at least 4 GB free disk space (create a 8+ GB partition). + * `/var`: at least 15 GB free disk space (create a 20+ GB partition). + +Manual steps: + +* If you're not using a versioned MongoDB repository, protect MongoDB from being updated with newer minor and major versions by running the [dnf_versionlock](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/dnf_versionlock.yml) playbook (role: [linuxfabrik.lfops.dnf_versionlock](https://github.com/Linuxfabrik/lfops/tree/main/roles/dnf_versionlock)). ## Tags diff --git a/roles/graylog_server/README.md b/roles/graylog_server/README.md index 1ac5be31..8329f3a4 100644 --- a/roles/graylog_server/README.md +++ b/roles/graylog_server/README.md @@ -15,20 +15,25 @@ Note that this role does NOT let you specify a particular Graylog Server version * This role only supports Graylog Data Nodes (not OpenSearch or Elasticsearch). -## Mandatory Requirements +## Dependent Roles -Properly set hostnames and ensure that communication via DNS among all participating hosts works. This especially affects clustered systems, because the datanode instance registers itself to the mongodb database with its hostname. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -Sizing of disks: +* MongoDB must be installed (role: [linuxfabrik.lfops.mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/mongodb)). +* The official [Graylog repository](https://go2docs.graylog.org/current/downloading_and_installing_graylog/red_hat_installation.htm) must be enabled (role: [linuxfabrik.lfops.repo_graylog](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_graylog)). -* `/`: at least 4 GB free disk space (create a 8+ GB partition). -* `/var`: at least 15 GB free disk space (create a 20+ GB partition). -If you use the ["Setup Graylog Server" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_graylog_server.yml), the following is automatically done for you: +## Requirements -* Install MongoDB. This can be done using the [linuxfabrik.lfops.mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/mongodb) role. -* If you're not using a versioned MongoDB repository, don't forget to protect MongoDB from being updated with newer minor and major versions. This can be done using the [linuxfabrik.lfops.dnf_versionlock](https://github.com/Linuxfabrik/lfops/tree/main/roles/dnf_versionlock) role. -* Enable the official [Graylog repository](https://go2docs.graylog.org/current/downloading_and_installing_graylog/red_hat_installation.htm). This can be done using the [linuxfabrik.lfops.repo_graylog](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_graylog) role. +* Size the disks before running the role: + + * `/`: at least 4 GB free disk space (create a 8+ GB partition). + * `/var`: at least 15 GB free disk space (create a 20+ GB partition). + +Manual steps: + +* Set hostnames properly and ensure that communication via DNS among all participating hosts works. This especially affects clustered systems, because the datanode instance registers itself to the mongodb database with its hostname. +* If you're not using a versioned MongoDB repository, protect MongoDB from being updated with newer minor and major versions by running the [dnf_versionlock](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/dnf_versionlock.yml) playbook (role: [linuxfabrik.lfops.dnf_versionlock](https://github.com/Linuxfabrik/lfops/tree/main/roles/dnf_versionlock)). ## Tags @@ -62,13 +67,6 @@ If you use the ["Setup Graylog Server" Playbook](https://github.com/Linuxfabrik/ * Triggers: none. -## Skip Variables - -This role is used in several playbooks that provide skip variables to disable specific dependencies. See the playbooks documentation for details: - -* [setup_graylog_server.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_graylog_serveryml) - - ## Mandatory Role Variables `graylog_server__password_secret` diff --git a/roles/hetzner_vm/README.md b/roles/hetzner_vm/README.md index fa800506..ee2daa09 100644 --- a/roles/hetzner_vm/README.md +++ b/roles/hetzner_vm/README.md @@ -19,15 +19,22 @@ This role does not configure the VM's network interfaces. *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Known Limitations -* Install the Python library `hcloud` on the Ansible control node (use `pip install --user --upgrade hcloud`). -* Import your public SSH-key into Hetzner (your project > Security > SSH Keys). +It is currently not possible to start a server with only an internal network *and* a fixed IP (see https://github.com/ansible-collections/hetzner.hcloud/issues/172). As a workaround, the server can be created with `hetzner_vm__state: 'stopped'` and then started: +```bash +ansible-playbook --inventory=inventory linuxfabrik.lfops.hetzner_vm --extra-vars="hetzner_vm__state='stopped'" +ansible-playbook --inventory=inventory linuxfabrik.lfops.hetzner_vm --extra-vars="hetzner_vm__state='started'" +``` -## Optional Requirements +## Requirements -* Install the [hcloud command line tool](https://github.com/hetznercloud/cli/releases). +Manual steps: + +* Install the Python library `hcloud` on the Ansible control node (use `pip install --user --upgrade hcloud`). +* Import your public SSH-key into Hetzner (your project > Security > SSH Keys). +* Optional: install the [hcloud command line tool](https://github.com/hetznercloud/cli/releases). ## Tags @@ -253,15 +260,6 @@ hetzner_vm__volumes: ``` -## Known Limitations - -It is currently not possible to start a server with only an internal network *and* a fixed IP (see https://github.com/ansible-collections/hetzner.hcloud/issues/172). As a workaround, the server can be created with `hetzner_vm__state: 'stopped'` and then started: -```bash -ansible-playbook --inventory=inventory linuxfabrik.lfops.hetzner_vm --extra-vars="hetzner_vm__state='stopped'" -ansible-playbook --inventory=inventory linuxfabrik.lfops.hetzner_vm --extra-vars="hetzner_vm__state='started'" -``` - - ## License [The Unlicense](https://unlicense.org/) diff --git a/roles/icinga2_agent/README.md b/roles/icinga2_agent/README.md index 867959cb..1e74cbde 100644 --- a/roles/icinga2_agent/README.md +++ b/roles/icinga2_agent/README.md @@ -8,10 +8,18 @@ Currently, this role only works if the host can reach the Icinga2 master API. *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the [Icinga Package Repository](https://packages.icinga.com/). This can be done using the [linuxfabrik.lfops.repo_icinga](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_icinga) role. -* A configured Icinga2 Master. This can be done using the [linuxfabrik.lfops.icinga2_master](https://github.com/Linuxfabrik/lfops/tree/main/roles/icinga2_master) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* The [Icinga Package Repository](https://packages.icinga.com/) must be enabled (role: [linuxfabrik.lfops.repo_icinga](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_icinga)). + + +## Requirements + +Manual steps: + +* Deploy a configured Icinga2 Master, reachable from this host, by running the [setup_icinga2_master](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_icinga2_master.yml) playbook (role: [linuxfabrik.lfops.icinga2_master](https://github.com/Linuxfabrik/lfops/tree/main/roles/icinga2_master)). The master runs on a separate host and is not set up by this role's playbook. ## Tags diff --git a/roles/icinga2_master/README.md b/roles/icinga2_master/README.md index 965d6bec..759217bd 100644 --- a/roles/icinga2_master/README.md +++ b/roles/icinga2_master/README.md @@ -6,13 +6,13 @@ This role installs and configures [Icinga2](https://icinga.com/docs/icinga-2/lat *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install InfluxDB, and create a database and a user for said database. This can be done using the [linuxfabrik.lfops.influxdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/influxdb) role. -* Install MariaDB, and create a database and a user for said database. This can be done using the [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server) role. -* On RHEL-compatible systems, enable the `icinga2_can_connect_all`, `icinga2_run_sudo` and `nagios_run_sudo` SELinux booleans. This can be done using the [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["Setup Icinga2 Master" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_icinga2_master.yml), this is automatically done for you. +* InfluxDB must be installed with a database and a user for said database (role: [linuxfabrik.lfops.influxdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/influxdb)). +* MariaDB must be installed with a database and a user for said database (role: [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server)). +* On RHEL-compatible systems, the `icinga2_can_connect_all`, `icinga2_run_sudo` and `nagios_run_sudo` SELinux booleans must be enabled (role: [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux)). ## Tags @@ -176,7 +176,8 @@ icinga2_master__influxdb_retention: '216d' icinga2_master__service_enabled: true ``` -### Primary-Secondary Setup + +## Optional Role Variables - Primary-Secondary Setup Adjust the following variables for the secondary Icinga2 master. diff --git a/roles/icinga_kubernetes/README.md b/roles/icinga_kubernetes/README.md index f60f376b..d51f42bb 100644 --- a/roles/icinga_kubernetes/README.md +++ b/roles/icinga_kubernetes/README.md @@ -10,9 +10,11 @@ This role is tested with the following Icinga for Kubernetes versions: *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* A configured Icinga2 Master Setup. This can be done using the [linuxfabrik.lfops.setup_icinga2_master](https://github.com/linuxfabrik/lfops/tree/main/playbooks/setup_icinga2_master.yml) playbook. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* A configured Icinga2 Master must be available (role: [linuxfabrik.lfops.icinga2_master](https://github.com/Linuxfabrik/lfops/tree/main/roles/icinga2_master)). ## Tags diff --git a/roles/icinga_kubernetes_web/README.md b/roles/icinga_kubernetes_web/README.md index e4e09787..21f4be5e 100644 --- a/roles/icinga_kubernetes_web/README.md +++ b/roles/icinga_kubernetes_web/README.md @@ -10,9 +10,11 @@ This role is tested with the following Icinga for Kubernetes Web versions: *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* A configured IcingaWeb2 must be available (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). ## Tags diff --git a/roles/icingadb/README.md b/roles/icingadb/README.md index 5cbc2179..f9e11bc5 100644 --- a/roles/icingadb/README.md +++ b/roles/icingadb/README.md @@ -21,9 +21,11 @@ Notes on high availability / Icinga2 Master clusters: *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* A configured Icinga2 Master Setup. This can be done using the [linuxfabrik.lfops.setup_icinga2_master](https://github.com/linuxfabrik/lfops/tree/main/playbooks/setup_icinga2_master.yml) playbook. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* A configured Icinga2 Master must be available (role: [linuxfabrik.lfops.icinga2_master](https://github.com/Linuxfabrik/lfops/tree/main/roles/icinga2_master)). ## Tags diff --git a/roles/icingadb_web/README.md b/roles/icingadb_web/README.md index 332154f5..42d4685e 100644 --- a/roles/icingadb_web/README.md +++ b/roles/icingadb_web/README.md @@ -6,9 +6,11 @@ *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* A configured Icinga2 Master Setup. This can be done using the [linuxfabrik.lfops.setup_icinga2_master](https://github.com/linuxfabrik/lfops/tree/main/playbooks/setup_icinga2_master.yml) playbook. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* A configured Icinga2 Master must be available (role: [linuxfabrik.lfops.icinga2_master](https://github.com/Linuxfabrik/lfops/tree/main/roles/icinga2_master)). ## Tags diff --git a/roles/icingaweb2/README.md b/roles/icingaweb2/README.md index 9d821072..50310a3c 100644 --- a/roles/icingaweb2/README.md +++ b/roles/icingaweb2/README.md @@ -6,17 +6,22 @@ This role installs and configures [IcingaWeb2](https://icinga.com/docs/icinga-we *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install MariaDB, and create a database and a user for said database. This can be done using the [linuxfabrik.lfops.mariadb-server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb-server) role. -* Install a web server (for example Apache httpd), and configure a virtual host for IcingaWeb2. This can be done using the [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd) role. -* Install PHP version >= 7.3. This can be done using the [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. +* MariaDB must be installed, with a database and a user for it (role: [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server)). +* PHP >= 7.3 must be installed (role: [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php)). -## Optional Requirements -* For exports to PDF also the following PHP modules are required: mbstring, GD, Imagick. -* LDAP PHP library when using Active Directory or LDAP for authentication. +## Requirements + +* Optional: the `mbstring`, `GD` and `Imagick` PHP modules are required for PDF exports. +* Optional: an LDAP PHP library is required when using Active Directory or LDAP for authentication. + +Manual steps: + +* Deploy a web server (for example Apache httpd) with a virtual host for IcingaWeb2 by running the [apache_httpd](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/apache_httpd.yml) playbook (role: [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd)). ## Tags diff --git a/roles/icingaweb2_module_businessprocess/README.md b/roles/icingaweb2_module_businessprocess/README.md index 0e608155..a8aa3b0e 100644 --- a/roles/icingaweb2_module_businessprocess/README.md +++ b/roles/icingaweb2_module_businessprocess/README.md @@ -17,10 +17,13 @@ This role is tested with the following IcingaWeb2 Business Process Module versio * `icingacli module enable businessprocess` is only invoked when `/etc/icingaweb2/enabledModules/businessprocess` does not yet exist (idempotent). -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* Internet access from the Ansible controller (downloads from `https://github.com/Icinga/icingaweb2-module-businessprocess/archive/`). +* The Ansible controller must have Internet access (downloads from `https://github.com/Icinga/icingaweb2-module-businessprocess/archive/`). + +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). ## Tags diff --git a/roles/icingaweb2_module_company/README.md b/roles/icingaweb2_module_company/README.md index 53b705bf..fac64a72 100644 --- a/roles/icingaweb2_module_company/README.md +++ b/roles/icingaweb2_module_company/README.md @@ -18,10 +18,13 @@ This role is tested with the following IcingaWeb2 Company Module versions: * `icingacli module enable company` is only invoked when `/etc/icingaweb2/enabledModules/company` does not yet exist (idempotent). -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* Internet access from the Ansible controller (downloads `https://github.com/Icinga/icingaweb2-theme-company/archive/v1.0.0.tar.gz`). +* The Ansible controller must have Internet access (downloads `https://github.com/Icinga/icingaweb2-theme-company/archive/v1.0.0.tar.gz`). + +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). ## Tags diff --git a/roles/icingaweb2_module_cube/README.md b/roles/icingaweb2_module_cube/README.md index 85d39278..3a189435 100644 --- a/roles/icingaweb2_module_cube/README.md +++ b/roles/icingaweb2_module_cube/README.md @@ -17,10 +17,13 @@ This role is tested with the following IcingaWeb2 Cube Module versions: * `icingacli module enable cube` is only invoked when `/etc/icingaweb2/enabledModules/cube` does not yet exist (idempotent). -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* Internet access from the Ansible controller (downloads from `https://github.com/Icinga/icingaweb2-module-cube/archive/`). +* The Ansible controller must have Internet access (downloads from `https://github.com/Icinga/icingaweb2-module-cube/archive/`). + +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). ## Tags diff --git a/roles/icingaweb2_module_director/README.md b/roles/icingaweb2_module_director/README.md index 5182da26..73058b8b 100644 --- a/roles/icingaweb2_module_director/README.md +++ b/roles/icingaweb2_module_director/README.md @@ -11,10 +11,12 @@ This role is tested with the following IcingaWeb2 Director Module versions: *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* A SQL database and user. This can be done using the [linuxfabrik.lfops.mariadb_server](https://github.com/linuxfabrik/lfops/tree/main/roles/mariadb_server) role. +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). +* Deploy a SQL database server and create the database and user by running the [mariadb_server](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/mariadb_server.yml) playbook (role: [linuxfabrik.lfops.mariadb_server](https://github.com/linuxfabrik/lfops/tree/main/roles/mariadb_server)). ## Tags diff --git a/roles/icingaweb2_module_doc/README.md b/roles/icingaweb2_module_doc/README.md index 3f04ccd8..07c54e2b 100644 --- a/roles/icingaweb2_module_doc/README.md +++ b/roles/icingaweb2_module_doc/README.md @@ -6,9 +6,11 @@ This role enables the [IcingaWeb2 Doc Module](https://icinga.com/docs/icinga-web *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). ## Tags diff --git a/roles/icingaweb2_module_fileshipper/README.md b/roles/icingaweb2_module_fileshipper/README.md index 27533a75..e6d5fcf7 100644 --- a/roles/icingaweb2_module_fileshipper/README.md +++ b/roles/icingaweb2_module_fileshipper/README.md @@ -18,11 +18,14 @@ This role is tested with the following IcingaWeb2 Fileshipper Module versions: * PHP runtime dependencies (`php-xml`, `php-yaml`, `php-zip`) are not installed by this role directly; they are injected into the `php` role via the `icingaweb2_module_fileshipper__php__modules__dependent_var` default. Install them via the [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php) role (the bundled playbook does this for you). -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* PHP with the `php-xml`, `php-yaml` and `php-zip` modules installed (see above). -* Internet access from the Ansible controller (downloads from `https://github.com/Icinga/icingaweb2-module-fileshipper/archive/`). +* The Ansible controller must have Internet access (downloads from `https://github.com/Icinga/icingaweb2-module-fileshipper/archive/`). + +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). +* Deploy PHP with the `php-xml`, `php-yaml` and `php-zip` modules by running the [php](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/php.yml) playbook (role: [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php)). ## Tags diff --git a/roles/icingaweb2_module_generictts/README.md b/roles/icingaweb2_module_generictts/README.md index c242dc18..112e3ee1 100644 --- a/roles/icingaweb2_module_generictts/README.md +++ b/roles/icingaweb2_module_generictts/README.md @@ -17,10 +17,13 @@ This role is tested with the following IcingaWeb2 GenericTTS Module versions: * `icingacli module enable generictts` is only invoked when `/etc/icingaweb2/enabledModules/generictts` does not yet exist (idempotent). -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* Internet access from the Ansible controller (downloads from `https://github.com/Icinga/icingaweb2-module-generictts/archive/`). +* The Ansible controller must have Internet access (downloads from `https://github.com/Icinga/icingaweb2-module-generictts/archive/`). + +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). ## Tags diff --git a/roles/icingaweb2_module_grafana/README.md b/roles/icingaweb2_module_grafana/README.md index 96006b94..6ef37276 100644 --- a/roles/icingaweb2_module_grafana/README.md +++ b/roles/icingaweb2_module_grafana/README.md @@ -11,12 +11,12 @@ This role is tested with the following IcingaWeb2 Grafana Module versions: *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* A configured Grafana. This can be done using the [linuxfabrik.lfops.grafana](https://github.com/linuxfabrik/lfops/tree/main/roles/grafana) role. +Manual steps: -If you use the [Setup Icinga2 Master Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_icinga2_master.yml), this is automatically done for you. +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). +* Deploy a configured Grafana by running the [grafana](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/grafana.yml) playbook (role: [linuxfabrik.lfops.grafana](https://github.com/linuxfabrik/lfops/tree/main/roles/grafana)). ## Tags diff --git a/roles/icingaweb2_module_incubator/README.md b/roles/icingaweb2_module_incubator/README.md index 7f16baec..3b0ceec7 100644 --- a/roles/icingaweb2_module_incubator/README.md +++ b/roles/icingaweb2_module_incubator/README.md @@ -18,10 +18,13 @@ This role is tested with the following IcingaWeb2 Incubator Module versions: * `icingacli module enable incubator` is only invoked when `/etc/icingaweb2/enabledModules/incubator` does not yet exist (idempotent). -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* Internet access from the Ansible controller (downloads from `https://github.com/Icinga/icingaweb2-module-incubator/archive/`). +* The Ansible controller must have Internet access (downloads from `https://github.com/Icinga/icingaweb2-module-incubator/archive/`). + +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). ## Tags diff --git a/roles/icingaweb2_module_jira/README.md b/roles/icingaweb2_module_jira/README.md index 99df30cb..371fd1d0 100644 --- a/roles/icingaweb2_module_jira/README.md +++ b/roles/icingaweb2_module_jira/README.md @@ -10,10 +10,12 @@ This role is tested with the following IcingaWeb2 Jira Module versions: *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* The module requires you to create two custom fields in Jira that represent "icingaKey" and "icingaStatus". Have a look at `icingaweb2_module_jira__key_fields_icinga_key` and `icingaweb2_module_jira__key_fields_icinga_status`. +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). +* Create two custom fields in Jira that represent "icingaKey" and "icingaStatus", then reference them via `icingaweb2_module_jira__key_fields_icinga_key` and `icingaweb2_module_jira__key_fields_icinga_status`. ## Tags diff --git a/roles/icingaweb2_module_pdfexport/README.md b/roles/icingaweb2_module_pdfexport/README.md index 51a028bc..59908791 100644 --- a/roles/icingaweb2_module_pdfexport/README.md +++ b/roles/icingaweb2_module_pdfexport/README.md @@ -19,13 +19,14 @@ This role is tested with the following IcingaWeb2 PDF Export Module versions: * This role only installs and configures the IcingaWeb2 module itself. The headless browser backend it talks to (see the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements)) is provided separately by the [linuxfabrik.lfops.chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless) role. -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* Internet access from the Ansible controller (downloads from `https://github.com/Icinga/icingaweb2-module-pdfexport/archive/`). -* A running headless Chromium instance providing the remote debugging interface this module talks to. This can be done using the [linuxfabrik.lfops.chromium_headless](https://github.com/Linuxfabrik/lfops/tree/main/roles/chromium_headless) role. +* The Ansible controller must have Internet access (downloads from `https://github.com/Icinga/icingaweb2-module-pdfexport/archive/`). -If you use the [IcingaWeb2 PDF Export Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2_module_pdfexport.yml), the headless Chromium backend is automatically installed for you. +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). +* Install and configure the runtime dependencies listed in the [module documentation](https://github.com/Icinga/icingaweb2-module-pdfexport#requirements) (typically a headless browser binary). ## Tags diff --git a/roles/icingaweb2_module_reporting/README.md b/roles/icingaweb2_module_reporting/README.md index fea9a7db..a3b3adf6 100644 --- a/roles/icingaweb2_module_reporting/README.md +++ b/roles/icingaweb2_module_reporting/README.md @@ -10,14 +10,13 @@ This role is tested with the following IcingaWeb2 Reporting Module versions: *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* A SQL database and user. This can be done using the [linuxfabrik.lfops.mariadb_server](https://github.com/linuxfabrik/lfops/tree/main/roles/mariadb_server) role. +Manual steps: -If you use the [Setup Icinga2 Master Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_icinga2_master.yml) and set `setup_icinga2_master__skip_icingaweb2_module_reporting: false`, this is automatically done for you. - -* Additionally, the [IcingaWeb2 PDF Export Module](https://github.com/Icinga/icingaweb2-module-pdfexport) for exporting to PDF (else only CSV and JSON are available). +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). +* Deploy a SQL database server and create the database and user by running the [mariadb_server](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/mariadb_server.yml) playbook (role: [linuxfabrik.lfops.mariadb_server](https://github.com/linuxfabrik/lfops/tree/main/roles/mariadb_server)). +* Optional: deploy the IcingaWeb2 PDF Export Module by running the [icingaweb2_module_pdfexport](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2_module_pdfexport.yml) playbook (role: [linuxfabrik.lfops.icingaweb2_module_pdfexport](https://github.com/Linuxfabrik/lfops/tree/main/roles/icingaweb2_module_pdfexport)). It enables exporting to PDF (else only CSV and JSON are available). ## Tags diff --git a/roles/icingaweb2_module_vspheredb/README.md b/roles/icingaweb2_module_vspheredb/README.md index d2915827..c707eb9a 100644 --- a/roles/icingaweb2_module_vspheredb/README.md +++ b/roles/icingaweb2_module_vspheredb/README.md @@ -11,12 +11,12 @@ This role is tested with the following IcingaWeb2 vSphereDB Module versions: *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* A SQL database and user. This can be done using the [linuxfabrik.lfops.mariadb_server](https://github.com/linuxfabrik/lfops/tree/main/roles/mariadb_server) role. +Manual steps: -If you use the [Setup Icinga2 Master Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_icinga2_master.yml) and set `setup_icinga2_master__skip_icingaweb2_module_vspheredb: false`, this is automatically done for you. +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). +* Deploy a SQL database server and create the database and user by running the [mariadb_server](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/mariadb_server.yml) playbook (role: [linuxfabrik.lfops.mariadb_server](https://github.com/linuxfabrik/lfops/tree/main/roles/mariadb_server)). ## Tags diff --git a/roles/icingaweb2_module_x509/README.md b/roles/icingaweb2_module_x509/README.md index 149d7716..956576ca 100644 --- a/roles/icingaweb2_module_x509/README.md +++ b/roles/icingaweb2_module_x509/README.md @@ -10,12 +10,12 @@ This role is tested with the following IcingaWeb2 x509 Module versions: *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* A SQL database and user. This can be done using the [linuxfabrik.lfops.mariadb_server](https://github.com/linuxfabrik/lfops/tree/main/roles/mariadb_server) role. +Manual steps: -If you use the [Setup Icinga2 Master Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_icinga2_master.yml) and set `setup_icinga2_master__skip_icingaweb2_module_x509: false`, this is automatically done for you. +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). +* Deploy a SQL database server and create the database and user by running the [mariadb_server](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/mariadb_server.yml) playbook (role: [linuxfabrik.lfops.mariadb_server](https://github.com/linuxfabrik/lfops/tree/main/roles/mariadb_server)). ## Tags diff --git a/roles/icingaweb2_theme_linuxfabrik/README.md b/roles/icingaweb2_theme_linuxfabrik/README.md index cac4f78b..a8e4ccff 100644 --- a/roles/icingaweb2_theme_linuxfabrik/README.md +++ b/roles/icingaweb2_theme_linuxfabrik/README.md @@ -15,10 +15,13 @@ The role does not have a dedicated playbook. It is normally pulled in via the [` * `icingacli module enable linuxfabrik` is only invoked when `/etc/icingaweb2/enabledModules/linuxfabrik` does not yet exist (idempotent). The theme has to be selected per user in IcingaWeb2 (or set as the default theme via the `theme` setting in `/etc/icingaweb2/config.ini`). -## Mandatory Requirements +## Requirements -* A configured IcingaWeb2. This can be done using the [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2) role. -* Internet access from the Ansible controller (downloads from `https://github.com/Linuxfabrik/icingaweb2-theme-linuxfabrik/archive/`). +* The Ansible controller must have Internet access (downloads from `https://github.com/Linuxfabrik/icingaweb2-theme-linuxfabrik/archive/`). + +Manual steps: + +* Deploy a configured IcingaWeb2 by running the [icingaweb2](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/icingaweb2.yml) playbook (role: [linuxfabrik.lfops.icingaweb2](https://github.com/linuxfabrik/lfops/tree/main/roles/icingaweb2)). ## Tags diff --git a/roles/influxdb/README.md b/roles/influxdb/README.md index 3c82088c..767283d2 100644 --- a/roles/influxdb/README.md +++ b/roles/influxdb/README.md @@ -6,10 +6,12 @@ This role installs and configures [InfluxDB](https://www.influxdata.com/products *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install `influxdb` and `requests` into a Python 3 virtual environment in `/opt/python-venv/influxdb`. This can be done using the [linuxfabrik.lfops.python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv) role. -* Enable the official [InfluxDB repository](https://docs.influxdata.com/influxdb/v1.8/introduction/install/?t=Red+Hat+%26amp%3B+CentOS). This can be done using the [linuxfabrik.lfops.repo_influxdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_influxdb) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* `influxdb` and `requests` must be installed into a Python 3 virtual environment in `/opt/python-venv/influxdb` (role: [linuxfabrik.lfops.python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv)). +* The official [InfluxDB repository](https://docs.influxdata.com/influxdb/v1.8/introduction/install/?t=Red+Hat+%26amp%3B+CentOS) must be enabled (role: [linuxfabrik.lfops.repo_influxdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_influxdb)). ## Tags diff --git a/roles/infomaniak_vm/README.md b/roles/infomaniak_vm/README.md index 55ecfcdc..4e975f17 100644 --- a/roles/infomaniak_vm/README.md +++ b/roles/infomaniak_vm/README.md @@ -6,9 +6,16 @@ This role creates and manages instances (virtual machines) on [Infomaniak](https *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Known Limitations -* Install the [openstack command line tool](https://docs.openstack.org/newton/user-guide/common/cli-install-openstack-command-line-clients.html). +* Resizing the separate boot volume currently does not seem to work (it should, according to the documentation). Resizing via the WebGUI works without reboot / downtime. + + +## Requirements + +Manual steps: + +* Install the [openstack command line tool](https://docs.openstack.org/newton/user-guide/common/cli-install-openstack-command-line-clients.html) on the Ansible controller. * Import your public SSH-key into Infomaniak ([here](https://api.pub1.infomaniak.cloud/horizon/project/key_pairs)). Ideally, set the key name to your local username (replace `.` with ` `), then you can use the default value for `infomaniak_vm__key_name`. @@ -173,7 +180,7 @@ infomaniak_vm__api_username: 'PCU-123456' `infomaniak_vm__separate_boot_volume_size` -* The size of the bootable root-volume in GB. This should only be used if the `infomaniak_vm__flavor` does not include a disk. Resizing currently does not seem to work (should work according to the documentation). Resizing via the WebGUI works without reboot / downtime. +* The size of the bootable root-volume in GB. This should only be used if the `infomaniak_vm__flavor` does not include a disk. * Type: Number. * Default: unset diff --git a/roles/keepalived/README.md b/roles/keepalived/README.md index 800de5a8..aea446c8 100644 --- a/roles/keepalived/README.md +++ b/roles/keepalived/README.md @@ -6,7 +6,7 @@ This role installs and configures [keepalived](https://www.keepalived.org/). *Available since LFOps `3.0.0`.* -## Scope +## How the Role Behaves The role intentionally covers a minimal VRRP setup: diff --git a/roles/kernel_settings/README.md b/roles/kernel_settings/README.md index 2ecd613c..9543c4e4 100644 --- a/roles/kernel_settings/README.md +++ b/roles/kernel_settings/README.md @@ -8,7 +8,9 @@ The role does nothing on its own and relies on the [linux_system_roles.kernel_se *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Requirements + +Manual steps: * Install the [Linux System Roles](https://linux-system-roles.github.io/) on the Ansible control node, for example by calling `ansible-galaxy collection install fedora.linux_system_roles`. diff --git a/roles/keycloak/README.md b/roles/keycloak/README.md index 1a6e8f15..de59c5d2 100644 --- a/roles/keycloak/README.md +++ b/roles/keycloak/README.md @@ -6,7 +6,14 @@ This role installs [Keycloak](https://www.keycloak.org/guides#getting-started). *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles + +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* A MariaDB database and user for Keycloak must be created (role: [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server)). + + +## Requirements Minimum supported Keycloak version: @@ -17,7 +24,7 @@ Make sure you have OpenJDK installed. * Keycloak 25+: OpenJDK 21 * Keycloak 24+: OpenJDK 17 -Install one of the following database servers and create a database and a user for said database. For MariaDB, this can be done using the [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server) role. +Keycloak supports one of the following database servers; create a database and a user for it. The "Setup Keycloak" playbook wires up MariaDB for you (see Dependent Roles), the others must be provided separately. * mariadb * mssql @@ -27,8 +34,6 @@ Install one of the following database servers and create a database and a user f If Keycloak itself should terminate TLS (e.g. when not running behind a reverse proxy, or when using a reverse proxy in reencrypt/passthrough mode), you need to provide SSL/TLS certificates via `keycloak__https_certificate_file` and `keycloak__https_certificate_key_file`. This can be done using the [linuxfabrik.lfops.acme_sh](https://github.com/Linuxfabrik/lfops/tree/main/roles/acme_sh) role. When running behind a reverse proxy that terminates TLS (edge mode), no certificates are needed, and you can leave the certificate variables empty (the default). -If you use the ["Setup Keycloak" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_keycloak.yml), this installation is automatically done for you (you still have to take care of providing the required versions). - All Keycloak config settings are described here: https://www.keycloak.org/server/all-config diff --git a/roles/kibana/README.md b/roles/kibana/README.md index b1411795..929c927b 100644 --- a/roles/kibana/README.md +++ b/roles/kibana/README.md @@ -8,12 +8,29 @@ Note that this role does NOT let you specify a particular Kibana version. It sim *Available since LFOps `5.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the official Elasticsearch repository (which also provides Kibana packages). This can be done using the [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch) role. -* A running Elasticsearch installation. This can be done using the [linuxfabrik.lfops.elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/elasticsearch) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [kibana playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/kibana.yml), the repository setup is automatically done for you. +* The official Elasticsearch repository (which also provides Kibana packages) must be enabled (role: [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch)). + + +## Requirements + +Manual steps: + +* Deploy a running Elasticsearch installation by running the [elasticsearch](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/elasticsearch.yml) playbook (role: [linuxfabrik.lfops.elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/elasticsearch)). +* Create a service account token for Kibana on an Elasticsearch node: + + ```bash + elastic_host='localhost' + elastic_cacert='/etc/elasticsearch/certs/http_ca.crt' + + curl --cacert "$elastic_cacert" \ + --user "elastic:${ELASTIC_PASSWORD}" \ + --request POST "https://$elastic_host:9200/_security/service/elastic/kibana/credential/token/kibana-token-01?pretty=true" \ + --header "Content-Type: application/json" + ``` ## Tags @@ -34,21 +51,6 @@ If you use the [kibana playbook](https://github.com/Linuxfabrik/lfops/blob/main/ * Triggers: none. -## Pre-Installation Steps - -Create a service account token for Kibana on an Elasticsearch node: - -```bash -elastic_host='localhost' -elastic_cacert='/etc/elasticsearch/certs/http_ca.crt' - -curl --cacert "$elastic_cacert" \ - --user "elastic:${ELASTIC_PASSWORD}" \ - --request POST "https://$elastic_host:9200/_security/service/elastic/kibana/credential/token/kibana-token-01?pretty=true" \ - --header "Content-Type: application/json" -``` - - ## Mandatory Role Variables `kibana__elasticsearch_service_account_token` diff --git a/roles/kvm_host/README.md b/roles/kvm_host/README.md index 7fcc7246..a9edab2d 100644 --- a/roles/kvm_host/README.md +++ b/roles/kvm_host/README.md @@ -6,9 +6,11 @@ This role installs the required packages and configures the host as a KVM host. *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install Python 3, and the python3-libvirt and python3-lxml modules. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* Python 3 and the `python3-libvirt` and `python3-lxml` modules must be installed (role: [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python)). ## Tags diff --git a/roles/kvm_vm/README.md b/roles/kvm_vm/README.md index a0879058..cd880ee3 100644 --- a/roles/kvm_vm/README.md +++ b/roles/kvm_vm/README.md @@ -10,9 +10,13 @@ If you want to create a VM with an existing disk, see the `kvm_vm__existing_boot *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Requirements -* Install Python 3, and the python3-libvirt and python3-lxml modules on the KVM host. This can be done using the [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python) role. If you use the [kvm_host Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/kvm_host.yml) to setup the KVM host, this is automatically done for you. +* Python 3, and the python3-libvirt and python3-lxml modules must be installed on the KVM host (role: [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python)). + +Manual steps: + +* Set up the KVM host first by running the [kvm_host](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/kvm_host.yml) playbook (role: [linuxfabrik.lfops.kvm_host](https://github.com/Linuxfabrik/lfops/tree/main/roles/kvm_host)), which installs Python 3 and the required libvirt/lxml modules on the host. * Place the base image in the `kvm_vm__pool` on the KVM host. If `kvm_vm__pool` is `default`, you get the storage path by running `virsh pool-dumpxml default | grep -i path` on the KVM host. diff --git a/roles/librenms/README.md b/roles/librenms/README.md index f420a9ba..8d873636 100644 --- a/roles/librenms/README.md +++ b/roles/librenms/README.md @@ -6,16 +6,16 @@ This role installs and configures [LibreNMS](https://www.librenms.org/). *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install Python 3, and the python3-policycoreutils module (required for the SELinux Ansible tasks). This can be done using the [linuxfabrik.lfops.policycoreutils](https://github.com/Linuxfabrik/lfops/tree/main/roles/policycoreutils) role. -* Install MariaDB, and create a database and a user for said database. This can be done using the [linuxfabrik.lfops.mariadb-server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb-server) role. -* Install a web server (for example Apache httpd), and configure a virtual host for LibreNMS. This can be done using the [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd) role. -* Install PHP version >= 7.3. This can be done using the [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php) role. -* On RHEL-compatible systems, enable the `httpd_can_connect_ldap` and `httpd_setrlimit` SELinux booleans. This can be done using the [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux) role. -* On RHEL-compatible systems, set the appropriate SELinux file contexts (have a look at `librenms__selinux__fcontexts__dependent_var` in the `defaults/main.yml`). This can be done using the [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["Setup LibreNMS" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_librenms.yml), this is automatically done for you. +* Python 3, and the python3-policycoreutils module (required for the SELinux Ansible tasks) must be installed (role: [linuxfabrik.lfops.policycoreutils](https://github.com/Linuxfabrik/lfops/tree/main/roles/policycoreutils)). +* MariaDB must be installed, with a database and a user for said database created (role: [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server)). +* A web server (for example Apache httpd) must be installed, with a virtual host for LibreNMS configured (role: [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd)). +* PHP version >= 7.3 must be installed (role: [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php)). +* On RHEL-compatible systems, the `httpd_can_connect_ldap` and `httpd_setrlimit` SELinux booleans must be enabled (role: [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux)). +* On RHEL-compatible systems, the appropriate SELinux file contexts must be set (have a look at `librenms__selinux__fcontexts__dependent_var` in the `defaults/main.yml`) (role: [linuxfabrik.lfops.selinux](https://github.com/Linuxfabrik/lfops/tree/main/roles/selinux)). ## Tags diff --git a/roles/libreoffice/README.md b/roles/libreoffice/README.md index 8bcefc55..4e01e7e9 100644 --- a/roles/libreoffice/README.md +++ b/roles/libreoffice/README.md @@ -8,9 +8,9 @@ By default it only installs `libreoffice-core` plus the writer / calc / impress *Available since LFOps `2.0.0`.* -## What `libreoffice__client_apache: true` Does +## How the Role Behaves -Setting this to `true` triggers a Red Hat-specific block. On Debian / Ubuntu the block is not skipped automatically; the SELinux compile steps will fail there, so do not enable this on Debian-family hosts. +Setting `libreoffice__client_apache: true` triggers a Red Hat-specific block. On Debian / Ubuntu the block is not skipped automatically; the SELinux compile steps will fail there, so do not enable this on Debian-family hosts. When enabled, the role: @@ -20,8 +20,6 @@ When enabled, the role: * Compiles two custom SELinux policy modules (`selinux-sofficebin`, `selinux-java`) via `checkmodule` / `semodule_package` and installs them with `semodule --install`. These grant `httpd_t` the additional permissions LibreOffice needs (`setattr` on directories under `lib_t`, `read` on `cgroup_t` files, etc.). * Via the companion playbook: also runs `linuxfabrik.lfops.selinux` to set the SELinux booleans `httpd_can_network_connect` and `httpd_execmem` to `on`, and to register fcontexts mapping `/usr/share/httpd/.cache` and `/usr/share/httpd/.config` to `httpd_sys_rw_content_t`. -The SELinux booleans / fcontexts are injected from `libreoffice__selinux__booleans__dependent_var` and `libreoffice__selinux__fcontexts__dependent_var` in `defaults/main.yml`. They are role-internal and not meant to be overridden from inventory. - ## Tags diff --git a/roles/login/README.md b/roles/login/README.md index f48927ce..bee09013 100644 --- a/roles/login/README.md +++ b/roles/login/README.md @@ -7,10 +7,13 @@ IMPORTANT: * The default behavior of this role is that it distributes SSH keys that it knows from the host/group variables and deletes any other keys that already exist on the target system in `.ssh/authorized_keys`. This might break things. Set `remove_other_sshd_authorized_keys` accordingly. + *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Requirements + +Manual steps: * Install the `passlib` Python module on the Ansible Controller (`dnf install python3-passlib` on Fedora). If you use the [LFOps Execution Environment](https://github.com/Linuxfabrik/lfops/pkgs/container/lfops_ee), this is already done for you. diff --git a/roles/logstash/README.md b/roles/logstash/README.md index 849879c3..6ac549fe 100644 --- a/roles/logstash/README.md +++ b/roles/logstash/README.md @@ -8,11 +8,11 @@ Note that this role does NOT let you specify a particular Logstash version. It s *Available since LFOps `6.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the official elasticsearch repository. This can be done using the [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [logstash playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/logstash.yml), this is automatically done for you. +* The official elasticsearch repository must be enabled (role: [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch)). ## Tags diff --git a/roles/mailto_root/README.md b/roles/mailto_root/README.md index 021045b7..3f2cd523 100644 --- a/roles/mailto_root/README.md +++ b/roles/mailto_root/README.md @@ -6,10 +6,12 @@ This role enables relaying all mail that is sent to the root user (or other serv *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install and configure postfix. This can be done using the [linuxfabrik.lfops.postfix](https://github.com/Linuxfabrik/lfops/tree/main/roles/postfix) role. -* Install mailx. This can be done using the [linuxfabrik.lfops.mailx](https://github.com/Linuxfabrik/lfops/tree/main/roles/mailx) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* postfix must be installed and configured (role: [linuxfabrik.lfops.postfix](https://github.com/Linuxfabrik/lfops/tree/main/roles/postfix)). +* mailx must be installed (role: [linuxfabrik.lfops.mailx](https://github.com/Linuxfabrik/lfops/tree/main/roles/mailx)). ## Tags diff --git a/roles/mailx/README.md b/roles/mailx/README.md index 46dc59d9..c255d8bb 100644 --- a/roles/mailx/README.md +++ b/roles/mailx/README.md @@ -15,16 +15,6 @@ This role installs [mailx](http://heirloom.sourceforge.net/mailx.html) and deplo * Triggers: none. -## Skip Variables - -This role is used in several playbooks that provide skip variables to disable specific dependencies. See the playbooks documentation for details: - -* [mailto_root.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#mailto_rootyml) -* [postfix.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#postfixyml) -* [setup_basic.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_basicyml) -* [system_update.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#system_updateyml) - - ## License [The Unlicense](https://unlicense.org/) diff --git a/roles/mariadb_server/README.md b/roles/mariadb_server/README.md index 71bec174..4cb0fa9f 100644 --- a/roles/mariadb_server/README.md +++ b/roles/mariadb_server/README.md @@ -27,17 +27,21 @@ Hardenings that can be covered by this role: See [STIGs](https://github.com/Linu *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* For some machines you might need to set `ansible_python_interpreter: '/usr/bin/python3'` to prevent the error message `A MySQL module is required: for Python 2.7 either PyMySQL, or MySQL-python, or for Python 3.X mysqlclient or PyMySQL. Consider setting ansible_python_interpreter to use the intended Python version.`. -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* Install the `python3-PyMySQL` library. This can be done using the [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). +* The `python3-PyMySQL` library must be installed (role: [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python)). +* Optional: the official [MariaDB Package Repository](https://mariadb.com/kb/en/mariadb-package-repository-setup-and-usage/) (role: [linuxfabrik.lfops.repo_mariadb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_mariadb)) provides a specific MariaDB version. +* Optional: a repository for [mydumper](https://github.com/mydumper/mydumper) (role: [linuxfabrik.lfops.repo_mydumper](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_mydumper)) provides the mydumper backup tool. -## Optional Requirements -* Enable the official [MariaDB Package Repository](https://mariadb.com/kb/en/mariadb-package-repository-setup-and-usage/). This can be done using the [linuxfabrik.lfops.repo_mariadb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_mariadb) role. -* Enable the a repository for [mydumper](https://github.com/mydumper/mydumper). This can be done using the [linuxfabrik.lfops.repo_mydumper](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_mydumper) role. +## Requirements + +Manual steps: + +* On some machines you may need to set `ansible_python_interpreter: '/usr/bin/python3'` to prevent the error `A MySQL module is required: for Python 2.7 either PyMySQL, or MySQL-python, or for Python 3.X mysqlclient or PyMySQL. Consider setting ansible_python_interpreter to use the intended Python version.`. ## Tags @@ -696,7 +700,7 @@ Variables for `z00-linuxfabrik.cnf` directives and their default values, defined * Type: String. * Default: `'256K'` -`mariadb_server__cnf_local_infile__group_var` `mariadb_server__cnf_local_infile__host_var` +`mariadb_server__cnf_local_infile__group_var` / `mariadb_server__cnf_local_infile__host_var` * [mariadb.com](https://mariadb.com/kb/en/server-system-variables/#local_infile) * Type: String. @@ -714,7 +718,7 @@ Variables for `z00-linuxfabrik.cnf` directives and their default values, defined * Type: String. * Default: `''` -`mariadb_server__cnf_log_bin_trust_function_creators__group_var` `mariadb_server__cnf_log_bin_trust_function_creators__host_var` +`mariadb_server__cnf_log_bin_trust_function_creators__group_var` / `mariadb_server__cnf_log_bin_trust_function_creators__host_var` * [mariadb.com](https://mariadb.com/docs/server/ha-and-performance/standard-replication/replication-and-binary-log-system-variables#log_bin_trust_function_creators) * Type: String. @@ -798,7 +802,7 @@ Variables for `z00-linuxfabrik.cnf` directives and their default values, defined * Type: String. * Default: `'OFF'` -`mariadb_server__cnf_server_id__group_var` `mariadb_server__cnf_server_id__host_var` +`mariadb_server__cnf_server_id__group_var` / `mariadb_server__cnf_server_id__host_var` * [mariadb.com](https://mariadb.com/kb/en/replication-and-binary-log-system-variables#server_id) * Type: Number. diff --git a/roles/mastodon/README.md b/roles/mastodon/README.md index e2a5a926..d93c536c 100644 --- a/roles/mastodon/README.md +++ b/roles/mastodon/README.md @@ -6,38 +6,40 @@ This role installs and configures [Mastodon](https://joinmastodon.org/), a feder *Available since LFOps `4.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the PostgreSQL repository. This can be done using the [linuxfabrik.lfops.repo_postgresql](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_postgresql) role. -* Install the PostgreSQL server. This can be done using the [linuxfabrik.lfops.postgresql_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/postgresql_server) role. -* Create a PostgreSQL user for Mastodon. This can be done using the [linuxfabrik.lfops.postgresql_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/postgresql_server) role. -* Install Redis. This can be done using the [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) and [linuxfabrik.lfops.redis](https://github.com/Linuxfabrik/lfops/tree/main/roles/redis) role. -* Enable the Elasticsearch repository (optional). This can be done using the [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch) role. -* Install Elasticsearch (optional). This can be done using the [linuxfabrik.lfops.elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/elasticsearch) role. -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* Install Apache HTTPd. This can be done using the [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["Setup Mastodon" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_mastodon.yml), this is automatically done for you (you still have to take care of providing the required versions). +* The PostgreSQL repository must be enabled (role: [linuxfabrik.lfops.repo_postgresql](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_postgresql)). +* The PostgreSQL server must be installed (role: [linuxfabrik.lfops.postgresql_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/postgresql_server)). +* A PostgreSQL user for Mastodon must be created (role: [linuxfabrik.lfops.postgresql_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/postgresql_server)). +* Redis must be installed (roles: [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) and [linuxfabrik.lfops.redis](https://github.com/Linuxfabrik/lfops/tree/main/roles/redis)). +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). +* Apache HTTPd must be installed (role: [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd)). +* Optional: the Elasticsearch repository enabled (role: [linuxfabrik.lfops.repo_elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_elasticsearch)). +* Optional: Elasticsearch installed (role: [linuxfabrik.lfops.elasticsearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/elasticsearch)). -* Make sure the container can access the databases: -```yaml -# PostgreSQL -postgresql_server__conf_listen_addresses: - - 'localhost' - - 'fqdn.example.com' # Allow access from container. Make sure the DNS entry (or /etc/hosts) points to the correct ip (not 127.) -# Redis -redis__conf_bind: 'fqdn.example.com' # Allow access from container. Make sure the DNS entry (or /etc/hosts) points to the correct ip (not 127.) +## Requirements -# Elasticsearch (if needed) -elasticsearch__network_host: 'fqdn.example.com' # Allow access from container. Make sure the DNS entry (or /etc/hosts) points to the correct ip (not 127.) -``` +Manual steps: + +* Optional: to allow the user to use `journalctl --user`, set `Storage=persistent` in `/etc/systemd/journald.conf` by running the [systemd_journald](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/systemd_journald.yml) playbook (role: [linuxfabrik.lfops.systemd_journald](https://github.com/Linuxfabrik/lfops/tree/main/roles/systemd_journald)). +* Optional: if the host should act as a Postfix MTA, make it listen on the IP address so that the container can reach it by running the [postfix](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/postfix.yml) playbook (role: [linuxfabrik.lfops.postfix](https://github.com/Linuxfabrik/lfops/tree/main/roles/postfix)). +* Make sure the container can access the databases: + ```yaml + # PostgreSQL + postgresql_server__conf_listen_addresses: + - 'localhost' + - 'fqdn.example.com' # Allow access from container. Make sure the DNS entry (or /etc/hosts) points to the correct ip (not 127.) -## Optional Requirements + # Redis + redis__conf_bind: 'fqdn.example.com' # Allow access from container. Make sure the DNS entry (or /etc/hosts) points to the correct ip (not 127.) -* It is recommended to set `Storage=presistent` in `/etc/systemd/journald.conf` to allow the user to use `journalctl --user`. This can be done using the [linuxfabrik.lfops.systemd_journald](https://github.com/Linuxfabrik/lfops/tree/main/roles/systemd_journald) role. -* If the host should act as a Postfix MTA, make sure it is listening on the IP address so that the container can reach it. This can be done using the [linuxfabrik.lfops.postfix](https://github.com/Linuxfabrik/lfops/tree/main/roles/postfix) role. + # Elasticsearch (if needed) + elasticsearch__network_host: 'fqdn.example.com' # Allow access from container. Make sure the DNS entry (or /etc/hosts) points to the correct ip (not 127.) + ``` ## Tags diff --git a/roles/maxmind_geoip/README.md b/roles/maxmind_geoip/README.md index 3cfb980f..b9fa0bca 100644 --- a/roles/maxmind_geoip/README.md +++ b/roles/maxmind_geoip/README.md @@ -20,7 +20,7 @@ For Maxmind, depending on your needs, you normally run three playbooks in this p * Outbound HTTPS access from the target host to `download.maxmind.com` is required for the script to work. -## Mandatory Requirements +## Requirements * A free Maxmind license key. * Outbound HTTPS access from each target host to `download.maxmind.com`. @@ -50,19 +50,6 @@ maxmind_geoip__lic: '1a1c5e4202784cec' ## Optional Role Variables -`maxmind_geoip__skip_systemd_unit` - -* If `true`, the playbook skips the `linuxfabrik.lfops.systemd_unit` role and therefore does not create the `update-maxmind` service / timer. Use this when you want to manage the schedule yourself (e.g. via cron). -* Type: Bool. -* Default: `false` - -Example: -```yaml -# optional -maxmind_geoip__skip_systemd_unit: true -``` - - `maxmind_geoip__systemd_unit__timers__dependent_var` * Schedule of the `update-maxmind` timer (passed through to the `linuxfabrik.lfops.systemd_unit` role). Override the whole list in your inventory to change `OnCalendar=` or any other timer directive. diff --git a/roles/mirror/README.md b/roles/mirror/README.md index d8695f56..7e395c88 100644 --- a/roles/mirror/README.md +++ b/roles/mirror/README.md @@ -6,14 +6,14 @@ This role installs and configures [mirror](https://github.com/Linuxfabrik/mirror *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install Python 3. This can be done using the [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python) role. -* Install `createrepo`. This can be done using the [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps) role. -* Install `git`. This can be done using the [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps) role. -* Install `yum-utils`. This can be done using the [linuxfabrik.lfops.yum_utils](https://github.com/Linuxfabrik/lfops/tree/main/roles/yum_utils) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [`mirror` Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/mirror.yml), this is automatically done for you. +* Python 3 must be installed (role: [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python)). +* `createrepo` must be installed (role: [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps)). +* `git` must be installed (role: [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps)). +* `yum-utils` must be installed (role: [linuxfabrik.lfops.yum_utils](https://github.com/Linuxfabrik/lfops/tree/main/roles/yum_utils)). ## Tags diff --git a/roles/mod_maxminddb/README.md b/roles/mod_maxminddb/README.md index 82bb8723..87236e88 100644 --- a/roles/mod_maxminddb/README.md +++ b/roles/mod_maxminddb/README.md @@ -15,11 +15,6 @@ For Maxmind, depending on your needs, you normally run three playbooks in this p *Available since LFOps `2.0.0`.* -## Mandatory Requirements - -Apache has to be installed and at least one `LoadModule` directive already has to exist, otherwise the compile step might fail. If you get `apxs:Error: Activation failed for custom /etc/httpd/conf/httpd.conf file..` or `apxs:Error: At least one 'LoadModule' directive already has to exist..`, check whether `mod_maxminddb.so` has been built (this is the reason why errors of `make install` are ignored — the module is compiled anyway). - - ## How the Role Behaves * Build dependencies are OS-specific: @@ -33,6 +28,11 @@ Apache has to be installed and at least one `LoadModule` directive already has t * On Debian / Ubuntu the role additionally runs the equivalent of `a2enmod maxminddb` (via `community.general.apache2_module`) so the freshly placed `.load` file gets symlinked into `/etc/apache2/mods-enabled/`. On Red Hat-family hosts the module is picked up automatically because it lives in `/etc/httpd/conf.modules.d/`. +## Requirements + +* Apache httpd must be installed, with at least one `LoadModule` directive already present (role: [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd)). Otherwise the compile step may fail with `apxs:Error: At least one 'LoadModule' directive already has to exist..`. See "How the Role Behaves" for why `make install` errors are ignored. + + ## Tags `mod_maxminddb` diff --git a/roles/mongodb/README.md b/roles/mongodb/README.md index 30c02ac6..0e2df6d3 100644 --- a/roles/mongodb/README.md +++ b/roles/mongodb/README.md @@ -15,9 +15,27 @@ This role is only compatible with the following MongoDB versions: *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the official [MongoDB repository](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-red-hat/#install-mongodb-community-edition). This can be done using the [linuxfabrik.lfops.repo_mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_mongodb) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* The official [MongoDB repository](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-red-hat/#install-mongodb-community-edition) must be enabled (role: [linuxfabrik.lfops.repo_mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_mongodb)). + + +## Replica Set Setup + +Important: When setting up a replica set across members, make sure that there is no data being written on any member until all members have joined the replica set. Else you need to [manually prepare the data files](https://www.mongodb.com/docs/manual/tutorial/expand-replica-set/#data-files) on the to-be-added secondary before joining. + +To setup a replica set from scratch: + +* Choose a name via the `mongodb__conf_replication_repl_set_name__*_var` (needs to be the same for all members). +* Make sure that the cluster members can reach each other by setting `mongodb__conf_net_bind_ip` accordingly. +* For production use, also make sure that `mongodb__conf_security_authorization` is enabled and `mongodb__keyfile_content` is set for all members. +* Set `mongodb__repl_set_skip_init` for all the secondaries. +* Rollout against the secondaries. +* Set `mongodb__repl_set_members` on the primary (see below). +* Rollout against the primary to initiate the replica set with the given members. +* Check the state of the cluster by using `mongosh --username mongodb-admin --password --eval 'rs.status()'` on any member. The output should contain all configured members. ## Tags @@ -281,20 +299,7 @@ mongodb__repl_set_skip_init: false ``` -### Replica Set across with multiple Members - -Important: When setting up a replica set across members, make sure that there is no data being written on any member until all members have joined the replica set. Else you need to [manually prepare the data files](https://www.mongodb.com/docs/manual/tutorial/expand-replica-set/#data-files) on the to-be-added secondary before joining. - -To setup a replica set from scratch: - -* Choose a name via the `mongodb__conf_replication_repl_set_name__*_var` (needs to be the same for all members). -* Make sure that the cluster members can reach each other by setting `mongodb__conf_net_bind_ip` accordingly. -* For production use, also make sure that `mongodb__conf_security_authorization` is enabled and `mongodb__keyfile_content` is set for all members. -* Set `mongodb__repl_set_skip_init` for all the secondaries. -* Rollout against the secondaries. -* Set `mongodb__repl_set_members` on the primary (see below). -* Rollout against the primary to initiate the replica set with the given members. -* Check the state of the cluster by using `mongosh --username mongodb-admin --password --eval 'rs.status()'` on any member. The output should contain all configured members. +## Optional Role Variables - Replica Set `mongodb__keyfile_content` diff --git a/roles/monitoring_plugins/README.md b/roles/monitoring_plugins/README.md index 3e797c06..0d45de8a 100644 --- a/roles/monitoring_plugins/README.md +++ b/roles/monitoring_plugins/README.md @@ -25,7 +25,7 @@ Notes: | Windows | Source Code | Currently not supported by this role | | -## Mandatory Requirements +## Requirements * See table above (depends on the use case). diff --git a/roles/monitoring_plugins_grafana_dashboards/README.md b/roles/monitoring_plugins_grafana_dashboards/README.md index d58465b6..9662189c 100644 --- a/roles/monitoring_plugins_grafana_dashboards/README.md +++ b/roles/monitoring_plugins_grafana_dashboards/README.md @@ -6,12 +6,12 @@ This role deploys the Monitoring Plugins Grafana Dashboards for a Grafana Server *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install and configure Grafana with the required provisioning config. This can be done using the [linuxfabrik.lfops.grafana](https://github.com/Linuxfabrik/lfops/tree/main/roles/grafana) role. -* Install [grizzly](https://grafana.github.io/grizzly/). This can be done using the [linuxfabrik.lfops.grafana_grizzly](https://github.com/Linuxfabrik/lfops/tree/main/roles/grafana_grizzly) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["Monitoring Plugins Grafana Dashboards" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/monitoring_plugins_grafana_dashboards.yml) or ["Setup Icinga2 Master" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_icinga2_master.yml), this is automatically done for you. +* Grafana must be installed and configured with the required provisioning config (role: [linuxfabrik.lfops.grafana](https://github.com/Linuxfabrik/lfops/tree/main/roles/grafana)). +* [grizzly](https://grafana.github.io/grizzly/) must be installed (role: [linuxfabrik.lfops.grafana_grizzly](https://github.com/Linuxfabrik/lfops/tree/main/roles/grafana_grizzly)). ## Tags diff --git a/roles/moodle/README.md b/roles/moodle/README.md index 9b4eef5b..13dbe4f1 100644 --- a/roles/moodle/README.md +++ b/roles/moodle/README.md @@ -15,14 +15,18 @@ Setting the version manually: *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles + +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* A web server (for example Apache httpd) must be installed and a virtual host for Moodle configured (role: [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd)). +* PHP v8.1 must be installed (roles: [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) and [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php)). +* Redis v7.2 must be installed (roles: [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) and [linuxfabrik.lfops.redis](https://github.com/Linuxfabrik/lfops/tree/main/roles/redis)). -* Attention: Moodle has very specific version requirements regarding PHP and Redis. See https://moodledev.io/general/development/policies/php. -* Install a web server (for example Apache httpd), and configure a virtual host for Moodle. This can be done using the [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd) role. -* Install PHP v8.1. This can be done using the [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) and [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php) role. -* Install Redis v7.2. This can be done using the [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) and [linuxfabrik.lfops.redis](https://github.com/Linuxfabrik/lfops/tree/main/roles/redis) role. -If you use the ["Setup Moodle" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_moodle.yml), this is automatically done for you. +## Requirements + +* Attention: Moodle has very specific version requirements regarding PHP and Redis. See https://moodledev.io/general/development/policies/php. ## Tags diff --git a/roles/network/README.md b/roles/network/README.md index 6e0a1954..c7707127 100644 --- a/roles/network/README.md +++ b/roles/network/README.md @@ -12,7 +12,9 @@ Concretely, this role: *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Requirements + +Manual steps: * Install the [Linux System Roles](https://linux-system-roles.github.io/) on the Ansible control node, e.g. via `ansible-galaxy collection install fedora.linux_system_roles`. @@ -25,7 +27,7 @@ Concretely, this role: * Triggers: none. -## Role Variables +## Optional Role Variables This role does not define its own variables. All configuration is passed straight through to `fedora.linux_system_roles.network`. See the [upstream README](https://github.com/linux-system-roles/network/blob/main/README.md) for the full list (`network_connections`, `network_provider`, `network_state`, ...). diff --git a/roles/nextcloud/README.md b/roles/nextcloud/README.md index f9f70e58..c00f3741 100644 --- a/roles/nextcloud/README.md +++ b/roles/nextcloud/README.md @@ -12,25 +12,29 @@ After installing Nextcloud, head over to your http(s)://nextcloud/index.php/sett *Available since LFOps `2.0.0`.* -## Mandatory Requirements - -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* Install a web server (for example Apache httpd), and configure a virtual host for Nextcloud. This can be done using the [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd) role. -* Install MariaDB 10.6+. This can be done using the [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server) role. -* Install PHP 8.1+. This can be done using the [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) and [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php) role. -* Install Redis 7+. This can be done using the [linuxfabrik.lfops.repo_redis](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_redis) and [linuxfabrik.lfops.redis](https://github.com/Linuxfabrik/lfops/tree/main/roles/redis) role. -* Set the size of your `/tmp` partition accordingly. For example: If you want to allow 5x simultaneous uploads with files each 10 GB in size, set it to 50 GB+. -* Configure the systemd service for [notify_push](https://github.com/nextcloud/notify_push). +## Dependent Roles + +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). +* A web server (for example Apache httpd) must be installed, with a virtual host for Nextcloud (role: [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd)). +* MariaDB 10.6+ must be installed (role: [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server)). +* PHP 8.1+ must be installed (roles: [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) and [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php)). +* Redis 7+ must be installed (roles: [linuxfabrik.lfops.repo_redis](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_redis) and [linuxfabrik.lfops.redis](https://github.com/Linuxfabrik/lfops/tree/main/roles/redis)). +* Optional: Collabora (role: [linuxfabrik.lfops.collabora](https://github.com/Linuxfabrik/lfops/tree/main/roles/collabora)) provides online document editing. +* Optional: Coturn (role: [linuxfabrik.lfops.coturn](https://github.com/Linuxfabrik/lfops/tree/main/roles/coturn)) provides the TURN server for Nextcloud Talk. -If you use the ["Setup Nextcloud" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_nextcloud.yml), this is automatically done for you (you still have to take care of providing the required versions). +These roles are not enabled by default; enable them via the playbook's skip variables if needed: +* The Collabora repository (role: [linuxfabrik.lfops.repo_collabora](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_collabora)) serves the Collabora packages from the official Collabora repository instead of the CODE repository. -## Optional Requirements -* Install Collabora. This can be done using the [linuxfabrik.lfops.collabora](https://github.com/Linuxfabrik/lfops/tree/main/roles/collabora) role. -* Install Coturn for Nextcloud Talk. This can be done using the [linuxfabrik.lfops.coturn](https://github.com/Linuxfabrik/lfops/tree/main/roles/coturn) role. +## Requirements -If you use the ["Setup Nextcloud" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_nextcloud.yml), this is automatically done for you. +Manual steps: + +* Size the `/tmp` partition for your upload load. For example, to allow 5 simultaneous uploads of 10 GB each, set it to 50 GB+. +* Configure the systemd service for [notify_push](https://github.com/nextcloud/notify_push). ## Tags diff --git a/roles/opensearch/README.md b/roles/opensearch/README.md index 435a3532..f84879e6 100644 --- a/roles/opensearch/README.md +++ b/roles/opensearch/README.md @@ -18,11 +18,11 @@ Hints for configuring TLS: *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the official OpenSearch repository. This can be done using the [linuxfabrik.lfops.repo_opensearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_opensearch) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [opensearch playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/opensearch.yml), this is automatically done for you. +* The official OpenSearch repository must be enabled (role: [linuxfabrik.lfops.repo_opensearch](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_opensearch)). ## Single-Node Setup @@ -30,63 +30,7 @@ If you use the [opensearch playbook](https://github.com/Linuxfabrik/lfops/blob/m For a single-node setup, no special configuration is needed beyond the mandatory variables. When `opensearch__discovery_seed_hosts` is not set, OpenSearch 2.x automatically runs in single-node mode (`discovery.type: single-node`). After installation, verify that OpenSearch is running (see Post-Installation Steps below). -## Tags - -`opensearch` - -* Installs OpenSearch. -* Deploys all configuration files. -* Deploys TLS certificates and runs `securityadmin.sh` (if security plugin is enabled). -* Manages the state of the OpenSearch service. -* Triggers: opensearch.service restart. - -`opensearch:configure` - -* Deploys `/etc/opensearch/opensearch.yml`. -* Deploys `/etc/sysconfig/opensearch`. -* Deploys internal users configuration (if security plugin is enabled). -* Triggers: opensearch.service restart. - -`opensearch:generate_certs` - -* Generates self-signed TLS certificates on the Ansible controller using the SearchGuard TLS Tool. -* Triggers: none. - -`opensearch:state` - -* Manages the state of the OpenSearch service (`systemctl enable/disable --now`). -* Triggers: none. - -`opensearch:user` - -* Manages internal users (generates hashed passwords, deploys `internal_users.yml`). -* Triggers: opensearch.service restart, `securityadmin.sh`. - - -## Mandatory Role Variables - -`opensearch__opensearch_initial_admin_password` - -* For new installations of OpenSearch 2.12 and later, a custom admin password is required. Minimum 8 characters, must contain at least one uppercase letter, one lowercase letter, one digit, and one special character. -* Type: String. - -Example: -```yaml -# mandatory -opensearch__opensearch_initial_admin_password: 'linuxfabrik' -``` - - -## Post-Installation Steps - -After setting up a single node or cluster, verify that OpenSearch is running: - -```bash -curl 'https://localhost:9200' --user admin:your-password --insecure -``` - - -## Setting Up an OpenSearch Cluster +## Cluster Setup This role supports creating a multi-node OpenSearch cluster using manual certificate distribution. TLS certificates are generated beforehand and distributed to all nodes via Ansible. The security plugin is configured with the certificate distinguished names of all cluster members. @@ -228,7 +172,7 @@ curl 'https://node1.example.com:9200/_cat/nodes?v' --user admin:your-password -- The status should be `green` with all nodes listed. -## Adding a New Node to an Existing Cluster +## Adding a Node to an Existing Cluster 1. Generate certificates for the new node using the same CA as the existing cluster. 2. Add the certificate files to your Ansible inventory. @@ -248,6 +192,62 @@ ansible-playbook --inventory inventory linuxfabrik.lfops.opensearch --tags opens ``` +## Post-Installation Steps + +After setting up a single node or cluster, verify that OpenSearch is running: + +```bash +curl 'https://localhost:9200' --user admin:your-password --insecure +``` + + +## Tags + +`opensearch` + +* Installs OpenSearch. +* Deploys all configuration files. +* Deploys TLS certificates and runs `securityadmin.sh` (if security plugin is enabled). +* Manages the state of the OpenSearch service. +* Triggers: opensearch.service restart. + +`opensearch:configure` + +* Deploys `/etc/opensearch/opensearch.yml`. +* Deploys `/etc/sysconfig/opensearch`. +* Deploys internal users configuration (if security plugin is enabled). +* Triggers: opensearch.service restart. + +`opensearch:generate_certs` + +* Generates self-signed TLS certificates on the Ansible controller using the SearchGuard TLS Tool. +* Triggers: none. + +`opensearch:state` + +* Manages the state of the OpenSearch service (`systemctl enable/disable --now`). +* Triggers: none. + +`opensearch:user` + +* Manages internal users (generates hashed passwords, deploys `internal_users.yml`). +* Triggers: opensearch.service restart, `securityadmin.sh`. + + +## Mandatory Role Variables + +`opensearch__opensearch_initial_admin_password` + +* For new installations of OpenSearch 2.12 and later, a custom admin password is required. Minimum 8 characters, must contain at least one uppercase letter, one lowercase letter, one digit, and one special character. +* Type: String. + +Example: +```yaml +# mandatory +opensearch__opensearch_initial_admin_password: 'linuxfabrik' +``` + + ## Optional Role Variables - General Only optional if `opensearch__plugins_security_disabled` is `true`. @@ -479,9 +479,9 @@ opensearch__plugins_security_nodes_dns: ``` -## TLS Certificate Generation Variables +## Optional Role Variables - TLS Certificate Generation -These variables are only needed when using the built-in certificate generator (see "Setting Up an OpenSearch Cluster" above). The tasks run against the Ansible controller. Internally, the [SearchGuard TLS Tool](https://docs.search-guard.com/latest/offline-tls-tool) is used, with the generated config at `/tmp/opensearch-certs/config/{{ inventory_hostname }}-tlsconfig.yml`. +These variables are only needed when using the built-in certificate generator (see "Cluster Setup" above). The tasks run against the Ansible controller. Internally, the [SearchGuard TLS Tool](https://docs.search-guard.com/latest/offline-tls-tool) is used, with the generated config at `/tmp/opensearch-certs/config/{{ inventory_hostname }}-tlsconfig.yml`. `opensearch__generate_certs_admin_cn` diff --git a/roles/openvpn_server/README.md b/roles/openvpn_server/README.md index ebd69398..8a5aef0a 100644 --- a/roles/openvpn_server/README.md +++ b/roles/openvpn_server/README.md @@ -8,13 +8,17 @@ This role does not configure OpenVPN logging via `log-append /var/log/openvpn.lo *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* Install Python 3, and the python3-policycoreutils module (required for the SELinux Ansible tasks). This can be done using the [linuxfabrik.lfops.policycoreutils](https://github.com/Linuxfabrik/lfops/tree/main/roles/policycoreutils) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). +* Python 3 and the python3-policycoreutils module must be installed (required for the SELinux Ansible tasks) (role: [linuxfabrik.lfops.policycoreutils](https://github.com/Linuxfabrik/lfops/tree/main/roles/policycoreutils)). -## Optional Requirements + +## Requirements + +Manual steps: * Create a certificate for the OpenVPN server and save it on the ansible control node as `{{ inventory_dir }}/host_vars/{{ inventory_hostname }}/files/etc/openvpn/server/server.p12`. * Generate a certificate revocation list and save it on the ansible control node as `{{ inventory_dir }}/host_vars/{{ inventory_hostname }}/files/etc/openvpn/server/crl.pem`. diff --git a/roles/php/README.md b/roles/php/README.md index af797c03..1a69de3a 100644 --- a/roles/php/README.md +++ b/roles/php/README.md @@ -29,9 +29,11 @@ This role never exposes to the world that PHP is installed on the server, no mat *Available since LFOps `2.0.0`.* -## Optional Requirements +## Dependent Roles -* Enable the [Remi's RPM repository](https://rpms.remirepo.net/) to get newer versions of PHP. This can be done using the [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* Optional: [Remi's RPM repository](https://rpms.remirepo.net/) (role: [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi)) provides newer PHP versions. ## Tags @@ -119,7 +121,7 @@ php__modules__host_var: ``` -### `php__ini_*` Config Directives +## Optional Role Variables - `php__ini_*` Config Directives Variables for `php.ini` directives and their default values, defined and supported by this role. @@ -303,7 +305,7 @@ php__ini_upload_max_filesize__host_var: '10000M' ``` -### PHP-FPM Pool Config Directives +## Optional Role Variables - PHP-FPM Pool Config Directives Variables for PHP-FPM Pool Config directives and their default values, defined and supported by this role. diff --git a/roles/podman_containers/README.md b/roles/podman_containers/README.md index 9e49c0d5..beba631a 100644 --- a/roles/podman_containers/README.md +++ b/roles/podman_containers/README.md @@ -6,9 +6,11 @@ This role installs [Podman](https://podman.io/) and deploys [Quadlets](https://d *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Requirements -* When running rootless containers, make sure to create a user with lingering enabled. This can be done using the [linuxfabrik.lfops.login](https://github.com/Linuxfabrik/lfops/tree/main/roles/login) role: +Manual steps: + +* Optional: when running rootless containers, create a user with lingering enabled by running the [login](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/login.yml) playbook (role: [linuxfabrik.lfops.login](https://github.com/Linuxfabrik/lfops/tree/main/roles/login)): ```yaml login__users__host_var: - name: 'example' diff --git a/roles/policycoreutils/README.md b/roles/policycoreutils/README.md index 31e88251..ec59d30b 100644 --- a/roles/policycoreutils/README.md +++ b/roles/policycoreutils/README.md @@ -14,30 +14,6 @@ This role installs the SELinux policy core python utilities. * Triggers: none. -## Skip Variables - -This role is used in several playbooks that provide skip variables to disable specific dependencies. See the playbooks documentation for details: - -* [apache_httpd.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#apache_httpdyml) -* [apache_tomcat.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#apache_tomcatyml) -* [clamav.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#clamavyml) -* [fail2ban.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#fail2banyml) -* [mariadb_server.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#mariadb_serveryml) -* [openvpn_server.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#openvpn_serveryml) -* [selinux.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#selinexyml) -* [setup_basic.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_basicyml) -* [setup_grav.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_gravyml) -* [setup_graylog_datanode.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_graylog_datanodeyml) -* [setup_graylog_server.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_graylog_serveryml) -* [setup_icinga2_master.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_icinga2_masteryml) -* [setup_keycloak.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_keycloakyml) -* [setup_librenms.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_librenmsyml) -* [setup_mastodon.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_mastodonyml) -* [setup_moodle.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_moodleyml) -* [setup_nextcloud.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_nextcloudyml) -* [setup_wordpress.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_wordpressyml) - - ## License [The Unlicense](https://unlicense.org/) diff --git a/roles/postgresql_server/README.md b/roles/postgresql_server/README.md index 5446dea9..a2ea64a7 100644 --- a/roles/postgresql_server/README.md +++ b/roles/postgresql_server/README.md @@ -6,18 +6,12 @@ This role installs and configures a [PostgreSQL](https://www.postgresql.org/) se *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install the `python3-psycopg2` library. This can be done using the [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [postgresql_server Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/postgresql_server.yml), this is automatically done for you. - - -## Optional Requirements - -* Enable the official [PostgreSQL Yum Repository](https://yum.postgresql.org/). This can be done using the [linuxfabrik.lfops.repo_postgresql](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_postgresql) role. - -If you use the [postgresql_server Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/postgresql_server.yml), this is automatically done for you. +* The `python3-psycopg2` library must be installed (role: [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python)). +* Optional: the official [PostgreSQL Yum Repository](https://yum.postgresql.org/) enabled (role: [linuxfabrik.lfops.repo_postgresql](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_postgresql)). ## Tags diff --git a/roles/python_venv/README.md b/roles/python_venv/README.md index 868b8e53..dcf21f79 100644 --- a/roles/python_venv/README.md +++ b/roles/python_venv/README.md @@ -6,10 +6,18 @@ This role creates and manages various [Python 3 virtual environments (venv)](htt *Available since LFOps `1.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install Python 3 -* On Rocky 9+, the EPEL and the CRB Repo ("Code Ready Builder") need to be enabled to be able to install `python3-virtualenv` - otherwise you'll get `No match for argument: python3-virtualenv` or `nothing provides python3-wheel-wheel needed by python3-virtualenv-20.21.1-1.el9.noarch from epel`. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* Python 3 must be installed (role: [linuxfabrik.lfops.python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python)). + + +## Requirements + +Manual steps: + +* On Rocky 9+, enable the EPEL repository by running the [repo_epel](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/repo_epel.yml) playbook (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)) and the CRB ("Code Ready Builder") repository by running the [repo_baseos](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/repo_baseos.yml) playbook (role: [linuxfabrik.lfops.repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos)). Both are required to install `python3-virtualenv`. Otherwise you get `No match for argument: python3-virtualenv` or `nothing provides python3-wheel-wheel needed by python3-virtualenv-20.21.1-1.el9.noarch from epel`. ## Tags diff --git a/roles/redis/README.md b/roles/redis/README.md index e13b6ad1..b20a0e2f 100644 --- a/roles/redis/README.md +++ b/roles/redis/README.md @@ -68,7 +68,7 @@ redis__service_timeout_stop_sec: 5 ``` -### `redis__conf_*` config directives +## Optional Role Variables - `redis__conf_*` Config Directives Variables for `redis.conf` directives and their default values, defined and supported by this role. diff --git a/roles/repo_icinga/README.md b/roles/repo_icinga/README.md index af2540e1..b3f6b2ac 100644 --- a/roles/repo_icinga/README.md +++ b/roles/repo_icinga/README.md @@ -6,7 +6,7 @@ This role deploys the [Icinga Package Repository](https://packages.icinga.com/). *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Requirements * For RHEL (and compatible) hosts an [Icinga Repo Subscription](https://www.linuxfabrik.ch/en/products/icinga-subscriptions) is required. diff --git a/roles/repo_rpmfusion/README.md b/roles/repo_rpmfusion/README.md index 55a225ae..5dacb9ab 100644 --- a/roles/repo_rpmfusion/README.md +++ b/roles/repo_rpmfusion/README.md @@ -6,11 +6,11 @@ This role deploys the [RPM Fusion](https://rpmfusion.org/RPM%20Fusion) free and *Available since LFOps `3.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the EPEL Repository. This can be done using the [linuxfabrik.lfops.epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/epel) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["Repo RPM Fusion" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/repo_rpmfusion.yml), this is automatically done for you. +* The EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). ## Tags diff --git a/roles/rocketchat/README.md b/roles/rocketchat/README.md index cfdc5743..b0f2965a 100644 --- a/roles/rocketchat/README.md +++ b/roles/rocketchat/README.md @@ -6,15 +6,22 @@ This role installs and configures [Rocket.Chat](https://www.rocket.chat/), an Op *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. -* Enable the MongoDB repository. This can be done using the [linuxfabrik.lfops.repo_mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_mongodb) role. -* Install MongoDB and configure a replica set. This can be done using the [linuxfabrik.lfops.mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/mongodb) role. -* Create a MongoDB user for Rocket.Chat. Mandatory when authentication in MongoDB is enabled. This can be done using the [linuxfabrik.lfops.mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/mongodb) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the ["Setup Rocket.Chat" Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_rocketchat.yml), this is automatically done for you (you still have to take care of providing the required versions). +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). +* The MongoDB repository must be enabled (role: [linuxfabrik.lfops.repo_mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_mongodb)). +* MongoDB must be installed and a replica set configured (role: [linuxfabrik.lfops.mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/mongodb)). +* A MongoDB user for Rocket.Chat must be created. Mandatory when authentication in MongoDB is enabled (role: [linuxfabrik.lfops.mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/mongodb)). + +## Requirements + +Manual steps: + +* Optional: set `Storage=presistent` in `/etc/systemd/journald.conf` to allow the user to use `journalctl --user` by running the [systemd_journald](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/systemd_journald.yml) playbook (role: [linuxfabrik.lfops.systemd_journald](https://github.com/Linuxfabrik/lfops/tree/main/roles/systemd_journald)). +* Optional: if the host should act as a Postfix MTA, make it listen on the IP address so that the container can reach it by running the [postfix](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/postfix.yml) playbook (role: [linuxfabrik.lfops.postfix](https://github.com/Linuxfabrik/lfops/tree/main/roles/postfix)). * Make sure the container can access the MongoDB instance: ```yaml mongodb__conf_net_bind_ip: @@ -25,12 +32,6 @@ mongodb__repl_set_members: ``` -## Optional Requirements - -* It is recommended to set `Storage=presistent` in `/etc/systemd/journald.conf` to allow the user to use `journalctl --user`. This can be done using the [linuxfabrik.lfops.systemd_journald](https://github.com/Linuxfabrik/lfops/tree/main/roles/systemd_journald) role. -* If the host should act as a Postfix MTA, make sure it is listening on the IP address so that the container can reach it. This can be done using the [linuxfabrik.lfops.systemd_journald](https://github.com/Linuxfabrik/lfops/tree/main/roles/systemd_journald) role. - - ## Tags `rocketchat` diff --git a/roles/selinux/README.md b/roles/selinux/README.md index 8561f9f0..0b009e10 100644 --- a/roles/selinux/README.md +++ b/roles/selinux/README.md @@ -13,9 +13,11 @@ *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install the SELinux python bindings. This can be done using the [linuxfabrik.lfops.policycoreutils](https://github.com/Linuxfabrik/lfops/tree/main/roles/policycoreutils) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* The SELinux python bindings must be installed (role: [linuxfabrik.lfops.policycoreutils](https://github.com/Linuxfabrik/lfops/tree/main/roles/policycoreutils)). ## Tags diff --git a/roles/sshd/README.md b/roles/sshd/README.md index efd3bb88..c02cdd56 100644 --- a/roles/sshd/README.md +++ b/roles/sshd/README.md @@ -8,9 +8,11 @@ Note that the role does not make use of `/etc/ssh/sshd_config.d/` since not all *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install Python 3, and the python3-policycoreutils module (required for the SELinux Ansible tasks). This can be done using the [linuxfabrik.lfops.policycoreutils](https://github.com/Linuxfabrik/lfops/tree/main/roles/policycoreutils) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* Python 3 and the python3-policycoreutils module must be installed (required for the SELinux Ansible tasks) (role: [linuxfabrik.lfops.policycoreutils](https://github.com/Linuxfabrik/lfops/tree/main/roles/policycoreutils)). ## Tags diff --git a/roles/system_update/README.md b/roles/system_update/README.md index 100e22c3..30e84114 100644 --- a/roles/system_update/README.md +++ b/roles/system_update/README.md @@ -10,14 +10,20 @@ This role configures the server to do (weekly) system updates by deploying two s *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install at. This can be done using the [linuxfabrik.lfops.at](https://github.com/Linuxfabrik/lfops/tree/main/roles/at) role. -* Install mailx. This can be done using the [linuxfabrik.lfops.mailx](https://github.com/Linuxfabrik/lfops/tree/main/roles/mailx) role. -* Install needrestart on Debian. This can be done using the [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps) role. -* Install yum-utils on RHEL. This can be done using the [linuxfabrik.lfops.yum_utils](https://github.com/Linuxfabrik/lfops/tree/main/roles/yum_utils) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [system_update Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/system_update.yml), this is automatically done for you. +* at must be installed (role: [linuxfabrik.lfops.at](https://github.com/Linuxfabrik/lfops/tree/main/roles/at)). +* mailx must be installed (role: [linuxfabrik.lfops.mailx](https://github.com/Linuxfabrik/lfops/tree/main/roles/mailx)). +* yum-utils must be installed on RHEL (role: [linuxfabrik.lfops.yum_utils](https://github.com/Linuxfabrik/lfops/tree/main/roles/yum_utils)). + + +## Requirements + +Manual steps: + +* On Debian, install needrestart by running the [apps](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/apps.yml) playbook (role: [linuxfabrik.lfops.apps](https://github.com/Linuxfabrik/lfops/tree/main/roles/apps)). ## Tags diff --git a/roles/telegraf/README.md b/roles/telegraf/README.md index 45819162..cd6d441d 100644 --- a/roles/telegraf/README.md +++ b/roles/telegraf/README.md @@ -6,11 +6,11 @@ This role installs and configures [Telegraf](https://www.influxdata.com/time-ser *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Enable the official [InfluxDB repository](https://docs.influxdata.com/influxdb/v1.8/introduction/install/?t=Red+Hat+%26amp%3B+CentOS). This can be done using the [linuxfabrik.lfops.repo_influxdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_influxdb) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [telegraf playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/telegraf.yml), this is automatically done for you. +* The official [InfluxDB repository](https://docs.influxdata.com/influxdb/v1.8/introduction/install/?t=Red+Hat+%26amp%3B+CentOS) must be enabled (role: [linuxfabrik.lfops.repo_influxdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_influxdb)). ## Tags diff --git a/roles/tools/README.md b/roles/tools/README.md index 2a4cd2d1..e22aae5a 100644 --- a/roles/tools/README.md +++ b/roles/tools/README.md @@ -33,9 +33,11 @@ Bash: *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* On RHEL-compatible systems, enable the EPEL repository. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). ## Tags diff --git a/roles/uptimerobot/README.md b/roles/uptimerobot/README.md index 5ade69ec..dcdb7293 100644 --- a/roles/uptimerobot/README.md +++ b/roles/uptimerobot/README.md @@ -2,19 +2,16 @@ This role applies a declarative UptimeRobot configuration (monitors, maintenance windows, public status pages, and alert-contact cleanup) against an [UptimeRobot](https://uptimerobot.com/) account. +UptimeRobot is a hosted uptime-monitoring service. You configure *monitors* (HTTP / keyword / port / ping / heartbeat checks), wire them up to *alert contacts* (email, SMS, webhook, Slack, ...), schedule *maintenance windows* during which alerts are suppressed, and optionally publish a *public status page* (PSP) that aggregates a set of monitors. Everything lives in UptimeRobot's cloud; there is no agent on your hosts. -*Available since LFOps `6.0.2`.* +*Available since LFOps `6.0.2`.* -## What is UptimeRobot -UptimeRobot is a hosted uptime-monitoring service. You configure *monitors* (HTTP / keyword / port / ping / heartbeat checks), wire them up to *alert contacts* (email, SMS, webhook, Slack, ...), schedule *maintenance windows* during which alerts are suppressed, and optionally publish a *public status page* (PSP) that aggregates a set of monitors. Everything lives in UptimeRobot's cloud — there is no agent on your hosts. +## How the Role Behaves The role does not deploy any software on the target hosts. It runs on the Ansible controller, talks to the UptimeRobot HTTP API, and brings the account into the desired state. Run it on `localhost` (or any host that has outbound HTTPS to `api.uptimerobot.com`). - -## Concepts - The role works with five concepts that map 1:1 to UptimeRobot's data model and to one Ansible CRUD module each (plus a parallel set of read-only `*_info` modules — see the migration section below): * **Monitor** (`linuxfabrik.lfops.uptimerobot_monitor`) — a single check (URL, host, heartbeat). Carries its own interval, timeout, HTTP method, optional auth, optional keyword search, and references to alert contacts and maintenance windows. @@ -28,7 +25,7 @@ The role chains the four CRUD modules in the right order: maintenance windows fi The modules currently target UptimeRobot API v2 (POST + form-urlencoded). The full reference for that API — endpoint paths, parameters, return shapes, enumerations — lives at . A future migration to API v3 stays local to `plugins/module_utils/uptimerobot.py` and does not change the role's variables or task layout. -## API Limits and Wire-Format Quirks +### API Limits and Wire-Format Quirks UptimeRobot's API has a few sharp edges that are useful to know up front. The modules absorb most of them, but the limits affect playbook design. @@ -46,6 +43,29 @@ UptimeRobot's API has a few sharp edges that are useful to know up front. The mo * **Monitor `type` is immutable** — UptimeRobot will not change a monitor's type after creation. Recreate the monitor (`state: 'absent'` then `state: 'present'`) if you need to switch. +### Performance / Caching + +To keep the per-item API cost down when the role loops a CRUD module over a long list, `module_utils.uptimerobot` writes the four heavy `getX` responses (`getMonitors`, `getAlertContacts`, `getMWindows`, `getPSPs`) to a short-lived on-disk cache under `~/.cache/ansible-uptimerobot/` with a 60-second TTL. Cache files are mode `0600` because they hold the full resource list returned by the API. + +* **Read calls** check the cache first; a hit replaces the (paginated) HTTP request entirely. Logged as `cache HIT (...)` in syslog. +* **Mutating calls** (`newX` / `editX` / `deleteX`) succeed against the API and then invalidate the matching `getX` cache so the next read sees the post-write state. +* Cache files are per-engineer (the path is in your `$HOME`), so different engineers running against the same UptimeRobot account don't share state. +* If you need to force a fresh read, delete the directory: + + ``` + rm -rf ~/.cache/ansible-uptimerobot/ + ``` + +Typical impact: a `--check` run over 56 monitors goes from ~56× 4 page `getMonitors` + 56× `getAlertContacts` + 56× `getMWindows` to one of each (the rest hit cache). Idempotent re-runs of the same playbook within a minute are nearly free. + + +## Requirements + +* An UptimeRobot account. +* An UptimeRobot API key with write access (account-wide). Pass it via `uptimerobot__api_key`, via `uptimerobot__api_key_file` (default `~/.uptimerobot`), or via the `UPTIMEROBOT_API_KEY` environment variable. The modules try those three sources in that order. +* Outbound HTTPS from the controller to `https://api.uptimerobot.com/`. + + ## Tags `uptimerobot` @@ -70,13 +90,6 @@ UptimeRobot's API has a few sharp edges that are useful to know up front. The mo * Delete the listed alert contacts only. -## Mandatory Requirements - -* An UptimeRobot account. -* An UptimeRobot API key with write access (account-wide). Pass it via `uptimerobot__api_key`, via `uptimerobot__api_key_file` (default `~/.uptimerobot`), or via the `UPTIMEROBOT_API_KEY` environment variable. The modules try those three sources in that order. -* Outbound HTTPS from the controller to `https://api.uptimerobot.com/`. - - ## Optional Role Variables `uptimerobot__api_key` @@ -419,23 +432,46 @@ The `[WARNING] No inventory was parsed, only implicit localhost is available` li ``` -## Performance / Caching +## Read-Only Inspection -To keep the per-item API cost down when the role loops a CRUD module over a long list, `module_utils.uptimerobot` writes the four heavy `getX` responses (`getMonitors`, `getAlertContacts`, `getMWindows`, `getPSPs`) to a short-lived on-disk cache under `~/.cache/ansible-uptimerobot/` with a 60-second TTL. Cache files are mode `0600` because they hold the full resource list returned by the API. +Each resource type has a parallel `*_info` module for read-only queries -- useful for ad-hoc inspection, dynamic inventories, or driving downstream tasks: -* **Read calls** check the cache first; a hit replaces the (paginated) HTTP request entirely. Logged as `cache HIT (...)` in syslog. -* **Mutating calls** (`newX` / `editX` / `deleteX`) succeed against the API and then invalidate the matching `getX` cache so the next read sees the post-write state. -* Cache files are per-engineer (the path is in your `$HOME`), so different engineers running against the same UptimeRobot account don't share state. -* If you need to force a fresh read, delete the directory: +| Resource | Read-only module | +|---|---| +| Account | `linuxfabrik.lfops.uptimerobot_account_info` | +| Monitors | `linuxfabrik.lfops.uptimerobot_monitor_info` | +| Maintenance windows | `linuxfabrik.lfops.uptimerobot_mwindow_info` | +| Alert contacts | `linuxfabrik.lfops.uptimerobot_alert_contact_info` | +| Public status pages | `linuxfabrik.lfops.uptimerobot_psp_info` | - ``` - rm -rf ~/.cache/ansible-uptimerobot/ - ``` +All info modules accept `friendly_name:` to filter to a single resource and (where supported by the API) `search:` for a server-side substring filter. Enum-style fields come back as labels (e.g. `http_method: 'get'`, `status: 'up'`), matching the user-facing parameter values you write in your inventory. -Typical impact: a `--check` run over 56 monitors goes from ~56× 4 page `getMonitors` + 56× `getAlertContacts` + 56× `getMWindows` to one of each (the rest hit cache). Idempotent re-runs of the same playbook within a minute are nearly free. +```yaml +- linuxfabrik.lfops.uptimerobot_monitor_info: + friendly_name: '001 www.example.com' + register: 'ur_monitor' + +- ansible.builtin.debug: + var: 'ur_monitor.monitors[0].interval' +``` + +To bulk-pause / -resume monitors or status pages, loop the matching CRUD module with `state: 'present'` over the result of the `*_info` module: + +```yaml +- linuxfabrik.lfops.uptimerobot_psp_info: + register: 'ur_psps' + +- linuxfabrik.lfops.uptimerobot_psp: + friendly_name: '{{ item.friendly_name }}' + status: 'paused' # or 'active' to resume + state: 'present' + loop: '{{ ur_psps.psps }}' + loop_control: + label: '{{ item.friendly_name }}' +``` -## Debugging / Verbose Output +## Troubleshooting The CRUD modules emit verbose output through three channels — useful when iterating against a live API: @@ -497,45 +533,6 @@ Typical values you will see: Combined with `--check --diff`, the `debug` dict makes it cheap to investigate false-positive `changed: true` (typically a missing read-side translation or a desired-vs-current normalisation gap). -## Read-Only Inspection - -Each resource type has a parallel `*_info` module for read-only queries -- useful for ad-hoc inspection, dynamic inventories, or driving downstream tasks: - -| Resource | Read-only module | -|---|---| -| Account | `linuxfabrik.lfops.uptimerobot_account_info` | -| Monitors | `linuxfabrik.lfops.uptimerobot_monitor_info` | -| Maintenance windows | `linuxfabrik.lfops.uptimerobot_mwindow_info` | -| Alert contacts | `linuxfabrik.lfops.uptimerobot_alert_contact_info` | -| Public status pages | `linuxfabrik.lfops.uptimerobot_psp_info` | - -All info modules accept `friendly_name:` to filter to a single resource and (where supported by the API) `search:` for a server-side substring filter. Enum-style fields come back as labels (e.g. `http_method: 'get'`, `status: 'up'`), matching the user-facing parameter values you write in your inventory. - -```yaml -- linuxfabrik.lfops.uptimerobot_monitor_info: - friendly_name: '001 www.example.com' - register: 'ur_monitor' - -- ansible.builtin.debug: - var: 'ur_monitor.monitors[0].interval' -``` - -To bulk-pause / -resume monitors or status pages, loop the matching CRUD module with `state: 'present'` over the result of the `*_info` module: - -```yaml -- linuxfabrik.lfops.uptimerobot_psp_info: - register: 'ur_psps' - -- linuxfabrik.lfops.uptimerobot_psp: - friendly_name: '{{ item.friendly_name }}' - status: 'paused' # or 'active' to resume - state: 'present' - loop: '{{ ur_psps.psps }}' - loop_control: - label: '{{ item.friendly_name }}' -``` - - ## License [The Unlicense](https://unlicense.org/) diff --git a/roles/wordpress/README.md b/roles/wordpress/README.md index f8213b2d..1ecc87e4 100644 --- a/roles/wordpress/README.md +++ b/roles/wordpress/README.md @@ -8,13 +8,13 @@ Attention: It is intended that when you call `http://{wordpress__url}}` you will *Available since LFOps `2.0.0`.* -## Mandatory Requirements +## Dependent Roles -* Install a web server (for example Apache httpd), and configure a virtual host for Nextcloud. This can be done using the [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd) role. -* Install MariaDB 10+. This can be done using the [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server) role. -* Install PHP 7+. This can be done using the [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php) role. +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -If you use the [WordPress Playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/setup_wordpress.yml), this is automatically done for you. +* A web server (for example Apache httpd) must be installed, with a virtual host configured for WordPress (role: [linuxfabrik.lfops.apache_httpd](https://github.com/Linuxfabrik/lfops/tree/main/roles/apache_httpd)). +* MariaDB 10+ must be installed (role: [linuxfabrik.lfops.mariadb_server](https://github.com/Linuxfabrik/lfops/tree/main/roles/mariadb_server)). +* PHP 7+ must be installed (role: [linuxfabrik.lfops.php](https://github.com/Linuxfabrik/lfops/tree/main/roles/php)). ## Tags diff --git a/roles/yum_utils/README.md b/roles/yum_utils/README.md index c5df63d3..fb380d01 100644 --- a/roles/yum_utils/README.md +++ b/roles/yum_utils/README.md @@ -14,22 +14,6 @@ This role installs the `yum-utils` or `dnf-utils` package. * Triggers: none. -## Skip Variables - -This role is used in several playbooks that provide skip variables to disable specific dependencies. See the playbooks documentation for details: - -* [icingaweb2.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#icingaweb2yml) -* [mirror.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#mirroryml) -* [php.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#phpyml) -* [setup_basic.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_basicyml) -* [setup_grav.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_gravyml) -* [setup_icinga2_master.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_icinga2_masteryml) -* [setup_librenms.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_librenmsyml) -* [setup_nextcloud.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_nextcloudyml) -* [setup_wordpress.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#setup_wordpressyml) -* [system_update.yml](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md#system_updateyml) - - ## License [The Unlicense](https://unlicense.org/) From 4ccc39885d55a70249fc3c7f1a5af2e00159d957 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:31:31 +0200 Subject: [PATCH 37/66] docs(roles/acme_sh): fix indentation --- roles/acme_sh/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/roles/acme_sh/README.md b/roles/acme_sh/README.md index 2963f263..e3b22d8a 100644 --- a/roles/acme_sh/README.md +++ b/roles/acme_sh/README.md @@ -28,14 +28,14 @@ Manual steps: * Configure a web server. The playbook does not set this up. If you are using LFOps to manage an Apache reverse proxy, a virtual host working for acme might be defined like this: - ```yaml - apache_httpd__vhosts__host_var: - - conf_server_name: 'www.example.com' - enabled: true - state: 'present' - template: 'redirect' - virtualhost_port: 80 - ``` +```yaml +apache_httpd__vhosts__host_var: + - conf_server_name: 'www.example.com' + enabled: true + state: 'present' + template: 'redirect' + virtualhost_port: 80 +``` ## Tags From dca1bf48c44c4f74b7bf3ee48bc7d32aa44a5d7b Mon Sep 17 00:00:00 2001 From: Jihan El Karz Date: Wed, 13 May 2026 16:58:44 +0200 Subject: [PATCH 38/66] fix(roles): enable CRB and EPEL --- CHANGELOG.md | 3 +++ playbooks/README.md | 2 ++ playbooks/clamav.yml | 15 +++++++++++++++ playbooks/duplicity.yml | 15 +++++++++++++++ playbooks/fangfrisch.yml | 15 +++++++++++++++ playbooks/influxdb.yml | 16 +++++++++++++++- playbooks/mongodb.yml | 15 +++++++++++++++ playbooks/python_venv.yml | 17 +++++++++++++++-- playbooks/setup_graylog_datanode.yml | 6 +++--- playbooks/setup_graylog_server.yml | 6 +++--- playbooks/setup_icinga2_master.yml | 6 +++--- playbooks/setup_rocketchat.yml | 8 ++++---- roles/duplicity/README.md | 2 ++ roles/fangfrisch/README.md | 2 ++ roles/influxdb/README.md | 4 +++- roles/mongodb/README.md | 1 - 16 files changed, 115 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c6256c..a743df32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **role:network**: README still claimed the role disables zeroconf, but the corresponding `NOZEROCONF=yes` task was removed in 2024 (NetworkManager no longer adds the zeroconf route by default). Bring the README in line with what the role actually does and call out the Hetzner-specific `hc-utils` cleanup explicitly. * **role:haveged**: Setting `haveged__service_state: 'stopped'` produced the invalid systemctl command `stopp` because of a `[:-2]` slice in the task name. The role now uses `ansible.builtin.service` directly with the configured state, so all four valid values (`reloaded` / `restarted` / `started` / `stopped`) work as expected. * **role:unattended_upgrades**: Correct README description; the role deactivates Unattended Upgrades by setting both `APT::Periodic` flags to `0` in `/etc/apt/apt.conf.d/20auto-upgrades` (Debian/Ubuntu), it does not remove the `unattended-upgrades` package. +* **playbooks/clamav, playbooks/duplicity, playbooks/fangfrisch, playbooks/influxdb, playbooks/mongodb, playbooks/python_venv**: Enable `repo_baseos` (CRB) and `repo_epel` on Rocky 9+ before the `python_venv` role to fix `No match for argument: python3-virtualenv` / `nothing provides python3-wheel-wheel needed by python3-virtualenv`. +* **playbooks/setup_graylog_datanode, playbooks/setup_graylog_server, playbooks/setup_icinga2_master, playbooks/setup_rocketchat**: Extend existing `repo_baseos` `when` condition from `== "9"` to `| int >= 9` (Rocky 10+), and extend `repo_epel` version list to include RHEL/Rocky 10. +* **role:glances**: Skip installation on RHEL 10+, where glances is not available in EPEL. * **playbooks/freeipa_client, playbooks/freeipa_server**: Set `strategy: 'linear'` explicitly so the playbooks work even when the user's `ansible.cfg` defaults to a strategy that reuses the target Python interpreter (e.g. `mitogen_linear`). The ansible-freeipa modules rely on `ipalib`'s global API singleton and otherwise fail with `API.bootstrap() already called` on the second module call. * **role:mariadb_server**: Stop writing the deprecated `innodb_buffer_pool_chunk_size` setting to the generated config for MariaDB 10.11, 11.4 and 11.8. MariaDB ignores the value from 10.11.12, 11.4.6 and 11.8.2 onwards and derives the chunk size automatically from `innodb_buffer_pool_size`. The user-facing role variables stay declared for backward compatibility but are now documented as deprecated. On MariaDB 10.6 the setting is unchanged. The role now also aborts at the start of the run with a clear error message if `innodb_buffer_pool_chunk_size` (on MariaDB 10.11+) or `innodb_file_per_table` (on MariaDB 11.0+) is still set in inventory, so that an upgrade from MariaDB 10.6 to 11.x does not silently keep a stale override around. * **role:mariadb_server**: Fix MariaDB starting in the `unconfined_service_t` SELinux domain on RHEL 10, which leaves `/var/lib/mysql/mysql.sock` mislabeled and breaks `php-fpm`/`httpd_t` clients (e.g. Icinga Web 2 login: `SQLSTATE[HY000] [2002] Permission denied`). The unit drop-in's `ExecStartPre=-/bin/chcon -t mysqld_exec_t /usr/sbin/mariadbd` workaround for [MDEV-30520](https://jira.mariadb.org/browse/MDEV-30520) cannot relabel the binary on EL10+, where the packaged `mariadb.service` applies `ProtectSystem` that mounts `/usr` read-only inside the service sandbox. The role now sets the `mysqld_exec_t` file context for `/usr/sbin/mariadbd` persistently via `semanage fcontext` + `restorecon` (outside the systemd sandbox) and notifies a restart so the daemon comes up in `mysqld_t`. diff --git a/playbooks/README.md b/playbooks/README.md index 810a660f..8df9dc6c 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -463,6 +463,8 @@ Calls the following roles (in order): Calls the following roles (in order): * [repo_influxdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_influxdb) +* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `influxdb__skip_repo_baseos` +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `influxdb__skip_repo_epel` * [python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv): `influxdb__skip_python_venv` * [influxdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/influxdb) diff --git a/playbooks/clamav.yml b/playbooks/clamav.yml index ec8e8839..6f15cb5e 100644 --- a/playbooks/clamav.yml +++ b/playbooks/clamav.yml @@ -39,6 +39,21 @@ - 'ansible_facts["os_family"] == "RedHat"' - 'not clamav__skip_selinux | default(false)' + # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + - role: 'linuxfabrik.lfops.repo_baseos' + repo_baseos__crb_repo_enabled__dependent_var: '{{ + repo_epel__repo_baseos__crb_repo_enabled__dependent_var + }}' + when: + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' + - 'not clamav__skip_repo_baseos | d(false)' + + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'ansible_facts["os_family"] == "RedHat"' + - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'not clamav__skip_repo_epel | d(false)' + - role: 'linuxfabrik.lfops.python_venv' python_venv__venvs__dependent_var: '{{ fangfrisch__python_venv__venvs__dependent_var diff --git a/playbooks/duplicity.yml b/playbooks/duplicity.yml index a5ac7772..59e2d41b 100644 --- a/playbooks/duplicity.yml +++ b/playbooks/duplicity.yml @@ -26,6 +26,21 @@ - 'ansible_facts["os_family"] == "RedHat"' - 'ansible_facts["distribution_major_version"] in ["7", "8", "9"]' + # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + - role: 'linuxfabrik.lfops.repo_baseos' + repo_baseos__crb_repo_enabled__dependent_var: '{{ + repo_epel__repo_baseos__crb_repo_enabled__dependent_var + }}' + when: + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' + - 'not duplicity__skip_repo_baseos | d(false)' + + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'ansible_facts["os_family"] == "RedHat"' + - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'not duplicity__skip_repo_epel | d(false)' + - role: 'linuxfabrik.lfops.python_venv' python_venv__venvs__dependent_var: '{{ duplicity__python_venv__venvs__dependent_var diff --git a/playbooks/fangfrisch.yml b/playbooks/fangfrisch.yml index c8d28bdb..2afab6f5 100644 --- a/playbooks/fangfrisch.yml +++ b/playbooks/fangfrisch.yml @@ -12,6 +12,21 @@ roles: + # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + - role: 'linuxfabrik.lfops.repo_baseos' + repo_baseos__crb_repo_enabled__dependent_var: '{{ + repo_epel__repo_baseos__crb_repo_enabled__dependent_var + }}' + when: + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' + - 'not fangfrisch__skip_repo_baseos | d(false)' + + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'ansible_facts["os_family"] == "RedHat"' + - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'not fangfrisch__skip_repo_epel | d(false)' + - role: 'linuxfabrik.lfops.python_venv' python_venv__venvs__dependent_var: '{{ fangfrisch__python_venv__venvs__dependent_var diff --git a/playbooks/influxdb.yml b/playbooks/influxdb.yml index 10c0b1c4..769b7490 100644 --- a/playbooks/influxdb.yml +++ b/playbooks/influxdb.yml @@ -14,13 +14,27 @@ - role: 'linuxfabrik.lfops.repo_influxdb' + # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + - role: 'linuxfabrik.lfops.repo_baseos' + repo_baseos__crb_repo_enabled__dependent_var: '{{ + repo_epel__repo_baseos__crb_repo_enabled__dependent_var + }}' + when: + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' + - 'not influxdb__skip_repo_baseos | d(false)' + + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'ansible_facts["os_family"] == "RedHat"' + - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'not influxdb__skip_repo_epel | d(false)' + - role: 'linuxfabrik.lfops.python_venv' python_venv__venvs__dependent_var: '{{ influxdb__python_venv__venvs__dependent_var }}' when: - 'not influxdb__skip_python_venv | d(false)' - - role: 'linuxfabrik.lfops.influxdb' diff --git a/playbooks/mongodb.yml b/playbooks/mongodb.yml index 403636fc..429c3252 100644 --- a/playbooks/mongodb.yml +++ b/playbooks/mongodb.yml @@ -18,6 +18,21 @@ when: - 'not mongodb__skip_kernel_settings | d(false)' + # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + - role: 'linuxfabrik.lfops.repo_baseos' + repo_baseos__crb_repo_enabled__dependent_var: '{{ + repo_epel__repo_baseos__crb_repo_enabled__dependent_var + }}' + when: + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' + - 'not mongodb__skip_repo_baseos | d(false)' + + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'ansible_facts["os_family"] == "RedHat"' + - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'not mongodb__skip_repo_epel | d(false)' + - role: 'linuxfabrik.lfops.python_venv' python_venv__venvs__dependent_var: '{{ mongodb__python_venv__venvs__dependent_var diff --git a/playbooks/python_venv.yml b/playbooks/python_venv.yml index 5ae7a64f..eb3ef98b 100644 --- a/playbooks/python_venv.yml +++ b/playbooks/python_venv.yml @@ -9,16 +9,29 @@ tags: - 'always' - roles: + # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + - role: 'linuxfabrik.lfops.repo_baseos' + repo_baseos__crb_repo_enabled__dependent_var: '{{ + repo_epel__repo_baseos__crb_repo_enabled__dependent_var + }}' + when: + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' + - 'not python_venv__skip_repo_baseos | d(false)' + + - role: 'linuxfabrik.lfops.repo_epel' + when: + - 'ansible_facts["os_family"] == "RedHat"' + - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'not python_venv__skip_repo_epel | d(false)' + - role: 'linuxfabrik.lfops.python' when: - 'not python_venv__skip_python | d(false)' - role: 'linuxfabrik.lfops.python_venv' - post_tasks: - ansible.builtin.import_role: name: 'shared' diff --git a/playbooks/setup_graylog_datanode.yml b/playbooks/setup_graylog_datanode.yml index 3ff91244..cc2680f6 100644 --- a/playbooks/setup_graylog_datanode.yml +++ b/playbooks/setup_graylog_datanode.yml @@ -17,18 +17,18 @@ - 'ansible_facts["os_family"] == "RedHat"' - 'not setup_graylog_datanode__skip_policycoreutils | default(false)' + # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var }}' - # Since the CRB repository is included in Rocky9 default repo file now, this will be done on Rocky9 Systems only. Formerly it came from epel repository. when: - - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] == "9"' + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' - 'not setup_graylog_datanode__skip_repo_baseos | d(false)' - role: 'linuxfabrik.lfops.repo_epel' when: - - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9"]' + - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9", "10"]' - role: 'linuxfabrik.lfops.python_venv' python_venv__venvs__dependent_var: '{{ diff --git a/playbooks/setup_graylog_server.yml b/playbooks/setup_graylog_server.yml index 608ba941..59e7ceb4 100644 --- a/playbooks/setup_graylog_server.yml +++ b/playbooks/setup_graylog_server.yml @@ -29,18 +29,18 @@ - 'ansible_facts["os_family"] == "RedHat"' - 'not setup_graylog_server__skip_selinux | default(false)' + # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var }}' - # Since the CRB repository is included in Rocky9 default repo file now, this will be done on Rocky9 Systems only. Formerly it came from epel repository. when: - - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] == "9"' + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' - 'not setup_graylog_server__skip_repo_baseos | d(false)' - role: 'linuxfabrik.lfops.repo_epel' when: - - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9"]' + - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9", "10"]' - role: 'linuxfabrik.lfops.python_venv' python_venv__venvs__dependent_var: '{{ diff --git a/playbooks/setup_icinga2_master.yml b/playbooks/setup_icinga2_master.yml index 68a66a9a..66a23ab1 100644 --- a/playbooks/setup_icinga2_master.yml +++ b/playbooks/setup_icinga2_master.yml @@ -82,18 +82,18 @@ roles: + # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var }}' - # Since the CRB repository is included in Rocky9 default repo file now, this will be done on Rocky9 Systems only. Formerly it came from epel repository. when: - - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] == "9"' + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' - 'not setup_icinga2_master__skip_repo_baseos | d(false)' - role: 'linuxfabrik.lfops.repo_epel' when: - - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9"]' + - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9", "10"]' - 'not setup_icinga2_master__repo_epel__skip_role__internal_var' - role: 'linuxfabrik.lfops.repo_mariadb' diff --git a/playbooks/setup_rocketchat.yml b/playbooks/setup_rocketchat.yml index 48c8a9b1..c391a4c4 100644 --- a/playbooks/setup_rocketchat.yml +++ b/playbooks/setup_rocketchat.yml @@ -17,19 +17,19 @@ when: - 'not setup_rocketchat__skip_kernel_settings | d(false)' + # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var }}' - # Since the CRB repository is included in Rocky9 default repo file now, this will be done on Rocky9 Systems only. Formerly it came from epel repository. when: - - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] == "9"' + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' - 'not setup_rocketchat__skip_repo_baseos | d(false)' - role: 'linuxfabrik.lfops.repo_epel' when: - - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9"]' - - 'not setup_rocketchat__skip_repo_epel | default(false)' + - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9", "10"]' + - 'not setup_rocketchat__skip_repo_epel | d(false)' - role: 'linuxfabrik.lfops.python_venv' python_venv__venvs__dependent_var: '{{ diff --git a/roles/duplicity/README.md b/roles/duplicity/README.md index 8bb65c2f..937040e1 100644 --- a/roles/duplicity/README.md +++ b/roles/duplicity/README.md @@ -20,6 +20,8 @@ Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/RE * On RHEL-compatible systems, the EPEL repository must be enabled (role: [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel)). * `duplicity`, `python-swiftclient` and `python-keystoneclient` must be installed into a Python 3 virtual environment in `/opt/python-venv/duplicity` (role: [linuxfabrik.lfops.python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv)). +* On RHEL-compatible systems, enable the EPEL repository. On Rocky 9+, also enable the CRB Repo ("Code Ready Builder") to be able to install `python3-virtualenv`. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) and [linuxfabrik.lfops.repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos) roles. +* Install `duplicity`, `python-swiftclient` and `python-keystoneclient` into a Python 3 virtual environment in `/opt/python-venv/duplicity`. This can be done using the [linuxfabrik.lfops.python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv) role. **Attention** diff --git a/roles/fangfrisch/README.md b/roles/fangfrisch/README.md index f6cf2d2e..f3b7b6b5 100644 --- a/roles/fangfrisch/README.md +++ b/roles/fangfrisch/README.md @@ -5,6 +5,8 @@ This role installs and configures [Fangfrisch](https://rseichter.github.io/fangf *Available since LFOps `3.0.0`.* +* A Python virtual environment `/opt/python-venv/clamav-fangfrisch/` with `fangfrisch` installed. This can be done using the [linuxfabrik.lfops.python_venv](https://github.com/linuxfabrik/lfops/tree/main/roles/python_venv) role. +* On Rocky 9+, the EPEL and the CRB Repo ("Code Ready Builder") need to be enabled to be able to install `python3-virtualenv`. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/linuxfabrik/lfops/tree/main/roles/repo_epel) and [linuxfabrik.lfops.repo_baseos](https://github.com/linuxfabrik/lfops/tree/main/roles/repo_baseos) roles. ## Dependent Roles diff --git a/roles/influxdb/README.md b/roles/influxdb/README.md index 767283d2..6754a259 100644 --- a/roles/influxdb/README.md +++ b/roles/influxdb/README.md @@ -5,13 +5,15 @@ This role installs and configures [InfluxDB](https://www.influxdata.com/products *Available since LFOps `2.0.0`.* - ## Dependent Roles Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. * `influxdb` and `requests` must be installed into a Python 3 virtual environment in `/opt/python-venv/influxdb` (role: [linuxfabrik.lfops.python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv)). * The official [InfluxDB repository](https://docs.influxdata.com/influxdb/v1.8/introduction/install/?t=Red+Hat+%26amp%3B+CentOS) must be enabled (role: [linuxfabrik.lfops.repo_influxdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_influxdb)). +* Install `influxdb` and `requests` into a Python 3 virtual environment in `/opt/python-venv/influxdb`. This can be done using the [linuxfabrik.lfops.python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv) role. +* On Rocky 9+, the EPEL and the CRB Repo ("Code Ready Builder") need to be enabled to be able to install `python3-virtualenv`. This can be done using the [linuxfabrik.lfops.repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) and [linuxfabrik.lfops.repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos) roles. +* Enable the official [InfluxDB repository](https://docs.influxdata.com/influxdb/v1.8/introduction/install/?t=Red+Hat+%26amp%3B+CentOS). This can be done using the [linuxfabrik.lfops.repo_influxdb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_influxdb) role. ## Tags diff --git a/roles/mongodb/README.md b/roles/mongodb/README.md index 0e2df6d3..67428bb9 100644 --- a/roles/mongodb/README.md +++ b/roles/mongodb/README.md @@ -14,7 +14,6 @@ This role is only compatible with the following MongoDB versions: *Available since LFOps `2.0.0`.* - ## Dependent Roles Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. From deabc5baacea7b9af88960f63fa015127b7754a0 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Thu, 21 May 2026 17:16:58 +0200 Subject: [PATCH 39/66] fix(playbooks): address review on CRB/EPEL enablement - Deduplicate the repo_baseos/repo_epel blocks in clamav and duplicity by folding them into the single block at the top of roles:. - Keep EPEL enabled on RHEL 7/8 by standardizing every repo_epel block on the ["7", "8", "9", "10"] version list. - Restore the "CRB formerly came from EPEL" rationale in the block comments and fix their indentation. - Document the new repo_baseos/repo_epel skip variables in playbooks/README.md for duplicity, fangfrisch, mongodb and python_venv. - Fix fangfrisch using the clamav__skip_python_venv variable. - Normalize | default(false) to | d(false) and restore stripped blank lines in influxdb and python_venv. - Drop the stale glances CHANGELOG entry left over from the revert. --- CHANGELOG.md | 1 - playbooks/README.md | 10 +++++++-- playbooks/clamav.yml | 32 ++++++++-------------------- playbooks/duplicity.yml | 20 +++-------------- playbooks/fangfrisch.yml | 8 +++---- playbooks/influxdb.yml | 7 +++--- playbooks/mongodb.yml | 8 +++---- playbooks/python_venv.yml | 8 ++++--- playbooks/setup_graylog_datanode.yml | 9 ++++---- playbooks/setup_graylog_server.yml | 9 ++++---- playbooks/setup_icinga2_master.yml | 3 ++- playbooks/setup_rocketchat.yml | 7 +++--- 12 files changed, 53 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a743df32..5a8776c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,7 +85,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **role:unattended_upgrades**: Correct README description; the role deactivates Unattended Upgrades by setting both `APT::Periodic` flags to `0` in `/etc/apt/apt.conf.d/20auto-upgrades` (Debian/Ubuntu), it does not remove the `unattended-upgrades` package. * **playbooks/clamav, playbooks/duplicity, playbooks/fangfrisch, playbooks/influxdb, playbooks/mongodb, playbooks/python_venv**: Enable `repo_baseos` (CRB) and `repo_epel` on Rocky 9+ before the `python_venv` role to fix `No match for argument: python3-virtualenv` / `nothing provides python3-wheel-wheel needed by python3-virtualenv`. * **playbooks/setup_graylog_datanode, playbooks/setup_graylog_server, playbooks/setup_icinga2_master, playbooks/setup_rocketchat**: Extend existing `repo_baseos` `when` condition from `== "9"` to `| int >= 9` (Rocky 10+), and extend `repo_epel` version list to include RHEL/Rocky 10. -* **role:glances**: Skip installation on RHEL 10+, where glances is not available in EPEL. * **playbooks/freeipa_client, playbooks/freeipa_server**: Set `strategy: 'linear'` explicitly so the playbooks work even when the user's `ansible.cfg` defaults to a strategy that reuses the target Python interpreter (e.g. `mitogen_linear`). The ansible-freeipa modules rely on `ipalib`'s global API singleton and otherwise fail with `API.bootstrap() already called` on the second module call. * **role:mariadb_server**: Stop writing the deprecated `innodb_buffer_pool_chunk_size` setting to the generated config for MariaDB 10.11, 11.4 and 11.8. MariaDB ignores the value from 10.11.12, 11.4.6 and 11.8.2 onwards and derives the chunk size automatically from `innodb_buffer_pool_size`. The user-facing role variables stay declared for backward compatibility but are now documented as deprecated. On MariaDB 10.6 the setting is unchanged. The role now also aborts at the start of the run with a clear error message if `innodb_buffer_pool_chunk_size` (on MariaDB 10.11+) or `innodb_file_per_table` (on MariaDB 11.0+) is still set in inventory, so that an upgrade from MariaDB 10.6 to 11.x does not silently keep a stale override around. * **role:mariadb_server**: Fix MariaDB starting in the `unconfined_service_t` SELinux domain on RHEL 10, which leaves `/var/lib/mysql/mysql.sock` mislabeled and breaks `php-fpm`/`httpd_t` clients (e.g. Icinga Web 2 login: `SQLSTATE[HY000] [2002] Permission denied`). The unit drop-in's `ExecStartPre=-/bin/chcon -t mysqld_exec_t /usr/sbin/mariadbd` workaround for [MDEV-30520](https://jira.mariadb.org/browse/MDEV-30520) cannot relabel the binary on EL10+, where the packaged `mariadb.service` applies `ProtectSystem` that mounts `/usr` read-only inside the service sandbox. The role now sets the `mysqld_exec_t` file context for `/usr/sbin/mariadbd` persistently via `semanage fcontext` + `restorecon` (outside the systemd sandbox) and notifies a restart so the daemon comes up in `mysqld_t`. diff --git a/playbooks/README.md b/playbooks/README.md index 8df9dc6c..1babb9b2 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -211,7 +211,7 @@ Calls the following roles (in order): Calls the following roles (in order): * [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `duplicity__skip_repo_baseos` -* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel) +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `duplicity__skip_repo_epel` * [python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv): `duplicity__skip_python_venv` * [haveged](https://github.com/Linuxfabrik/lfops/tree/main/roles/haveged): `duplicity__skip_haveged` * [duplicity](https://github.com/Linuxfabrik/lfops/tree/main/roles/duplicity) @@ -266,7 +266,9 @@ Calls the following roles (in order): Calls the following roles (in order): -* [python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv): `clamav__skip_python_venv` +* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `fangfrisch__skip_repo_baseos` +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `fangfrisch__skip_repo_epel` +* [python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv): `fangfrisch__skip_python_venv` * [fangfrisch](https://github.com/Linuxfabrik/lfops/tree/main/roles/fangfrisch) @@ -619,6 +621,8 @@ Calls the following roles (in order): Calls the following roles (in order): * [kernel_settings](https://github.com/Linuxfabrik/lfops/tree/main/roles/kernel_settings): `mongodb__skip_kernel_settings` +* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `mongodb__skip_repo_baseos` +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `mongodb__skip_repo_epel` * [python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv): `mongodb__skip_python_venv` * [repo_mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_mongodb): `mongodb__skip_repo_mongodb` * [mongodb](https://github.com/Linuxfabrik/lfops/tree/main/roles/mongodb) @@ -768,6 +772,8 @@ Calls the following roles (in order): Calls the following roles (in order): +* [repo_baseos](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_baseos): `python_venv__skip_repo_baseos` +* [repo_epel](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_epel): `python_venv__skip_repo_epel` * [python](https://github.com/Linuxfabrik/lfops/tree/main/roles/python): `python_venv__skip_python` * [python_venv](https://github.com/Linuxfabrik/lfops/tree/main/roles/python_venv) diff --git a/playbooks/clamav.yml b/playbooks/clamav.yml index 6f15cb5e..79fab68f 100644 --- a/playbooks/clamav.yml +++ b/playbooks/clamav.yml @@ -12,24 +12,25 @@ roles: + # On Rocky 9+, CRB (now in the Rocky default repo file, formerly in EPEL) must be + # enabled so python3-virtualenv can be installed. - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var }}' - # Since the CRB repository is included in Rocky9 default repo file now, this will be done on Rocky9 Systems only. Formerly it came from epel repository. when: - - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] == "9"' + - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' - 'not clamav__skip_repo_baseos | d(false)' - role: 'linuxfabrik.lfops.repo_epel' when: - - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9"]' - - 'not clamav__skip_repo_epel | default(false)' + - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9", "10"]' + - 'not clamav__skip_repo_epel | d(false)' - role: 'linuxfabrik.lfops.policycoreutils' when: - 'ansible_facts["os_family"] == "RedHat"' - - 'not clamav__skip_policycoreutils | default(false)' + - 'not clamav__skip_policycoreutils | d(false)' - role: 'linuxfabrik.lfops.selinux' selinux__booleans__dependent_var: '{{ @@ -37,35 +38,20 @@ }}' when: - 'ansible_facts["os_family"] == "RedHat"' - - 'not clamav__skip_selinux | default(false)' - - # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv - - role: 'linuxfabrik.lfops.repo_baseos' - repo_baseos__crb_repo_enabled__dependent_var: '{{ - repo_epel__repo_baseos__crb_repo_enabled__dependent_var - }}' - when: - - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] | int >= 9' - - 'not clamav__skip_repo_baseos | d(false)' - - - role: 'linuxfabrik.lfops.repo_epel' - when: - - 'ansible_facts["os_family"] == "RedHat"' - - 'ansible_facts["distribution_major_version"] | int >= 9' - - 'not clamav__skip_repo_epel | d(false)' + - 'not clamav__skip_selinux | d(false)' - role: 'linuxfabrik.lfops.python_venv' python_venv__venvs__dependent_var: '{{ fangfrisch__python_venv__venvs__dependent_var }}' when: - - 'not clamav__skip_python_venv | default(false)' + - 'not clamav__skip_python_venv | d(false)' - role: 'linuxfabrik.lfops.clamav' - role: 'linuxfabrik.lfops.fangfrisch' when: - - 'not clamav__skip_fangfrisch | default(false)' + - 'not clamav__skip_fangfrisch | d(false)' post_tasks: diff --git a/playbooks/duplicity.yml b/playbooks/duplicity.yml index 59e2d41b..c321bcf6 100644 --- a/playbooks/duplicity.yml +++ b/playbooks/duplicity.yml @@ -12,21 +12,8 @@ roles: - - role: 'linuxfabrik.lfops.repo_baseos' - repo_baseos__crb_repo_enabled__dependent_var: '{{ - repo_epel__repo_baseos__crb_repo_enabled__dependent_var - }}' - # Since the CRB repository is included in Rocky9 default repo file now, this will be done on Rocky9 Systems only. Formerly it came from epel repository. - when: - - 'ansible_facts["distribution"] == "Rocky" and ansible_facts["distribution_major_version"] == "9"' - - 'not duplicity__skip_repo_baseos | d(false)' - - - role: 'linuxfabrik.lfops.repo_epel' - when: - - 'ansible_facts["os_family"] == "RedHat"' - - 'ansible_facts["distribution_major_version"] in ["7", "8", "9"]' - - # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + # On Rocky 9+, CRB (now in the Rocky default repo file, formerly in EPEL) must be + # enabled so python3-virtualenv can be installed. - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var @@ -37,8 +24,7 @@ - role: 'linuxfabrik.lfops.repo_epel' when: - - 'ansible_facts["os_family"] == "RedHat"' - - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9", "10"]' - 'not duplicity__skip_repo_epel | d(false)' - role: 'linuxfabrik.lfops.python_venv' diff --git a/playbooks/fangfrisch.yml b/playbooks/fangfrisch.yml index 2afab6f5..36cc05b9 100644 --- a/playbooks/fangfrisch.yml +++ b/playbooks/fangfrisch.yml @@ -12,7 +12,8 @@ roles: - # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + # On Rocky 9+, CRB (now in the Rocky default repo file, formerly in EPEL) must be + # enabled so python3-virtualenv can be installed. - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var @@ -23,8 +24,7 @@ - role: 'linuxfabrik.lfops.repo_epel' when: - - 'ansible_facts["os_family"] == "RedHat"' - - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9", "10"]' - 'not fangfrisch__skip_repo_epel | d(false)' - role: 'linuxfabrik.lfops.python_venv' @@ -32,7 +32,7 @@ fangfrisch__python_venv__venvs__dependent_var }}' when: - - 'not clamav__skip_python_venv | default(false)' + - 'not fangfrisch__skip_python_venv | d(false)' - role: 'linuxfabrik.lfops.fangfrisch' diff --git a/playbooks/influxdb.yml b/playbooks/influxdb.yml index 769b7490..fa5402f7 100644 --- a/playbooks/influxdb.yml +++ b/playbooks/influxdb.yml @@ -14,7 +14,8 @@ - role: 'linuxfabrik.lfops.repo_influxdb' - # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + # On Rocky 9+, CRB (now in the Rocky default repo file, formerly in EPEL) must be + # enabled so python3-virtualenv can be installed. - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var @@ -25,8 +26,7 @@ - role: 'linuxfabrik.lfops.repo_epel' when: - - 'ansible_facts["os_family"] == "RedHat"' - - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9", "10"]' - 'not influxdb__skip_repo_epel | d(false)' - role: 'linuxfabrik.lfops.python_venv' @@ -35,6 +35,7 @@ }}' when: - 'not influxdb__skip_python_venv | d(false)' + - role: 'linuxfabrik.lfops.influxdb' diff --git a/playbooks/mongodb.yml b/playbooks/mongodb.yml index 429c3252..2b37d85d 100644 --- a/playbooks/mongodb.yml +++ b/playbooks/mongodb.yml @@ -18,7 +18,8 @@ when: - 'not mongodb__skip_kernel_settings | d(false)' - # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + # On Rocky 9+, CRB (now in the Rocky default repo file, formerly in EPEL) must be + # enabled so python3-virtualenv can be installed. - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var @@ -29,8 +30,7 @@ - role: 'linuxfabrik.lfops.repo_epel' when: - - 'ansible_facts["os_family"] == "RedHat"' - - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9", "10"]' - 'not mongodb__skip_repo_epel | d(false)' - role: 'linuxfabrik.lfops.python_venv' @@ -42,7 +42,7 @@ - role: 'linuxfabrik.lfops.repo_mongodb' when: - - 'not mongodb__skip_repo_mongodb | default(false)' + - 'not mongodb__skip_repo_mongodb | d(false)' - role: 'linuxfabrik.lfops.mongodb' diff --git a/playbooks/python_venv.yml b/playbooks/python_venv.yml index eb3ef98b..9c54090f 100644 --- a/playbooks/python_venv.yml +++ b/playbooks/python_venv.yml @@ -9,9 +9,11 @@ tags: - 'always' + roles: - # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + # On Rocky 9+, CRB (now in the Rocky default repo file, formerly in EPEL) must be + # enabled so python3-virtualenv can be installed. - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var @@ -22,8 +24,7 @@ - role: 'linuxfabrik.lfops.repo_epel' when: - - 'ansible_facts["os_family"] == "RedHat"' - - 'ansible_facts["distribution_major_version"] | int >= 9' + - 'ansible_facts["os_family"] == "RedHat" and ansible_facts["distribution_major_version"] in ["7", "8", "9", "10"]' - 'not python_venv__skip_repo_epel | d(false)' - role: 'linuxfabrik.lfops.python' @@ -32,6 +33,7 @@ - role: 'linuxfabrik.lfops.python_venv' + post_tasks: - ansible.builtin.import_role: name: 'shared' diff --git a/playbooks/setup_graylog_datanode.yml b/playbooks/setup_graylog_datanode.yml index cc2680f6..2998d00c 100644 --- a/playbooks/setup_graylog_datanode.yml +++ b/playbooks/setup_graylog_datanode.yml @@ -15,9 +15,10 @@ - role: 'linuxfabrik.lfops.policycoreutils' when: - 'ansible_facts["os_family"] == "RedHat"' - - 'not setup_graylog_datanode__skip_policycoreutils | default(false)' + - 'not setup_graylog_datanode__skip_policycoreutils | d(false)' - # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + # On Rocky 9+, CRB (now in the Rocky default repo file, formerly in EPEL) must be + # enabled so python3-virtualenv can be installed. - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var @@ -49,7 +50,7 @@ - role: 'linuxfabrik.lfops.mongodb' when: - - 'not setup_graylog_datanode__skip_mongodb | default(false)' + - 'not setup_graylog_datanode__skip_mongodb | d(false)' - role: 'linuxfabrik.lfops.repo_graylog' when: @@ -61,7 +62,7 @@ }}' when: - 'ansible_facts["os_family"] == "RedHat"' - - 'not setup_graylog_datanode__skip_selinux | default(false)' + - 'not setup_graylog_datanode__skip_selinux | d(false)' - role: 'linuxfabrik.lfops.graylog_datanode' diff --git a/playbooks/setup_graylog_server.yml b/playbooks/setup_graylog_server.yml index 59e7ceb4..140e5b29 100644 --- a/playbooks/setup_graylog_server.yml +++ b/playbooks/setup_graylog_server.yml @@ -15,7 +15,7 @@ - role: 'linuxfabrik.lfops.policycoreutils' when: - 'ansible_facts["os_family"] == "RedHat"' - - 'not setup_graylog_server__skip_policycoreutils | default(false)' + - 'not setup_graylog_server__skip_policycoreutils | d(false)' - role: 'linuxfabrik.lfops.selinux' selinux__booleans__dependent_var: '{{ @@ -27,9 +27,10 @@ }}' when: - 'ansible_facts["os_family"] == "RedHat"' - - 'not setup_graylog_server__skip_selinux | default(false)' + - 'not setup_graylog_server__skip_selinux | d(false)' - # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + # On Rocky 9+, CRB (now in the Rocky default repo file, formerly in EPEL) must be + # enabled so python3-virtualenv can be installed. - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var @@ -64,7 +65,7 @@ - role: 'linuxfabrik.lfops.mongodb' when: - - 'not setup_graylog_server__skip_mongodb | default(false)' + - 'not setup_graylog_server__skip_mongodb | d(false)' - role: 'linuxfabrik.lfops.repo_graylog' when: diff --git a/playbooks/setup_icinga2_master.yml b/playbooks/setup_icinga2_master.yml index 66a23ab1..c8073077 100644 --- a/playbooks/setup_icinga2_master.yml +++ b/playbooks/setup_icinga2_master.yml @@ -82,7 +82,8 @@ roles: - # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + # On Rocky 9+, CRB (now in the Rocky default repo file, formerly in EPEL) must be + # enabled so python3-virtualenv can be installed. - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var diff --git a/playbooks/setup_rocketchat.yml b/playbooks/setup_rocketchat.yml index c391a4c4..6cf87425 100644 --- a/playbooks/setup_rocketchat.yml +++ b/playbooks/setup_rocketchat.yml @@ -17,7 +17,8 @@ when: - 'not setup_rocketchat__skip_kernel_settings | d(false)' - # On Rocky 9+ the EPEL and CRB Repo need to be enabled to install python3-virtualenv + # On Rocky 9+, CRB (now in the Rocky default repo file, formerly in EPEL) must be + # enabled so python3-virtualenv can be installed. - role: 'linuxfabrik.lfops.repo_baseos' repo_baseos__crb_repo_enabled__dependent_var: '{{ repo_epel__repo_baseos__crb_repo_enabled__dependent_var @@ -40,7 +41,7 @@ - role: 'linuxfabrik.lfops.repo_mongodb' when: - - 'not setup_rocketchat__skip_repo_mongodb | default(false)' + - 'not setup_rocketchat__skip_repo_mongodb | d(false)' - role: 'linuxfabrik.lfops.mongodb' mongodb__conf_replication_repl_set_name__dependent_var: '{{ rocketchat__mongodb__conf_replication_repl_set_name__dependent_var }}' @@ -48,7 +49,7 @@ rocketchat__mongodb__users__dependent_var }}' when: - - 'not setup_rocketchat__skip_mongodb | default(false)' + - 'not setup_rocketchat__skip_mongodb | d(false)' - role: 'linuxfabrik.lfops.login' login__users__dependent_var: '{{ From 77ca813394559bb0da70b46910b986b3308197ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 05:22:57 +0000 Subject: [PATCH 40/66] chore(deps): bump actions/dependency-review-action from 4.9.0 to 5.0.0 Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.9.0 to 5.0.0. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/2031cfc080254a8a887f58cffee85186f0e49e48...a1d282b36b6f3519aa1f3fc636f609c47dddb294) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 40c180da..8f80edf1 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -19,4 +19,4 @@ jobs: uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2 - name: 'Dependency Review' - uses: 'actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48' # v4.9.0 + uses: 'actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294' # v5.0.0 From e6df15bca876887330615f6380681273b6fccaea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 05:22:53 +0000 Subject: [PATCH 41/66] chore(deps): bump step-security/harden-runner from 2.19.3 to 2.19.4 (#255) Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.19.3 to 2.19.4. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/ab7a9404c0f3da075243ca237b5fac12c98deaa5...9af89fc71515a100421586dfdb3dc9c984fbf411) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-version: 2.19.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/docs.yml | 4 ++-- .github/workflows/lf-build.yml | 2 +- .github/workflows/lf-release.yml | 2 +- .github/workflows/pre-commit-autoupdate.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 72d19d1c..7c949ad8 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -31,7 +31,7 @@ jobs: steps: - name: 'Harden Runner' - uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 + uses: 'step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411' # v2.19.4 with: egress-policy: 'audit' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 8f80edf1..d0bd9fe0 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -11,7 +11,7 @@ jobs: runs-on: 'ubuntu-latest' steps: - name: 'Harden Runner' - uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 + uses: 'step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411' # v2.19.4 with: egress-policy: 'audit' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b9d69c9f..3e04e6ec 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: runs-on: 'ubuntu-latest' steps: - name: 'Harden the runner (Audit all outbound calls)' - uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 + uses: 'step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411' # v2.19.4 with: egress-policy: 'audit' @@ -53,7 +53,7 @@ jobs: url: '${{ steps.deployment.outputs.page_url }}' steps: - name: 'Harden the runner (Audit all outbound calls)' - uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 + uses: 'step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411' # v2.19.4 with: egress-policy: 'audit' diff --git a/.github/workflows/lf-build.yml b/.github/workflows/lf-build.yml index 88d9e549..c894bc2a 100644 --- a/.github/workflows/lf-build.yml +++ b/.github/workflows/lf-build.yml @@ -28,7 +28,7 @@ jobs: steps: - name: 'Harden the runner (Audit all outbound calls)' - uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 + uses: 'step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411' # v2.19.4 with: egress-policy: 'audit' diff --git a/.github/workflows/lf-release.yml b/.github/workflows/lf-release.yml index 92872f67..f48f903d 100644 --- a/.github/workflows/lf-release.yml +++ b/.github/workflows/lf-release.yml @@ -17,7 +17,7 @@ jobs: steps: - name: 'Harden the runner (Audit all outbound calls)' - uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 + uses: 'step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411' # v2.19.4 with: egress-policy: 'audit' diff --git a/.github/workflows/pre-commit-autoupdate.yml b/.github/workflows/pre-commit-autoupdate.yml index 92fa589e..07c5971f 100644 --- a/.github/workflows/pre-commit-autoupdate.yml +++ b/.github/workflows/pre-commit-autoupdate.yml @@ -15,7 +15,7 @@ jobs: pull-requests: 'write' steps: - name: 'Harden the runner (Audit all outbound calls)' - uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 + uses: 'step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411' # v2.19.4 with: egress-policy: 'audit' diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8e1a9a09..2e791381 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -18,7 +18,7 @@ jobs: steps: - name: 'Harden Runner' - uses: 'step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5' # v2.19.3 + uses: 'step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411' # v2.19.4 with: egress-policy: 'audit' From 917297d5745a38a4c0274a59a218c3e22f1f0987 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 10:29:27 +0200 Subject: [PATCH 42/66] chore(deps): bump github/codeql-action from 4.35.4 to 4.35.5 (#256) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.35.4 to 4.35.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7c949ad8..120a142f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -39,14 +39,14 @@ jobs: uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2 - name: 'Initialize CodeQL' - uses: 'github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e' # v4.35.4 + uses: 'github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba' # v4.35.5 with: languages: '${{ matrix.language }}' - name: 'Autobuild' - uses: 'github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e' # v4.35.4 + uses: 'github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba' # v4.35.5 - name: 'Perform CodeQL Analysis' - uses: 'github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e' # v4.35.4 + uses: 'github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba' # v4.35.5 with: category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2e791381..19c32213 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -42,6 +42,6 @@ jobs: retention-days: 5 - name: 'Upload to code-scanning' - uses: 'github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e' # v4.35.4 + uses: 'github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba' # v4.35.5 with: sarif_file: 'results.sarif' From 77a7e480f9b51db3059375c5c3507d48f0e11a07 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 22 May 2026 11:42:07 +0200 Subject: [PATCH 43/66] docs(roles/alternatives): fix whitespace --- roles/alternatives/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/alternatives/README.md b/roles/alternatives/README.md index 6be42d3b..2a7e132b 100644 --- a/roles/alternatives/README.md +++ b/roles/alternatives/README.md @@ -1,6 +1,6 @@ # Ansible Role linuxfabrik.lfops.alternatives -This role manages symbolic links using the `update-alternatives` tool. Useful when multiple programs are installed but provide similar functionality (for example, different editors or Python interpreters). +This role manages symbolic links using the `update-alternatives` tool. Useful when multiple programs are installed but provide similar functionality (for example, different editors or Python interpreters). Hints: From b0f7eb23fb4126f7b7b662c545aa95f68c433df5 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 22 May 2026 11:43:32 +0200 Subject: [PATCH 44/66] fix(roles/influxdb): always install `curl` which is required to start influxdb but missing as a package dependency --- CHANGELOG.md | 1 + roles/influxdb/tasks/main.yml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a8776c1..7e74968a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **role:inflxudb**: Always install `curl`, which is required to start influxdb but missing as a package dependency. * **role:redis**: Added missing paths for running against Debian. * **role:icingaweb2_module_pdfexport**: PDF export now works out of the box. The headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role (wired into the `icingaweb2_module_pdfexport` and `setup_icinga2_master` playbooks); previously it had to be set up by hand, so fresh deployments ended up without working PDF export. * **role:graylog_datanode**: Fix the `Conditional result ... was of type 'str'` deprecation warning. diff --git a/roles/influxdb/tasks/main.yml b/roles/influxdb/tasks/main.yml index 19899af3..bbf91fff 100644 --- a/roles/influxdb/tasks/main.yml +++ b/roles/influxdb/tasks/main.yml @@ -1,8 +1,9 @@ - block: - - name: 'Install influxdb' + - name: 'Install curl influxdb' ansible.builtin.package: name: + - 'curl' # required by the `/usr/lib/influxdb/scripts/influxd-systemd-start.sh` script - 'influxdb' state: 'present' From 197522ecb10a7981ae623a77bcad24958fc45f8e Mon Sep 17 00:00:00 2001 From: Ali Bhatti Date: Fri, 22 May 2026 12:27:57 +0200 Subject: [PATCH 45/66] feat(roles/repo_baseos): add Rocky security repo, enabled by default --- CHANGELOG.md | 1 + roles/repo_baseos/README.md | 7 ++ roles/repo_baseos/defaults/main.yml | 11 ++++ .../etc/yum.repos.d/rocky-security.repo.j2 | 64 +++++++++++++++++++ .../etc/yum.repos.d/Rocky-Security.repo.j2 | 28 ++++++++ .../etc/yum.repos.d/rocky-security.repo.j2 | 64 +++++++++++++++++++ roles/repo_baseos/vars/Rocky10.yml | 1 + roles/repo_baseos/vars/Rocky8.yml | 1 + roles/repo_baseos/vars/Rocky9.yml | 1 + 9 files changed, 178 insertions(+) create mode 100644 roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-security.repo.j2 create mode 100644 roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Security.repo.j2 create mode 100644 roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-security.repo.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e74968a..7a8f3395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:repo_baseos**: Add the Rocky Linux `security` repository (critical CVE fixes), enabled by default. Opt out per host or group via `repo_baseos__security_repo_enabled__host_var` / `repo_baseos__security_repo_enabled__group_var`. * **role:chromium_headless**: New role. Provides a hardened, socket-activated headless Chromium backend (started on the first request, stopped again after an idle timeout, so it uses no RAM while unused) for tools such as the Icinga Web 2 PDF Export Module. Installs `chromium-headless` from EPEL instead of Google's proprietary repository. * **role:graylog_datanode, role:graylog_server**: Add template for Graylog 7.1. * **role:sshd**: Add Debian 13 support. diff --git a/roles/repo_baseos/README.md b/roles/repo_baseos/README.md index 6b6849b8..96932437 100644 --- a/roles/repo_baseos/README.md +++ b/roles/repo_baseos/README.md @@ -34,6 +34,12 @@ This role deploys the BaseOS repositories, which can be used to set a custom mir * Type: String. * Default: `'{{ lfops__repo_mirror_url | default("") }}'` +`repo_baseos__security_repo_enabled__host_var` / `repo_baseos__security_repo_enabled__group_var` + +* Whether the Rocky Linux `security` repository should be enabled. This repository delivers critical CVE fixes and is enabled by default in LFOPS (Rocky ships it disabled). Set to `false` to opt out on a specific host or group. +* Type: Bool. +* Default: `true` + Example: ```yaml # optional @@ -42,6 +48,7 @@ repo_baseos__basic_auth_login: password: 'linuxfabrik' repo_baseos__crb_repo_enabled__host_var: true repo_baseos__mirror_url: 'https://mirror.example.com' +repo_baseos__security_repo_enabled__host_var: false ``` diff --git a/roles/repo_baseos/defaults/main.yml b/roles/repo_baseos/defaults/main.yml index 9cbf58f6..f40a0b52 100644 --- a/roles/repo_baseos/defaults/main.yml +++ b/roles/repo_baseos/defaults/main.yml @@ -11,3 +11,14 @@ repo_baseos__crb_repo_enabled__combined_var: '{{ repo_baseos__crb_repo_enabled__dependent_var if (repo_baseos__crb_repo_enabled__dependent_var | bool) else repo_baseos__crb_repo_enabled__role_var }}' + +repo_baseos__security_repo_enabled__dependent_var: '' +repo_baseos__security_repo_enabled__group_var: '' +repo_baseos__security_repo_enabled__host_var: '' +repo_baseos__security_repo_enabled__role_var: true +repo_baseos__security_repo_enabled__combined_var: '{{ + repo_baseos__security_repo_enabled__host_var if (repo_baseos__security_repo_enabled__host_var | string | length) else + repo_baseos__security_repo_enabled__group_var if (repo_baseos__security_repo_enabled__group_var | string | length) else + repo_baseos__security_repo_enabled__dependent_var if (repo_baseos__security_repo_enabled__dependent_var | string | length) else + repo_baseos__security_repo_enabled__role_var + }}' diff --git a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-security.repo.j2 b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-security.repo.j2 new file mode 100644 index 00000000..3d5f6275 --- /dev/null +++ b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-security.repo.j2 @@ -0,0 +1,64 @@ +# {{ ansible_managed }} +# 2026052201 + +# rocky-security.repo +# +# The mirrorlist system uses the connecting IP address of the client and the +# update status of each mirror to pick current mirrors that are geographically +# close to the client. You should use this for Rocky updates unless you are +# manually picking other mirrors. +# +# If the mirrorlist does not work for you, you can try the commented out +# baseurl line instead. + +[security] +name=Rocky Linux $releasever - Security +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length %} +baseurl={{ repo_baseos__mirror_url }}/rocky/10/security/x86_64/os/ +{% else %} +mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=security-$releasever$rltype +{% endif %} +#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/security/$basearch/os/ +gpgcheck=1 +enabled={{ repo_baseos__security_repo_enabled__combined_var | bool | ternary(1, 0) }} +countme=1 +metadata_expire=6h +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 +{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +username={{ repo_baseos__basic_auth_login["username"] }} +password={{ repo_baseos__basic_auth_login["password"] }} +{% endif %} + +[security-debuginfo] +name=Rocky Linux $releasever - Security Debug +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length %} +baseurl={{ repo_baseos__mirror_url }}/rocky/10/security/x86_64/debug/tree/ +{% else %} +mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=security-$releasever-debug$rltype +{% endif %} +#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/security/$basearch/debug/tree/ +gpgcheck=1 +enabled=0 +metadata_expire=6h +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 +{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +username={{ repo_baseos__basic_auth_login["username"] }} +password={{ repo_baseos__basic_auth_login["password"] }} +{% endif %} + +[security-source] +name=Rocky Linux $releasever - Security Source +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length %} +baseurl={{ repo_baseos__mirror_url }}/rocky/10/security/source/tree/ +{% else %} +mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=security-$releasever-source$rltype +{% endif %} +#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/security/source/tree/ +gpgcheck=1 +enabled=0 +metadata_expire=6h +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 +{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +username={{ repo_baseos__basic_auth_login["username"] }} +password={{ repo_baseos__basic_auth_login["password"] }} +{% endif %} diff --git a/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Security.repo.j2 b/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Security.repo.j2 new file mode 100644 index 00000000..caf83ad0 --- /dev/null +++ b/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Security.repo.j2 @@ -0,0 +1,28 @@ +# {{ ansible_managed }} +# 2026052201 + +# Rocky-Security.repo +# +# The mirrorlist system uses the connecting IP address of the client and the +# update status of each mirror to pick current mirrors that are geographically +# close to the client. You should use this for Rocky updates unless you are +# manually picking other mirrors. +# +# If the mirrorlist does not work for you, you can try the commented out +# baseurl line instead. + +[security] +name=Rocky Linux $releasever - Security +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length %} +baseurl={{ repo_baseos__mirror_url }}/rocky/8/security/x86_64/os/ +{% else %} +mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=security-$releasever +{% endif %} +#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/security/$basearch/os/ +gpgcheck=1 +enabled={{ repo_baseos__security_repo_enabled__combined_var | bool | ternary(1, 0) }} +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial +{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +username={{ repo_baseos__basic_auth_login["username"] }} +password={{ repo_baseos__basic_auth_login["password"] }} +{% endif %} diff --git a/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-security.repo.j2 b/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-security.repo.j2 new file mode 100644 index 00000000..d6a2a8be --- /dev/null +++ b/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-security.repo.j2 @@ -0,0 +1,64 @@ +# {{ ansible_managed }} +# 2026052201 + +# rocky-security.repo +# +# The mirrorlist system uses the connecting IP address of the client and the +# update status of each mirror to pick current mirrors that are geographically +# close to the client. You should use this for Rocky updates unless you are +# manually picking other mirrors. +# +# If the mirrorlist does not work for you, you can try the commented out +# baseurl line instead. + +[security] +name=Rocky Linux $releasever - Security +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length %} +baseurl={{ repo_baseos__mirror_url }}/rocky/9/security/x86_64/os/ +{% else %} +mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=security-$releasever$rltype +{% endif %} +#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/security/$basearch/os/ +gpgcheck=1 +enabled={{ repo_baseos__security_repo_enabled__combined_var | bool | ternary(1, 0) }} +countme=1 +metadata_expire=6h +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 +{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +username={{ repo_baseos__basic_auth_login["username"] }} +password={{ repo_baseos__basic_auth_login["password"] }} +{% endif %} + +[security-debuginfo] +name=Rocky Linux $releasever - Security Debug +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length %} +baseurl={{ repo_baseos__mirror_url }}/rocky/9/security/x86_64/debug/tree/ +{% else %} +mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=security-$releasever-debug$rltype +{% endif %} +#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/security/$basearch/debug/tree/ +gpgcheck=1 +enabled=0 +metadata_expire=6h +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 +{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +username={{ repo_baseos__basic_auth_login["username"] }} +password={{ repo_baseos__basic_auth_login["password"] }} +{% endif %} + +[security-source] +name=Rocky Linux $releasever - Security Source +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length %} +baseurl={{ repo_baseos__mirror_url }}/rocky/9/security/source/tree/ +{% else %} +mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=security-$releasever-source$rltype +{% endif %} +#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/security/source/tree/ +gpgcheck=1 +enabled=0 +metadata_expire=6h +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 +{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +username={{ repo_baseos__basic_auth_login["username"] }} +password={{ repo_baseos__basic_auth_login["password"] }} +{% endif %} diff --git a/roles/repo_baseos/vars/Rocky10.yml b/roles/repo_baseos/vars/Rocky10.yml index 6b3df1d0..cc8f8dc6 100644 --- a/roles/repo_baseos/vars/Rocky10.yml +++ b/roles/repo_baseos/vars/Rocky10.yml @@ -2,4 +2,5 @@ repo_baseos__repo_files: - 'Rocky10/etc/yum.repos.d/rocky-addons.repo' - 'Rocky10/etc/yum.repos.d/rocky-devel.repo' - 'Rocky10/etc/yum.repos.d/rocky-extras.repo' + - 'Rocky10/etc/yum.repos.d/rocky-security.repo' - 'Rocky10/etc/yum.repos.d/rocky.repo' diff --git a/roles/repo_baseos/vars/Rocky8.yml b/roles/repo_baseos/vars/Rocky8.yml index 09e6409a..e9742ac3 100644 --- a/roles/repo_baseos/vars/Rocky8.yml +++ b/roles/repo_baseos/vars/Rocky8.yml @@ -2,3 +2,4 @@ repo_baseos__repo_files: - 'Rocky8/etc/yum.repos.d/Rocky-AppStream.repo' - 'Rocky8/etc/yum.repos.d/Rocky-BaseOS.repo' - 'Rocky8/etc/yum.repos.d/Rocky-Extras.repo' + - 'Rocky8/etc/yum.repos.d/Rocky-Security.repo' diff --git a/roles/repo_baseos/vars/Rocky9.yml b/roles/repo_baseos/vars/Rocky9.yml index a5f0e957..63321688 100644 --- a/roles/repo_baseos/vars/Rocky9.yml +++ b/roles/repo_baseos/vars/Rocky9.yml @@ -1,3 +1,4 @@ repo_baseos__repo_files: - 'Rocky9/etc/yum.repos.d/rocky.repo' - 'Rocky9/etc/yum.repos.d/rocky-extras.repo' + - 'Rocky9/etc/yum.repos.d/rocky-security.repo' From 612cfa7fcd6e5ac778676a737f25908c4299749a Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 22 May 2026 14:31:02 +0200 Subject: [PATCH 46/66] docs(contributing): improve content --- CONTRIBUTING.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba0e7a6e..f36d25c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -238,6 +238,7 @@ When creating a new role, make sure to deliver: * Do not over-engineer the role during the development — it should fulfill its use case, but can grow and be improved on later. * There should be one role per software application. If there are multiple versions of the software, e.g. PHP 7.1, 7.2, 7.3, etc., they all should be supported by a single role. * Do not use role dependencies via `meta/main.yml`. Dependencies are handled in playbooks. +* Do not use the general-purpose roles `apps`, `files`, and `systemd_unit` as dependent roles (via `__dependent_var`). They are meant to be driven directly by the user from the inventory; wiring dependencies into them makes the order in which playbooks can run overly restrictive. Perform such tasks in the consuming role directly instead. * Whenever the role requires a list as an input, use a list of dictionaries with `state: present/absent`. See "Combined Variables" below. * Fail loudly. Avoid constructs that could suppress error messages, like `IfModule` in Apache HTTPd. This makes debugging and troubleshooting a lot easier. * Do not support software versions that are EOL. @@ -295,7 +296,7 @@ When creating a new role, make sure to deliver: * `ansible.builtin.template` over `ansible.builtin.copy`, `ansible.builtin.lineinfile` or `ansible.builtin.blockinfile`. Templating the whole file leads to more consistent, deterministic, and expected results. * Do not use `state: 'latest'` for the `ansible.builtin.package` module as this is not idempotent. Always use `state: 'present'`. * Always use `delegate_to: 'localhost'` instead of `local_action`. -* Always set `become: false` on every task delegated to localhost. When a play sets `become: true` at the play level (the typical case), it propagates to delegated tasks too and tries to escalate via sudo on the Ansible controller. On a controller without passwordless sudo this fails with `sudo: a password is required`, even though the delegated task only writes to `/tmp` or hits a remote API and does not need root locally. Example: +* Always set `become: false` on every task delegated to localhost. When a play sets `become: true` at the play level (not typical for lfops, but useful if others import our roles in their playbooks), it propagates to delegated tasks too and tries to escalate via sudo on the Ansible controller. On a controller without passwordless sudo this fails with `sudo: a password is required`, even though the delegated task only writes to `/tmp` or hits a remote API and does not need root locally. Example: ```yaml - name: 'curl --output /tmp/ansible.example.tar.gz https://example.com/releases/example.tar.gz' @@ -311,6 +312,14 @@ When creating a new role, make sure to deliver: ``` * Always provide `changed_when`, `creates`, or `removes` for `ansible.builtin.command` and `ansible.builtin.shell` tasks to ensure idempotency. Use `changed_when: false` for read-only commands. +* Prefer a `chown -R --changes` command over `ansible.builtin.file` with `recurse: true`; the module's recursive mode is slow on large trees. Register the result and derive `changed_when` from the `--changes` output for idempotency: + + ```yaml + - name: 'chown -R --changes apache:apache {{ wordpress__install_dir | quote }}' + ansible.builtin.command: 'chown -R --changes apache:apache {{ wordpress__install_dir | quote }}' + register: '__wordpress__chown_result' + changed_when: '__wordpress__chown_result["stdout"] | length > 0' + ``` * When deploying files with `ansible.builtin.template`, always set `backup`, `src`, `dest`, `owner`, `group`, and `mode`. * Prefer `ansible.builtin.assert` over `ansible.builtin.fail` with `when` for validation checks. There is basically no technical difference; this guideline is only for consistency. * Optionally add `ansible.builtin.debug` tasks for `__combined_var` variables so the user can see what the role will do. @@ -363,6 +372,7 @@ When creating a new role, make sure to deliver: * `./defaults`: Default variables for the role, might be overridden by the user in the inventory. * Document all user-facing variables in the README. Have a look at `roles/example/README.md` for the format. * Do not set defaults for mandatory variables. +* Software versions must always be mandatory variables, never role defaults. A default version drifts: bumping it in the role silently changes what an existing inventory deploys, effectively a breaking change on every bump. Forcing the user to pin the version keeps inventory and the deployed state consistent. * Naming scheme: `___`, for example `apache_httpd__server_admin`. * No need to invent new names, use the key-names from the config file (if possible), for example `redis__conf_maxmemory`. * Prefix role-internal variables with `__`, for example `__example__sysconfig_path`. This makes it easy to determine which variables are user-facing and therefore should be in the README. @@ -370,6 +380,7 @@ When creating a new role, make sure to deliver: * If you need random but predictable/idempotent values, use the `inventory_hostname` as seed. Example for setting the minutes of an hour: `{{ 59 | random(seed=inventory_hostname) }}`. * When guarding optional role variables (strings or lists) that may be undefined, use `is defined and my_var | length > 0`. This catches both undefined variables and empty values (e.g. `my_var: ''`). Bare `is defined` is fine for dict subkeys where presence alone is the signal (e.g. `item["cidr"] is defined`) or for result attributes (e.g. `result["failed"] is defined`). * Any secrets (passwords, tokens etc.) should not be provided with default values in the role. It is important for a secure-by-default implementation to ensure that an environment is not vulnerable due to the production use of default secrets. Users must be forced to properly provide their own secret variable values. +* Group credentials as subkeys of a single dictionary variable (e.g. `__login` with `username` and `password` subkeys) rather than as separate top-level variables. This integrates cleanly with the `linuxfabrik.lfops.bitwarden_item` lookup, which returns the whole item as one dict. * Always use the `ansible_facts` dictionary (e.g. `ansible_facts["os_family"]` instead of `ansible_os_family`). The old pre-2.5 "facts injected as separate variables" naming system will be deprecated in a future release of Ansible. @@ -377,9 +388,7 @@ When creating a new role, make sure to deliver: Every role should include a `meta/argument_specs.yml` that declares all user-facing variables with their types. Ansible validates these automatically at role entry (before any tasks run), catching type mismatches and missing required variables without manual assert code. -Include all variables documented in the README: mandatory variables, simple optional variables, and the `__host_var`/`__group_var`/`__dependent_var` variants of injection variables. Do not include the purely internal `__role_var` and `__combined_var` slots. - -`__dependent_var` must be declared even though it is conceptually internal, because `setup_*` playbooks pass it into the role via `vars:` and Ansible validates role-vars against `argument_specs`. Omitting it causes "Supported parameters include: ..." errors at role entry. +Include all variables documented in the README: mandatory variables, simple optional variables, and the `__host_var`/`__group_var`/`__dependent_var` variants of injection variables. Do not include the purely internal `__role_var` and `__combined_var` slots. `__dependent_var` must be declared even though it is conceptually internal, because `setup_*` playbooks pass it into the role via `vars:` and Ansible validates role-vars against `argument_specs`. Omitting it causes `Supported parameters include: ...` errors at role entry. Guidelines for `argument_specs`: @@ -397,11 +406,11 @@ Have a look at the `example` role's `meta/argument_specs.yml` for a complete ref ##### Combined Variables -The goal of combined variables is that variables can be set in multiple places, and then merged in order to be used in the role. For example, the user can overwrite a specific configuration role default (`__role_var`) from their inventory (`__host_var` / `__group_var`). +The goal of combined variables is that variables can be set in multiple places, and then merged in order to be used in the role. For example, the user can overwrite *parts* of the role's default (`__role_var`) from their inventory (`__host_var` / `__group_var`). Furthermore, other roles can also inject their sensible defaults via the `__dependent_var`, with a higher precedence than the role defaults, but lower than the user's inventory. -To enable this behavior, you must define the `__combined_var` as follows: +To enable this behavior, you must define the `__combined_var` in the `defaults/main.yml` as follows: ```yaml # for list of dictionaries my_role__my_var__dependent_var: [] From 5170633dfc6b02adf885070125b1324c7f3a7699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20B=C3=BCrki?= Date: Fri, 22 May 2026 14:59:30 +0200 Subject: [PATCH 47/66] fix(roles/mariadb_server): add mariadb_server__cnf_innodb_snapshot_isolation variable (mariadb 10.6+), defaulting to 'on' --- CHANGELOG.md | 4 ++++ roles/mariadb_server/README.md | 6 ++++++ roles/mariadb_server/defaults/main.yml | 10 ++++++++++ .../etc/my.cnf.d/10.11-z00-linuxfabrik.cnf.j2 | 3 ++- .../templates/etc/my.cnf.d/10.6-z00-linuxfabrik.cnf.j2 | 3 ++- .../templates/etc/my.cnf.d/11.4-z00-linuxfabrik.cnf.j2 | 3 ++- .../templates/etc/my.cnf.d/11.8-z00-linuxfabrik.cnf.j2 | 3 ++- roles/mariadb_server/vars/10.11.yml | 1 + roles/mariadb_server/vars/10.6.yml | 1 + roles/mariadb_server/vars/11.4.yml | 1 + roles/mariadb_server/vars/11.8.yml | 1 + 11 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8f3395..ed41509e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +* **role:mariadb_server**: Add `mariadb_server__cnf_innodb_snapshot_isolation` variable (MariaDB 10.6+), defaulting to `'ON'`. + ### Security * **ci**: Scope `GITHUB_TOKEN` permissions in the dependabot-auto-merge workflow to the job level, with top-level now `read-all`. Matches the pattern used by the other LFOps workflows and addresses the OpenSSF Scorecard `Token-Permissions` finding. diff --git a/roles/mariadb_server/README.md b/roles/mariadb_server/README.md index 4cb0fa9f..35d0c072 100644 --- a/roles/mariadb_server/README.md +++ b/roles/mariadb_server/README.md @@ -682,6 +682,12 @@ Variables for `z00-linuxfabrik.cnf` directives and their default values, defined * Type: Number. * Default: `4` +`mariadb_server__cnf_innodb_snapshot_isolation__group_var` / `mariadb_server__cnf_innodb_snapshot_isolation__host_var` + +* [mariadb.com](https://mariadb.com/kb/en/innodb-system-variables/#innodb_snapshot_isolation). +* Type: String. +* Default: `'ON'` + `mariadb_server__cnf_innodb_strict_mode__group_var` / `mariadb_server__cnf_innodb_strict_mode__host_var` * [mariadb.com](https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-system-variables#innodb_strict_mode) diff --git a/roles/mariadb_server/defaults/main.yml b/roles/mariadb_server/defaults/main.yml index 86c78c43..b947c31a 100644 --- a/roles/mariadb_server/defaults/main.yml +++ b/roles/mariadb_server/defaults/main.yml @@ -462,6 +462,16 @@ mariadb_server__cnf_innodb_read_io_threads__combined_var: '{{ mariadb_server__cnf_innodb_read_io_threads__role_var }}' +mariadb_server__cnf_innodb_snapshot_isolation__dependent_var: '' +mariadb_server__cnf_innodb_snapshot_isolation__group_var: '' +mariadb_server__cnf_innodb_snapshot_isolation__host_var: '' +mariadb_server__cnf_innodb_snapshot_isolation__combined_var: '{{ + mariadb_server__cnf_innodb_snapshot_isolation__host_var if (mariadb_server__cnf_innodb_snapshot_isolation__host_var | string | length) else + mariadb_server__cnf_innodb_snapshot_isolation__group_var if (mariadb_server__cnf_innodb_snapshot_isolation__group_var | string | length) else + mariadb_server__cnf_innodb_snapshot_isolation__dependent_var if (mariadb_server__cnf_innodb_snapshot_isolation__dependent_var | string | length) else + mariadb_server__cnf_innodb_snapshot_isolation__role_var + }}' + mariadb_server__cnf_innodb_strict_mode__dependent_var: '' mariadb_server__cnf_innodb_strict_mode__group_var: '' mariadb_server__cnf_innodb_strict_mode__host_var: '' diff --git a/roles/mariadb_server/templates/etc/my.cnf.d/10.11-z00-linuxfabrik.cnf.j2 b/roles/mariadb_server/templates/etc/my.cnf.d/10.11-z00-linuxfabrik.cnf.j2 index f2bbb0f1..8b776c6e 100644 --- a/roles/mariadb_server/templates/etc/my.cnf.d/10.11-z00-linuxfabrik.cnf.j2 +++ b/roles/mariadb_server/templates/etc/my.cnf.d/10.11-z00-linuxfabrik.cnf.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052201 # 10.11 [client] @@ -59,6 +59,7 @@ innodb_open_files = {{ mariadb_server__cnf_innodb_o innodb_print_all_deadlocks = {{ mariadb_server__cnf_innodb_print_all_deadlocks__combined_var }} innodb_purge_batch_size = {{ mariadb_server__cnf_innodb_purge_batch_size__combined_var }} innodb_read_io_threads = {{ mariadb_server__cnf_innodb_read_io_threads__combined_var }} +innodb_snapshot_isolation = {{ mariadb_server__cnf_innodb_snapshot_isolation__combined_var }} innodb_strict_mode = {{ mariadb_server__cnf_innodb_strict_mode__combined_var }} innodb_sync_spin_loops = {{ mariadb_server__cnf_innodb_sync_spin_loops__combined_var }} innodb_write_io_threads = {{ mariadb_server__cnf_innodb_write_io_threads__combined_var }} diff --git a/roles/mariadb_server/templates/etc/my.cnf.d/10.6-z00-linuxfabrik.cnf.j2 b/roles/mariadb_server/templates/etc/my.cnf.d/10.6-z00-linuxfabrik.cnf.j2 index f0e0f7ca..677ee70e 100644 --- a/roles/mariadb_server/templates/etc/my.cnf.d/10.6-z00-linuxfabrik.cnf.j2 +++ b/roles/mariadb_server/templates/etc/my.cnf.d/10.6-z00-linuxfabrik.cnf.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026031902 +# 2026052201 # 10.6 [client] @@ -60,6 +60,7 @@ innodb_open_files = {{ mariadb_server__cnf_innodb_open_files__combi innodb_print_all_deadlocks = {{ mariadb_server__cnf_innodb_print_all_deadlocks__combined_var }} innodb_purge_batch_size = {{ mariadb_server__cnf_innodb_purge_batch_size__combined_var }} innodb_read_io_threads = {{ mariadb_server__cnf_innodb_read_io_threads__combined_var }} +innodb_snapshot_isolation = {{ mariadb_server__cnf_innodb_snapshot_isolation__combined_var }} innodb_strict_mode = {{ mariadb_server__cnf_innodb_strict_mode__combined_var }} innodb_sync_spin_loops = {{ mariadb_server__cnf_innodb_sync_spin_loops__combined_var }} innodb_write_io_threads = {{ mariadb_server__cnf_innodb_write_io_threads__combined_var }} diff --git a/roles/mariadb_server/templates/etc/my.cnf.d/11.4-z00-linuxfabrik.cnf.j2 b/roles/mariadb_server/templates/etc/my.cnf.d/11.4-z00-linuxfabrik.cnf.j2 index f8ad815f..485111c8 100644 --- a/roles/mariadb_server/templates/etc/my.cnf.d/11.4-z00-linuxfabrik.cnf.j2 +++ b/roles/mariadb_server/templates/etc/my.cnf.d/11.4-z00-linuxfabrik.cnf.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052201 # 11.4 [client] @@ -58,6 +58,7 @@ innodb_open_files = {{ mariadb_server__cnf_innodb_o innodb_print_all_deadlocks = {{ mariadb_server__cnf_innodb_print_all_deadlocks__combined_var }} innodb_purge_batch_size = {{ mariadb_server__cnf_innodb_purge_batch_size__combined_var }} innodb_read_io_threads = {{ mariadb_server__cnf_innodb_read_io_threads__combined_var }} +innodb_snapshot_isolation = {{ mariadb_server__cnf_innodb_snapshot_isolation__combined_var }} innodb_strict_mode = {{ mariadb_server__cnf_innodb_strict_mode__combined_var }} innodb_sync_spin_loops = {{ mariadb_server__cnf_innodb_sync_spin_loops__combined_var }} innodb_write_io_threads = {{ mariadb_server__cnf_innodb_write_io_threads__combined_var }} diff --git a/roles/mariadb_server/templates/etc/my.cnf.d/11.8-z00-linuxfabrik.cnf.j2 b/roles/mariadb_server/templates/etc/my.cnf.d/11.8-z00-linuxfabrik.cnf.j2 index 006feba2..1cf26dc3 100644 --- a/roles/mariadb_server/templates/etc/my.cnf.d/11.8-z00-linuxfabrik.cnf.j2 +++ b/roles/mariadb_server/templates/etc/my.cnf.d/11.8-z00-linuxfabrik.cnf.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052201 # 11.8 [client] @@ -58,6 +58,7 @@ innodb_open_files = {{ mariadb_server__cnf_innodb_o innodb_print_all_deadlocks = {{ mariadb_server__cnf_innodb_print_all_deadlocks__combined_var }} innodb_purge_batch_size = {{ mariadb_server__cnf_innodb_purge_batch_size__combined_var }} innodb_read_io_threads = {{ mariadb_server__cnf_innodb_read_io_threads__combined_var }} +innodb_snapshot_isolation = {{ mariadb_server__cnf_innodb_snapshot_isolation__combined_var }} innodb_strict_mode = {{ mariadb_server__cnf_innodb_strict_mode__combined_var }} innodb_sync_spin_loops = {{ mariadb_server__cnf_innodb_sync_spin_loops__combined_var }} innodb_write_io_threads = {{ mariadb_server__cnf_innodb_write_io_threads__combined_var }} diff --git a/roles/mariadb_server/vars/10.11.yml b/roles/mariadb_server/vars/10.11.yml index f9b79a73..99790dee 100644 --- a/roles/mariadb_server/vars/10.11.yml +++ b/roles/mariadb_server/vars/10.11.yml @@ -34,6 +34,7 @@ mariadb_server__cnf_innodb_open_files__role_var: 0 # results in autosized mariadb_server__cnf_innodb_print_all_deadlocks__role_var: 'OFF' mariadb_server__cnf_innodb_purge_batch_size__role_var: '127' mariadb_server__cnf_innodb_read_io_threads__role_var: '4' +mariadb_server__cnf_innodb_snapshot_isolation__role_var: 'ON' mariadb_server__cnf_innodb_strict_mode__role_var: 'ON' mariadb_server__cnf_innodb_sync_spin_loops__role_var: '30' mariadb_server__cnf_innodb_write_io_threads__role_var: '4' diff --git a/roles/mariadb_server/vars/10.6.yml b/roles/mariadb_server/vars/10.6.yml index 69c863e4..43dcd28f 100644 --- a/roles/mariadb_server/vars/10.6.yml +++ b/roles/mariadb_server/vars/10.6.yml @@ -35,6 +35,7 @@ mariadb_server__cnf_innodb_open_files__role_var: 0 # results in autosized mariadb_server__cnf_innodb_print_all_deadlocks__role_var: 'OFF' mariadb_server__cnf_innodb_purge_batch_size__role_var: '127' mariadb_server__cnf_innodb_read_io_threads__role_var: '4' +mariadb_server__cnf_innodb_snapshot_isolation__role_var: 'ON' mariadb_server__cnf_innodb_strict_mode__role_var: 'ON' mariadb_server__cnf_innodb_sync_spin_loops__role_var: '30' mariadb_server__cnf_innodb_write_io_threads__role_var: '4' diff --git a/roles/mariadb_server/vars/11.4.yml b/roles/mariadb_server/vars/11.4.yml index fcf886db..ac953759 100644 --- a/roles/mariadb_server/vars/11.4.yml +++ b/roles/mariadb_server/vars/11.4.yml @@ -33,6 +33,7 @@ mariadb_server__cnf_innodb_open_files__role_var: 0 # results in autosized mariadb_server__cnf_innodb_print_all_deadlocks__role_var: 'OFF' mariadb_server__cnf_innodb_purge_batch_size__role_var: '127' mariadb_server__cnf_innodb_read_io_threads__role_var: '4' +mariadb_server__cnf_innodb_snapshot_isolation__role_var: 'ON' mariadb_server__cnf_innodb_strict_mode__role_var: 'ON' mariadb_server__cnf_innodb_sync_spin_loops__role_var: '30' mariadb_server__cnf_innodb_write_io_threads__role_var: '4' diff --git a/roles/mariadb_server/vars/11.8.yml b/roles/mariadb_server/vars/11.8.yml index 8cd19428..9650f596 100644 --- a/roles/mariadb_server/vars/11.8.yml +++ b/roles/mariadb_server/vars/11.8.yml @@ -33,6 +33,7 @@ mariadb_server__cnf_innodb_open_files__role_var: 0 # results in autosized mariadb_server__cnf_innodb_print_all_deadlocks__role_var: 'OFF' mariadb_server__cnf_innodb_purge_batch_size__role_var: '127' mariadb_server__cnf_innodb_read_io_threads__role_var: '4' +mariadb_server__cnf_innodb_snapshot_isolation__role_var: 'ON' mariadb_server__cnf_innodb_strict_mode__role_var: 'ON' mariadb_server__cnf_innodb_sync_spin_loops__role_var: '30' mariadb_server__cnf_innodb_write_io_threads__role_var: '4' From 90bfd7da6042d79c39fb00ce5f56c507a19e3ce3 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Fri, 22 May 2026 17:18:19 +0200 Subject: [PATCH 48/66] fix(roles/repo_*): only write basic-auth credentials when a custom mirror URL is set The basic-auth block in the repo templates was gated solely on `*_basic_auth_login`, independent of `*_mirror_url`. Setting `lfops__repo_basic_auth_login` without `lfops__repo_mirror_url` wrote username/password into repo files that still pointed at the public vendor mirrors, causing the package manager to send those credentials to servers that do not use basic auth. Tighten the guard so credentials are only emitted when a custom mirror URL is configured, and clarify the affected role READMEs accordingly. repo_icinga is intentionally left unchanged: its public subscription URL legitimately requires basic auth without a custom mirror. repo_redis (apt) never emitted basic auth and is out of scope. --- CHANGELOG.md | 1 + roles/repo_baseos/README.md | 2 +- .../etc/yum.repos.d/almalinux.repo.j2 | 16 +++++----- .../etc/yum.repos.d/CentOS-Base.repo.j2 | 10 +++--- .../CentOS-Linux-AppStream.repo.j2 | 4 +-- .../yum.repos.d/CentOS-Linux-BaseOS.repo.j2 | 4 +-- .../yum.repos.d/CentOS-Linux-Extras.repo.j2 | 4 +-- .../etc/yum.repos.d/rocky-addons.repo.j2 | 32 +++++++++---------- .../etc/yum.repos.d/rocky-devel.repo.j2 | 8 ++--- .../etc/yum.repos.d/rocky-extras.repo.j2 | 14 ++++---- .../etc/yum.repos.d/rocky-security.repo.j2 | 6 ++-- .../Rocky10/etc/yum.repos.d/rocky.repo.j2 | 20 ++++++------ .../etc/yum.repos.d/Rocky-AppStream.repo.j2 | 4 +-- .../etc/yum.repos.d/Rocky-BaseOS.repo.j2 | 4 +-- .../etc/yum.repos.d/Rocky-Extras.repo.j2 | 4 +-- .../etc/yum.repos.d/Rocky-Security.repo.j2 | 2 +- .../etc/yum.repos.d/rocky-extras.repo.j2 | 14 ++++---- .../etc/yum.repos.d/rocky-security.repo.j2 | 6 ++-- .../Rocky9/etc/yum.repos.d/rocky.repo.j2 | 20 ++++++------ roles/repo_collabora/README.md | 2 +- .../etc/yum.repos.d/CollaboraOnline.repo.j2 | 4 +-- roles/repo_collabora_code/README.md | 2 +- .../RedHat7-CollaboraOnline_CODE.repo.j2 | 4 +-- .../RedHat8-CollaboraOnline_CODE.repo.j2 | 4 +-- .../RedHat9-CollaboraOnline_CODE.repo.j2 | 4 +-- roles/repo_docker/README.md | 2 +- .../etc/yum.repos.d/docker-ce.repo.j2 | 20 ++++++------ roles/repo_elasticsearch/README.md | 2 +- .../etc/yum.repos.d/elasticsearch.repo.j2 | 4 +-- roles/repo_epel/README.md | 2 +- .../yum.repos.d/almalinux-powertools.repo.j2 | 8 ++--- .../CentOS-Linux-PowerTools.repo.j2 | 4 +-- .../etc/yum.repos.d/epel-testing.repo.j2 | 8 ++--- .../RedHat10/etc/yum.repos.d/epel.repo.j2 | 8 ++--- .../etc/yum.repos.d/epel-testing.repo.j2 | 8 ++--- .../RedHat7/etc/yum.repos.d/epel.repo.j2 | 8 ++--- .../etc/yum.repos.d/epel-testing.repo.j2 | 8 ++--- .../RedHat8/etc/yum.repos.d/epel.repo.j2 | 8 ++--- .../yum.repos.d/epel-cisco-openh264.repo.j2 | 8 ++--- .../etc/yum.repos.d/epel-testing.repo.j2 | 8 ++--- .../RedHat9/etc/yum.repos.d/epel.repo.j2 | 8 ++--- .../etc/yum.repos.d/Rocky-PowerTools.repo.j2 | 4 +-- roles/repo_gitlab_ce/README.md | 2 +- .../etc/yum.repos.d/gitlab_gitlab-ce.repo.j2 | 6 ++-- roles/repo_gitlab_runner/README.md | 2 +- .../etc/yum.repos.d/gitlab_gitlab-ce.repo.j2 | 6 ++-- roles/repo_grafana/README.md | 2 +- .../templates/etc/yum.repos.d/grafana.repo.j2 | 4 +-- roles/repo_graylog/README.md | 2 +- .../templates/etc/yum.repos.d/graylog.repo.j2 | 4 +-- roles/repo_influxdb/README.md | 2 +- .../etc/yum.repos.d/influxdb.repo.j2 | 4 +-- roles/repo_mariadb/README.md | 2 +- .../etc/yum.repos.d/RedHat-MariaDB.repo.j2 | 4 +-- roles/repo_mongodb/README.md | 2 +- .../etc/yum.repos.d/mongodb-org.repo.j2 | 4 +-- roles/repo_monitoring_plugins/README.md | 2 +- .../linuxfabrik-monitoring-plugins.repo.j2 | 6 ++-- .../linuxfabrik-monitoring-plugins.repo.j2 | 6 ++-- roles/repo_mydumper/README.md | 2 +- .../etc/yum.repos.d/mydumper.repo.j2 | 4 +-- roles/repo_opensearch/README.md | 2 +- .../etc/yum.repos.d/opensearch.repo.j2 | 4 +-- roles/repo_postgresql/README.md | 2 +- .../etc/yum.repos.d/pgdg-redhat-all.repo.j2 | 16 +++++----- roles/repo_proxysql/README.md | 2 +- .../etc/yum.repos.d/proxysql.repo.j2 | 4 +-- roles/repo_remi/README.md | 2 +- .../etc/yum.repos.d/remi-modular.repo.j2 | 10 +++--- .../etc/yum.repos.d/remi-safe.repo.j2 | 6 ++-- .../RedHat10/etc/yum.repos.d/remi.repo.j2 | 10 +++--- .../etc/yum.repos.d/remi-modular.repo.j2 | 6 ++-- .../RedHat8/etc/yum.repos.d/remi-safe.repo.j2 | 6 ++-- .../RedHat8/etc/yum.repos.d/remi.repo.j2 | 10 +++--- .../etc/yum.repos.d/remi-modular.repo.j2 | 10 +++--- .../RedHat9/etc/yum.repos.d/remi-safe.repo.j2 | 6 ++-- .../RedHat9/etc/yum.repos.d/remi.repo.j2 | 10 +++--- roles/repo_rpmfusion/README.md | 2 +- .../rpmfusion-free-updates-testing.repo.j2 | 8 ++--- .../rpmfusion-free-updates.repo.j2 | 8 ++--- .../rpmfusion-nonfree-updates-testing.repo.j2 | 8 ++--- .../rpmfusion-nonfree-updates.repo.j2 | 8 ++--- 82 files changed, 260 insertions(+), 259 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed41509e..968019db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +* **role:repo_\***: HTTP basic auth credentials are now only written to the repository config files when a custom mirror URL is set. Previously, setting `lfops__repo_basic_auth_login` without `lfops__repo_mirror_url` wrote the credentials into repo files that still pointed at the public vendor mirrors, causing the package manager to send them to servers that do not use basic auth. The Icinga repo is intentionally unchanged, since its subscription URL legitimately requires basic auth. * **ci**: Scope `GITHUB_TOKEN` permissions in the dependabot-auto-merge workflow to the job level, with top-level now `read-all`. Matches the pattern used by the other LFOps workflows and addresses the OpenSSF Scorecard `Token-Permissions` finding. ### Removed diff --git a/roles/repo_baseos/README.md b/roles/repo_baseos/README.md index 96932437..9c6d9cee 100644 --- a/roles/repo_baseos/README.md +++ b/roles/repo_baseos/README.md @@ -18,7 +18,7 @@ This role deploys the BaseOS repositories, which can be used to set a custom mir `repo_baseos__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_baseos/templates/AlmaLinux8/etc/yum.repos.d/almalinux.repo.j2 b/roles/repo_baseos/templates/AlmaLinux8/etc/yum.repos.d/almalinux.repo.j2 index 42bd2a5c..bb37c9c0 100644 --- a/roles/repo_baseos/templates/AlmaLinux8/etc/yum.repos.d/almalinux.repo.j2 +++ b/roles/repo_baseos/templates/AlmaLinux8/etc/yum.repos.d/almalinux.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [baseos] name=AlmaLinux 8 - BaseOS @@ -13,7 +13,7 @@ enabled=1 gpgcheck=1 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -30,7 +30,7 @@ enabled=1 gpgcheck=1 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -56,7 +56,7 @@ mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/baseos-source enabled=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -68,7 +68,7 @@ mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/appstream-source enabled=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -89,7 +89,7 @@ mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/baseos-debuginfo enabled=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -101,7 +101,7 @@ mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/appstream-debugi enabled=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -113,7 +113,7 @@ mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/extras-debuginfo enabled=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/CentOS7/etc/yum.repos.d/CentOS-Base.repo.j2 b/roles/repo_baseos/templates/CentOS7/etc/yum.repos.d/CentOS-Base.repo.j2 index 5ab51aee..ee0ef83e 100644 --- a/roles/repo_baseos/templates/CentOS7/etc/yum.repos.d/CentOS-Base.repo.j2 +++ b/roles/repo_baseos/templates/CentOS7/etc/yum.repos.d/CentOS-Base.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122902 +# 2026052201 [base] name=CentOS-$releasever - Base @@ -11,7 +11,7 @@ mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo {% endif %} gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -27,7 +27,7 @@ mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo {% endif %} gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -43,7 +43,7 @@ mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo {% endif %} gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -60,7 +60,7 @@ mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo gpgcheck=1 enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-AppStream.repo.j2 b/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-AppStream.repo.j2 index 9172191c..56d2fabd 100644 --- a/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-AppStream.repo.j2 +++ b/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-AppStream.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # CentOS-Linux-AppStream.repo # @@ -22,7 +22,7 @@ mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-BaseOS.repo.j2 b/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-BaseOS.repo.j2 index 07530714..6f02ab8a 100644 --- a/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-BaseOS.repo.j2 +++ b/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-BaseOS.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # CentOS-Linux-BaseOS.repo # @@ -22,7 +22,7 @@ mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-Extras.repo.j2 b/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-Extras.repo.j2 index e06dcd80..d0961346 100644 --- a/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-Extras.repo.j2 +++ b/roles/repo_baseos/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-Extras.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # CentOS-Linux-Extras.repo # @@ -22,7 +22,7 @@ mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-addons.repo.j2 b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-addons.repo.j2 index f2e140c0..e33c6424 100644 --- a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-addons.repo.j2 +++ b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-addons.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026010801 +# 2026052201 # rocky-addons.repo # @@ -24,7 +24,7 @@ enabled=0 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -41,7 +41,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -58,7 +58,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -76,7 +76,7 @@ enabled=0 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -93,7 +93,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -110,7 +110,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -128,7 +128,7 @@ enabled=0 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -145,7 +145,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -162,7 +162,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -180,7 +180,7 @@ enabled=0 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -197,7 +197,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -214,7 +214,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -232,7 +232,7 @@ enabled=0 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -249,7 +249,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -266,7 +266,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-devel.repo.j2 b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-devel.repo.j2 index 0fbb257b..a6338b62 100644 --- a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-devel.repo.j2 +++ b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-devel.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026010801 +# 2026052201 # rocky-devel.repo # @@ -17,7 +17,7 @@ gpgcheck=1 enabled=0 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -34,7 +34,7 @@ gpgcheck=1 enabled=0 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -51,7 +51,7 @@ gpgcheck=1 enabled=0 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-extras.repo.j2 b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-extras.repo.j2 index db0fa0d2..50c47e9e 100644 --- a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-extras.repo.j2 +++ b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-extras.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026010801 +# 2026052201 # rocky-extras.repo # @@ -24,7 +24,7 @@ enabled=1 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -41,7 +41,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -58,7 +58,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -76,7 +76,7 @@ enabled=0 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -93,7 +93,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -110,7 +110,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-security.repo.j2 b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-security.repo.j2 index 3d5f6275..788fdabf 100644 --- a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-security.repo.j2 +++ b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky-security.repo.j2 @@ -24,7 +24,7 @@ enabled={{ repo_baseos__security_repo_enabled__combined_var | bool | ternary(1, countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -41,7 +41,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -58,7 +58,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky.repo.j2 b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky.repo.j2 index de8ab1ef..6dd7eab6 100644 --- a/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky.repo.j2 +++ b/roles/repo_baseos/templates/Rocky10/etc/yum.repos.d/rocky.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026010801 +# 2026052201 # rocky.repo # @@ -24,7 +24,7 @@ enabled=1 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -41,7 +41,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -59,7 +59,7 @@ enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -77,7 +77,7 @@ enabled=1 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -94,7 +94,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -111,7 +111,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -129,7 +129,7 @@ enabled={{ repo_baseos__crb_repo_enabled__combined_var | ternary(1, 0) }} countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -146,7 +146,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -163,7 +163,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-10 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-AppStream.repo.j2 b/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-AppStream.repo.j2 index bb40a098..106add8f 100644 --- a/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-AppStream.repo.j2 +++ b/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-AppStream.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # Rocky-AppStream.repo # @@ -22,7 +22,7 @@ mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=AppStre gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-BaseOS.repo.j2 b/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-BaseOS.repo.j2 index 70d51aae..79e6831f 100644 --- a/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-BaseOS.repo.j2 +++ b/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-BaseOS.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # Rocky-BaseOS.repo # @@ -22,7 +22,7 @@ mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=BaseOS- gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Extras.repo.j2 b/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Extras.repo.j2 index a5a353db..fe3f75d3 100644 --- a/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Extras.repo.j2 +++ b/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Extras.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # Rocky-Extras.repo # @@ -22,7 +22,7 @@ mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=extras- gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Security.repo.j2 b/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Security.repo.j2 index caf83ad0..98a163f5 100644 --- a/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Security.repo.j2 +++ b/roles/repo_baseos/templates/Rocky8/etc/yum.repos.d/Rocky-Security.repo.j2 @@ -22,7 +22,7 @@ mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=securit gpgcheck=1 enabled={{ repo_baseos__security_repo_enabled__combined_var | bool | ternary(1, 0) }} gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-extras.repo.j2 b/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-extras.repo.j2 index 84cc2aed..b04c088e 100644 --- a/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-extras.repo.j2 +++ b/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-extras.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # rocky-extras.repo # @@ -24,7 +24,7 @@ enabled=1 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -41,7 +41,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -58,7 +58,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -76,7 +76,7 @@ enabled=0 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -93,7 +93,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -110,7 +110,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-security.repo.j2 b/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-security.repo.j2 index d6a2a8be..5e40c7e7 100644 --- a/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-security.repo.j2 +++ b/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky-security.repo.j2 @@ -24,7 +24,7 @@ enabled={{ repo_baseos__security_repo_enabled__combined_var | bool | ternary(1, countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -41,7 +41,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -58,7 +58,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky.repo.j2 b/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky.repo.j2 index 64212d97..cc97e1b7 100644 --- a/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky.repo.j2 +++ b/roles/repo_baseos/templates/Rocky9/etc/yum.repos.d/rocky.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024120501 +# 2026052201 # rocky.repo # @@ -24,7 +24,7 @@ enabled=1 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -41,7 +41,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -59,7 +59,7 @@ enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -77,7 +77,7 @@ enabled=1 countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -94,7 +94,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -111,7 +111,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -129,7 +129,7 @@ enabled={{ repo_baseos__crb_repo_enabled__combined_var | ternary(1, 0) }} countme=1 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -146,7 +146,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} @@ -163,7 +163,7 @@ gpgcheck=1 enabled=0 metadata_expire=6h gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9 -{% if repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} +{% if repo_baseos__mirror_url is defined and repo_baseos__mirror_url | length and repo_baseos__basic_auth_login is defined and repo_baseos__basic_auth_login | length %} username={{ repo_baseos__basic_auth_login["username"] }} password={{ repo_baseos__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_collabora/README.md b/roles/repo_collabora/README.md index 3e9e6f42..7b9301cf 100644 --- a/roles/repo_collabora/README.md +++ b/roles/repo_collabora/README.md @@ -43,7 +43,7 @@ repo_collabora__version: '24.04' `repo_collabora__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_collabora/templates/etc/yum.repos.d/CollaboraOnline.repo.j2 b/roles/repo_collabora/templates/etc/yum.repos.d/CollaboraOnline.repo.j2 index 0ab13dcf..1aa5cb8a 100644 --- a/roles/repo_collabora/templates/etc/yum.repos.d/CollaboraOnline.repo.j2 +++ b/roles/repo_collabora/templates/etc/yum.repos.d/CollaboraOnline.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2025043001 +# 2026052201 [CollaboraOnline_{{ repo_collabora__version }}_customer] name = Collabora Office Customer repo @@ -9,7 +9,7 @@ baseurl={{ repo_collabora__mirror_url }}/CollaboraOnline/{{ repo_collabora__vers baseurl=https://www.collaboraoffice.com/repos/CollaboraOnline/{{ repo_collabora__version }}/customer-rpm-{{ repo_collabora__customer_token }} {% endif %} enabled=1 -{% if repo_collabora__basic_auth_login is defined and repo_collabora__basic_auth_login | length %} +{% if repo_collabora__mirror_url is defined and repo_collabora__mirror_url | length and repo_collabora__basic_auth_login is defined and repo_collabora__basic_auth_login | length %} username={{ repo_collabora__basic_auth_login["username"] }} password={{ repo_collabora__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_collabora_code/README.md b/roles/repo_collabora_code/README.md index edbb36a6..8bdf721e 100644 --- a/roles/repo_collabora_code/README.md +++ b/roles/repo_collabora_code/README.md @@ -18,7 +18,7 @@ This role deploys the official [Collabora CODE Repository](https://docs.fedorapr `repo_collabora_code__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat7-CollaboraOnline_CODE.repo.j2 b/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat7-CollaboraOnline_CODE.repo.j2 index ab49ff1a..0a725fd0 100644 --- a/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat7-CollaboraOnline_CODE.repo.j2 +++ b/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat7-CollaboraOnline_CODE.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [CollaboraOnline_CODE] name = Collabora Office YUM repo @@ -10,7 +10,7 @@ baseurl = https://www.collaboraoffice.com/repos/CollaboraOnline/CODE-centos7 {% endif %} enabled = 1 gpgcheck = 0 -{% if repo_collabora_code__basic_auth_login is defined and repo_collabora_code__basic_auth_login | length %} +{% if repo_collabora_code__mirror_url is defined and repo_collabora_code__mirror_url | length and repo_collabora_code__basic_auth_login is defined and repo_collabora_code__basic_auth_login | length %} username={{ repo_collabora_code__basic_auth_login["username"] }} password={{ repo_collabora_code__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat8-CollaboraOnline_CODE.repo.j2 b/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat8-CollaboraOnline_CODE.repo.j2 index c2c1f6db..e820cd98 100644 --- a/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat8-CollaboraOnline_CODE.repo.j2 +++ b/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat8-CollaboraOnline_CODE.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [CollaboraOnline_CODE] name = Collabora Office YUM repo @@ -10,7 +10,7 @@ baseurl = https://www.collaboraoffice.com/repos/CollaboraOnline/CODE-centos8 {% endif %} enabled = 1 gpgcheck = 0 -{% if repo_collabora_code__basic_auth_login is defined and repo_collabora_code__basic_auth_login | length %} +{% if repo_collabora_code__mirror_url is defined and repo_collabora_code__mirror_url | length and repo_collabora_code__basic_auth_login is defined and repo_collabora_code__basic_auth_login | length %} username={{ repo_collabora_code__basic_auth_login["username"] }} password={{ repo_collabora_code__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat9-CollaboraOnline_CODE.repo.j2 b/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat9-CollaboraOnline_CODE.repo.j2 index 69e34ade..7d514876 100644 --- a/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat9-CollaboraOnline_CODE.repo.j2 +++ b/roles/repo_collabora_code/templates/etc/yum.repos.d/RedHat9-CollaboraOnline_CODE.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024072301 +# 2026052201 [CollaboraOnline_CODE] name = Collabora Office YUM repo @@ -10,7 +10,7 @@ baseurl = https://www.collaboraoffice.com/repos/CollaboraOnline/CODE-rpm {% endif %} enabled = 1 gpgcheck = 0 -{% if repo_collabora_code__basic_auth_login is defined and repo_collabora_code__basic_auth_login | length %} +{% if repo_collabora_code__mirror_url is defined and repo_collabora_code__mirror_url | length and repo_collabora_code__basic_auth_login is defined and repo_collabora_code__basic_auth_login | length %} username={{ repo_collabora_code__basic_auth_login["username"] }} password={{ repo_collabora_code__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_docker/README.md b/roles/repo_docker/README.md index b45a7859..6d2349e2 100644 --- a/roles/repo_docker/README.md +++ b/roles/repo_docker/README.md @@ -18,7 +18,7 @@ This role deploys the package repository for [Docker CE](https://www.docker.com/ `repo_docker__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_docker/templates/etc/yum.repos.d/docker-ce.repo.j2 b/roles/repo_docker/templates/etc/yum.repos.d/docker-ce.repo.j2 index 3dabeb6b..0c7c3915 100644 --- a/roles/repo_docker/templates/etc/yum.repos.d/docker-ce.repo.j2 +++ b/roles/repo_docker/templates/etc/yum.repos.d/docker-ce.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [docker-ce-stable] name=Docker CE Stable - $basearch @@ -11,7 +11,7 @@ baseurl=https://download.docker.com/linux/centos/$releasever/$basearch/stable enabled=1 gpgcheck=1 gpgkey=https://download.docker.com/linux/centos/gpg -{% if repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} +{% if repo_docker__mirror_url is defined and repo_docker__mirror_url | length and repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} username={{ repo_docker__basic_auth_login["username"] }} password={{ repo_docker__basic_auth_login["password"] }} {% endif %} @@ -26,7 +26,7 @@ baseurl=https://download.docker.com/linux/centos/$releasever/debug-$basearch/sta enabled=0 gpgcheck=1 gpgkey=https://download.docker.com/linux/centos/gpg -{% if repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} +{% if repo_docker__mirror_url is defined and repo_docker__mirror_url | length and repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} username={{ repo_docker__basic_auth_login["username"] }} password={{ repo_docker__basic_auth_login["password"] }} {% endif %} @@ -41,7 +41,7 @@ baseurl=https://download.docker.com/linux/centos/$releasever/source/stable enabled=0 gpgcheck=1 gpgkey=https://download.docker.com/linux/centos/gpg -{% if repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} +{% if repo_docker__mirror_url is defined and repo_docker__mirror_url | length and repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} username={{ repo_docker__basic_auth_login["username"] }} password={{ repo_docker__basic_auth_login["password"] }} {% endif %} @@ -56,7 +56,7 @@ baseurl=https://download.docker.com/linux/centos/$releasever/$basearch/test enabled=0 gpgcheck=1 gpgkey=https://download.docker.com/linux/centos/gpg -{% if repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} +{% if repo_docker__mirror_url is defined and repo_docker__mirror_url | length and repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} username={{ repo_docker__basic_auth_login["username"] }} password={{ repo_docker__basic_auth_login["password"] }} {% endif %} @@ -71,7 +71,7 @@ baseurl=https://download.docker.com/linux/centos/$releasever/debug-$basearch/tes enabled=0 gpgcheck=1 gpgkey=https://download.docker.com/linux/centos/gpg -{% if repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} +{% if repo_docker__mirror_url is defined and repo_docker__mirror_url | length and repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} username={{ repo_docker__basic_auth_login["username"] }} password={{ repo_docker__basic_auth_login["password"] }} {% endif %} @@ -86,7 +86,7 @@ baseurl=https://download.docker.com/linux/centos/$releasever/source/test enabled=0 gpgcheck=1 gpgkey=https://download.docker.com/linux/centos/gpg -{% if repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} +{% if repo_docker__mirror_url is defined and repo_docker__mirror_url | length and repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} username={{ repo_docker__basic_auth_login["username"] }} password={{ repo_docker__basic_auth_login["password"] }} {% endif %} @@ -101,7 +101,7 @@ baseurl=https://download.docker.com/linux/centos/$releasever/$basearch/nightly enabled=0 gpgcheck=1 gpgkey=https://download.docker.com/linux/centos/gpg -{% if repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} +{% if repo_docker__mirror_url is defined and repo_docker__mirror_url | length and repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} username={{ repo_docker__basic_auth_login["username"] }} password={{ repo_docker__basic_auth_login["password"] }} {% endif %} @@ -116,7 +116,7 @@ baseurl=https://download.docker.com/linux/centos/$releasever/debug-$basearch/nig enabled=0 gpgcheck=1 gpgkey=https://download.docker.com/linux/centos/gpg -{% if repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} +{% if repo_docker__mirror_url is defined and repo_docker__mirror_url | length and repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} username={{ repo_docker__basic_auth_login["username"] }} password={{ repo_docker__basic_auth_login["password"] }} {% endif %} @@ -131,7 +131,7 @@ baseurl=https://download.docker.com/linux/centos/$releasever/source/nightly enabled=0 gpgcheck=1 gpgkey=https://download.docker.com/linux/centos/gpg -{% if repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} +{% if repo_docker__mirror_url is defined and repo_docker__mirror_url | length and repo_docker__basic_auth_login is defined and repo_docker__basic_auth_login | length %} username={{ repo_docker__basic_auth_login["username"] }} password={{ repo_docker__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_elasticsearch/README.md b/roles/repo_elasticsearch/README.md index 687b4c9a..ba81a77d 100644 --- a/roles/repo_elasticsearch/README.md +++ b/roles/repo_elasticsearch/README.md @@ -34,7 +34,7 @@ repo_elasticsearch__version: '8.x' `repo_elasticsearch__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_elasticsearch/templates/etc/yum.repos.d/elasticsearch.repo.j2 b/roles/repo_elasticsearch/templates/etc/yum.repos.d/elasticsearch.repo.j2 index 1100dd8f..38da983c 100644 --- a/roles/repo_elasticsearch/templates/etc/yum.repos.d/elasticsearch.repo.j2 +++ b/roles/repo_elasticsearch/templates/etc/yum.repos.d/elasticsearch.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2025101601 +# 2026052201 # This package contains both free and subscription features [elasticsearch-{{ repo_elasticsearch__version }}] @@ -13,7 +13,7 @@ gpgcheck=1 gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch enabled=1 type=rpm-md -{% if repo_elasticsearch__basic_auth_login is defined and repo_elasticsearch__basic_auth_login | length %} +{% if repo_elasticsearch__mirror_url is defined and repo_elasticsearch__mirror_url | length and repo_elasticsearch__basic_auth_login is defined and repo_elasticsearch__basic_auth_login | length %} username={{ repo_elasticsearch__basic_auth_login["username"] }} password={{ repo_elasticsearch__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/README.md b/roles/repo_epel/README.md index 6f161f38..7d8f5ee9 100644 --- a/roles/repo_epel/README.md +++ b/roles/repo_epel/README.md @@ -18,7 +18,7 @@ This role deploys the [Extra Packages for Enterprise Linux (EPEL) Repository](ht `repo_epel__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_epel/templates/AlmaLinux8/etc/yum.repos.d/almalinux-powertools.repo.j2 b/roles/repo_epel/templates/AlmaLinux8/etc/yum.repos.d/almalinux-powertools.repo.j2 index d4a605aa..01fa9ca9 100644 --- a/roles/repo_epel/templates/AlmaLinux8/etc/yum.repos.d/almalinux-powertools.repo.j2 +++ b/roles/repo_epel/templates/AlmaLinux8/etc/yum.repos.d/almalinux-powertools.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # almalinux-powertools.repo @@ -15,7 +15,7 @@ enabled=1 gpgcheck=1 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -28,7 +28,7 @@ mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/powertools-sourc enabled=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -41,7 +41,7 @@ mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/powertools-debug enabled=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-AlmaLinux -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-PowerTools.repo.j2 b/roles/repo_epel/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-PowerTools.repo.j2 index 5bddb95f..8759c6b1 100644 --- a/roles/repo_epel/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-PowerTools.repo.j2 +++ b/roles/repo_epel/templates/CentOS8/etc/yum.repos.d/CentOS-Linux-PowerTools.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # CentOS-Linux-PowerTools.repo # @@ -22,7 +22,7 @@ mirrorlist=http://mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/RedHat10/etc/yum.repos.d/epel-testing.repo.j2 b/roles/repo_epel/templates/RedHat10/etc/yum.repos.d/epel-testing.repo.j2 index 6d04ff61..931b8d19 100644 --- a/roles/repo_epel/templates/RedHat10/etc/yum.repos.d/epel-testing.repo.j2 +++ b/roles/repo_epel/templates/RedHat10/etc/yum.repos.d/epel-testing.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026010801 +# 2026052201 [epel-testing] name=Extra Packages for Enterprise Linux $releasever - Testing - $basearch @@ -18,7 +18,7 @@ repo_gpgcheck=0 metadata_expire=24h countme=1 enabled=0 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -39,7 +39,7 @@ gpgcheck=1 repo_gpgcheck=0 metadata_expire=24h enabled=0 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -60,7 +60,7 @@ gpgcheck=1 repo_gpgcheck=0 metadata_expire=24h enabled=0 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/RedHat10/etc/yum.repos.d/epel.repo.j2 b/roles/repo_epel/templates/RedHat10/etc/yum.repos.d/epel.repo.j2 index 2d6629a4..a734af4c 100644 --- a/roles/repo_epel/templates/RedHat10/etc/yum.repos.d/epel.repo.j2 +++ b/roles/repo_epel/templates/RedHat10/etc/yum.repos.d/epel.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026042401 +# 2026052201 [epel] name=Extra Packages for Enterprise Linux $releasever - $basearch @@ -18,7 +18,7 @@ repo_gpgcheck=0 metadata_expire=24h countme=1 enabled=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -39,7 +39,7 @@ gpgcheck=1 repo_gpgcheck=0 metadata_expire=24h enabled=0 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -60,7 +60,7 @@ gpgcheck=1 repo_gpgcheck=0 metadata_expire=24h enabled=0 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/RedHat7/etc/yum.repos.d/epel-testing.repo.j2 b/roles/repo_epel/templates/RedHat7/etc/yum.repos.d/epel-testing.repo.j2 index a3759a3c..3f31af2b 100644 --- a/roles/repo_epel/templates/RedHat7/etc/yum.repos.d/epel-testing.repo.j2 +++ b/roles/repo_epel/templates/RedHat7/etc/yum.repos.d/epel-testing.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [epel-testing] name=Extra Packages for Enterprise Linux 7 - Testing - $basearch @@ -12,7 +12,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=testing-epel7&arch=$bas enabled=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -28,7 +28,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=testing-debug-epel7&arc enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -44,7 +44,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=testing-source-epel7&ar enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/RedHat7/etc/yum.repos.d/epel.repo.j2 b/roles/repo_epel/templates/RedHat7/etc/yum.repos.d/epel.repo.j2 index 0a32041f..5ea8498f 100644 --- a/roles/repo_epel/templates/RedHat7/etc/yum.repos.d/epel.repo.j2 +++ b/roles/repo_epel/templates/RedHat7/etc/yum.repos.d/epel.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [epel] name=Extra Packages for Enterprise Linux 7 - $basearch @@ -12,7 +12,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-7&arch=$basearch enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -28,7 +28,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-debug-7&arch=$base enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -44,7 +44,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-source-7&arch=$bas enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/RedHat8/etc/yum.repos.d/epel-testing.repo.j2 b/roles/repo_epel/templates/RedHat8/etc/yum.repos.d/epel-testing.repo.j2 index 1da31a13..6cf44ffe 100644 --- a/roles/repo_epel/templates/RedHat8/etc/yum.repos.d/epel-testing.repo.j2 +++ b/roles/repo_epel/templates/RedHat8/etc/yum.repos.d/epel-testing.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [epel-testing] name=Extra Packages for Enterprise Linux $releasever - Testing - $basearch @@ -13,7 +13,7 @@ enabled=0 gpgcheck=1 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -29,7 +29,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=testing-debug-epel8&arc enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8 gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -45,7 +45,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=testing-source-epel8&ar enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8 gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/RedHat8/etc/yum.repos.d/epel.repo.j2 b/roles/repo_epel/templates/RedHat8/etc/yum.repos.d/epel.repo.j2 index 2702a6d9..f7a2888b 100644 --- a/roles/repo_epel/templates/RedHat8/etc/yum.repos.d/epel.repo.j2 +++ b/roles/repo_epel/templates/RedHat8/etc/yum.repos.d/epel.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [epel] name=Extra Packages for Enterprise Linux $releasever - $basearch @@ -13,7 +13,7 @@ enabled=1 gpgcheck=1 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -29,7 +29,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-debug-8&arch=$base enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8 gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -45,7 +45,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-source-8&arch=$bas enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8 gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel-cisco-openh264.repo.j2 b/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel-cisco-openh264.repo.j2 index ce1aeddc..4ac7926c 100644 --- a/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel-cisco-openh264.repo.j2 +++ b/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel-cisco-openh264.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2025052201 +# 2026052201 [epel-cisco-openh264] name=Extra Packages for Enterprise Linux $releasever openh264 (From Cisco) - $basearch @@ -15,7 +15,7 @@ repo_gpgcheck=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-$releasever skip_if_unavailable=True -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -34,7 +34,7 @@ repo_gpgcheck=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-$releasever skip_if_unavailable=True -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -53,7 +53,7 @@ repo_gpgcheck=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-$releasever skip_if_unavailable=True -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel-testing.repo.j2 b/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel-testing.repo.j2 index 898485ec..f4575fc4 100644 --- a/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel-testing.repo.j2 +++ b/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel-testing.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024080201 +# 2026052201 [epel-testing] name=Extra Packages for Enterprise Linux $releasever - Testing - $basearch @@ -16,7 +16,7 @@ enabled=0 gpgcheck=1 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-$releasever -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -35,7 +35,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=testing-debug-epel$rele enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-$releasever gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -54,7 +54,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=testing-source-epel$rel enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-$releasever gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel.repo.j2 b/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel.repo.j2 index f8f87643..b44caa7a 100644 --- a/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel.repo.j2 +++ b/roles/repo_epel/templates/RedHat9/etc/yum.repos.d/epel.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024080201 +# 2026052201 [epel] name=Extra Packages for Enterprise Linux $releasever - $basearch @@ -16,7 +16,7 @@ enabled=1 gpgcheck=1 countme=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-$releasever -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -35,7 +35,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-debug-$releasever& enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-$releasever gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} @@ -54,7 +54,7 @@ metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-source-$releasever enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-$releasever gpgcheck=1 -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_epel/templates/Rocky8/etc/yum.repos.d/Rocky-PowerTools.repo.j2 b/roles/repo_epel/templates/Rocky8/etc/yum.repos.d/Rocky-PowerTools.repo.j2 index fb4de84e..cea4bb42 100644 --- a/roles/repo_epel/templates/Rocky8/etc/yum.repos.d/Rocky-PowerTools.repo.j2 +++ b/roles/repo_epel/templates/Rocky8/etc/yum.repos.d/Rocky-PowerTools.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # Rocky-PowerTools.repo # @@ -22,7 +22,7 @@ mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=PowerTo gpgcheck=1 enabled=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rockyofficial -{% if repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} +{% if repo_epel__mirror_url is defined and repo_epel__mirror_url | length and repo_epel__basic_auth_login is defined and repo_epel__basic_auth_login | length %} username={{ repo_epel__basic_auth_login["username"] }} password={{ repo_epel__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_gitlab_ce/README.md b/roles/repo_gitlab_ce/README.md index 6c66dcfd..3b4a9fe2 100644 --- a/roles/repo_gitlab_ce/README.md +++ b/roles/repo_gitlab_ce/README.md @@ -18,7 +18,7 @@ This role deploys the package repository for [GitLab CE](https://about.gitlab.co `repo_gitlab_ce__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_gitlab_ce/templates/etc/yum.repos.d/gitlab_gitlab-ce.repo.j2 b/roles/repo_gitlab_ce/templates/etc/yum.repos.d/gitlab_gitlab-ce.repo.j2 index d63ca0c0..97447dd6 100644 --- a/roles/repo_gitlab_ce/templates/etc/yum.repos.d/gitlab_gitlab-ce.repo.j2 +++ b/roles/repo_gitlab_ce/templates/etc/yum.repos.d/gitlab_gitlab-ce.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2025042401 +# 2026052201 [gitlab_gitlab-ce] name=gitlab_gitlab-ce @@ -17,7 +17,7 @@ enabled=1 sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt metadata_expire=300 -{% if repo_gitlab_ce__basic_auth_login is defined and repo_gitlab_ce__basic_auth_login | length %} +{% if repo_gitlab_ce__mirror_url is defined and repo_gitlab_ce__mirror_url | length and repo_gitlab_ce__basic_auth_login is defined and repo_gitlab_ce__basic_auth_login | length %} username={{ repo_gitlab_ce__basic_auth_login["username"] }} password={{ repo_gitlab_ce__basic_auth_login["password"] }} {% endif %} @@ -38,7 +38,7 @@ enabled=0 sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt metadata_expire=300 -{% if repo_gitlab_ce__basic_auth_login is defined and repo_gitlab_ce__basic_auth_login | length %} +{% if repo_gitlab_ce__mirror_url is defined and repo_gitlab_ce__mirror_url | length and repo_gitlab_ce__basic_auth_login is defined and repo_gitlab_ce__basic_auth_login | length %} username={{ repo_gitlab_ce__basic_auth_login["username"] }} password={{ repo_gitlab_ce__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_gitlab_runner/README.md b/roles/repo_gitlab_runner/README.md index 96f081a7..43490f76 100644 --- a/roles/repo_gitlab_runner/README.md +++ b/roles/repo_gitlab_runner/README.md @@ -18,7 +18,7 @@ This role deploys the package repository for [GitLab Runner](https://docs.gitlab `repo_gitlab_runner__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_gitlab_runner/templates/etc/yum.repos.d/gitlab_gitlab-ce.repo.j2 b/roles/repo_gitlab_runner/templates/etc/yum.repos.d/gitlab_gitlab-ce.repo.j2 index 9422e87e..5dd5ad7e 100644 --- a/roles/repo_gitlab_runner/templates/etc/yum.repos.d/gitlab_gitlab-ce.repo.j2 +++ b/roles/repo_gitlab_runner/templates/etc/yum.repos.d/gitlab_gitlab-ce.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [runner_gitlab-runner] name=runner_gitlab-runner @@ -17,7 +17,7 @@ gpgkey=https://packages.gitlab.com/runner/gitlab-runner/gpgkey sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt metadata_expire=300 -{% if repo_gitlab_runner__basic_auth_login is defined and repo_gitlab_runner__basic_auth_login | length %} +{% if repo_gitlab_runner__mirror_url is defined and repo_gitlab_runner__mirror_url | length and repo_gitlab_runner__basic_auth_login is defined and repo_gitlab_runner__basic_auth_login | length %} username={{ repo_gitlab_runner__basic_auth_login["username"] }} password={{ repo_gitlab_runner__basic_auth_login["password"] }} {% endif %} @@ -38,7 +38,7 @@ gpgkey=https://packages.gitlab.com/runner/gitlab-runner/gpgkey sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt metadata_expire=300 -{% if repo_gitlab_runner__basic_auth_login is defined and repo_gitlab_runner__basic_auth_login | length %} +{% if repo_gitlab_runner__mirror_url is defined and repo_gitlab_runner__mirror_url | length and repo_gitlab_runner__basic_auth_login is defined and repo_gitlab_runner__basic_auth_login | length %} username={{ repo_gitlab_runner__basic_auth_login["username"] }} password={{ repo_gitlab_runner__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_grafana/README.md b/roles/repo_grafana/README.md index b7068032..31c6bf61 100644 --- a/roles/repo_grafana/README.md +++ b/roles/repo_grafana/README.md @@ -18,7 +18,7 @@ This role deploys the package repository for [Grafana OSS](https://grafana.com/o `repo_grafana__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_grafana/templates/etc/yum.repos.d/grafana.repo.j2 b/roles/repo_grafana/templates/etc/yum.repos.d/grafana.repo.j2 index 89ec9ec7..a66c7556 100644 --- a/roles/repo_grafana/templates/etc/yum.repos.d/grafana.repo.j2 +++ b/roles/repo_grafana/templates/etc/yum.repos.d/grafana.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024050901 +# 2026052201 [grafana] name=grafana @@ -16,7 +16,7 @@ sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt # To prevent beta versions from being installed, add the following exclude line to your .repo file. exclude=*beta* -{% if repo_grafana__basic_auth_login is defined and repo_grafana__basic_auth_login | length %} +{% if repo_grafana__mirror_url is defined and repo_grafana__mirror_url | length and repo_grafana__basic_auth_login is defined and repo_grafana__basic_auth_login | length %} username={{ repo_grafana__basic_auth_login["username"] }} password={{ repo_grafana__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_graylog/README.md b/roles/repo_graylog/README.md index 049c978b..82e1934c 100644 --- a/roles/repo_graylog/README.md +++ b/roles/repo_graylog/README.md @@ -32,7 +32,7 @@ repo_graylog__version: '5.2' `repo_graylog__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_graylog/templates/etc/yum.repos.d/graylog.repo.j2 b/roles/repo_graylog/templates/etc/yum.repos.d/graylog.repo.j2 index 2211eb12..f147a205 100644 --- a/roles/repo_graylog/templates/etc/yum.repos.d/graylog.repo.j2 +++ b/roles/repo_graylog/templates/etc/yum.repos.d/graylog.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024110701 +# 2026052201 # https://docs.graylog.org/docs/operating-system-packages [graylog-{{ repo_graylog__version }}] @@ -12,7 +12,7 @@ baseurl=https://downloads.graylog.org/repo/el/stable/{{ repo_graylog__version }} gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-graylog -{% if repo_graylog__basic_auth_login is defined and repo_graylog__basic_auth_login | length %} +{% if repo_graylog__mirror_url is defined and repo_graylog__mirror_url | length and repo_graylog__basic_auth_login is defined and repo_graylog__basic_auth_login | length %} username={{ repo_graylog__basic_auth_login["username"] }} password={{ repo_graylog__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_influxdb/README.md b/roles/repo_influxdb/README.md index 8623706d..afe37774 100644 --- a/roles/repo_influxdb/README.md +++ b/roles/repo_influxdb/README.md @@ -18,7 +18,7 @@ This role deploys the package repository for [InfluxDB](https://www.influxdata.c `repo_influxdb__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_influxdb/templates/etc/yum.repos.d/influxdb.repo.j2 b/roles/repo_influxdb/templates/etc/yum.repos.d/influxdb.repo.j2 index 3ab5a399..a1bf19bb 100644 --- a/roles/repo_influxdb/templates/etc/yum.repos.d/influxdb.repo.j2 +++ b/roles/repo_influxdb/templates/etc/yum.repos.d/influxdb.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026042901 +# 2026052201 [influxdb] name = InfluxDB Repository - RHEL $releasever @@ -14,7 +14,7 @@ baseurl = https://repos.influxdata.com/rhel/$releasever/$basearch/stable enabled = 1 gpgcheck = 1 gpgkey = file:///etc/pki/rpm-gpg/influxdata-archive.key -{% if repo_influxdb__basic_auth_login is defined and repo_influxdb__basic_auth_login | length %} +{% if repo_influxdb__mirror_url is defined and repo_influxdb__mirror_url | length and repo_influxdb__basic_auth_login is defined and repo_influxdb__basic_auth_login | length %} username={{ repo_influxdb__basic_auth_login["username"] }} password={{ repo_influxdb__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_mariadb/README.md b/roles/repo_mariadb/README.md index 8a2be917..8c7cbc6e 100644 --- a/roles/repo_mariadb/README.md +++ b/roles/repo_mariadb/README.md @@ -32,7 +32,7 @@ repo_mariadb__version: '10.6' `repo_mariadb__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_mariadb/templates/etc/yum.repos.d/RedHat-MariaDB.repo.j2 b/roles/repo_mariadb/templates/etc/yum.repos.d/RedHat-MariaDB.repo.j2 index d0e77aaf..5b566ef3 100644 --- a/roles/repo_mariadb/templates/etc/yum.repos.d/RedHat-MariaDB.repo.j2 +++ b/roles/repo_mariadb/templates/etc/yum.repos.d/RedHat-MariaDB.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026042701 +# 2026052201 [mariadb-main] name = MariaDB Server @@ -14,7 +14,7 @@ enabled = 1 {% if ansible_facts["distribution_major_version"] | int in [8, 9] %} module_hotfixes = 1 {% endif %} -{% if repo_mariadb__basic_auth_login is defined and repo_mariadb__basic_auth_login | length %} +{% if repo_mariadb__mirror_url is defined and repo_mariadb__mirror_url | length and repo_mariadb__basic_auth_login is defined and repo_mariadb__basic_auth_login | length %} username={{ repo_mariadb__basic_auth_login["username"] }} password={{ repo_mariadb__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_mongodb/README.md b/roles/repo_mongodb/README.md index dac62bc5..7210e8aa 100644 --- a/roles/repo_mongodb/README.md +++ b/roles/repo_mongodb/README.md @@ -32,7 +32,7 @@ repo_mongodb__version: '6.0' `repo_mongodb__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_mongodb/templates/etc/yum.repos.d/mongodb-org.repo.j2 b/roles/repo_mongodb/templates/etc/yum.repos.d/mongodb-org.repo.j2 index f1e0169d..ec4a3501 100644 --- a/roles/repo_mongodb/templates/etc/yum.repos.d/mongodb-org.repo.j2 +++ b/roles/repo_mongodb/templates/etc/yum.repos.d/mongodb-org.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024020801 +# 2026052201 [mongodb-org-{{ repo_mongodb__version }}] name=MongoDB Repository for Version {{ repo_mongodb__version }} @@ -12,7 +12,7 @@ enabled=1 gpgcheck=1 # gpgkey=https://www.mongodb.org/static/pgp/server-{{ repo_mongodb__version }}.asc gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-MONGODB.asc -{% if repo_mongodb__basic_auth_login is defined and repo_mongodb__basic_auth_login | length %} +{% if repo_mongodb__mirror_url is defined and repo_mongodb__mirror_url | length and repo_mongodb__basic_auth_login is defined and repo_mongodb__basic_auth_login | length %} username={{ repo_mongodb__basic_auth_login["username"] }} password={{ repo_mongodb__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_monitoring_plugins/README.md b/roles/repo_monitoring_plugins/README.md index f16316c6..03b67c82 100644 --- a/roles/repo_monitoring_plugins/README.md +++ b/roles/repo_monitoring_plugins/README.md @@ -18,7 +18,7 @@ This role deploys the repository at repo.linuxfabrik.ch for the Linuxfabrik Moni `repo_monitoring_plugins__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_monitoring_plugins/templates/etc/yum.repos.d/linuxfabrik-monitoring-plugins.repo.j2 b/roles/repo_monitoring_plugins/templates/etc/yum.repos.d/linuxfabrik-monitoring-plugins.repo.j2 index 92a7ca74..69788724 100644 --- a/roles/repo_monitoring_plugins/templates/etc/yum.repos.d/linuxfabrik-monitoring-plugins.repo.j2 +++ b/roles/repo_monitoring_plugins/templates/etc/yum.repos.d/linuxfabrik-monitoring-plugins.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026050701 +# 2026052201 [linuxfabrik-monitoring-plugins-release] name=Linuxfabrik Monitoring Plugins (release) @@ -11,7 +11,7 @@ baseurl=https://repo.linuxfabrik.ch/monitoring-plugins/rhel/$releasever/release/ enabled={{ 0 if repo_monitoring_plugins__testing | bool else 1 }} gpgcheck=1 gpgkey=https://repo.linuxfabrik.ch/linuxfabrik.key -{% if repo_monitoring_plugins__basic_auth_login is defined and repo_monitoring_plugins__basic_auth_login | length %} +{% if repo_monitoring_plugins__mirror_url is defined and repo_monitoring_plugins__mirror_url | length and repo_monitoring_plugins__basic_auth_login is defined and repo_monitoring_plugins__basic_auth_login | length %} username={{ repo_monitoring_plugins__basic_auth_login["username"] }} password={{ repo_monitoring_plugins__basic_auth_login["password"] }} {% endif %} @@ -26,7 +26,7 @@ baseurl=https://repo.linuxfabrik.ch/monitoring-plugins/rhel/$releasever/testing/ enabled={{ 1 if repo_monitoring_plugins__testing | bool else 0 }} gpgcheck=1 gpgkey=https://repo.linuxfabrik.ch/linuxfabrik.key -{% if repo_monitoring_plugins__basic_auth_login is defined and repo_monitoring_plugins__basic_auth_login | length %} +{% if repo_monitoring_plugins__mirror_url is defined and repo_monitoring_plugins__mirror_url | length and repo_monitoring_plugins__basic_auth_login is defined and repo_monitoring_plugins__basic_auth_login | length %} username={{ repo_monitoring_plugins__basic_auth_login["username"] }} password={{ repo_monitoring_plugins__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_monitoring_plugins/templates/etc/zypp/repos.d/linuxfabrik-monitoring-plugins.repo.j2 b/roles/repo_monitoring_plugins/templates/etc/zypp/repos.d/linuxfabrik-monitoring-plugins.repo.j2 index c39a5f7b..9fbe9031 100644 --- a/roles/repo_monitoring_plugins/templates/etc/zypp/repos.d/linuxfabrik-monitoring-plugins.repo.j2 +++ b/roles/repo_monitoring_plugins/templates/etc/zypp/repos.d/linuxfabrik-monitoring-plugins.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026050801 +# 2026052201 [linuxfabrik-monitoring-plugins-release] name=Linuxfabrik Monitoring Plugins (release) @@ -11,7 +11,7 @@ baseurl=https://repo.linuxfabrik.ch/monitoring-plugins/sle/$releasever_major/rel enabled={{ 0 if repo_monitoring_plugins__testing | bool else 1 }} gpgcheck=1 gpgkey=https://repo.linuxfabrik.ch/linuxfabrik.key -{% if repo_monitoring_plugins__basic_auth_login is defined and repo_monitoring_plugins__basic_auth_login | length %} +{% if repo_monitoring_plugins__mirror_url is defined and repo_monitoring_plugins__mirror_url | length and repo_monitoring_plugins__basic_auth_login is defined and repo_monitoring_plugins__basic_auth_login | length %} username={{ repo_monitoring_plugins__basic_auth_login["username"] }} password={{ repo_monitoring_plugins__basic_auth_login["password"] }} {% endif %} @@ -26,7 +26,7 @@ baseurl=https://repo.linuxfabrik.ch/monitoring-plugins/sle/$releasever_major/tes enabled={{ 1 if repo_monitoring_plugins__testing | bool else 0 }} gpgcheck=1 gpgkey=https://repo.linuxfabrik.ch/linuxfabrik.key -{% if repo_monitoring_plugins__basic_auth_login is defined and repo_monitoring_plugins__basic_auth_login | length %} +{% if repo_monitoring_plugins__mirror_url is defined and repo_monitoring_plugins__mirror_url | length and repo_monitoring_plugins__basic_auth_login is defined and repo_monitoring_plugins__basic_auth_login | length %} username={{ repo_monitoring_plugins__basic_auth_login["username"] }} password={{ repo_monitoring_plugins__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_mydumper/README.md b/roles/repo_mydumper/README.md index 37a9e0ee..87e515ed 100644 --- a/roles/repo_mydumper/README.md +++ b/roles/repo_mydumper/README.md @@ -19,7 +19,7 @@ Note that Linuxfabrik currently uses its [own repository server](https://repo.li `repo_mydumper__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_mydumper/templates/etc/yum.repos.d/mydumper.repo.j2 b/roles/repo_mydumper/templates/etc/yum.repos.d/mydumper.repo.j2 index 2ff24b28..fde4c836 100644 --- a/roles/repo_mydumper/templates/etc/yum.repos.d/mydumper.repo.j2 +++ b/roles/repo_mydumper/templates/etc/yum.repos.d/mydumper.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024021301 +# 2026052201 [mydumper] name = mydumper @@ -10,7 +10,7 @@ baseurl=https://repo.linuxfabrik.ch/mydumper/el/{{ ansible_facts["distribution_m {% endif %} gpgcheck = 0 enabled = 1 -{% if repo_mydumper__basic_auth_login is defined and repo_mydumper__basic_auth_login | length %} +{% if repo_mydumper__mirror_url is defined and repo_mydumper__mirror_url | length and repo_mydumper__basic_auth_login is defined and repo_mydumper__basic_auth_login | length %} username={{ repo_mydumper__basic_auth_login["username"] }} password={{ repo_mydumper__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_opensearch/README.md b/roles/repo_opensearch/README.md index 7ae10554..f0f8a764 100644 --- a/roles/repo_opensearch/README.md +++ b/roles/repo_opensearch/README.md @@ -32,7 +32,7 @@ repo_opensearch__version__host_var: '2.x' `repo_opensearch__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_opensearch/templates/etc/yum.repos.d/opensearch.repo.j2 b/roles/repo_opensearch/templates/etc/yum.repos.d/opensearch.repo.j2 index f8f7caf2..9bb002c7 100644 --- a/roles/repo_opensearch/templates/etc/yum.repos.d/opensearch.repo.j2 +++ b/roles/repo_opensearch/templates/etc/yum.repos.d/opensearch.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024050101 +# 2026052201 [opensearch-{{ repo_opensearch__version__combined_var }}] name=OpenSearch {{ repo_opensearch__version__combined_var }} @@ -15,7 +15,7 @@ gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-opensearch autorefresh=1 type=rpm-md -{% if repo_opensearch__basic_auth_login is defined and repo_opensearch__basic_auth_login | length %} +{% if repo_opensearch__mirror_url is defined and repo_opensearch__mirror_url | length and repo_opensearch__basic_auth_login is defined and repo_opensearch__basic_auth_login | length %} username={{ repo_opensearch__basic_auth_login["username"] }} password={{ repo_opensearch__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_postgresql/README.md b/roles/repo_postgresql/README.md index d89f9076..d9ca4604 100644 --- a/roles/repo_postgresql/README.md +++ b/roles/repo_postgresql/README.md @@ -18,7 +18,7 @@ This role deploys the official [PostgreSQL Repo](https://www.postgresql.org/down `repo_postgresql__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_postgresql/templates/etc/yum.repos.d/pgdg-redhat-all.repo.j2 b/roles/repo_postgresql/templates/etc/yum.repos.d/pgdg-redhat-all.repo.j2 index d0438ed9..f5d43378 100644 --- a/roles/repo_postgresql/templates/etc/yum.repos.d/pgdg-redhat-all.repo.j2 +++ b/roles/repo_postgresql/templates/etc/yum.repos.d/pgdg-redhat-all.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026011201 +# 2026052201 {# taken from the RPM package at https://yum.postgresql.org/repopackages/ #} ######################################################################### @@ -20,7 +20,7 @@ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/PGDG-RPM-GPG-KEY-RHEL repo_gpgcheck = 1 -{% if repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} +{% if repo_postgresql__mirror_url is defined and repo_postgresql__mirror_url | length and repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} username={{ repo_postgresql__basic_auth_login["username"] }} password={{ repo_postgresql__basic_auth_login["password"] }} {% endif %} @@ -39,7 +39,7 @@ enabled=0 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/PGDG-RPM-GPG-KEY-RHEL repo_gpgcheck = 1 -{% if repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} +{% if repo_postgresql__mirror_url is defined and repo_postgresql__mirror_url | length and repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} username={{ repo_postgresql__basic_auth_login["username"] }} password={{ repo_postgresql__basic_auth_login["password"] }} {% endif %} @@ -57,7 +57,7 @@ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/PGDG-RPM-GPG-KEY-RHEL repo_gpgcheck = 1 -{% if repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} +{% if repo_postgresql__mirror_url is defined and repo_postgresql__mirror_url | length and repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} username={{ repo_postgresql__basic_auth_login["username"] }} password={{ repo_postgresql__basic_auth_login["password"] }} {% endif %} @@ -73,7 +73,7 @@ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/PGDG-RPM-GPG-KEY-RHEL repo_gpgcheck = 1 -{% if repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} +{% if repo_postgresql__mirror_url is defined and repo_postgresql__mirror_url | length and repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} username={{ repo_postgresql__basic_auth_login["username"] }} password={{ repo_postgresql__basic_auth_login["password"] }} {% endif %} @@ -89,7 +89,7 @@ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/PGDG-RPM-GPG-KEY-RHEL repo_gpgcheck = 1 -{% if repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} +{% if repo_postgresql__mirror_url is defined and repo_postgresql__mirror_url | length and repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} username={{ repo_postgresql__basic_auth_login["username"] }} password={{ repo_postgresql__basic_auth_login["password"] }} {% endif %} @@ -105,7 +105,7 @@ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/PGDG-RPM-GPG-KEY-RHEL repo_gpgcheck = 1 -{% if repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} +{% if repo_postgresql__mirror_url is defined and repo_postgresql__mirror_url | length and repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} username={{ repo_postgresql__basic_auth_login["username"] }} password={{ repo_postgresql__basic_auth_login["password"] }} {% endif %} @@ -121,7 +121,7 @@ enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/PGDG-RPM-GPG-KEY-RHEL repo_gpgcheck = 1 -{% if repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} +{% if repo_postgresql__mirror_url is defined and repo_postgresql__mirror_url | length and repo_postgresql__basic_auth_login is defined and repo_postgresql__basic_auth_login | length %} username={{ repo_postgresql__basic_auth_login["username"] }} password={{ repo_postgresql__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_proxysql/README.md b/roles/repo_proxysql/README.md index d51a689a..3ca769fa 100644 --- a/roles/repo_proxysql/README.md +++ b/roles/repo_proxysql/README.md @@ -32,7 +32,7 @@ repo_proxysql__version__host_var: '2.7' # or '2.6', '2.5', '2.4', '2.3', '2.2' `repo_proxysql__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: Dictionary. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_proxysql/templates/etc/yum.repos.d/proxysql.repo.j2 b/roles/repo_proxysql/templates/etc/yum.repos.d/proxysql.repo.j2 index 0148ea3a..5a68d603 100644 --- a/roles/repo_proxysql/templates/etc/yum.repos.d/proxysql.repo.j2 +++ b/roles/repo_proxysql/templates/etc/yum.repos.d/proxysql.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2025010901 +# 2026052201 [proxysql-{{ repo_proxysql__version__combined_var }}] name=ProxySQL {{ repo_proxysql__version__combined_var }} @@ -13,7 +13,7 @@ gpgcheck=1 # gpgkey=https://repo.proxysql.com/ProxySQL/proxysql-{{ repo_proxysql__version__combined_var }}.x/repo_pub_key gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-proxysql autorefresh=1 -{% if repo_proxysql__basic_auth_login is defined and repo_proxysql__basic_auth_login | length %} +{% if repo_proxysql__mirror_url is defined and repo_proxysql__mirror_url | length and repo_proxysql__basic_auth_login is defined and repo_proxysql__basic_auth_login | length %} username={{ repo_proxysql__basic_auth_login["username"] }} password={{ repo_proxysql__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_remi/README.md b/roles/repo_remi/README.md index ba1ebcd2..8a973c02 100644 --- a/roles/repo_remi/README.md +++ b/roles/repo_remi/README.md @@ -18,7 +18,7 @@ This role deploys the [Remi's RPM repository](https://rpms.remirepo.net/). `repo_remi__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi-modular.repo.j2 b/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi-modular.repo.j2 index 3806de4b..8cbf227b 100644 --- a/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi-modular.repo.j2 +++ b/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi-modular.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052201 # Repository: https://rpms.remirepo.net/ # Blog: https://blog.remirepo.net/ @@ -19,7 +19,7 @@ gpgcheck=1 # can be enabled if not behind a proxy because of possible cache issue repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el10 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -37,7 +37,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el10 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -55,7 +55,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el10 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -73,7 +73,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el10 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi-safe.repo.j2 b/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi-safe.repo.j2 index b39cb50e..aedf4836 100644 --- a/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi-safe.repo.j2 +++ b/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi-safe.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052201 # This repository is safe to use with RHEL/CentOS base repository # it only provides additional packages for the PHP stack @@ -19,7 +19,7 @@ gpgcheck=1 # can be enabled if not behind a proxy because of possible cache issue repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el10 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -35,7 +35,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el10 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi.repo.j2 b/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi.repo.j2 index eb178d68..660140bd 100644 --- a/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi.repo.j2 +++ b/roles/repo_remi/templates/RedHat10/etc/yum.repos.d/remi.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026051201 +# 2026052201 # Repository: https://rpms.remirepo.net/ # Blog: https://blog.remirepo.net/ @@ -19,7 +19,7 @@ gpgcheck=1 # can be enabled if not behind a proxy because of possible cache issue repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el10 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -38,7 +38,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el10 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -54,7 +54,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el10 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -70,7 +70,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el10 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi-modular.repo.j2 b/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi-modular.repo.j2 index 33564803..52034731 100644 --- a/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi-modular.repo.j2 +++ b/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi-modular.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # Repository: https://rpms.remirepo.net/ # Blog: https://blog.remirepo.net/ @@ -19,7 +19,7 @@ gpgcheck=1 # can be enabled if not behind a proxy because of possible cache issue repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el8 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -51,7 +51,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el8 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi-safe.repo.j2 b/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi-safe.repo.j2 index 41d8ed25..08994c57 100644 --- a/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi-safe.repo.j2 +++ b/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi-safe.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # This repository is safe to use with RHEL/CentOS base repository # it only provides additional packages for the PHP stack @@ -19,7 +19,7 @@ gpgcheck=1 # can be enabled if not behind a proxy because of possible cache issue repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el8 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -35,7 +35,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el8 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi.repo.j2 b/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi.repo.j2 index e84091db..2c9e5fae 100644 --- a/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi.repo.j2 +++ b/roles/repo_remi/templates/RedHat8/etc/yum.repos.d/remi.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 # Repository: https://rpms.remirepo.net/ # Blog: https://blog.remirepo.net/ @@ -19,7 +19,7 @@ gpgcheck=1 # can be enabled if not behind a proxy because of possible cache issue repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el8 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -38,7 +38,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el8 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -54,7 +54,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el8 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -70,7 +70,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el8 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi-modular.repo.j2 b/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi-modular.repo.j2 index d277ce52..e13b623f 100644 --- a/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi-modular.repo.j2 +++ b/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi-modular.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024050901 +# 2026052201 # Repository: https://rpms.remirepo.net/ # Blog: https://blog.remirepo.net/ @@ -19,7 +19,7 @@ gpgcheck=1 # can be enabled if not being a proxy because of possible cache issue repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el9 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -37,7 +37,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el9 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -55,7 +55,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el9 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -73,7 +73,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el9 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi-safe.repo.j2 b/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi-safe.repo.j2 index 9774432e..a8490028 100644 --- a/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi-safe.repo.j2 +++ b/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi-safe.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024050901 +# 2026052201 # This repository is safe to use with RHEL/CentOS base repository # it only provides additional packages for the PHP stack @@ -19,7 +19,7 @@ gpgcheck=1 # can be enabled if not being a proxy because of possible cache issue repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el9 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -35,7 +35,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el9 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi.repo.j2 b/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi.repo.j2 index 5cb54827..e8ec2170 100644 --- a/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi.repo.j2 +++ b/roles/repo_remi/templates/RedHat9/etc/yum.repos.d/remi.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024050901 +# 2026052201 # Repository: https://rpms.remirepo.net/ # Blog: https://blog.remirepo.net/ @@ -19,7 +19,7 @@ gpgcheck=1 # can be enabled if not being a proxy because of possible cache issue repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el9 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -38,7 +38,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el9 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -54,7 +54,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el9 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} @@ -70,7 +70,7 @@ enabled=0 gpgcheck=1 repo_gpgcheck=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-remi.el9 -{% if repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} +{% if repo_remi__mirror_url is defined and repo_remi__mirror_url | length and repo_remi__basic_auth_login is defined and repo_remi__basic_auth_login | length %} username={{ repo_remi__basic_auth_login["username"] }} password={{ repo_remi__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_rpmfusion/README.md b/roles/repo_rpmfusion/README.md index 5dacb9ab..037ff725 100644 --- a/roles/repo_rpmfusion/README.md +++ b/roles/repo_rpmfusion/README.md @@ -25,7 +25,7 @@ Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/RE `repo_rpmfusion__basic_auth_login` -* Use HTTP basic auth to login to the repository. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. +* Use HTTP basic auth to login to the repository. Only takes effect together with a custom mirror URL; the default public repositories do not use basic auth. Defaults to `lfops__repo_basic_auth_login`, making it easy to set this for all `repo_*` roles. * Type: String. * Default: `'{{ lfops__repo_basic_auth_login | default("") }}'` diff --git a/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-free-updates-testing.repo.j2 b/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-free-updates-testing.repo.j2 index 92eeea00..d7f961f0 100644 --- a/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-free-updates-testing.repo.j2 +++ b/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-free-updates-testing.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [rpmfusion-free-updates-testing] name=RPM Fusion for EL 8 - Free - Test Updates @@ -12,7 +12,7 @@ enabled=0 type=rpm-md gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-free-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} @@ -24,7 +24,7 @@ enabled=0 type=rpm-md gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-free-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} @@ -36,7 +36,7 @@ enabled=0 type=rpm-md gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-free-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-free-updates.repo.j2 b/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-free-updates.repo.j2 index f98f34b1..39834fdf 100644 --- a/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-free-updates.repo.j2 +++ b/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-free-updates.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [rpmfusion-free-updates] name=RPM Fusion for EL 8 - Free - Updates @@ -11,7 +11,7 @@ mirrorlist=http://mirrors.rpmfusion.org/mirrorlist?repo=free-el-updates-released enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-free-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} @@ -23,7 +23,7 @@ enabled=0 type=rpm-md gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-free-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} @@ -35,7 +35,7 @@ enabled=0 type=rpm-md gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-free-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-nonfree-updates-testing.repo.j2 b/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-nonfree-updates-testing.repo.j2 index 9505e6ac..58832b78 100644 --- a/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-nonfree-updates-testing.repo.j2 +++ b/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-nonfree-updates-testing.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [rpmfusion-nonfree-updates-testing] name=RPM Fusion for EL 8 - Nonfree - Test Updates @@ -12,7 +12,7 @@ enabled=0 type=rpm-md gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-nonfree-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} @@ -24,7 +24,7 @@ enabled=0 type=rpm-md gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-nonfree-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} @@ -36,7 +36,7 @@ enabled=0 type=rpm-md gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-nonfree-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} diff --git a/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-nonfree-updates.repo.j2 b/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-nonfree-updates.repo.j2 index 5fb45d0a..0ce91ab6 100644 --- a/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-nonfree-updates.repo.j2 +++ b/roles/repo_rpmfusion/templates/etc/yum.repos.d/rpmfusion-nonfree-updates.repo.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2023122901 +# 2026052201 [rpmfusion-nonfree-updates] name=RPM Fusion for EL 8 - Nonfree - Updates @@ -11,7 +11,7 @@ mirrorlist=http://mirrors.rpmfusion.org/mirrorlist?repo=nonfree-el-updates-relea enabled=1 gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-nonfree-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} @@ -23,7 +23,7 @@ enabled=0 type=rpm-md gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-nonfree-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} @@ -35,7 +35,7 @@ enabled=0 type=rpm-md gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-nonfree-el-8 -{% if repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} +{% if repo_rpmfusion__mirror_url is defined and repo_rpmfusion__mirror_url | length and repo_rpmfusion__basic_auth_login is defined and repo_rpmfusion__basic_auth_login | length %} username={{ repo_rpmfusion__basic_auth_login["username"] }} password={{ repo_rpmfusion__basic_auth_login["password"] }} {% endif %} From 246fe7a5d0b5581d391db0fa488578961abbfb70 Mon Sep 17 00:00:00 2001 From: Markus Frei Date: Sun, 24 May 2026 20:59:12 +0200 Subject: [PATCH 49/66] fix(roles/kernel_settings): actually apply systemd_cpu_affinity setting The combined value was computed and printed in the debug output but never passed to fedora.linux_system_roles.kernel_settings, so a configured CPU affinity had no effect. --- CHANGELOG.md | 4 ++++ roles/kernel_settings/tasks/main.yml | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 968019db..44f8f2ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **role:apache_httpd**: bump Core Rule Set to 4.26.0 * **role:apache_httpd**: Update the two reverse-proxy snippets in `EXAMPLES.md` to use `ProxyPass` instead of `RewriteRule ^/(.*) ... [proxy,last]`. The RewriteRule variant `%`-decodes the URI pattern and forwards characters such as `?` unencoded to the backend, which breaks WebDAV apps (file-not-found on rename in Nextcloud). The examples now also carry a comment explaining the choice and link to the corresponding [blog post](https://www.linuxfabrik.ch/blog/nextcloud-rewriterules-vs-proxypass). +### Fixed + +* **role:kernel_settings**: The `systemd_cpu_affinity` setting is now actually applied. The value was computed and shown in the debug output but never passed to the underlying system role, so a configured CPU affinity had no effect. + ### Added * **role:repo_baseos**: Add the Rocky Linux `security` repository (critical CVE fixes), enabled by default. Opt out per host or group via `repo_baseos__security_repo_enabled__host_var` / `repo_baseos__security_repo_enabled__group_var`. diff --git a/roles/kernel_settings/tasks/main.yml b/roles/kernel_settings/tasks/main.yml index 8a0b5360..421396a0 100644 --- a/roles/kernel_settings/tasks/main.yml +++ b/roles/kernel_settings/tasks/main.yml @@ -23,6 +23,7 @@ vars: kernel_settings_sysctl: '{{ kernel_settings__sysctl__combined_var }}' # noqa var-naming[pattern] kernel_settings_sysfs: '{{ kernel_settings__sysfs__combined_var }}' # noqa var-naming[pattern] + kernel_settings_systemd_cpu_affinity: '{{ kernel_settings__systemd_cpu_affinity__combined_var }}' # noqa var-naming[pattern] kernel_settings_transparent_hugepages: '{{ kernel_settings__transparent_hugepages__combined_var }}' # noqa var-naming[pattern] kernel_settings_transparent_hugepages_defrag: '{{ kernel_settings__transparent_hugepages_defrag__combined_var }}' # noqa var-naming[pattern] From 76785d8faa225c9a895c5adce58d8280692969ca Mon Sep 17 00:00:00 2001 From: Markus Frei Date: Mon, 25 May 2026 08:03:49 +0200 Subject: [PATCH 50/66] chore: remove leftover particle/Vagrantfile The tools/particle runner and the bundled lib submodule were already dropped in f428973a, but the root particle/Vagrantfile and its .gitignore entry remained. Remove them too. The Linuxfabrik lib stays: it is still deployed at runtime by monitoring_plugins (install_method: source) and demonstrated by the example role; no bundled local lib exists and no lfops plugin imports it. --- .gitignore | 1 - CHANGELOG.md | 2 +- particle/Vagrantfile | 57 -------------------------------------------- 3 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 particle/Vagrantfile diff --git a/.gitignore b/.gitignore index 7007b164..236cffbd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ tests/output/* playbooks/test.yml roles/test context/ -particle/.vagrant # mkdocs documentation /docs/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f8f2ca..83cb14bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed * **role:repo_remi**: Drop support for RHEL 7 and Fedora 35. Both are EOL (RHEL 7: June 2024, Fedora 35: December 2022). The per-platform `tasks/RedHat7.yml`, `vars/{RedHat7,Fedora}.yml` and `templates/{RedHat7,Fedora}/` trees are removed. -* **tool:particle**: Remove the `tools/particle` Vagrant-based role test runner, its sample inventories under `tests/`, and the bundled `linuxfabrik/lib` git submodule (whose only consumer was `particle`). The runner and the submodule were tightly wired together, and Dependabot did not have a `gitsubmodule` config for this repo, so the bundled lib was silently drifting behind upstream. Since role testing is moving to Molecule anyway, dropping the whole stack is cleaner than keeping the wiring around. Older revisions remain accessible through git history. +* **tool:particle**: Remove the `tools/particle` Vagrant-based role test runner, its leftover `particle/Vagrantfile`, its sample inventories under `tests/`, and the bundled `linuxfabrik/lib` git submodule (whose only consumer was `particle`). The runner and the submodule were tightly wired together, and Dependabot did not have a `gitsubmodule` config for this repo, so the bundled lib was silently drifting behind upstream. Since role testing is moving to Molecule anyway, dropping the whole stack is cleaner than keeping the wiring around. Older revisions remain accessible through git history. ### Breaking Changes diff --git a/particle/Vagrantfile b/particle/Vagrantfile deleted file mode 100644 index c5648221..00000000 --- a/particle/Vagrantfile +++ /dev/null @@ -1,57 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -Vagrant.configure("2") do |config| - # Vagrantfile for the 'particle' test runner. - # - # Virtual machines must be declared in this file for particle to be able to detect them. - # Multiple machines can be declared by using the mult-machine format: https://developer.hashicorp.com/vagrant/docs/multi-machine. - # - # For documentation about the Vagrantfile itself see https://docs.vagrantup.com, - # for available boxes refer to https://vagrantcloud.com/search. - - config.vm.define "debian11", autostart: false do |debian11| - debian11.vm.box = "debian/bullseye64" - end - - config.vm.define "debian12", autostart: false do |debian12| - debian12.vm.box = "debian/bookworm64" - end - - config.vm.define "rocky8", autostart: false do |rocky8| - rocky8.vm.box = "generic/rocky8" # "rockylinux/8" BIOS booting is broken (and vagrant-libvirt only partially supports UEFI) - end - - config.vm.define "rocky9", autostart: false do |rocky9| - rocky9.vm.box = "rockylinux/9" - end - - # "rockylinux/10" does not work yet - #config.vm.define "rocky10", autostart: false do |rocky10| - # rocky10.vm.box = "rockylinux/10" - #end - - config.vm.define "ubuntu2204", autostart: false do |ubuntu2204| - ubuntu2204.vm.box = "generic/ubuntu2204" - end - - config.vm.define "ubuntu2404", autostart: false do |ubuntu2404| - ubuntu2404.vm.box = "bento/ubuntu-24.04" - end - - # Global configuration - - config.nfs.verify_installed = false - config.vm.synced_folder ".", "/vagrant", disabled: true - - config.vm.provider "libvirt" do |libvirt| - libvirt.memory = 4096 - libvirt.cpus = 2 - - # Better defaults than `vagrant-libvirt` provides; for improved performance - libvirt.machine_type = "q35" - libvirt.graphics_type = "spice" - libvirt.video_type = "virtio" - end - -end From 0060cf15e2688c623124b243c7199927e644db12 Mon Sep 17 00:00:00 2001 From: Markus Frei <31855393+markuslf@users.noreply.github.com> Date: Mon, 25 May 2026 09:10:48 +0200 Subject: [PATCH 51/66] Add plugin unit-test infrastructure + combine_lod fixes (#264) * test: add plugin unit-test infrastructure Adds a tox-driven matrix (Python 3.9-3.13 x ansible-core 2.15-2.18 and latest) for the controller-side plugins, a pytest layout under tests/, the 'Linuxfabrik: Unit Tests' CI workflow, and a local pre-commit hook that runs the unit tests on every commit. Documents the controller vs managed-node (RHEL 8 / Python 3.6) test tiers in tests/README.md and CONTRIBUTING.md, where plugin unit tests are now mandatory. * fix(plugins/filter/combine_lod): error on incomplete composite unique_key A composite unique_key (list of keys) produced a tuple that is always truthy, so an item missing one of the keys was silently grouped under a (None, ...) key instead of raising. Validate each component explicitly. Also adopt the standard Linuxfabrik file header, switch to f-strings, fix the DOCUMENTATION so ansible-doc renders the filter again, and move the embedded tests into tests/unit/plugins/filter/test_combine_lod.py (plus new composite-key cases). --- .github/workflows/lf-unit-tests.yml | 48 +++ .pre-commit-config.yaml | 14 + CHANGELOG.md | 1 + CONTRIBUTING.md | 31 ++ plugins/filter/combine_lod.py | 388 +++--------------- pyproject.toml | 5 + tests/README.md | 57 +++ tests/unit/plugins/filter/test_combine_lod.py | 339 +++++++++++++++ tox.ini | 63 +++ 9 files changed, 606 insertions(+), 340 deletions(-) create mode 100644 .github/workflows/lf-unit-tests.yml create mode 100644 tests/README.md create mode 100644 tests/unit/plugins/filter/test_combine_lod.py create mode 100644 tox.ini diff --git a/.github/workflows/lf-unit-tests.yml b/.github/workflows/lf-unit-tests.yml new file mode 100644 index 00000000..5c07bf7e --- /dev/null +++ b/.github/workflows/lf-unit-tests.yml @@ -0,0 +1,48 @@ +name: 'Linuxfabrik: Unit Tests' + +on: + push: + branches: + - 'main' + pull_request: {} + +permissions: + contents: 'read' + +jobs: + controller-plugins: + name: 'Controller plugins (Python ${{ matrix.python-version }})' + runs-on: 'ubuntu-latest' + strategy: + fail-fast: false + matrix: + # Controller-side plugins (filter, lookup). The managed-node tier + # (modules on RHEL 8 / Python 3.6) needs a UBI 8 container and is + # scaffolded in tox.ini, not run here yet. + python-version: + - '3.9' + - '3.10' + - '3.11' + - '3.12' + - '3.13' + steps: + - name: 'Harden the runner (Audit all outbound calls)' + uses: 'step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411' # v2.19.4 + with: + egress-policy: 'audit' + + - name: 'Checkout repository' + uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2 + + - name: 'Set up Python' + uses: 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405' # v6.2.0 + with: + python-version: '${{ matrix.python-version }}' + + - name: 'Install tox' + run: 'pip install tox' + + - name: 'Run tox for this Python (all matching ansible-core envs)' + # `-f pyXYZ` selects every tox env carrying this Python's factor, + # e.g. py311 -> py311-ansible215/216/217/218. + run: 'tox -f py$(echo "${{ matrix.python-version }}" | tr -d ".")' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c1dec563..89eae262 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,3 +51,17 @@ repos: - id: 'vulture' args: ['--min-confidence=80'] types_or: ['python'] + + - repo: 'local' + hooks: + - id: 'pytest-unit' + name: 'pytest (plugin unit tests)' + # Fast single-interpreter run of the controller-plugin unit tests on + # every commit. The full Python x ansible-core matrix runs in CI via + # tox; see tests/README.md. + entry: 'pytest tests/unit' + language: 'python' + additional_dependencies: ['ansible-core', 'pytest', 'pyyaml'] + pass_filenames: false + files: '^(plugins/|tests/unit/)' + types_or: ['python'] diff --git a/CHANGELOG.md b/CHANGELOG.md index 83cb14bb..c8f6ce2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **plugin:combine_lod**: The `combine_lod` filter now reports an error when an item is missing part of a composite `unique_key` (a list of keys), instead of silently grouping such items together. Inventories with incomplete composite keys that previously merged by accident now fail loudly and must be corrected. Also fixed its documentation so `ansible-doc` renders it again. * **role:kernel_settings**: The `systemd_cpu_affinity` setting is now actually applied. The value was computed and shown in the debug output but never passed to the underlying system role, so a configured CPU affinity had no effect. ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f36d25c5..e396f980 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -754,6 +754,37 @@ Some files under `plugins/modules/` are not authored by Linuxfabrik but vendored * Drop when: LFOps raises its minimum ansible-core to >= 2.18; switch to `community.general.lvm_pv` and update `roles/lvm` accordingly. +### Plugins + +In-house plugins live under `plugins/` following the standard Ansible collection layout: `filter/`, `lookup/`, `modules/` and `module_utils/`. The `## Tasks` rules above (FQCN, meta modules, idempotency) are about role tasks; the points below are specific to writing the plugins themselves. + +* Every plugin carries `DOCUMENTATION` (and `RETURN` / `EXAMPLES` where applicable). Keep it valid YAML: in a `description` list, a bullet containing a colon followed by a space is parsed as a mapping and makes `ansible-doc` fail, so rephrase or quote such bullets. Verify with `ansible-doc -t linuxfabrik.lfops.`. +* Set `version_added` to the LFOps release the plugin first shipped in, and never change it afterwards. +* `module_utils` holds code shared between plugins. Do not import the external Linuxfabrik Python Libraries (`lib`) into a plugin; copy what you need and note the origin in a comment. + + +#### Plugin Tests + +Unit tests are **mandatory** for every in-house plugin. Any pull request that adds or changes a plugin must add or update its test, and `git grep` should never find a plugin without one. + +* **Where**: under `tests/unit/`, mirroring the plugin tree, named `test_.py` (e.g. `tests/unit/plugins/filter/test_combine_lod.py`). Load the plugin by path (the plugins are not an importable package) and assert behavior, not implementation details. +* **Two tiers**, because plugins run in different environments: + + * Controller plugins (`plugins/filter/`, `plugins/lookup/`) are evaluated on the Ansible controller and only ever see the controller's Python (>= 3.10). They run on the standard CI matrix. + * Managed-node plugins (`plugins/modules/`, `plugins/module_utils/`) are executed on the target host and must keep working down to the oldest managed-node Python we maintain (Python 3.6 on RHEL 8). That tier runs inside a RHEL 8 / UBI 8 container; it is scaffolded in `tox.ini` (`[testenv:py36-target]`) and gets enabled once such tests exist. + +* **How to run / verify** (the matrix of Python and ansible-core versions is driven by `tox`; see `tests/README.md` and `tox.ini`): + + ```bash + tox # full controller matrix (every Python x ansible-core combination) + tox -e py311-ansible216 # a single combination + tox -f py311 # every ansible-core for one Python + pytest tests/unit # against the active interpreter (needs pytest, pyyaml, ansible-core) + ``` + +* The `Linuxfabrik: Unit Tests` workflow runs the controller matrix on every push and pull request. + + ### Credits * diff --git a/plugins/filter/combine_lod.py b/plugins/filter/combine_lod.py index 8ade1fbc..2a789be1 100644 --- a/plugins/filter/combine_lod.py +++ b/plugins/filter/combine_lod.py @@ -1,10 +1,13 @@ -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. -# Copyright: (c) 2022, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) - -# Make coding more python3-ish from __future__ import absolute_import, division, print_function + __metaclass__ = type import collections @@ -18,9 +21,11 @@ description: - Merges one or more lists of dictionaries into a single list. Dictionaries that share the same value under I(unique_key) are folded into one entry; later entries overwrite earlier ones (non-recursive, like Python C(dict.update)). - The folding also collapses duplicates inside a single input list, so passing one list with duplicates is a valid way to deduplicate it. - - Useful for layering inventory: a role can ship sensible defaults, and the user can selectively override individual keys per item from their inventory without having to redeclare the whole list. + - Useful for layering inventory. A role can ship sensible defaults, and the user can selectively override individual keys per item from their inventory without having to redeclare the whole list. - Sub-dictionaries and sub-lists are replaced wholesale, not merged recursively. To merge nested structures, build the nested values up using this filter at each level. - - Every input dictionary must contain the unique key (or all keys, if a list is passed). A missing or falsy key value (C(None), C(0), empty string) raises an error. + - The result keeps the order in which each unique key is first seen across all input lists; later updates to an existing key do not move it. + - Every list item must be a dictionary; a non-dictionary item raises an error. + - Every input dictionary must contain the unique key (or all of them, when a list of keys is passed). A missing or falsy key value (C(None), C(0), empty string) raises an error. positional: _input, _dicts options: _input: @@ -50,7 +55,7 @@ first_value: 'sensible default for first value' second_value: 'sensible default for second value' - name: 'setting 2' - allow_all: False + allow_all: false allowed_users: - 'root' @@ -67,8 +72,7 @@ debug: msg: '{{ role_defaults | linuxfabrik.lfops.combine_lod(my_user_adjustments) }}' -# => -# (reordered for readability) +# => (keys keep their first-seen order) # - name: setting 1 # first_value: better setting for me # second_value: sensible default for second value @@ -78,6 +82,24 @@ # - linuxfabrik +# composite unique_key: the same server_name on different ports stays separate +- name: 'merge vhosts by name + port' + debug: + msg: >- + {{ [{'server_name': 'example.com', 'server_port': 80, 'root': '/var/www'}, + {'server_name': 'example.com', 'server_port': 443, 'root': '/var/www'}] + | linuxfabrik.lfops.combine_lod([{'server_name': 'example.com', 'server_port': 443, 'root': '/srv/tls'}], + unique_key=['server_name', 'server_port']) }} + +# => +# - server_name: example.com +# server_port: 80 +# root: /var/www +# - server_name: example.com +# server_port: 443 +# root: /srv/tls + + # more complicated example # defaults: mariadb_server__kernel_settings__sysctl__dependent_var: @@ -161,15 +183,26 @@ def combine_lod(*args, **kwargs): for lod in list(args): for item in lod: if not isinstance(item, collections.abc.MutableMapping): - raise AnsibleFilterError("found non-dictionary item in the list, this is not supported") + raise AnsibleFilterError('found a non-dictionary item in the list, this is not supported') if isinstance(unique_key, collections.abc.MutableSequence): - key = tuple(item.get(k, None) for k in unique_key) + key_components = [item.get(k, None) for k in unique_key] + # a tuple is always truthy, even `(None, None)`, so the `not key` + # check used for single keys would not catch a composite key with + # missing components. Validate each component explicitly instead. + if not all(key_components): + raise AnsibleFilterError( + f'found a dictionary missing one of the unique keys {unique_key}, ' + 'this is not supported' + ) + key = tuple(key_components) else: key = item.get(unique_key, None) - - if not key: - raise AnsibleFilterError("found an dictionary without the unique key, this is not supported") + if not key: + raise AnsibleFilterError( + f"found a dictionary without the unique key '{unique_key}', " + 'this is not supported' + ) # the python dict.update function does exactly what we want. # it is also used for ansible.builtin.combine(..., recurse=False, list_merge='replace'). @@ -186,328 +219,3 @@ def filters(self): return { 'combine_lod': combine_lod, } - - - -if __name__ == '__main__': - import textwrap - import unittest - - import yaml - - class Test(unittest.TestCase): - - def test_combine_lod_non_dict_item(self): - ''' - non-dictionaries list elements are not supported - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'test1' - - 'im a string' - ''')) - - with self.assertRaises(AnsibleFilterError): - combine_lod(input1) - - - def test_combine_lod_last(self): - ''' - the last element should always win and overwrite the earlier ones - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'test1' - ''')) - - input2 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'test2' - ''')) - - expected = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'test2' - ''')) - - result = combine_lod(input1, input2) - - # print('input1:') - # print(yaml.dump(input1, default_flow_style=False)) - # print('input2:') - # print(yaml.dump(input2, default_flow_style=False)) - # print('result:') - # print(yaml.dump(result, default_flow_style=False)) - # print('expected:') - # print(yaml.dump(expected, default_flow_style=False)) - - self.assertEqual(result, expected) - - - def test_combine_lod_multiple(self): - ''' - test the basic functionality if there are multiple list elements - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - name: 'first variable' - value: 'test1' - - name: 'second variable' - value: 'test2' - - name: 'other_var' - value: 'linuxfabrik' - ''')) - - input2 = yaml.safe_load(textwrap.dedent(''' - - name: 'first variable' - value: 'test1 - edited' - - name: 'second variable' - value: 'test2 - edited' - new_value: 'new here' - ''')) - - expected = yaml.safe_load(textwrap.dedent(''' - - name: 'first variable' - value: 'test1' - value: 'test1 - edited' - - name: 'second variable' - value: 'test2 - edited' - new_value: 'new here' - - name: 'other_var' - value: 'linuxfabrik' - ''')) - - result = combine_lod(input1, input2) - self.assertEqual(result, expected) - - - def test_combine_lod_single_input(self): - ''' - test if everything works if the lists are already combined beforehand - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'test1' - - name: 'myvar' - value: 'test2' - ''')) - - expected = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'test2' - ''')) - - result = combine_lod(input1) - self.assertEqual(result, expected) - - - def test_combine_lod_replace_given_keys(self): - ''' - only the given keys are overwritten, not the whole list element - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'value 1' - my_list: - - 'input1' - - 'lots of default entries' - ''')) - - input2 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'value 2' - ''')) - - expected = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'value 2' - my_list: - - 'input1' - - 'lots of default entries' - ''')) - - result = combine_lod(input1, input2) - self.assertEqual(result, expected) - - - def test_combine_lod_different_single_unique_key(self): - ''' - test if using a different single unique_key works - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - filename: 'myvar 1' - value: 'value 1' - - filename: 'myvar 2' - value: 'value 1' - ''')) - - input2 = yaml.safe_load(textwrap.dedent(''' - - filename: 'myvar 1' - value: 'value 1 - edited' - ''')) - - expected = yaml.safe_load(textwrap.dedent(''' - - filename: 'myvar 1' - value: 'value 1 - edited' - - filename: 'myvar 2' - value: 'value 1' - ''')) - - result = combine_lod(input1, input2, unique_key="filename") - self.assertEqual(result, expected) - - - def test_combine_lod_different_list_unique_key(self): - ''' - test if using a a list of unique_keys works - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - server_name: 'myvar' - server_port: 80 - value: 'value 80' - - server_name: 'myvar' - server_port: 443 - value: 'value 443' - ''')) - - input2 = yaml.safe_load(textwrap.dedent(''' - - server_name: 'myvar' - server_port: 80 - value: 'value 81' - ''')) - - expected = yaml.safe_load(textwrap.dedent(''' - - server_name: 'myvar' - server_port: 80 - value: 'value 81' - - server_name: 'myvar' - server_port: 443 - value: 'value 443' - ''')) - - result = combine_lod(input1, input2, unique_key=["server_name", "server_port"]) - self.assertEqual(result, expected) - - - def test_combine_lod_missing_unique_key(self): - ''' - the plugin should throw an error if it cannot find the unique_key for all list elements - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'value 1' - ''')) - - input2 = yaml.safe_load(textwrap.dedent(''' - - wrong_name: 'myvar' - value: 'value 1' - ''')) - - with self.assertRaises(AnsibleFilterError): - combine_lod(input1, input2) - - - def test_combine_lod_list_merge(self): - ''' - if one of the keys in the dictionaries contains a list, - the list should just replace the previous lists. - no append / prepend of the list elements - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - my_list: - - 'input1' - - 'input_repeated' - ''')) - - input2 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - my_list: - - 'input2' - - 'input_repeated' - ''')) - - expected = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - my_list: - - 'input2' - - 'input_repeated' - ''')) - - result = combine_lod(input1, input2) - self.assertEqual(result, expected) - - - def test_combine_lod_no_recursion(self): - ''' - the plugin should not recurse into dicts or lists - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'value 1' - my_list: - - 'input1' - - name: 'my sub var' - value: 'sub value 1' - - 'input_repeated' - ''')) - - input2 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'value 2' - my_dict: - name: 'my sub var' - value: 'sub value 1' - ''')) - - expected = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'value 2' - my_list: - - 'input1' - - name: 'my sub var' - value: 'sub value 1' - - 'input_repeated' - my_dict: - name: 'my sub var' - value: 'sub value 1' - ''')) - - result = combine_lod(input1, input2) - self.assertEqual(result, expected) - - - def test_combine_lod_no_modification(self): - ''' - in this case the plugin should not modify anything - ''' - - input1 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'value 1' - ''')) - - input2 = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'value 1' - ''')) - - expected = yaml.safe_load(textwrap.dedent(''' - - name: 'myvar' - value: 'value 1' - ''')) - - result = combine_lod(input1, input2) - self.assertEqual(result, expected) - - - unittest.main() diff --git a/pyproject.toml b/pyproject.toml index 0347aee6..cc86c240 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,3 +14,8 @@ # real issues are fixed. paths = ["plugins", ".vulture_whitelist.py"] min_confidence = 80 + +[tool.pytest.ini_options] +# Unit tests for the in-house plugins. The matrix of Python / ansible-core +# versions is driven by tox; see tox.ini. +testpaths = ["tests/unit"] diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..7271b66c --- /dev/null +++ b/tests/README.md @@ -0,0 +1,57 @@ +# Tests + +Unit tests for the in-house LFOps plugins under `plugins/`. + +## Two tiers + +LFOps plugins run in two different environments, so they are tested differently: + +| Tier | Plugins | Runs on | Python / ansible-core | +|---|---|---|---| +| Controller | `plugins/filter/`, `plugins/lookup/` | Ansible controller | Python >= 3.10, ansible-core 2.15 - latest | +| Managed node | `plugins/modules/`, `plugins/module_utils/` | target host | down to Python 3.6 (RHEL 8) | + +Filter and lookup plugins are evaluated on the controller during templating, so they only ever see the controller's Python. Modules and module utils are shipped to and executed on the managed node, which on RHEL 8 still uses the system Python 3.6. + +## Layout + +Tests mirror the plugin tree: + +``` +tests/unit/plugins/filter/test_.py +tests/unit/plugins/lookup/test_.py +tests/unit/plugins/modules/test_.py # managed-node tier +tests/unit/plugins/module_utils/test_.py # managed-node tier +``` + +Each test loads its plugin by path (the plugins are not an importable package) and may import `ansible.errors` / `ansible.module_utils`, so ansible-core must be installed in the test environment. + +## Running + +Run the controller matrix (Python x ansible-core combinations) with tox: + +```bash +tox +``` + +Run a single environment, or everything for one Python: + +```bash +tox -e py311-ansible216 +tox -f py311 +``` + +Run the tests directly against the active interpreter (needs `pytest`, `pyyaml` and `ansible-core`): + +```bash +pytest tests/unit +``` + +## Managed-node tier (Python 3.6 / RHEL 8) + +CI runners no longer ship Python 3.6, so module tests have to run inside a RHEL 8 / UBI 8 container. The `[testenv:py36-target]` env in `tox.ini` is scaffolded for this but not yet enabled, since the only plugin with tests so far (`combine_lod`) is a controller-side filter. Enable it once module tests exist, for example: + +```bash +podman run --rm -v "$PWD":/src:Z -w /src registry.access.redhat.com/ubi8/ubi \ + bash -c 'dnf -y install python3 python3-pip && pip3 install tox && tox -e py36-target' +``` diff --git a/tests/unit/plugins/filter/test_combine_lod.py b/tests/unit/plugins/filter/test_combine_lod.py new file mode 100644 index 00000000..e20473fc --- /dev/null +++ b/tests/unit/plugins/filter/test_combine_lod.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for the `combine_lod` filter plugin. + +`combine_lod` is a filter plugin and therefore runs on the Ansible +controller only. The controller requires Python >= 3.10 (ansible-core +2.16/2.17) or >= 3.11 (2.18), so this test runs on the controller +matrix, not on the Python 3.6 (RHEL 8) managed-node tier. See +`tests/README.md`. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import importlib.util +import os +import textwrap +import unittest + +import yaml + +from ansible.errors import AnsibleFilterError + +# The plugin lives outside any importable package, so load it by path +# (repo_root/plugins/filter/combine_lod.py) relative to this test file. +_PLUGIN_PATH = os.path.join( + os.path.dirname(__file__), + '..', '..', '..', '..', + 'plugins', 'filter', 'combine_lod.py', +) +_spec = importlib.util.spec_from_file_location('combine_lod', _PLUGIN_PATH) +_module = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_module) +combine_lod = _module.combine_lod + + +class Test(unittest.TestCase): + + def test_combine_lod_non_dict_item(self): + """non-dictionary list elements are not supported""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'test1' + - 'im a string' + ''')) + + with self.assertRaises(AnsibleFilterError): + combine_lod(input1) + + def test_combine_lod_last(self): + """the last element should always win and overwrite the earlier ones""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'test1' + ''')) + + input2 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'test2' + ''')) + + expected = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'test2' + ''')) + + result = combine_lod(input1, input2) + self.assertEqual(result, expected) + + def test_combine_lod_multiple(self): + """test the basic functionality if there are multiple list elements""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - name: 'first variable' + value: 'test1' + - name: 'second variable' + value: 'test2' + - name: 'other_var' + value: 'linuxfabrik' + ''')) + + input2 = yaml.safe_load(textwrap.dedent(''' + - name: 'first variable' + value: 'test1 - edited' + - name: 'second variable' + value: 'test2 - edited' + new_value: 'new here' + ''')) + + expected = yaml.safe_load(textwrap.dedent(''' + - name: 'first variable' + value: 'test1 - edited' + - name: 'second variable' + value: 'test2 - edited' + new_value: 'new here' + - name: 'other_var' + value: 'linuxfabrik' + ''')) + + result = combine_lod(input1, input2) + self.assertEqual(result, expected) + + def test_combine_lod_single_input(self): + """test if everything works if the lists are already combined beforehand""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'test1' + - name: 'myvar' + value: 'test2' + ''')) + + expected = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'test2' + ''')) + + result = combine_lod(input1) + self.assertEqual(result, expected) + + def test_combine_lod_replace_given_keys(self): + """only the given keys are overwritten, not the whole list element""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'value 1' + my_list: + - 'input1' + - 'lots of default entries' + ''')) + + input2 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'value 2' + ''')) + + expected = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'value 2' + my_list: + - 'input1' + - 'lots of default entries' + ''')) + + result = combine_lod(input1, input2) + self.assertEqual(result, expected) + + def test_combine_lod_different_single_unique_key(self): + """test if using a different single unique_key works""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - filename: 'myvar 1' + value: 'value 1' + - filename: 'myvar 2' + value: 'value 1' + ''')) + + input2 = yaml.safe_load(textwrap.dedent(''' + - filename: 'myvar 1' + value: 'value 1 - edited' + ''')) + + expected = yaml.safe_load(textwrap.dedent(''' + - filename: 'myvar 1' + value: 'value 1 - edited' + - filename: 'myvar 2' + value: 'value 1' + ''')) + + result = combine_lod(input1, input2, unique_key="filename") + self.assertEqual(result, expected) + + def test_combine_lod_different_list_unique_key(self): + """test if using a list of unique_keys works""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - server_name: 'myvar' + server_port: 80 + value: 'value 80' + - server_name: 'myvar' + server_port: 443 + value: 'value 443' + ''')) + + input2 = yaml.safe_load(textwrap.dedent(''' + - server_name: 'myvar' + server_port: 80 + value: 'value 81' + ''')) + + expected = yaml.safe_load(textwrap.dedent(''' + - server_name: 'myvar' + server_port: 80 + value: 'value 81' + - server_name: 'myvar' + server_port: 443 + value: 'value 443' + ''')) + + result = combine_lod(input1, input2, unique_key=["server_name", "server_port"]) + self.assertEqual(result, expected) + + def test_combine_lod_missing_unique_key(self): + """the plugin should throw an error if it cannot find the unique_key for all list elements""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'value 1' + ''')) + + input2 = yaml.safe_load(textwrap.dedent(''' + - wrong_name: 'myvar' + value: 'value 1' + ''')) + + with self.assertRaises(AnsibleFilterError): + combine_lod(input1, input2) + + def test_combine_lod_composite_key_missing_component(self): + """a composite key with a missing component must raise, not silently group under None""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - server_name: 'myvar' + server_port: 80 + value: 'value 80' + - server_name: 'myvar' + value: 'no port here' + ''')) + + with self.assertRaises(AnsibleFilterError): + combine_lod(input1, unique_key=["server_name", "server_port"]) + + def test_combine_lod_composite_key_all_components_missing(self): + """a composite key where every component is missing must raise as well""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - value: 'no keys at all' + ''')) + + with self.assertRaises(AnsibleFilterError): + combine_lod(input1, unique_key=["server_name", "server_port"]) + + def test_combine_lod_list_merge(self): + """a key holding a list should be replaced wholesale, no append / prepend""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + my_list: + - 'input1' + - 'input_repeated' + ''')) + + input2 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + my_list: + - 'input2' + - 'input_repeated' + ''')) + + expected = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + my_list: + - 'input2' + - 'input_repeated' + ''')) + + result = combine_lod(input1, input2) + self.assertEqual(result, expected) + + def test_combine_lod_no_recursion(self): + """the plugin should not recurse into dicts or lists""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'value 1' + my_list: + - 'input1' + - name: 'my sub var' + value: 'sub value 1' + - 'input_repeated' + ''')) + + input2 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'value 2' + my_dict: + name: 'my sub var' + value: 'sub value 1' + ''')) + + expected = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'value 2' + my_list: + - 'input1' + - name: 'my sub var' + value: 'sub value 1' + - 'input_repeated' + my_dict: + name: 'my sub var' + value: 'sub value 1' + ''')) + + result = combine_lod(input1, input2) + self.assertEqual(result, expected) + + def test_combine_lod_no_modification(self): + """in this case the plugin should not modify anything""" + + input1 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'value 1' + ''')) + + input2 = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'value 1' + ''')) + + expected = yaml.safe_load(textwrap.dedent(''' + - name: 'myvar' + value: 'value 1' + ''')) + + result = combine_lod(input1, input2) + self.assertEqual(result, expected) + + +if __name__ == '__main__': + unittest.main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..89438458 --- /dev/null +++ b/tox.ini @@ -0,0 +1,63 @@ +# Test matrix for the in-house LFOps plugins. +# +# LFOps plugins fall into two tiers with different runtime environments: +# +# * Controller plugins (filter, lookup) run on the Ansible controller. +# The controller requires Python >= 3.10 (ansible-core 2.16/2.17) or +# >= 3.11 (2.18). These are covered by the `py3XX-ansibleXYZ` envs +# below and run on standard CI runners via setup-python. +# +# * Target plugins (modules, module_utils) run on the managed node and +# must therefore support the oldest managed-node Python we still +# maintain: Python 3.6 on RHEL 8. CI runners no longer ship Python +# 3.6, so that tier has to run inside a RHEL 8 / UBI 8 container. It +# is scaffolded in `[testenv:py36-target]` below but not yet wired +# into CI, because the only plugin with tests so far (combine_lod) is +# a controller-side filter. +# +# ansible-core / controller-Python compatibility (see README.md): +# 2.15: py3.9-3.11 2.16: py3.10-3.12 2.17: py3.10-3.12 2.18: py3.11-3.13 +# `requires_ansible` in meta/runtime.yml is `>=2.15.0`, so 2.15 is the floor. + +[tox] +envlist = + py39-ansible215 + py310-ansible{215,216,217} + py311-ansible{215,216,217,218} + py312-ansible{216,217,218} + py313-ansible{218,latest} +skip_missing_interpreters = true +# this collection is not a Python package, do not build an sdist from the repo root. +no_package = true + +[testenv] +description = 'Controller-plugin unit tests (filter, lookup)' +deps = + pytest + pyyaml + ansible215: ansible-core~=2.15.0 + ansible216: ansible-core~=2.16.0 + ansible217: ansible-core~=2.17.0 + ansible218: ansible-core~=2.18.0 + ansiblelatest: ansible-core +commands = + # discovers every controller-plugin test under tests/unit; managed-node + # plugin tests (modules) get their own env, see [testenv:py36-target]. + pytest {posargs:tests/unit} + +# Scaffold for the managed-node tier (modules / module_utils). Not in the +# default envlist. Run it inside a RHEL 8 / UBI 8 container where the system +# Python is 3.6, against the matching ansible-core, e.g.: +# +# podman run --rm -v "$PWD":/src:Z -w /src registry.access.redhat.com/ubi8/ubi \ +# bash -c 'dnf -y install python3 python3-pip && pip3 install tox && tox -e py36-target' +# +# [testenv:py36-target] +# description = 'Managed-node plugin unit tests (modules) on RHEL 8 Python 3.6' +# basepython = python3.6 +# deps = +# pytest<7 # pytest 7+ dropped Python 3.6 +# pyyaml +# ansible-core<2.12 # last series supporting a Python 3.6 runner +# commands = +# pytest {posargs:tests/unit/plugins/modules tests/unit/plugins/module_utils} From 22c33f79824170e65e4fe09a43196d7af4a9cf0d Mon Sep 17 00:00:00 2001 From: Markus Frei <31855393+markuslf@users.noreply.github.com> Date: Mon, 25 May 2026 09:38:12 +0200 Subject: [PATCH 52/66] refactor(plugins): unify bitwarden family and add unit tests (#265) Bring the bitwarden lookup, module and module_util to the standard plugin style (header, f-strings, single quotes, modern ansible.module_utils.common.text.converters) without changing behavior. Safe fixes only: - fix the lookup DOCUMENTATION so ansible-doc renders it again - module: fail_json(msg=...) instead of positional, drop dead try/except - module_util: drop the (object) base, correct get_item_by_id docstring, nosec the /tmp cache fallback and the charset default (false positives) - remove the dead commented example block from the lookup Behaviour-changing bugs (check_mode mutation, None-password overwrite, get_item_by_id returns-or-raises contract) are intentionally left for separate, individually tested fixes. Add unit tests for the family plus tests/conftest.py, which makes this checkout importable as ansible_collections.linuxfabrik.lfops so module/lookup tests resolve their collection imports under pytest/tox. Exclude tests/ from bandit (fixture passwords are expected). --- .pre-commit-config.yaml | 4 +- CHANGELOG.md | 1 + CONTRIBUTING.md | 15 +- plugins/lookup/bitwarden_item.py | 53 ++---- plugins/module_utils/bitwarden.py | 94 ++++++----- plugins/modules/bitwarden_item.py | 31 ++-- pyproject.toml | 4 + tests/conftest.py | 42 +++++ .../plugins/lookup/test_bitwarden_item.py | 96 +++++++++++ .../plugins/module_utils/test_bitwarden.py | 158 ++++++++++++++++++ .../plugins/modules/test_bitwarden_item.py | 66 ++++++++ 11 files changed, 463 insertions(+), 101 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/unit/plugins/lookup/test_bitwarden_item.py create mode 100644 tests/unit/plugins/module_utils/test_bitwarden.py create mode 100644 tests/unit/plugins/modules/test_bitwarden_item.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89eae262..b73007a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,9 @@ repos: # false-positives on the project's own code style (`shell=dict(...)` # in argument_spec triggers B604, the literal `'on_create'` sentinel # triggers B105). Out of scope for in-tree review. - exclude: '^plugins/modules/ipa.*\.py$' + # `tests/` holds unit tests whose fixtures use throwaway passwords + # (B105/B106); scanning test fixtures for hardcoded secrets is noise. + exclude: '^(plugins/modules/ipa.*|tests/.*)\.py$' types_or: ['python'] - repo: 'https://github.com/jendrikseipp/vulture' diff --git a/CHANGELOG.md b/CHANGELOG.md index c8f6ce2e..934c33a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **plugin:bitwarden_item**: Fixed the lookup's documentation so `ansible-doc` renders it again. * **plugin:combine_lod**: The `combine_lod` filter now reports an error when an item is missing part of a composite `unique_key` (a list of keys), instead of silently grouping such items together. Inventories with incomplete composite keys that previously merged by accident now fail loudly and must be corrected. Also fixed its documentation so `ansible-doc` renders it again. * **role:kernel_settings**: The `systemd_cpu_affinity` setting is now actually applied. The value was computed and shown in the debug output but never passed to the underlying system role, so a configured CPU affinity had no effect. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e396f980..eb48a41b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -758,6 +758,19 @@ Some files under `plugins/modules/` are not authored by Linuxfabrik but vendored In-house plugins live under `plugins/` following the standard Ansible collection layout: `filter/`, `lookup/`, `modules/` and `module_utils/`. The `## Tasks` rules above (FQCN, meta modules, idempotency) are about role tasks; the points below are specific to writing the plugins themselves. +* Every in-house plugin starts with the standard file header, followed by `from __future__ import absolute_import, division, print_function` and `__metaclass__ = type`: + + ```python + #!/usr/bin/env python3 + # -*- coding: utf-8; py-indent-offset: 4 -*- + # + # Author: Linuxfabrik GmbH, Zurich, Switzerland + # Contact: info (at) linuxfabrik (dot) ch + # https://www.linuxfabrik.ch/ + # License: The Unlicense, see LICENSE file. + ``` + +* Use single quotes and f-strings consistently (vendored plugins keep their upstream style, see below). * Every plugin carries `DOCUMENTATION` (and `RETURN` / `EXAMPLES` where applicable). Keep it valid YAML: in a `description` list, a bullet containing a colon followed by a space is parsed as a mapping and makes `ansible-doc` fail, so rephrase or quote such bullets. Verify with `ansible-doc -t linuxfabrik.lfops.`. * Set `version_added` to the LFOps release the plugin first shipped in, and never change it afterwards. * `module_utils` holds code shared between plugins. Do not import the external Linuxfabrik Python Libraries (`lib`) into a plugin; copy what you need and note the origin in a comment. @@ -767,7 +780,7 @@ In-house plugins live under `plugins/` following the standard Ansible collection Unit tests are **mandatory** for every in-house plugin. Any pull request that adds or changes a plugin must add or update its test, and `git grep` should never find a plugin without one. -* **Where**: under `tests/unit/`, mirroring the plugin tree, named `test_.py` (e.g. `tests/unit/plugins/filter/test_combine_lod.py`). Load the plugin by path (the plugins are not an importable package) and assert behavior, not implementation details. +* **Where**: under `tests/unit/`, mirroring the plugin tree, named `test_.py` (e.g. `tests/unit/plugins/filter/test_combine_lod.py`). A plugin with no collection-qualified imports (e.g. the `combine_lod` filter) can be loaded by file path. A plugin that imports `ansible_collections.linuxfabrik.lfops...` (modules, or lookups pulling in a module_util) is imported through that path; `tests/conftest.py` makes this checkout importable as the collection so the imports resolve under plain pytest/tox. Same-named test files in different plugin-type directories are fine (`--import-mode=importlib`). Assert behavior, not implementation details. * **Two tiers**, because plugins run in different environments: * Controller plugins (`plugins/filter/`, `plugins/lookup/`) are evaluated on the Ansible controller and only ever see the controller's Python (>= 3.10). They run on the standard CI matrix. diff --git a/plugins/lookup/bitwarden_item.py b/plugins/lookup/bitwarden_item.py index d197ee39..b23c539d 100644 --- a/plugins/lookup/bitwarden_item.py +++ b/plugins/lookup/bitwarden_item.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2022, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -15,7 +17,7 @@ description: - Looks up a password item in Bitwarden by name (and optional username, folder, collection and organization), or directly by Bitwarden item ID. - - If the lookup is by name and no matching item is found, a new login item is created on the fly. This makes the plugin idempotent for automation: the first run creates the secret, every subsequent run returns the same item. + - If the lookup is by name and no matching item is found, a new login item is created on the fly. This makes the plugin idempotent for automation. The first run creates the secret, every subsequent run returns the same item. - If the lookup is by I(id) and the item is not in the local cache, the plugin falls back to a single API call. If the ID still does not resolve, the plugin fails - IDs are never auto-created. - If a name-based search returns more than one match, the plugin fails because it cannot decide which item to use. - On success, the plugin returns the full Bitwarden item object. C(username) and C(password) are additionally lifted to the top level so they can be addressed without going through the C(login) sub-dictionary. @@ -284,7 +286,7 @@ from ansible_collections.linuxfabrik.lfops.plugins.module_utils.bitwarden import \ Bitwarden -display = Display() # lfbwlp = Linuxfabrik Bitwarden Lookup Plugin +display = Display() # log prefix "lfbwlp" = Linuxfabrik Bitwarden Lookup Plugin # https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#developing-lookup-plugins # inspired by the lookup plugins lastpass (same topic) and redis (more modern) @@ -302,7 +304,7 @@ def run(self, terms, variables=None, **kwargs): ret = [] for term in terms: - display.vvv('lfbwlp - run - lookup term: {}'.format(term)) + display.vvv(f'lfbwlp - run - lookup term: {term}') try: collection_id = term.get('collection_id', None) folder_id = term.get('folder_id', None) @@ -317,11 +319,11 @@ def run(self, terms, variables=None, **kwargs): uris = term.get('uris', []) username = term.get('username', None) except Exception as e: - raise AnsibleError('Encountered exception while fetching {}: {}'.format(term, e)) + raise AnsibleError(f'Encountered exception while fetching {term}: {e}') if id_: result = bw.get_item_by_id(id_) - display.vvv('lfbwlp - run - get item by id: {}'.format(id_)) + display.vvv(f'lfbwlp - run - get item by id: {id_}') if result: # move username and password higher for easier access result['username'] = result['login']['username'] @@ -330,10 +332,10 @@ def run(self, terms, variables=None, **kwargs): continue # done here, go to next term else: # item not found by ID. if there is an ID given we expect it to exist - raise AnsibleError('Item with id {} not found.'.format(id_)) + raise AnsibleError(f'Item with id {id_} not found.') name = Bitwarden.get_pretty_name(name, hostname, purpose) - display.vvv('lfbwlp - run - get item: {}'.format(name)) + display.vvv(f'lfbwlp - run - get item: {name}') result = bw.get_items(name, username, folder_id, collection_id, organization_id) if len(result) > 1: @@ -380,30 +382,5 @@ def run(self, terms, variables=None, **kwargs): out['password'] = out['login']['password'] ret.append(out) - # always returns a list of dicts - # - # example: - # - # - collectionIds: - # - 47b22450-fb65-4ad2-836a-03f25c982fb1 - # favorite: false - # folderId: null - # id: 2656edf2-3600-4d8d-88e8-bcdda35d1ccf - # login: - # password: d2Dft5FqGK4yhzmsDcjWJD5LMAPGDsN8oZpXsxx6 - # passwordRevisionDate: null - # totp: null - # uris: - # - match: null - # uri: https://www.example.com - # - match: null - # uri: https://git.example.com - # username: mariadb-admin - # name: app4711 - MariaDB - # notes: Automatically generated by Ansible. - # object: item - # organizationId: 5ae8f510-1f84-4243-8c35-bec35091706c - # reprompt: 0 - # revisionDate: '2019-01-28T15:31:34.300Z' - ## type: 1 + # always returns a list of dicts; see the RETURN block above for the shape return ret diff --git a/plugins/module_utils/bitwarden.py b/plugins/module_utils/bitwarden.py index bc96de43..16a28f39 100644 --- a/plugins/module_utils/bitwarden.py +++ b/plugins/module_utils/bitwarden.py @@ -1,12 +1,14 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2022, Linuxfabrik, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function -# This module requires Python 3.8+ (secrets, f-strings with =, os.replace, json.JSONDecodeError). This should be fine since it will always run on localhost and the Ansible Controller has to be Python 3.9+ anyway +__metaclass__ = type import copy import email.encoders @@ -24,11 +26,10 @@ from urllib.error import HTTPError, URLError from ansible.module_utils.common.collections import Mapping +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.module_utils.six import string_types +from ansible.module_utils.urls import ConnectionError, SSLValidationError, open_url -from ansible.module_utils._text import to_bytes, to_native, to_text -from ansible.module_utils.urls import (ConnectionError, SSLValidationError, - open_url) try: from ansible.utils.display import Display display = Display() @@ -42,8 +43,9 @@ def vvv(self, msg, **kwargs): def prepare_multipart_no_base64(fields): - """Taken from ansible.module_utils.urls, but adjusted to not no encoding as the bitwarden API does not work with that - (even though it should according to the RFC, Content-Transfer-Encoding is deprecated but not removed). + """Taken from ansible.module_utils.urls, but adjusted to not encode the payload, as the + Bitwarden API does not work with that (even though it should according to the RFC, + Content-Transfer-Encoding is deprecated but not removed). See https://github.com/ansible/ansible/issues/73621 Takes a mapping, and prepares a multipart/form-data body @@ -74,7 +76,7 @@ def prepare_multipart_no_base64(fields): if not isinstance(fields, Mapping): raise TypeError( - 'Mapping is required, cannot be type %s' % fields.__class__.__name__ + f'Mapping is required, cannot be type {fields.__class__.__name__}' ) m = email.mime.multipart.MIMEMultipart('form-data') @@ -99,14 +101,14 @@ def prepare_multipart_no_base64(fields): main_type, sep, sub_type = mime.partition('/') else: raise TypeError( - 'value must be a string, or mapping, cannot be type %s' % value.__class__.__name__ + f'value must be a string, or mapping, cannot be type {value.__class__.__name__}' ) if not content and filename: with open(to_bytes(filename, errors='surrogate_or_strict'), 'rb') as f: part = email.mime.application.MIMEApplication(f.read(), _encoder=email.encoders.encode_noop) del part['Content-Type'] - part.add_header('Content-Type', '%s/%s' % (main_type, sub_type)) + part.add_header('Content-Type', f'{main_type}/{sub_type}') else: part = email.mime.nonmultipart.MIMENonMultipart(main_type, sub_type) part.set_payload(to_bytes(content)) @@ -143,7 +145,7 @@ def prepare_multipart_no_base64(fields): ) -CACHE_DIR = os.environ.get('XDG_RUNTIME_DIR', '/tmp') +CACHE_DIR = os.environ.get('XDG_RUNTIME_DIR', '/tmp') # nosec B108 - cache files are created with mkstemp (mode 0600) and atomically replaced CACHE_FILE = os.path.join(CACHE_DIR, 'lfops_bitwarden_cache.json') CACHE_VERSION = 2026032701 @@ -152,16 +154,16 @@ class BitwardenException(Exception): pass -class Bitwarden(object): +class Bitwarden: # https://bitwarden.com/help/vault-management-api def __init__(self, hostname='127.0.0.1', port=8087): - self._base_url = 'http://%s:%s' % (hostname, port) + self._base_url = f'http://{hostname}:{port}' self._cache = None self._load_cache() def _api_call(self, url_path, method='GET', body=None, body_format='json'): - url = '%s/%s' % (self._base_url, url_path) + url = f'{self._base_url}/{url_path}' headers = {} if body: @@ -172,7 +174,7 @@ def _api_call(self, url_path, method='GET', body=None, body_format='json'): try: content_type, body = prepare_multipart_no_base64(body) except (TypeError, ValueError) as e: - raise BitwardenException('failed to parse body as form-multipart: %s' % to_native(e)) + raise BitwardenException(f'failed to parse body as form-multipart: {to_native(e)}') headers['Content-Type'] = content_type # mostly taken from ansible.builtin.url lookup plugin @@ -180,21 +182,21 @@ def _api_call(self, url_path, method='GET', body=None, body_format='json'): # increased the timeout since listing all items via `list/object/items` takes forever (13s for ~2500 items) response = open_url(url, method=method, data=body, headers=headers, timeout=60) except HTTPError as e: - raise BitwardenException("Received HTTP error for %s : %s" % (url, to_native(e))) + raise BitwardenException(f'Received HTTP error for {url} : {to_native(e)}') except URLError as e: - raise BitwardenException("Failed lookup url for %s : %s" % (url, to_native(e))) + raise BitwardenException(f'Failed lookup url for {url} : {to_native(e)}') except SSLValidationError as e: - raise BitwardenException("Error validating the server's certificate for %s: %s" % (url, to_native(e))) + raise BitwardenException(f"Error validating the server's certificate for {url}: {to_native(e)}") except ConnectionError as e: - raise BitwardenException("Error connecting to %s: %s" % (url, to_native(e))) + raise BitwardenException(f'Error connecting to {url}: {to_native(e)}') try: result = json.loads(to_text(response.read())) except json.decoder.JSONDecodeError as e: - raise BitwardenException('Unable to load JSON: %s' % (to_native(e))) + raise BitwardenException(f'Unable to load JSON: {to_native(e)}') if not result.get('success'): - raise BitwardenException('API call failed: %s' % (result.get('data'))) + raise BitwardenException(f"API call failed: {result.get('data')}") return result @@ -209,7 +211,7 @@ def _load_cache(self): if data.get('version') == CACHE_VERSION: self._cache = data item_count = len(self._cache['items']) if self._cache['items'] is not None else 0 - display.vvv('lfbw - cache loaded from %s (%d items)' % (CACHE_FILE, item_count)) + display.vvv(f'lfbw - cache loaded from {CACHE_FILE} ({item_count} items)') return except (IOError, OSError, ValueError, json.decoder.JSONDecodeError): pass @@ -234,12 +236,12 @@ def _save_cache(self): with os.fdopen(fd, 'w') as f: json.dump(self._cache, f) os.replace(tmp_path, CACHE_FILE) - display.vvv('lfbw - cache saved to %s' % (CACHE_FILE)) + display.vvv(f'lfbw - cache saved to {CACHE_FILE}') except Exception: os.unlink(tmp_path) raise except (IOError, OSError): - display.vvv('lfbw - failed to save cache to %s' % (CACHE_FILE)) + display.vvv(f'lfbw - failed to save cache to {CACHE_FILE}') def _get_template(self, template_name): @@ -247,12 +249,12 @@ def _get_template(self, template_name): Templates are static API schema definitions that never change. """ if template_name not in self._cache['templates']: - display.vvv('lfbw - fetching template "%s" from API' % (template_name)) - result = self._api_call('object/template/%s' % (template_name)) + display.vvv(f'lfbw - fetching template "{template_name}" from API') + result = self._api_call(f'object/template/{template_name}') self._cache['templates'][template_name] = result['data']['template'] self._save_cache() else: - display.vvv('lfbw - using cached template "%s"' % (template_name)) + display.vvv(f'lfbw - using cached template "{template_name}"') return copy.deepcopy(self._cache['templates'][template_name]) @@ -271,12 +273,12 @@ def sync(self, force=False, interval=60): if not force and time.time() - self._cache.get('sync_timestamp', 0) < interval: display.vvv('lfbw - sync skipped, last sync was recent enough') return - display.vvv('lfbw - syncing vault (force=%s)' % (force)) + display.vvv(f'lfbw - syncing vault (force={force})') self._api_call('sync', method='POST') result = self._api_call('list/object/items') self._cache['items'] = result['data']['data'] self._cache['sync_timestamp'] = time.time() - display.vvv('lfbw - sync complete, cached %d items' % (len(self._cache['items']))) + display.vvv(f"lfbw - sync complete, cached {len(self._cache['items'])} items") self._save_cache() @@ -324,7 +326,7 @@ def get_items(self, name, username=None, folder_id=None, collection_id=None, org if isinstance(organization_id, str) and len(organization_id.strip()) == 0: organization_id = None - display.vvv('lfbw - searching cache for name="%s", username="%s"' % (name, username)) + display.vvv(f'lfbw - searching cache for name="{name}", username="{username}"') matching_items = [] for item in self._cache['items']: if item.get('type') != 1: @@ -341,25 +343,27 @@ def get_items(self, name, username=None, folder_id=None, collection_id=None, org and (item.get('organizationId') == organization_id): matching_items.append(item) - display.vvv('lfbw - found %d matching item(s)' % (len(matching_items))) + display.vvv(f'lfbw - found {len(matching_items)} matching item(s)') return matching_items def get_item_by_id(self, item_id): - """Get an item by ID from Bitwarden. Returns the item or None. Throws an exception if the id leads to unambiguous results. + """Get an item by ID from Bitwarden. Looks in the cache first, then falls back to the + API (the item may have been created externally). Returns the item; raises + BitwardenException if the API does not know the ID. """ - display.vvv('lfbw - looking up item by id=%s' % (item_id)) + display.vvv(f'lfbw - looking up item by id={item_id}') for item in self._cache['items']: if item.get('id') == item_id: display.vvv('lfbw - found item in cache') return item # fallback to API if not found in cache (item could have been created externally) display.vvv('lfbw - item not in cache, falling back to API') - result = self._api_call('object/item/%s' % (item_id)) + result = self._api_call(f'object/item/{item_id}') return result['data'] - def generate(self, password_length=60, password_choice='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'): + def generate(self, password_length=60, password_choice='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'): # nosec B107 - this is the character set to draw from, not a password """Generates a random password of a given length. If you want to generate a hex-based password, ensure that password_length is positive and even (as hex characters typically come in pairs representing bytes), and that password_choice is set to '0123456789abcdef'. @@ -452,7 +456,7 @@ def get_template_item(self, name, login=None, notes=None, organization_id=None, def create_item(self, item): """Creates an item object in Bitwarden. """ - display.vvv('lfbw - creating item "%s"' % (item.get('name', ''))) + display.vvv(f'lfbw - creating item "{item.get("name", "")}"') result = self._api_call('object/item', method='POST', body=item) self._cache['items'].append(result['data']) self._save_cache() @@ -463,8 +467,8 @@ def create_item(self, item): def edit_item(self, item, item_id): """Edits an item object in Bitwarden. """ - display.vvv('lfbw - editing item %s' % (item_id)) - result = self._api_call('object/item/%s' % (item_id), method='PUT', body=item) + display.vvv(f'lfbw - editing item {item_id}') + result = self._api_call(f'object/item/{item_id}', method='PUT', body=item) for i, cached_item in enumerate(self._cache['items']): if cached_item.get('id') == item_id: self._cache['items'][i] = result['data'] @@ -477,14 +481,14 @@ def edit_item(self, item, item_id): def add_attachment(self, item_id, attachment_path): """Adds the file at `attachment_path` to the item specified by `item_id` """ - display.vvv('lfbw - adding attachment "%s" to item %s' % (attachment_path, item_id)) + display.vvv(f'lfbw - adding attachment "{attachment_path}" to item {item_id}') body = { 'file': { 'filename': attachment_path, }, } - result = self._api_call('attachment?itemId=%s' % (item_id), method='POST', body=body, body_format='form-multipart') + result = self._api_call(f'attachment?itemId={item_id}', method='POST', body=body, body_format='form-multipart') for i, cached_item in enumerate(self._cache['items']): if cached_item.get('id') == item_id: self._cache['items'][i] = result['data'] @@ -503,6 +507,6 @@ def get_pretty_name(name, hostname=None, purpose=None): if not name: name = hostname if purpose: - name += ' - {}'.format(purpose) + name += f' - {purpose}' return name diff --git a/plugins/modules/bitwarden_item.py b/plugins/modules/bitwarden_item.py index 44b069e4..36e6ec13 100644 --- a/plugins/modules/bitwarden_item.py +++ b/plugins/modules/bitwarden_item.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2022, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -261,9 +263,9 @@ def diff_and_update(current, target): - '''Diffs the current item with the target item and checks if there are relevant changes. - Returns (changed, updated_item). The updated_item can be send to `bw` to update the remote item. - ''' + """Diffs the current item with the target item and checks if there are relevant changes. + Returns (changed, updated_item). The updated_item can be sent to `bw` to update the remote item. + """ def check_dict_for_changes(current, target): changed = False @@ -273,7 +275,7 @@ def check_dict_for_changes(current, target): changed = True elif (value != current.get(key)) \ - and not (not value and not current.get(key)): # compare None to emtpy lists and empty strings + and not (not value and not current.get(key)): # compare None to empty lists and empty strings changed = True return changed @@ -316,11 +318,11 @@ def run_module(): if attachments: basenames = [os.path.basename(attachment) for attachment in attachments] if len(set(basenames)) < len(basenames): - module.fail_json('This module cannot handle multiple attachments with the same basename.') + module.fail_json(msg='This module cannot handle multiple attachments with the same basename.') for attachment in attachments: if not os.access(attachment, os.R_OK): - module.fail_json('Could not read the attachments at "{}".'.format(attachment)) + module.fail_json(msg=f'Could not read the attachments at "{attachment}".') # extract the variables to make the code more readable collection_id = module.params['collection_id'] @@ -338,7 +340,7 @@ def run_module(): bw = Bitwarden() if not bw.is_unlocked: - module.fail_json('Not logged into Bitwarden, or Bitwarden Vault is locked. Please run `bw login` and `bw unlock` first.') + module.fail_json(msg='Not logged into Bitwarden, or Bitwarden Vault is locked. Please run `bw login` and `bw unlock` first.') # to be sure we are up to date bw.sync() @@ -353,10 +355,7 @@ def run_module(): if len(current_items) > 1: module.fail_json(msg='Found multiple Bitwarden items with the same name/title and username, cannot decide which one to use. Aborting.') - try: - current_item = current_items[0] - except IndexError: - current_item = None + current_item = current_items[0] if current_items else None login_uris = bw.get_template_item_login_uri(uris) login = bw.get_template_item_login(username, password, login_uris) diff --git a/pyproject.toml b/pyproject.toml index cc86c240..1035d543 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,3 +19,7 @@ min_confidence = 80 # Unit tests for the in-house plugins. The matrix of Python / ansible-core # versions is driven by tox; see tox.ini. testpaths = ["tests/unit"] +# importlib mode lets same-named test files live in different plugin-type +# directories (e.g. modules/ and lookup/ both have test_bitwarden_item.py) +# without needing __init__.py packages. +addopts = ["--import-mode=importlib"] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..1e4b9f4c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Make this checkout importable as `ansible_collections.linuxfabrik.lfops`. + +Modules and lookups import their shared code via the collection-qualified +path, e.g. + + from ansible_collections.linuxfabrik.lfops.plugins.module_utils.bitwarden import Bitwarden + +For that to resolve under a plain pytest/tox run (without installing the +collection), build a temporary `ansible_collections/linuxfabrik/lfops` +tree that symlinks back to the repo and put it on sys.path. Filter and +lookup tests that load a plugin by file path do not need this, but it is +harmless for them. +""" + +import os +import pathlib +import sys +import tempfile + +_REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent + + +def _make_collection_importable(): + root = pathlib.Path(tempfile.mkdtemp(prefix='lfops_ansible_collections_')) + link_parent = root / 'ansible_collections' / 'linuxfabrik' + link_parent.mkdir(parents=True, exist_ok=True) + link = link_parent / 'lfops' + if not link.exists(): + link.symlink_to(_REPO_ROOT, target_is_directory=True) + sys.path.insert(0, str(root)) + os.environ.setdefault('ANSIBLE_COLLECTIONS_PATH', str(root)) + + +_make_collection_importable() diff --git a/tests/unit/plugins/lookup/test_bitwarden_item.py b/tests/unit/plugins/lookup/test_bitwarden_item.py new file mode 100644 index 00000000..3f913e6a --- /dev/null +++ b/tests/unit/plugins/lookup/test_bitwarden_item.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for the bitwarden_item lookup plugin. + +The lookup runs on the controller. All Bitwarden I/O goes through the +Bitwarden client, which is replaced with a fake here, so no server or +cache is touched. The collection import is wired up by tests/conftest.py. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from ansible.errors import AnsibleError +from ansible_collections.linuxfabrik.lfops.plugins.lookup import bitwarden_item as lookup_mod + + +class _FakeBitwarden: + """Minimal stand-in for the Bitwarden client used by the lookup.""" + + items_by_search = [] + item_by_id = None + + def __init__(self, *args, **kwargs): + pass + + @property + def is_unlocked(self): + return True + + def sync(self, *args, **kwargs): + pass + + def get_items(self, name, username=None, folder_id=None, collection_id=None, organization_id=None): + return list(type(self).items_by_search) + + def get_item_by_id(self, item_id): + return type(self).item_by_id + + @staticmethod + def get_pretty_name(name, hostname=None, purpose=None): + return name or hostname + + +class _BitwardenLookupTestCase(unittest.TestCase): + + def setUp(self): + self._orig = lookup_mod.Bitwarden + lookup_mod.Bitwarden = _FakeBitwarden + _FakeBitwarden.items_by_search = [] + _FakeBitwarden.item_by_id = None + self.lookup = lookup_mod.LookupModule(loader=None, templar=None) + + def tearDown(self): + lookup_mod.Bitwarden = self._orig + + +class TestRun(_BitwardenLookupTestCase): + + def test_existing_single_item_lifts_credentials(self): + _FakeBitwarden.items_by_search = [ + {'name': 'host - db', 'login': {'username': 'dba', 'password': 'linuxfabrik'}}, + ] + result = self.lookup.run([{'name': 'host - db', 'username': 'dba'}]) + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['username'], 'dba') + self.assertEqual(result[0]['password'], 'linuxfabrik') + + def test_multiple_matches_raise(self): + _FakeBitwarden.items_by_search = [ + {'name': 'host - db', 'login': {'username': 'dba', 'password': 'linuxfabrik'}}, + {'name': 'host - db', 'login': {'username': 'dba', 'password': 'linuxfabrik'}}, + ] + with self.assertRaises(AnsibleError): + self.lookup.run([{'name': 'host - db', 'username': 'dba'}]) + + def test_lookup_by_id_lifts_credentials(self): + _FakeBitwarden.item_by_id = { + 'id': 'abc', 'login': {'username': 'dba', 'password': 'linuxfabrik'}, + } + result = self.lookup.run([{'id': 'abc'}]) + self.assertEqual(len(result), 1) + self.assertEqual(result[0]['username'], 'dba') + self.assertEqual(result[0]['password'], 'linuxfabrik') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/plugins/module_utils/test_bitwarden.py b/tests/unit/plugins/module_utils/test_bitwarden.py new file mode 100644 index 00000000..97367368 --- /dev/null +++ b/tests/unit/plugins/module_utils/test_bitwarden.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for the bitwarden module_util. + +The util runs on the Ansible controller (it is imported by the +bitwarden_item lookup) and, via AnsiballZ, on the managed node for the +bitwarden_item module. All network access funnels through +ansible.module_utils.urls.open_url, which the tests mock; no real +Bitwarden server or cache file is touched. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import importlib.util +import io +import json +import os +import unittest +from urllib.error import HTTPError + +_MODULE_PATH = os.path.join( + os.path.dirname(__file__), + '..', '..', '..', '..', + 'plugins', 'module_utils', 'bitwarden.py', +) +_spec = importlib.util.spec_from_file_location('bitwarden', _MODULE_PATH) +bitwarden = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(bitwarden) + + +class _FakeResponse: + def __init__(self, payload): + self._payload = payload + + def read(self): + return json.dumps(self._payload).encode('utf-8') + + +def _make_bitwarden(tmp_path='/nonexistent/lfops_bw_test_cache.json'): + """Instantiate Bitwarden without touching a real cache file. + + Pointing CACHE_FILE at a missing path makes _load_cache start fresh, + and the constructor performs no network I/O. + """ + bitwarden.CACHE_FILE = tmp_path + return bitwarden.Bitwarden() + + +class TestGenerate(unittest.TestCase): + + def test_length_and_charset(self): + bw = _make_bitwarden() + result = bw.generate(password_length=32, password_choice='abc') + self.assertEqual(len(result), 32) + self.assertTrue(set(result).issubset(set('abc'))) + + def test_rejects_non_positive_length(self): + bw = _make_bitwarden() + with self.assertRaises(ValueError): + bw.generate(password_length=0) + + def test_hex_requires_even_length(self): + bw = _make_bitwarden() + with self.assertRaises(ValueError): + bw.generate(password_length=3, password_choice='0123456789abcdef') + # even length is fine + self.assertEqual(len(bw.generate(password_length=4, password_choice='0123456789abcdef')), 4) + + +class TestGetPrettyName(unittest.TestCase): + + def test_explicit_name_wins(self): + self.assertEqual(bitwarden.Bitwarden.get_pretty_name('myname', 'host', 'purpose'), 'myname') + + def test_hostname_only(self): + self.assertEqual(bitwarden.Bitwarden.get_pretty_name('', hostname='app4711'), 'app4711') + + def test_hostname_and_purpose(self): + self.assertEqual( + bitwarden.Bitwarden.get_pretty_name('', hostname='app4711', purpose='MariaDB'), + 'app4711 - MariaDB', + ) + + +class TestApiCall(unittest.TestCase): + + def setUp(self): + self.bw = _make_bitwarden() + self._orig_open_url = bitwarden.open_url + + def tearDown(self): + bitwarden.open_url = self._orig_open_url + + def test_success_returns_result(self): + bitwarden.open_url = lambda *a, **k: _FakeResponse({'success': True, 'data': {'x': 1}}) + result = self.bw._api_call('status') + self.assertEqual(result, {'success': True, 'data': {'x': 1}}) + + def test_unsuccessful_payload_raises(self): + bitwarden.open_url = lambda *a, **k: _FakeResponse({'success': False, 'data': 'nope'}) + with self.assertRaises(bitwarden.BitwardenException): + self.bw._api_call('status') + + def test_http_error_raises_bitwarden_exception(self): + def _raise(*a, **k): + raise HTTPError('http://127.0.0.1:8087/status', 500, 'err', {}, io.BytesIO(b'')) + bitwarden.open_url = _raise + with self.assertRaises(bitwarden.BitwardenException): + self.bw._api_call('status') + + def test_invalid_json_raises_bitwarden_exception(self): + class _BadResponse: + def read(self): + return b'not json' + bitwarden.open_url = lambda *a, **k: _BadResponse() + with self.assertRaises(bitwarden.BitwardenException): + self.bw._api_call('status') + + +class TestGetItems(unittest.TestCase): + + def setUp(self): + self.bw = _make_bitwarden() + # seed the in-memory cache directly; get_items only reads it + self.bw._cache = { + 'items': [ + {'type': 1, 'name': 'host - db', 'login': {'username': 'dba'}, + 'folderId': None, 'collectionIds': [], 'organizationId': None}, + {'type': 2, 'name': 'host - db', 'login': {'username': 'dba'}}, # non-login, skipped + {'type': 1, 'name': 'other', 'login': {'username': 'dba'}, + 'folderId': None, 'collectionIds': [], 'organizationId': None}, + ], + } + + def test_matches_login_item_by_name_and_username(self): + matches = self.bw.get_items('host - db', username='dba') + self.assertEqual(len(matches), 1) + self.assertEqual(matches[0]['name'], 'host - db') + + def test_skips_non_login_items(self): + # the type=2 entry shares name+username but must not match + matches = self.bw.get_items('host - db', username='dba') + self.assertTrue(all(item.get('type') == 1 for item in matches)) + + def test_no_match_returns_empty(self): + self.assertEqual(self.bw.get_items('does-not-exist', username='dba'), []) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/plugins/modules/test_bitwarden_item.py b/tests/unit/plugins/modules/test_bitwarden_item.py new file mode 100644 index 00000000..3fbba453 --- /dev/null +++ b/tests/unit/plugins/modules/test_bitwarden_item.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for the bitwarden_item module's pure diff helper. + +The module itself runs via AnsiballZ, but `diff_and_update` is a plain +function and is tested in isolation. The collection import is wired up by +tests/conftest.py. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from ansible_collections.linuxfabrik.lfops.plugins.modules.bitwarden_item import diff_and_update + + +class TestDiffAndUpdate(unittest.TestCase): + + def test_takes_over_id(self): + current = {'id': 'abc', 'name': 'x'} + target = {'name': 'x'} + changed, updated = diff_and_update(current, target) + self.assertEqual(updated['id'], 'abc') + + def test_no_change_when_equal(self): + current = {'id': 'abc', 'name': 'x', 'notes': 'n'} + target = {'name': 'x', 'notes': 'n'} + changed, _ = diff_and_update(current, target) + self.assertFalse(changed) + + def test_change_detected_on_differing_value(self): + current = {'id': 'abc', 'name': 'x', 'notes': 'old'} + target = {'name': 'x', 'notes': 'new'} + changed, _ = diff_and_update(current, target) + self.assertTrue(changed) + + def test_falsy_vs_falsy_is_not_a_change(self): + # None vs empty list / empty string must not count as a change + current = {'id': 'abc', 'collectionIds': None, 'notes': ''} + target = {'collectionIds': [], 'notes': None} + changed, _ = diff_and_update(current, target) + self.assertFalse(changed) + + def test_nested_dict_change_detected(self): + current = {'id': 'abc', 'login': {'username': 'old', 'totp': ''}} + target = {'login': {'username': 'new', 'totp': ''}} + changed, _ = diff_and_update(current, target) + self.assertTrue(changed) + + def test_nested_dict_no_change(self): + current = {'id': 'abc', 'login': {'username': 'same', 'totp': ''}} + target = {'login': {'username': 'same', 'totp': ''}} + changed, _ = diff_and_update(current, target) + self.assertFalse(changed) + + +if __name__ == '__main__': + unittest.main() From bd178b711bb1ab46d72e419811a024dca60441bd Mon Sep 17 00:00:00 2001 From: Markus Frei <31855393+markuslf@users.noreply.github.com> Date: Mon, 25 May 2026 09:40:34 +0200 Subject: [PATCH 53/66] fix(plugins): make ansible-doc render all in-house plugins + add guard (#267) A description bullet containing a colon followed by a space is parsed by YAML as a mapping, which makes ansible-doc abort with 'expected str instance, AnsibleMapping found'. Rephrase the offending bullets in the nextcloud_occ_app_config / nextcloud_occ_system_config modules and the alert_contacts / mwindows / monitors option docs of the uptimerobot_monitor / uptimerobot_psp modules. Add tests/unit/test_plugin_docs.py, which parses every in-house plugin's DOCUMENTATION/RETURN and asserts each description is a string or list of strings, so this class of error fails at unit-test time instead of only at render time. Vendored plugins are out of scope (the ipa* modules also fail ansible-doc, but because their ansible-freeipa doc_fragment is not installed, which is unrelated). --- CHANGELOG.md | 1 + CONTRIBUTING.md | 2 +- plugins/modules/nextcloud_occ_app_config.py | 2 +- .../modules/nextcloud_occ_system_config.py | 2 +- plugins/modules/uptimerobot_monitor.py | 4 +- plugins/modules/uptimerobot_psp.py | 2 +- tests/unit/test_plugin_docs.py | 114 ++++++++++++++++++ 7 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 tests/unit/test_plugin_docs.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 934c33a0..a28b32b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **plugin:nextcloud_occ_app_config, plugin:nextcloud_occ_system_config, plugin:uptimerobot_monitor, plugin:uptimerobot_psp**: Fixed their documentation so `ansible-doc` renders them again. A unit-test guard now catches this class of error for every in-house plugin. * **plugin:bitwarden_item**: Fixed the lookup's documentation so `ansible-doc` renders it again. * **plugin:combine_lod**: The `combine_lod` filter now reports an error when an item is missing part of a composite `unique_key` (a list of keys), instead of silently grouping such items together. Inventories with incomplete composite keys that previously merged by accident now fail loudly and must be corrected. Also fixed its documentation so `ansible-doc` renders it again. * **role:kernel_settings**: The `systemd_cpu_affinity` setting is now actually applied. The value was computed and shown in the debug output but never passed to the underlying system role, so a configured CPU affinity had no effect. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb48a41b..5057fb2c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -771,7 +771,7 @@ In-house plugins live under `plugins/` following the standard Ansible collection ``` * Use single quotes and f-strings consistently (vendored plugins keep their upstream style, see below). -* Every plugin carries `DOCUMENTATION` (and `RETURN` / `EXAMPLES` where applicable). Keep it valid YAML: in a `description` list, a bullet containing a colon followed by a space is parsed as a mapping and makes `ansible-doc` fail, so rephrase or quote such bullets. Verify with `ansible-doc -t linuxfabrik.lfops.`. +* Every plugin carries `DOCUMENTATION` (and `RETURN` / `EXAMPLES` where applicable). Keep it valid YAML: in a `description` list, a bullet containing a colon followed by a space is parsed as a mapping and makes `ansible-doc` fail, so rephrase or quote such bullets. Verify with `ansible-doc -t linuxfabrik.lfops.`; `tests/unit/test_plugin_docs.py` guards against this class of error for all in-house plugins. * Set `version_added` to the LFOps release the plugin first shipped in, and never change it afterwards. * `module_utils` holds code shared between plugins. Do not import the external Linuxfabrik Python Libraries (`lib`) into a plugin; copy what you need and note the origin in a comment. diff --git a/plugins/modules/nextcloud_occ_app_config.py b/plugins/modules/nextcloud_occ_app_config.py index a62bd5a7..515895a8 100644 --- a/plugins/modules/nextcloud_occ_app_config.py +++ b/plugins/modules/nextcloud_occ_app_config.py @@ -17,7 +17,7 @@ - Drives C(occ config:app:set) and C(config:app:delete) to bring a single app config key into the desired state. - The current value and type are read from C(occ config:app:get --details --output=json) (or from a pre-fetched C(occ config:list --output=json --private) listing passed via I(installed_config_json)). C(occ config:app:set) is only called when the stored value or type does not already match. - When I(name) contains spaces, each whitespace-separated token is passed as a separate argument to C(occ), matching how Nextcloud addresses nested keys (e.g. C(name="endpoint enabled")). - - Booleans are normalized for Nextcloud's storage: I(value) values C(true)/C(1)/C(on)/C(yes) (case-insensitive) become C(1) in the database; everything else becomes C(0). When reading via I(installed_config_json), the type is inferred from the JSON value type (Python C(bool)/C(int)/C(float)/C(list)/C(str)), since C(occ config:list) returns values already cast by C(convertTypedValue()). + - Booleans are normalized for Nextcloud's storage. I(value) values C(true)/C(1)/C(on)/C(yes) (case-insensitive) become C(1) in the database; everything else becomes C(0). When reading via I(installed_config_json), the type is inferred from the JSON value type (Python C(bool)/C(int)/C(float)/C(list)/C(str)), since C(occ config:list) returns values already cast by C(convertTypedValue()). requirements: - A working Nextcloud installation with the C(occ) command available. diff --git a/plugins/modules/nextcloud_occ_system_config.py b/plugins/modules/nextcloud_occ_system_config.py index ce8c8c35..88e995f7 100644 --- a/plugins/modules/nextcloud_occ_system_config.py +++ b/plugins/modules/nextcloud_occ_system_config.py @@ -17,7 +17,7 @@ - Drives C(occ config:system:set) and C(config:system:delete) to bring a single system config key into the desired state. - The current value is read from C(occ config:system:get) (or from a pre-fetched C(occ config:list --output=json --private) listing passed via I(installed_config_json)). C(occ config:system:set) is only called when the stored value does not already match I(value). - When I(name) contains spaces, each whitespace-separated token is passed as a separate argument to C(occ), matching how Nextcloud addresses nested keys (e.g. C(name="trusted_domains 0"), C(name="forbidden_filename_characters 0")). - - Booleans are normalized for C(occ): I(value) values C(true)/C(1)/C(on)/C(yes) (case-insensitive) become the literal string C(true); everything else becomes C(false). This matches what Nextcloud's CastHelper accepts on C(config:system:set). Note that this differs from C(nextcloud_occ_app_config), which stores booleans as C(1)/C(0). + - Booleans are normalized for C(occ). I(value) values C(true)/C(1)/C(on)/C(yes) (case-insensitive) become the literal string C(true); everything else becomes C(false). This matches what Nextcloud's CastHelper accepts on C(config:system:set). Note that this differs from C(nextcloud_occ_app_config), which stores booleans as C(1)/C(0). requirements: - A working Nextcloud installation with the C(occ) command available. diff --git a/plugins/modules/uptimerobot_monitor.py b/plugins/modules/uptimerobot_monitor.py index d1f9142b..581db8c6 100644 --- a/plugins/modules/uptimerobot_monitor.py +++ b/plugins/modules/uptimerobot_monitor.py @@ -167,7 +167,7 @@ alert_contacts: description: - Alert contacts to attach to the monitor. Each item references an existing alert contact, plus the per-monitor I(threshold) and I(recurrence). The list is replaced on every run; pass an empty list to clear all attached contacts. - - Resolution: when an item has I(id), it is used directly. Otherwise I(friendly_name) is resolved against C(getAlertContacts); an unknown name fails the play. + - When an item has I(id), it is used directly. Otherwise I(friendly_name) is resolved against C(getAlertContacts); an unknown name fails the play. type: list elements: dict required: false @@ -189,7 +189,7 @@ mwindows: description: - Maintenance windows to attach to the monitor. Each item references an existing maintenance window. The list is replaced on every run; pass an empty list to detach all windows. - - Resolution: when an item has I(id), it is used directly. Otherwise I(friendly_name) is resolved against C(getMWindows); an unknown name fails the play. + - When an item has I(id), it is used directly. Otherwise I(friendly_name) is resolved against C(getMWindows); an unknown name fails the play. type: list elements: dict required: false diff --git a/plugins/modules/uptimerobot_psp.py b/plugins/modules/uptimerobot_psp.py index 25374955..80d3c628 100644 --- a/plugins/modules/uptimerobot_psp.py +++ b/plugins/modules/uptimerobot_psp.py @@ -42,7 +42,7 @@ monitors: description: - Monitors to display on the status page. Each item references an existing monitor. - - Resolution: when an item has I(id), it is used directly. Otherwise I(friendly_name) is resolved against C(getMonitors); an unknown name fails the play. + - When an item has I(id), it is used directly. Otherwise I(friendly_name) is resolved against C(getMonitors); an unknown name fails the play. type: list elements: dict suboptions: diff --git a/tests/unit/test_plugin_docs.py b/tests/unit/test_plugin_docs.py new file mode 100644 index 00000000..18cd3d04 --- /dev/null +++ b/tests/unit/test_plugin_docs.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Guard against the DOCUMENTATION YAML bug that breaks `ansible-doc`. + +In a `description` block (a YAML list), a bullet that contains a colon +followed by a space is parsed as a mapping instead of a string, e.g. + + description: + - Useful for X: it does Y. # parsed as {"Useful for X": "it does Y."} + +`ansible-doc` then aborts with "expected str instance, AnsibleMapping +found". This test parses every in-house plugin's DOCUMENTATION (and +RETURN) and asserts that every `description` is a string or a list of +strings, catching the bug at unit-test time instead of at render time. + +Vendored plugins keep their upstream docs and are out of scope. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import ast +import glob +import os +import unittest + +import yaml + +_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +_PLUGIN_GLOBS = [ + 'plugins/filter/*.py', + 'plugins/lookup/*.py', + 'plugins/modules/*.py', +] +# Vendored plugins are kept in lockstep with upstream and not restyled here. +_VENDORED_PREFIXES = ('ipa',) +_VENDORED_NAMES = {'lvm_pv.py'} + + +def _in_house_plugin_files(): + files = [] + for pattern in _PLUGIN_GLOBS: + for path in glob.glob(os.path.join(_REPO_ROOT, pattern)): + name = os.path.basename(path) + if name.startswith(_VENDORED_PREFIXES) or name in _VENDORED_NAMES: + continue + files.append(path) + return sorted(files) + + +def _extract_doc_constants(source): + """Return {const_name: yaml_obj} for DOCUMENTATION/RETURN string assignments.""" + tree = ast.parse(source) + docs = {} + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + names = [t.id for t in node.targets if isinstance(t, ast.Name)] + for wanted in ('DOCUMENTATION', 'RETURN'): + if wanted in names and isinstance(node.value, ast.Constant) \ + and isinstance(node.value.value, str): + docs[wanted] = yaml.safe_load(node.value.value) + return docs + + +def _iter_description_problems(obj, path=''): + """Yield human-readable paths where a `description` is not str / list[str].""" + if isinstance(obj, dict): + for key, value in obj.items(): + if key == 'description': + if isinstance(value, str): + pass + elif isinstance(value, list): + for i, item in enumerate(value): + if not isinstance(item, str): + yield f'{path}.description[{i}] is {type(item).__name__}, expected str' + else: + yield f'{path}.description is {type(value).__name__}, expected str or list[str]' + yield from _iter_description_problems(value, f'{path}.{key}') + elif isinstance(obj, list): + for i, item in enumerate(obj): + yield from _iter_description_problems(item, f'{path}[{i}]') + + +class TestPluginDocs(unittest.TestCase): + + def test_in_house_plugins_have_renderable_descriptions(self): + files = _in_house_plugin_files() + self.assertTrue(files, 'no in-house plugin files found') + for path in files: + with self.subTest(plugin=os.path.relpath(path, _REPO_ROOT)): + with open(path, 'r') as f: + source = f.read() + docs = _extract_doc_constants(source) + problems = [] + for const_name, doc in docs.items(): + problems += [f'{const_name}{p}' for p in _iter_description_problems(doc)] + self.assertEqual( + problems, [], + 'description fields must be str or list[str] ' + '(a colon + space in a bullet makes ansible-doc fail):\n ' + + '\n '.join(problems), + ) + + +if __name__ == '__main__': + unittest.main() From 7912dcdba683e695b8a8ee925ed27bbc570e4744 Mon Sep 17 00:00:00 2001 From: Markus Frei <31855393+markuslf@users.noreply.github.com> Date: Mon, 25 May 2026 09:54:53 +0200 Subject: [PATCH 54/66] refactor(plugins): unify uptimerobot family and add unit tests (#268) Bring the uptimerobot module_util and all nine uptimerobot_* modules to the standard plugin style: standard file header (also unifies the module_util copyright line) and f-strings throughout, replacing every str.format() call. No behavior change from the formatting. Safe fixes: - module_util: the four get_* functions now pass a non-list API response (the stat-ok message fallback) straight through instead of iterating it and crashing - module_util: drop the no-op ternary and the now-unused is_paginated_field bookkeeping in _request_uncached Add unit tests for the pure helpers: the module_util wire builders, secret redaction, cache-key hashing, friendly-name resolution and the read-direction response translators; the monitor alert_contacts/mwindows normalizers; and the mwindow time helpers (incl. the midnight wrap). --- CHANGELOG.md | 1 + plugins/module_utils/uptimerobot.py | 111 ++++++-------- plugins/modules/uptimerobot_account_info.py | 23 +-- plugins/modules/uptimerobot_alert_contact.py | 27 ++-- .../modules/uptimerobot_alert_contact_info.py | 14 +- plugins/modules/uptimerobot_monitor.py | 48 +++--- plugins/modules/uptimerobot_monitor_info.py | 16 +- plugins/modules/uptimerobot_mwindow.py | 42 +++--- plugins/modules/uptimerobot_mwindow_info.py | 14 +- plugins/modules/uptimerobot_psp.py | 46 +++--- plugins/modules/uptimerobot_psp_info.py | 14 +- .../plugins/module_utils/test_uptimerobot.py | 137 ++++++++++++++++++ .../modules/test_uptimerobot_monitor.py | 64 ++++++++ .../modules/test_uptimerobot_mwindow.py | 56 +++++++ 14 files changed, 426 insertions(+), 187 deletions(-) create mode 100644 tests/unit/plugins/module_utils/test_uptimerobot.py create mode 100644 tests/unit/plugins/modules/test_uptimerobot_monitor.py create mode 100644 tests/unit/plugins/modules/test_uptimerobot_mwindow.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a28b32b5..736e45dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **plugin:uptimerobot_\***: The modules no longer crash when the UptimeRobot API returns a non-list response for a list endpoint; the response is passed through instead. * **plugin:nextcloud_occ_app_config, plugin:nextcloud_occ_system_config, plugin:uptimerobot_monitor, plugin:uptimerobot_psp**: Fixed their documentation so `ansible-doc` renders them again. A unit-test guard now catches this class of error for every in-house plugin. * **plugin:bitwarden_item**: Fixed the lookup's documentation so `ansible-doc` renders it again. * **plugin:combine_lod**: The `combine_lod` filter now reports an error when an item is missing part of a composite `unique_key` (a list of keys), instead of silently grouping such items together. Inventories with incomplete composite keys that previously merged by accident now fail loudly and must be corrected. Also fixed its documentation so `ansible-doc` renders it again. diff --git a/plugins/module_utils/uptimerobot.py b/plugins/module_utils/uptimerobot.py index 031a9463..c96eeda0 100644 --- a/plugins/module_utils/uptimerobot.py +++ b/plugins/module_utils/uptimerobot.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. """Shared client for the UptimeRobot API used by the linuxfabrik.lfops.uptimerobot_* modules. @@ -137,21 +139,21 @@ def resolve_api_key(module, api_key, api_key_file): with open(path) as fh: value = fh.read().strip() if value: - module.log('uptimerobot: api_key resolved from file {0}'.format(path)) + module.log(f'uptimerobot: api_key resolved from file {path}') return value except OSError: pass env = os.environ.get(ENV_API_KEY, '').strip() if env: - module.log('uptimerobot: api_key resolved from env {0}'.format(ENV_API_KEY)) + module.log(f'uptimerobot: api_key resolved from env {ENV_API_KEY}') return env module.fail_json(msg=( 'No UptimeRobot API key found. Provide one via the `api_key` parameter, ' - 'an `api_key_file` (default: {default}), or the {env} environment ' - 'variable.' - ).format(default=DEFAULT_API_KEY_FILE, env=ENV_API_KEY)) + f'an `api_key_file` (default: {DEFAULT_API_KEY_FILE}), or the {ENV_API_KEY} ' + 'environment variable.' + )) # --- Wire format helpers ----------------------------------------------------- @@ -166,11 +168,9 @@ def alert_contacts_wire(items): """ parts = [] for item in items: - parts.append('{id}_{t}_{r}'.format( - id=item['id'], - t=item.get('threshold', 0), - r=item.get('recurrence', 0), - )) + parts.append( + f"{item['id']}_{item.get('threshold', 0)}_{item.get('recurrence', 0)}" + ) return '-'.join(parts) @@ -202,17 +202,15 @@ def _safe_keys(params): without leaking secrets to syslog. """ return sorted( - '{0}='.format(k) if k in _SENSITIVE_KEYS else k + f'{k}=' if k in _SENSITIVE_KEYS else k for k in params ) def _cache_key(endpoint, api_key, params): """Stable hash of endpoint + api_key + params; truncated for filename use.""" - blob = '{e}|{k}|{p}'.format( - e=endpoint, k=api_key, - p=json.dumps(params or {}, sort_keys=True, default=str), - ) + serialized = json.dumps(params or {}, sort_keys=True, default=str) + blob = f'{endpoint}|{api_key}|{serialized}' return hashlib.sha256(blob.encode('utf-8')).hexdigest()[:16] @@ -221,9 +219,10 @@ def _cache_path(endpoint, api_key, params): os.makedirs(CACHE_DIR, exist_ok=True) except OSError: return None - return os.path.join(CACHE_DIR, '{e}-{h}.json'.format( - e=endpoint, h=_cache_key(endpoint, api_key, params), - )) + return os.path.join( + CACHE_DIR, + f'{endpoint}-{_cache_key(endpoint, api_key, params)}.json', + ) def _cache_read(endpoint, api_key, params): @@ -286,11 +285,8 @@ def _request(module, api_key, endpoint, params, result_key): if endpoint in _CACHEABLE_GETS: cached = _cache_read(endpoint, api_key, params) if cached is not None: - module.log('uptimerobot: cache HIT {endpoint} ({n} items, ttl={ttl}s)'.format( - endpoint=endpoint, - n=len(cached) if isinstance(cached, list) else 1, - ttl=CACHE_TTL_SECONDS, - )) + n = len(cached) if isinstance(cached, list) else 1 + module.log(f'uptimerobot: cache HIT {endpoint} ({n} items, ttl={CACHE_TTL_SECONDS}s)') return True, cached success, result = _request_uncached(module, api_key, endpoint, params, result_key) @@ -310,13 +306,10 @@ def _request_uncached(module, api_key, endpoint, params, result_key): body['api_key'] = api_key body['format'] = 'json' - module.log('uptimerobot: POST {endpoint} keys={keys}'.format( - endpoint=endpoint, keys=_safe_keys(params), - )) + module.log(f'uptimerobot: POST {endpoint} keys={_safe_keys(params)}') aggregated = [] offset = 0 - is_paginated_field = None pages = 0 while True: @@ -341,9 +334,7 @@ def _request_uncached(module, api_key, endpoint, params, result_key): retry_after = int(info.get('retry-after') or DEFAULT_RATE_LIMIT_RETRY_SECONDS) sleep_for = min(retry_after, 60) module.warn( - 'uptimerobot: rate limited on {endpoint} (HTTP 429); sleeping {sec}s and retrying once'.format( - endpoint=endpoint, sec=sleep_for, - ), + f'uptimerobot: rate limited on {endpoint} (HTTP 429); sleeping {sleep_for}s and retrying once', ) time.sleep(sleep_for) resp, info = fetch_url( @@ -357,46 +348,31 @@ def _request_uncached(module, api_key, endpoint, params, result_key): status = info.get('status', -1) if status < 200 or status >= 300: - module.log('uptimerobot: POST {endpoint} -> HTTP {status}'.format( - endpoint=endpoint, status=status, - )) - return False, 'HTTP {status} from {url}: {msg}'.format( - status=status, - url=url, - msg=info.get('msg') or info.get('body') or '', - ) + module.log(f'uptimerobot: POST {endpoint} -> HTTP {status}') + msg = info.get('msg') or info.get('body') or '' + return False, f'HTTP {status} from {url}: {msg}' try: payload = json.loads(resp.read().decode('utf-8')) except (ValueError, AttributeError) as exc: - return False, 'Could not parse JSON from {url}: {exc}'.format(url=url, exc=exc) + return False, f'Could not parse JSON from {url}: {exc}' if payload.get('stat') != 'ok': err = payload.get('error') or {} - module.log('uptimerobot: POST {endpoint} stat=fail type={type}'.format( - endpoint=endpoint, type=err.get('type', 'unknown'), - )) - return False, '{type}: {message}'.format( - type=err.get('type', 'unknown'), - message=err.get('message', payload), - ) + module.log(f"uptimerobot: POST {endpoint} stat=fail type={err.get('type', 'unknown')}") + return False, f"{err.get('type', 'unknown')}: {err.get('message', payload)}" if payload.get(result_key) is None: # Some endpoints return only `stat: 'ok'` (e.g. delete, edit when no # detail is included). Fall back to the message field if present. - module.log('uptimerobot: POST {endpoint} stat=ok (no {key} in payload)'.format( - endpoint=endpoint, key=result_key, - )) + module.log(f'uptimerobot: POST {endpoint} stat=ok (no {result_key} in payload)') return True, payload.get('message', payload) item = payload[result_key] if isinstance(item, list): aggregated += item - is_paginated_field = True else: - module.log('uptimerobot: POST {endpoint} stat=ok pages={pages}'.format( - endpoint=endpoint, pages=pages, - )) + module.log(f'uptimerobot: POST {endpoint} stat=ok pages={pages}') return True, item pagination = payload.get('pagination') or {} @@ -407,10 +383,8 @@ def _request_uncached(module, api_key, endpoint, params, result_key): break offset += PAGE_SIZE - module.log('uptimerobot: POST {endpoint} stat=ok pages={pages} items={n}'.format( - endpoint=endpoint, pages=pages, n=len(aggregated), - )) - return True, aggregated if is_paginated_field else aggregated + module.log(f'uptimerobot: POST {endpoint} stat=ok pages={pages} items={len(aggregated)}') + return True, aggregated def _filter_keys(params, allowed): @@ -481,6 +455,9 @@ def get_monitors(module, api_key, search=None): success, monitors = _request(module, api_key, 'getMonitors', params, 'monitors') if not success: return success, monitors + if not isinstance(monitors, list): + # an endpoint that returned only `stat: ok` yields the message fallback + return True, monitors for item in monitors: _translate_monitor_response(item) return True, monitors @@ -552,6 +529,8 @@ def get_mwindows(module, api_key): success, mwindows = _request(module, api_key, 'getMWindows', {}, 'mwindows') if not success: return success, mwindows + if not isinstance(mwindows, list): + return True, mwindows for item in mwindows: _translate_mwindow_response(item) return True, mwindows @@ -616,6 +595,8 @@ def get_psps(module, api_key): success, psps = _request(module, api_key, 'getPSPs', {}, 'psps') if not success: return success, psps + if not isinstance(psps, list): + return True, psps for item in psps: _translate_psp_response(item) return True, psps @@ -676,6 +657,8 @@ def get_alert_contacts(module, api_key): success, contacts = _request(module, api_key, 'getAlertContacts', {}, 'alert_contacts') if not success: return success, contacts + if not isinstance(contacts, list): + return True, contacts for item in contacts: _translate_alert_contact_response(item) return True, contacts @@ -728,9 +711,7 @@ def resolve_friendly_names(items, names, kind): for name in names: if name not in by_name: raise ValueError( - '{kind} with friendly_name {name!r} not found on UptimeRobot'.format( - kind=kind, name=name, - ), + f'{kind} with friendly_name {name!r} not found on UptimeRobot', ) out.append(int(by_name[name]['id'])) return out diff --git a/plugins/modules/uptimerobot_account_info.py b/plugins/modules/uptimerobot_account_info.py index 2228ca39..9a58aebb 100644 --- a/plugins/modules/uptimerobot_account_info.py +++ b/plugins/modules/uptimerobot_account_info.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -100,11 +102,12 @@ def main(): module.log('uptimerobot_account_info: fetching account details') success, account = ur.get_account_details(module, api_key) if not success: - module.fail_json(msg='Could not fetch UptimeRobot account details: {0}'.format(account)) - module.log('uptimerobot_account_info: monitor_limit={0} up={1} down={2} paused={3}'.format( - account.get('monitor_limit'), account.get('up_monitors'), - account.get('down_monitors'), account.get('paused_monitors'), - )) + module.fail_json(msg=f'Could not fetch UptimeRobot account details: {account}') + module.log( + f"uptimerobot_account_info: monitor_limit={account.get('monitor_limit')} " + f"up={account.get('up_monitors')} down={account.get('down_monitors')} " + f"paused={account.get('paused_monitors')}" + ) module.exit_json(changed=False, account=account, debug={ 'operation': 'read', 'fields': sorted(account.keys()) if isinstance(account, dict) else None, diff --git a/plugins/modules/uptimerobot_alert_contact.py b/plugins/modules/uptimerobot_alert_contact.py index 33e839b7..c8b69b03 100644 --- a/plugins/modules/uptimerobot_alert_contact.py +++ b/plugins/modules/uptimerobot_alert_contact.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -120,15 +122,13 @@ def main(): contact_id = module.params.get('id') friendly_name = module.params.get('friendly_name') - module.log('uptimerobot_alert_contact: looking up id={0} friendly_name={1!r}'.format( - contact_id, friendly_name, - )) + module.log(f'uptimerobot_alert_contact: looking up id={contact_id} friendly_name={friendly_name!r}') target = None if contact_id is None: success, contacts = ur.get_alert_contacts(module, api_key) if not success: - module.fail_json(msg='Could not list alert contacts: {0}'.format(contacts)) + module.fail_json(msg=f'Could not list alert contacts: {contacts}') target = ur.find_by_friendly_name(contacts, friendly_name) if target is None: module.exit_json(changed=False, alert_contact={}, debug={ @@ -162,12 +162,13 @@ def main(): 'friendly_name': target.get('friendly_name'), }) - module.log('uptimerobot_alert_contact: deleting id={0} friendly_name={1!r}'.format( - contact_id, target.get('friendly_name'), - )) + module.log( + f"uptimerobot_alert_contact: deleting id={contact_id} " + f"friendly_name={target.get('friendly_name')!r}" + ) success, result = ur.delete_alert_contact(module, api_key, contact_id) if not success: - module.fail_json(msg='Could not delete alert contact {0!r}: {1}'.format(target, result)) + module.fail_json(msg=f'Could not delete alert contact {target!r}: {result}') module.exit_json(changed=True, alert_contact=target, debug={ 'operation': 'delete', 'contact_id': contact_id, diff --git a/plugins/modules/uptimerobot_alert_contact_info.py b/plugins/modules/uptimerobot_alert_contact_info.py index 0005807d..76f8dc50 100644 --- a/plugins/modules/uptimerobot_alert_contact_info.py +++ b/plugins/modules/uptimerobot_alert_contact_info.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -103,7 +105,7 @@ def main(): module.log('uptimerobot_alert_contact_info: fetching alert contacts') success, contacts = ur.get_alert_contacts(module, api_key) if not success: - module.fail_json(msg='Could not list alert contacts: {0}'.format(contacts)) + module.fail_json(msg=f'Could not list alert contacts: {contacts}') if friendly_name: match = ur.find_by_friendly_name(contacts, friendly_name) diff --git a/plugins/modules/uptimerobot_monitor.py b/plugins/modules/uptimerobot_monitor.py index 581db8c6..4abfa139 100644 --- a/plugins/modules/uptimerobot_monitor.py +++ b/plugins/modules/uptimerobot_monitor.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -340,7 +342,7 @@ def _build_alert_contacts(module, api_key, items): if needs_resolution: success, contacts = ur.get_alert_contacts(module, api_key) if not success: - module.fail_json(msg='Could not list alert contacts: {0}'.format(contacts)) + module.fail_json(msg=f'Could not list alert contacts: {contacts}') by_name = {c.get('friendly_name'): c for c in contacts} resolved = [] @@ -349,7 +351,7 @@ def _build_alert_contacts(module, api_key, items): if contact_id is None: name = item['friendly_name'] if name not in by_name: - module.fail_json(msg='Alert contact {0!r} not found on UptimeRobot'.format(name)) + module.fail_json(msg=f'Alert contact {name!r} not found on UptimeRobot') contact_id = int(by_name[name]['id']) resolved.append({ 'id': contact_id, @@ -367,7 +369,7 @@ def _build_mwindows(module, api_key, items): if needs_resolution: success, mwindows = ur.get_mwindows(module, api_key) if not success: - module.fail_json(msg='Could not list maintenance windows: {0}'.format(mwindows)) + module.fail_json(msg=f'Could not list maintenance windows: {mwindows}') by_name = {m.get('friendly_name'): m for m in mwindows} ids = [] @@ -376,7 +378,7 @@ def _build_mwindows(module, api_key, items): if wid is None: name = item['friendly_name'] if name not in by_name: - module.fail_json(msg='Maintenance window {0!r} not found on UptimeRobot'.format(name)) + module.fail_json(msg=f'Maintenance window {name!r} not found on UptimeRobot') wid = int(by_name[name]['id']) ids.append(int(wid)) return ur.mwindows_wire(ids) @@ -467,17 +469,15 @@ def main(): friendly_name = module.params['friendly_name'] state = module.params['state'] - module.log('uptimerobot_monitor: looking up friendly_name={0!r}'.format(friendly_name)) + module.log(f'uptimerobot_monitor: looking up friendly_name={friendly_name!r}') # Step 1: locate the monitor by friendly_name. We can't trust `search` to # be exact-match, so list all and filter ourselves. success, monitors = ur.get_monitors(module, api_key) if not success: - module.fail_json(msg='Could not list monitors: {0}'.format(monitors)) + module.fail_json(msg=f'Could not list monitors: {monitors}') current = ur.find_by_friendly_name(monitors, friendly_name) - module.log('uptimerobot_monitor: existing={0} (out of {1} monitors on the account)'.format( - bool(current), len(monitors), - )) + module.log(f'uptimerobot_monitor: existing={bool(current)} (out of {len(monitors)} monitors on the account)') # Step 2: build the desired payload (only fields the user actually set). desired = {} @@ -520,12 +520,10 @@ def main(): 'friendly_name': friendly_name, 'monitor_id': current['id'], }) - module.log('uptimerobot_monitor: deleting id={0} friendly_name={1!r}'.format( - current['id'], friendly_name, - )) + module.log(f"uptimerobot_monitor: deleting id={current['id']} friendly_name={friendly_name!r}") success, result = ur.delete_monitor(module, api_key, current['id']) if not success: - module.fail_json(msg='Could not delete monitor {0!r}: {1}'.format(friendly_name, result)) + module.fail_json(msg=f'Could not delete monitor {friendly_name!r}: {result}') module.exit_json(changed=True, monitor=current, diff={'before': delete_before, 'after': {}}, debug={ @@ -555,12 +553,10 @@ def main(): 'friendly_name': friendly_name, 'sent_keys': sorted(body.keys()), }) - module.log('uptimerobot_monitor: creating friendly_name={0!r} sent_keys={1}'.format( - friendly_name, sorted(body.keys()), - )) + module.log(f'uptimerobot_monitor: creating friendly_name={friendly_name!r} sent_keys={sorted(body.keys())}') success, result = ur.new_monitor(module, api_key, body) if not success: - module.fail_json(msg='Could not create monitor {0!r}: {1}'.format(friendly_name, result)) + module.fail_json(msg=f'Could not create monitor {friendly_name!r}: {result}') module.exit_json(changed=True, monitor=result, diff=create_diff, debug={ @@ -614,7 +610,7 @@ def main(): field_diff = ur.diff_for_update(current_compare, desired_compare, diff_fields) if not field_diff: - module.log('uptimerobot_monitor: id={0} no diff -> changed=false'.format(current['id'])) + module.log(f"uptimerobot_monitor: id={current['id']} no diff -> changed=false") module.exit_json(changed=False, monitor=current, debug={ 'operation': 'noop', 'reason': 'no diff', @@ -622,9 +618,7 @@ def main(): 'monitor_id': current['id'], }) - module.log('uptimerobot_monitor: id={0} diff_fields={1}'.format( - current['id'], sorted(field_diff.keys()), - )) + module.log(f"uptimerobot_monitor: id={current['id']} diff_fields={sorted(field_diff.keys())}") update_diff = { 'before': {k: current_compare.get(k) for k in field_diff}, @@ -647,7 +641,7 @@ def main(): body['id'] = current['id'] success, result = ur.edit_monitor(module, api_key, body) if not success: - module.fail_json(msg='Could not edit monitor {0!r}: {1}'.format(friendly_name, result)) + module.fail_json(msg=f'Could not edit monitor {friendly_name!r}: {result}') module.exit_json(changed=True, monitor=result, diff=update_diff, debug={ diff --git a/plugins/modules/uptimerobot_monitor_info.py b/plugins/modules/uptimerobot_monitor_info.py index caebf1cc..d5bd49f0 100644 --- a/plugins/modules/uptimerobot_monitor_info.py +++ b/plugins/modules/uptimerobot_monitor_info.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -129,10 +131,10 @@ def main(): search = module.params.get('search') or None friendly_name = module.params.get('friendly_name') - module.log('uptimerobot_monitor_info: fetching monitors search={0!r}'.format(search)) + module.log(f'uptimerobot_monitor_info: fetching monitors search={search!r}') success, monitors = ur.get_monitors(module, api_key, search=search) if not success: - module.fail_json(msg='Could not list monitors: {0}'.format(monitors)) + module.fail_json(msg=f'Could not list monitors: {monitors}') if friendly_name: match = ur.find_by_friendly_name(monitors, friendly_name) diff --git a/plugins/modules/uptimerobot_mwindow.py b/plugins/modules/uptimerobot_mwindow.py index b9ee2299..a7d91b22 100644 --- a/plugins/modules/uptimerobot_mwindow.py +++ b/plugins/modules/uptimerobot_mwindow.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -152,7 +154,7 @@ def _synthesise_name(params): parts = [params['type']] if params.get('value'): parts.append(str(params['value'])) - parts.append('{0}-{1}'.format(params['start_time'], params['end_time'])) + parts.append(f"{params['start_time']}-{params['end_time']}") return ' '.join(parts) @@ -186,14 +188,12 @@ def main(): module.fail_json(msg='Either pass `friendly_name` or pass `type`, `start_time`, `end_time` so it can be synthesised.') friendly_name = _synthesise_name(module.params) - module.log('uptimerobot_mwindow: looking up friendly_name={0!r}'.format(friendly_name)) + module.log(f'uptimerobot_mwindow: looking up friendly_name={friendly_name!r}') success, mwindows = ur.get_mwindows(module, api_key) if not success: - module.fail_json(msg='Could not list maintenance windows: {0}'.format(mwindows)) + module.fail_json(msg=f'Could not list maintenance windows: {mwindows}') current = ur.find_by_friendly_name(mwindows, friendly_name) if friendly_name else None - module.log('uptimerobot_mwindow: existing={0} (out of {1} mwindows on the account)'.format( - bool(current), len(mwindows), - )) + module.log(f'uptimerobot_mwindow: existing={bool(current)} (out of {len(mwindows)} mwindows on the account)') if state == 'absent': if current is None: @@ -215,10 +215,10 @@ def main(): 'friendly_name': friendly_name, 'mwindow_id': current['id'], }) - module.log('uptimerobot_mwindow: deleting id={0}'.format(current['id'])) + module.log(f"uptimerobot_mwindow: deleting id={current['id']}") success, result = ur.delete_mwindow(module, api_key, current['id']) if not success: - module.fail_json(msg='Could not delete maintenance window {0!r}: {1}'.format(friendly_name, result)) + module.fail_json(msg=f'Could not delete maintenance window {friendly_name!r}: {result}') module.exit_json(changed=True, mwindow=current, diff={'before': delete_before, 'after': {}}, debug={ @@ -248,7 +248,7 @@ def main(): # Create. type/start_time/duration are required for new mwindows. for required in ('type', 'start_time'): if not desired.get(required): - module.fail_json(msg='`{0}` is required when creating a new maintenance window.'.format(required)) + module.fail_json(msg=f'`{required}` is required when creating a new maintenance window.') if not desired.get('duration'): module.fail_json(msg='Either `end_time` or `duration` is required when creating a new maintenance window.') # `status` not honoured on create. @@ -263,12 +263,10 @@ def main(): 'friendly_name': friendly_name, 'sent_keys': sorted(body.keys()), }) - module.log('uptimerobot_mwindow: creating friendly_name={0!r} sent_keys={1}'.format( - friendly_name, sorted(body.keys()), - )) + module.log(f'uptimerobot_mwindow: creating friendly_name={friendly_name!r} sent_keys={sorted(body.keys())}') success, result = ur.new_mwindow(module, api_key, body) if not success: - module.fail_json(msg='Could not create maintenance window {0!r}: {1}'.format(friendly_name, result)) + module.fail_json(msg=f'Could not create maintenance window {friendly_name!r}: {result}') module.exit_json(changed=True, mwindow=result, diff=create_diff, debug={ @@ -288,7 +286,7 @@ def main(): current_compare = {field: current.get(field) for field in diff_fields} field_diff = ur.diff_for_update(current_compare, desired, diff_fields) if not field_diff: - module.log('uptimerobot_mwindow: id={0} no diff -> changed=false'.format(current['id'])) + module.log(f"uptimerobot_mwindow: id={current['id']} no diff -> changed=false") module.exit_json(changed=False, mwindow=current, debug={ 'operation': 'noop', 'reason': 'no diff', @@ -296,9 +294,7 @@ def main(): 'mwindow_id': current['id'], }) - module.log('uptimerobot_mwindow: id={0} diff_fields={1}'.format( - current['id'], sorted(field_diff.keys()), - )) + module.log(f"uptimerobot_mwindow: id={current['id']} diff_fields={sorted(field_diff.keys())}") update_diff = { 'before': {k: current_compare.get(k) for k in field_diff}, @@ -321,7 +317,7 @@ def main(): body['id'] = current['id'] success, result = ur.edit_mwindow(module, api_key, body) if not success: - module.fail_json(msg='Could not edit maintenance window {0!r}: {1}'.format(friendly_name, result)) + module.fail_json(msg=f'Could not edit maintenance window {friendly_name!r}: {result}') module.exit_json(changed=True, mwindow=result, diff=update_diff, debug={ diff --git a/plugins/modules/uptimerobot_mwindow_info.py b/plugins/modules/uptimerobot_mwindow_info.py index 0122e969..aa2ce29d 100644 --- a/plugins/modules/uptimerobot_mwindow_info.py +++ b/plugins/modules/uptimerobot_mwindow_info.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -104,7 +106,7 @@ def main(): module.log('uptimerobot_mwindow_info: fetching maintenance windows') success, mwindows = ur.get_mwindows(module, api_key) if not success: - module.fail_json(msg='Could not list maintenance windows: {0}'.format(mwindows)) + module.fail_json(msg=f'Could not list maintenance windows: {mwindows}') if friendly_name: match = ur.find_by_friendly_name(mwindows, friendly_name) diff --git a/plugins/modules/uptimerobot_psp.py b/plugins/modules/uptimerobot_psp.py index 80d3c628..7522ba6e 100644 --- a/plugins/modules/uptimerobot_psp.py +++ b/plugins/modules/uptimerobot_psp.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -145,7 +147,7 @@ def _resolve_monitor_ids(module, api_key, items): if needs_resolution: success, monitors = ur.get_monitors(module, api_key) if not success: - module.fail_json(msg='Could not list monitors: {0}'.format(monitors)) + module.fail_json(msg=f'Could not list monitors: {monitors}') by_name = {m.get('friendly_name'): m for m in monitors} ids = [] for item in items: @@ -153,7 +155,7 @@ def _resolve_monitor_ids(module, api_key, items): if mid is None: name = item['friendly_name'] if name not in by_name: - module.fail_json(msg='Monitor {0!r} not found on UptimeRobot'.format(name)) + module.fail_json(msg=f'Monitor {name!r} not found on UptimeRobot') mid = int(by_name[name]['id']) ids.append(int(mid)) return ur.monitors_wire(ids) @@ -184,14 +186,12 @@ def main(): friendly_name = module.params['friendly_name'] state = module.params['state'] - module.log('uptimerobot_psp: looking up friendly_name={0!r}'.format(friendly_name)) + module.log(f'uptimerobot_psp: looking up friendly_name={friendly_name!r}') success, psps = ur.get_psps(module, api_key) if not success: - module.fail_json(msg='Could not list PSPs: {0}'.format(psps)) + module.fail_json(msg=f'Could not list PSPs: {psps}') current = ur.find_by_friendly_name(psps, friendly_name) - module.log('uptimerobot_psp: existing={0} (out of {1} PSPs on the account)'.format( - bool(current), len(psps), - )) + module.log(f'uptimerobot_psp: existing={bool(current)} (out of {len(psps)} PSPs on the account)') if state == 'absent': if current is None: @@ -213,10 +213,10 @@ def main(): 'friendly_name': friendly_name, 'psp_id': current['id'], }) - module.log('uptimerobot_psp: deleting id={0}'.format(current['id'])) + module.log(f"uptimerobot_psp: deleting id={current['id']}") success, result = ur.delete_psp(module, api_key, current['id']) if not success: - module.fail_json(msg='Could not delete PSP {0!r}: {1}'.format(friendly_name, result)) + module.fail_json(msg=f'Could not delete PSP {friendly_name!r}: {result}') module.exit_json(changed=True, psp=current, diff={'before': delete_before, 'after': {}}, debug={ @@ -252,12 +252,10 @@ def main(): 'friendly_name': friendly_name, 'sent_keys': sorted(body.keys()), }) - module.log('uptimerobot_psp: creating friendly_name={0!r} sent_keys={1}'.format( - friendly_name, sorted(body.keys()), - )) + module.log(f'uptimerobot_psp: creating friendly_name={friendly_name!r} sent_keys={sorted(body.keys())}') success, result = ur.new_psp(module, api_key, body) if not success: - module.fail_json(msg='Could not create PSP {0!r}: {1}'.format(friendly_name, result)) + module.fail_json(msg=f'Could not create PSP {friendly_name!r}: {result}') module.exit_json(changed=True, psp=result, diff=create_diff, debug={ @@ -284,7 +282,7 @@ def main(): diff_fields = ['monitors', 'custom_domain', 'sort', 'hide_url_links', 'status'] field_diff = ur.diff_for_update(current_compare, desired_compare, diff_fields) if not field_diff and 'password' not in desired: - module.log('uptimerobot_psp: id={0} no diff -> changed=false'.format(current['id'])) + module.log(f"uptimerobot_psp: id={current['id']} no diff -> changed=false") module.exit_json(changed=False, psp=current, debug={ 'operation': 'noop', 'reason': 'no diff', @@ -292,10 +290,10 @@ def main(): 'psp_id': current['id'], }) - module.log('uptimerobot_psp: id={0} diff_fields={1}{2}'.format( - current['id'], sorted(field_diff.keys()), - ' (+password)' if 'password' in desired else '', - )) + module.log( + f"uptimerobot_psp: id={current['id']} diff_fields={sorted(field_diff.keys())}" + f"{' (+password)' if 'password' in desired else ''}" + ) update_diff = { 'before': {k: current_compare.get(k) for k in field_diff}, @@ -322,7 +320,7 @@ def main(): body['id'] = current['id'] success, result = ur.edit_psp(module, api_key, body) if not success: - module.fail_json(msg='Could not edit PSP {0!r}: {1}'.format(friendly_name, result)) + module.fail_json(msg=f'Could not edit PSP {friendly_name!r}: {result}') module.exit_json(changed=True, psp=result, diff=update_diff, debug={ diff --git a/plugins/modules/uptimerobot_psp_info.py b/plugins/modules/uptimerobot_psp_info.py index 2d5dd024..f87f5596 100644 --- a/plugins/modules/uptimerobot_psp_info.py +++ b/plugins/modules/uptimerobot_psp_info.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -102,7 +104,7 @@ def main(): module.log('uptimerobot_psp_info: fetching public status pages') success, psps = ur.get_psps(module, api_key) if not success: - module.fail_json(msg='Could not list PSPs: {0}'.format(psps)) + module.fail_json(msg=f'Could not list PSPs: {psps}') if friendly_name: match = ur.find_by_friendly_name(psps, friendly_name) diff --git a/tests/unit/plugins/module_utils/test_uptimerobot.py b/tests/unit/plugins/module_utils/test_uptimerobot.py new file mode 100644 index 00000000..8dbf2c6c --- /dev/null +++ b/tests/unit/plugins/module_utils/test_uptimerobot.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for the uptimerobot module_util pure helpers. + +These cover the wire-format builders, the secret-redaction helper, the +cache-key hashing, the friendly-name resolution and the read-direction +response translators (which encode the idempotency-critical mapping +between UptimeRobot integer IDs and the human-readable labels). All are +pure functions; no HTTP and no filesystem is touched. The collection +import is wired up by tests/conftest.py. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from ansible_collections.linuxfabrik.lfops.plugins.module_utils import uptimerobot as ur + + +class TestWireBuilders(unittest.TestCase): + + def test_alert_contacts_wire_with_defaults(self): + wire = ur.alert_contacts_wire([{'id': 1, 'threshold': 5, 'recurrence': 0}, {'id': 2}]) + self.assertEqual(wire, '1_5_0-2_0_0') + + def test_mwindows_wire(self): + self.assertEqual(ur.mwindows_wire([3, 4, 5]), '3-4-5') + + def test_monitors_wire(self): + self.assertEqual(ur.monitors_wire([7, 8]), '7,8') + + +class TestTranslateHelpers(unittest.TestCase): + + def test_translate_known_and_unknown(self): + self.assertEqual(ur._translate('http', ur.MONITOR_TYPE), 1) + self.assertEqual(ur._translate('nope', ur.MONITOR_TYPE), 'nope') + # non-string passes through untouched + self.assertEqual(ur._translate(5, ur.MONITOR_TYPE), 5) + + def test_filter_keys_drops_unknown_and_empty(self): + out = ur._filter_keys({'a': 1, 'b': None, 'c': '', 'd': 'x'}, {'a', 'b', 'c'}) + self.assertEqual(out, {'a': 1}) + + def test_translate_keys_only_strings(self): + params = {'type': 'http', 'interval': 300} + ur._translate_keys(params, {'type': ur.MONITOR_TYPE}) + self.assertEqual(params['type'], 1) + self.assertEqual(params['interval'], 300) + + +class TestSafeKeysAndCache(unittest.TestCase): + + def test_safe_keys_redacts_secrets(self): + self.assertEqual( + ur._safe_keys({'api_key': 'x', 'password': 'y', 'friendly_name': 'n'}), + ['api_key=', 'friendly_name', 'password='], + ) + + def test_cache_key_stable_and_param_sensitive(self): + a = ur._cache_key('getMonitors', 'k', {'x': 1}) + b = ur._cache_key('getMonitors', 'k', {'x': 1}) + c = ur._cache_key('getMonitors', 'k', {'x': 2}) + self.assertEqual(a, b) + self.assertNotEqual(a, c) + + +class TestFriendlyNames(unittest.TestCase): + + def test_find_by_friendly_name(self): + items = [{'friendly_name': 'a'}, {'friendly_name': 'b'}] + self.assertEqual(ur.find_by_friendly_name(items, 'b'), {'friendly_name': 'b'}) + self.assertIsNone(ur.find_by_friendly_name(items, 'missing')) + + def test_resolve_friendly_names_ok(self): + items = [{'friendly_name': 'a', 'id': 1}, {'friendly_name': 'b', 'id': 2}] + self.assertEqual(ur.resolve_friendly_names(items, ['b', 'a'], 'monitor'), [2, 1]) + + def test_resolve_friendly_names_unknown_raises(self): + with self.assertRaises(ValueError): + ur.resolve_friendly_names([{'friendly_name': 'a', 'id': 1}], ['zzz'], 'monitor') + + +class TestDiffForUpdate(unittest.TestCase): + + def test_diff_compares_stringified(self): + # int 1 vs string '1' must be considered equal (no spurious change) + out = ur.diff_for_update({'x': 1, 'y': 2}, {'x': '1', 'y': '3'}, ['x', 'y']) + self.assertEqual(out, {'y': '3'}) + + def test_diff_skips_fields_not_in_desired(self): + out = ur.diff_for_update({'x': 1}, {}, ['x']) + self.assertEqual(out, {}) + + +class TestResponseTranslators(unittest.TestCase): + + def test_monitor_response_maps_ids_to_labels(self): + item = {'type': 1, 'status': 2, 'http_method': 2, 'auth_type': 1} + ur._translate_monitor_response(item) + self.assertEqual(item['type'], 'http') + self.assertEqual(item['status'], 'up') + self.assertEqual(item['http_method'], 'get') + # auth_type is mirrored to http_auth_type (write-side name) + self.assertEqual(item['http_auth_type'], 'basic') + + def test_mwindow_response_translates_weekly_days(self): + item = {'type': 3, 'status': 1, 'value': '1-3-5'} + ur._translate_mwindow_response(item) + self.assertEqual(item['type'], 'weekly') + self.assertEqual(item['status'], 'active') + self.assertEqual(item['value'], 'mon-wed-fri') + + def test_psp_response_mirrors_custom_url(self): + item = {'sort': 1, 'status': 1, 'custom_url': 'status.example.com'} + ur._translate_psp_response(item) + self.assertEqual(item['sort'], 'a-z') + self.assertEqual(item['status'], 'active') + self.assertEqual(item['custom_domain'], 'status.example.com') + + def test_alert_contact_response_maps_ids(self): + item = {'status': 2, 'type': 2} + ur._translate_alert_contact_response(item) + self.assertEqual(item['status'], 'active') + self.assertEqual(item['type'], 'email') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/plugins/modules/test_uptimerobot_monitor.py b/tests/unit/plugins/modules/test_uptimerobot_monitor.py new file mode 100644 index 00000000..54491ee9 --- /dev/null +++ b/tests/unit/plugins/modules/test_uptimerobot_monitor.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for the uptimerobot_monitor pure normalizers. + +These reduce the API form and the user/wire form of alert_contacts and +mwindows to the same canonical, order-independent string, which is what +keeps the module idempotent. The collection import is wired up by +tests/conftest.py. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from ansible_collections.linuxfabrik.lfops.plugins.modules import uptimerobot_monitor as mod + + +class TestNormalizeAlertContacts(unittest.TestCase): + + def test_current_is_sorted_by_id(self): + current = [ + {'id': 2, 'threshold': 5, 'recurrence': 0, 'friendly_name': 'b'}, + {'id': 1, 'threshold': 0, 'recurrence': 0, 'friendly_name': 'a'}, + ] + self.assertEqual(mod._normalize_current_alert_contacts(current), '1_0_0-2_5_0') + + def test_empty_current(self): + self.assertEqual(mod._normalize_current_alert_contacts([]), '') + + def test_desired_wire_is_sorted(self): + self.assertEqual(mod._normalize_desired_alert_contacts('2_5_0-1_0_0'), '1_0_0-2_5_0') + + def test_current_and_desired_match_when_equivalent(self): + current = [{'id': 1, 'threshold': 0, 'recurrence': 0}, + {'id': 2, 'threshold': 5, 'recurrence': 0}] + self.assertEqual( + mod._normalize_current_alert_contacts(current), + mod._normalize_desired_alert_contacts('2_5_0-1_0_0'), + ) + + +class TestNormalizeMwindows(unittest.TestCase): + + def test_current_sorted(self): + self.assertEqual(mod._normalize_current_mwindows([{'id': 3}, {'id': 1}]), '1-3') + + def test_desired_sorted(self): + self.assertEqual(mod._normalize_desired_mwindows('3-1-2'), '1-2-3') + + def test_empty(self): + self.assertEqual(mod._normalize_current_mwindows([]), '') + self.assertEqual(mod._normalize_desired_mwindows(''), '') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/plugins/modules/test_uptimerobot_mwindow.py b/tests/unit/plugins/modules/test_uptimerobot_mwindow.py new file mode 100644 index 00000000..fd3eeeb7 --- /dev/null +++ b/tests/unit/plugins/modules/test_uptimerobot_mwindow.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for the uptimerobot_mwindow pure time helpers. + +The collection import is wired up by tests/conftest.py. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from ansible_collections.linuxfabrik.lfops.plugins.modules import uptimerobot_mwindow as mod + + +class TestHhmmToMinutes(unittest.TestCase): + + def test_basic(self): + self.assertEqual(mod._hhmm_to_minutes('00:00'), 0) + self.assertEqual(mod._hhmm_to_minutes('01:30'), 90) + self.assertEqual(mod._hhmm_to_minutes('23:59'), 1439) + + +class TestComputeDuration(unittest.TestCase): + + def test_same_day(self): + self.assertEqual(mod._compute_duration('09:00', '17:00'), 480) + + def test_wraps_over_midnight(self): + self.assertEqual(mod._compute_duration('22:00', '02:00'), 240) + + def test_equal_start_end_is_full_day(self): + # duration <= 0 wraps to a full 24h window + self.assertEqual(mod._compute_duration('03:00', '03:00'), 1440) + + +class TestSynthesiseName(unittest.TestCase): + + def test_with_value(self): + params = {'type': 'weekly', 'value': 'mon-wed', 'start_time': '03:30', 'end_time': '05:30'} + self.assertEqual(mod._synthesise_name(params), 'weekly mon-wed 03:30-05:30') + + def test_without_value(self): + params = {'type': 'daily', 'start_time': '01:00', 'end_time': '02:00'} + self.assertEqual(mod._synthesise_name(params), 'daily 01:00-02:00') + + +if __name__ == '__main__': + unittest.main() From b7701b93a0d044474aeaafa4b3e5ed6361d6c843 Mon Sep 17 00:00:00 2001 From: Markus Frei <31855393+markuslf@users.noreply.github.com> Date: Mon, 25 May 2026 10:05:30 +0200 Subject: [PATCH 55/66] refactor(plugins): unify nextcloud/sqlite/gpg_key/ipa_diff + safe fixes + tests (#269) Bring the remaining in-house plugins to the standard style: standard file header, single quotes, f-strings (replacing the last .format() calls in sqlite_query and gpg_key), modern ansible.module_utils.common.text.converters instead of the deprecated _text, fixed import ordering, and removal of leftover boilerplate / commented-out debug code. ipa_diff gains the standard header it lacked. Security: - gpg_key no longer passes input_data (which contains the cleartext passphrase) into fail_json on a failed key generation. Safe fixes: - sqlite_query: REGEXP no longer raises on NULL column values (returns no-match); bare 'except:' narrowed to 'except Exception:'; mutable default argument replaced with None. Add unit tests: ipa_diff (pure diff helpers), sqlite_query (connect / select / regexp / close against a real temp DB) and gpg_key (match_key). Deferred (behaviour-changing, separate PRs): sqlite_query reporting a failed query as a successful run, and nextcloud_occ_app_config array idempotency. --- CHANGELOG.md | 2 + plugins/module_utils/ipa_diff.py | 24 +++-- plugins/modules/gpg_key.py | 35 +++---- plugins/modules/nextcloud_occ_app.py | 12 ++- plugins/modules/nextcloud_occ_app_config.py | 18 ++-- .../modules/nextcloud_occ_system_config.py | 18 ++-- plugins/modules/sqlite_query.py | 37 +++++--- .../plugins/module_utils/test_ipa_diff.py | 93 ++++++++++++++++++ tests/unit/plugins/modules/test_gpg_key.py | 78 +++++++++++++++ .../unit/plugins/modules/test_sqlite_query.py | 95 +++++++++++++++++++ 10 files changed, 344 insertions(+), 68 deletions(-) create mode 100644 tests/unit/plugins/module_utils/test_ipa_diff.py create mode 100644 tests/unit/plugins/modules/test_gpg_key.py create mode 100644 tests/unit/plugins/modules/test_sqlite_query.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 736e45dd..4810c4d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +* **plugin:gpg_key**: The cleartext passphrase is no longer included in the module's failure output when key generation fails. * **role:repo_\***: HTTP basic auth credentials are now only written to the repository config files when a custom mirror URL is set. Previously, setting `lfops__repo_basic_auth_login` without `lfops__repo_mirror_url` wrote the credentials into repo files that still pointed at the public vendor mirrors, causing the package manager to send them to servers that do not use basic auth. The Icinga repo is intentionally unchanged, since its subscription URL legitimately requires basic auth. * **ci**: Scope `GITHUB_TOKEN` permissions in the dependabot-auto-merge workflow to the job level, with top-level now `read-all`. Matches the pattern used by the other LFOps workflows and addresses the OpenSSF Scorecard `Token-Permissions` finding. @@ -42,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **plugin:sqlite_query**: A `REGEXP` query against a column that contains NULL values no longer fails; a NULL value simply does not match. * **plugin:uptimerobot_\***: The modules no longer crash when the UptimeRobot API returns a non-list response for a list endpoint; the response is passed through instead. * **plugin:nextcloud_occ_app_config, plugin:nextcloud_occ_system_config, plugin:uptimerobot_monitor, plugin:uptimerobot_psp**: Fixed their documentation so `ansible-doc` renders them again. A unit-test guard now catches this class of error for every in-house plugin. * **plugin:bitwarden_item**: Fixed the lookup's documentation so `ansible-doc` renders it again. diff --git a/plugins/module_utils/ipa_diff.py b/plugins/module_utils/ipa_diff.py index 2f308bdb..7f25fd54 100644 --- a/plugins/module_utils/ipa_diff.py +++ b/plugins/module_utils/ipa_diff.py @@ -1,12 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + # Temporary diff helpers for ansible-freeipa modules. # Remove once https://github.com/freeipa/ansible-freeipa/pull/1415 # is merged and released. -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text def _compare_key(arg, ipa_arg): @@ -28,7 +36,7 @@ def _compare_key(arg, ipa_arg): return arg == ipa_arg -class IPADiffTracker(object): +class IPADiffTracker: """Track before/after state for Ansible --diff output.""" def __init__(self): @@ -38,17 +46,17 @@ def build_diff(self): """Return kwargs for exit_json (empty dict if no changes).""" if not self._diffs: return {} - return {"diff": self._diffs} + return {'diff': self._diffs} def add_entry_diff(self, name, before, after): """Record a diff entry for one IPA object.""" if before == after: return self._diffs.append({ - "before_header": name, - "after_header": name, - "before": before, - "after": after, + 'before_header': name, + 'after_header': name, + 'before': before, + 'after': after, }) diff --git a/plugins/modules/gpg_key.py b/plugins/modules/gpg_key.py index ec507305..3a1f3c9d 100644 --- a/plugins/modules/gpg_key.py +++ b/plugins/modules/gpg_key.py @@ -1,10 +1,12 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. -# Copyright: (c) 2022, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) - -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -244,14 +246,14 @@ import re import traceback -logger = logging.getLogger('gnupg') -logger.setLevel(logging.DEBUG) - from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible_collections.linuxfabrik.lfops.plugins.module_utils.gnupg import GPG +logger = logging.getLogger('gnupg') +logger.setLevel(logging.DEBUG) + # taken from https://www.iana.org/assignments/pgp-parameters/pgp-parameters.xhtml#pgp-parameters-12 algo_ids = { 1: 'RSA', @@ -362,12 +364,6 @@ def run_module(): supports_check_mode=True ) - # if debug - # console_logger = logging.StreamHandler() - # console_logger.setLevel(logging.DEBUG) - # console_logger.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) - # logger.addHandler(console_logger) - gnupghome = module.params['gnupghome'] if gnupghome and not os.path.isdir(gnupghome): @@ -384,7 +380,7 @@ def run_module(): gnupghome=gnupghome, ) except (OSError, ValueError) as e: - module.fail_json(msg='There was an error executing gpg: {}'.format(to_native(e)), exception=traceback.format_exc(), **result) + module.fail_json(msg=f'There was an error executing gpg: {to_native(e)}', exception=traceback.format_exc(), **result) # use whatever logic you need to determine whether or not this module # made any modifications to your target @@ -425,11 +421,10 @@ def run_module(): params['no_protection'] = True input_data = gpg.gen_key_input(**params) - # print(params) - # print(input_data) new_key = gpg.gen_key(input_data) if not new_key: - module.fail_json(msg='Failed to generate a new key.', rc=new_key.returncode, stdout=new_key.data, stderr=new_key.stderr, input_data=input_data, **result) + # do not echo input_data here: it contains the cleartext passphrase + module.fail_json(msg='Failed to generate a new key.', rc=new_key.returncode, stdout=new_key.data, stderr=new_key.stderr, **result) # list the keys again, as we only got the fingerprint from gen_key() keys = gpg.list_keys(secret=True) diff --git a/plugins/modules/nextcloud_occ_app.py b/plugins/modules/nextcloud_occ_app.py index bb7856db..d379b145 100644 --- a/plugins/modules/nextcloud_occ_app.py +++ b/plugins/modules/nextcloud_occ_app.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function diff --git a/plugins/modules/nextcloud_occ_app_config.py b/plugins/modules/nextcloud_occ_app_config.py index 515895a8..ffd583c5 100644 --- a/plugins/modules/nextcloud_occ_app_config.py +++ b/plugins/modules/nextcloud_occ_app_config.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -128,10 +130,6 @@ def main(): installed_config_json=dict(type='raw'), ) - # the AnsibleModule object will be our abstraction working with Ansible - # this includes instantiation, a couple of common attr would be the - # args/params passed to the execution, as well as if this module - # supports check mode module = AnsibleModule( argument_spec=module_args, supports_check_mode=True, @@ -169,7 +167,7 @@ def main(): try: installed_config_json = json.loads(installed_config_json) except (json.JSONDecodeError, ValueError): - module.fail_json(msg=f'Failed to parse installed_config_json') + module.fail_json(msg='Failed to parse installed_config_json') app_configs = installed_config_json.get('apps', {}).get(app, {}) key_exists = name in app_configs diff --git a/plugins/modules/nextcloud_occ_system_config.py b/plugins/modules/nextcloud_occ_system_config.py index 88e995f7..2ae8e6da 100644 --- a/plugins/modules/nextcloud_occ_system_config.py +++ b/plugins/modules/nextcloud_occ_system_config.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -119,10 +121,6 @@ def main(): installed_config_json=dict(type='raw'), ) - # the AnsibleModule object will be our abstraction working with Ansible - # this includes instantiation, a couple of common attr would be the - # args/params passed to the execution, as well as if this module - # supports check mode module = AnsibleModule( argument_spec=module_args, supports_check_mode=True, @@ -158,7 +156,7 @@ def main(): try: installed_config_json = json.loads(installed_config_json) except (json.JSONDecodeError, ValueError): - module.fail_json(msg=f'Failed to parse installed_config_json') + module.fail_json(msg='Failed to parse installed_config_json') # navigate nested config by path parts (e.g. "trusted_domains 0") current = installed_config_json.get('system', {}) diff --git a/plugins/modules/sqlite_query.py b/plugins/modules/sqlite_query.py index 4fc5b642..c354b177 100644 --- a/plugins/modules/sqlite_query.py +++ b/plugins/modules/sqlite_query.py @@ -1,8 +1,10 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2026, Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch -# The Unlicense (see LICENSE or https://unlicense.org/) +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. from __future__ import absolute_import, division, print_function @@ -107,14 +109,14 @@ ''' -from ansible.module_utils.basic import AnsibleModule +import os +import re +import sqlite3 +from ansible.module_utils.basic import AnsibleModule -# all sqlite functions taken from +# the close / connect / select / regexp helpers below are taken from # https://git.linuxfabrik.ch/linuxfabrik/lib/-/blob/master/db_mysql3.py -import os -import sqlite3 -import re def close(conn): @@ -124,7 +126,7 @@ def close(conn): """ try: conn.close() - except: + except Exception: pass return True @@ -143,16 +145,18 @@ def connect(path='', filename=''): conn.text_factory = str conn.create_function("REGEXP", 2, regexp) except Exception as e: - return(False, 'Connecting to DB {} failed, Error: {}, CWD: {}'.format(db, e, os.getcwd())) + return (False, f'Connecting to DB {db} failed, Error: {e}, CWD: {os.getcwd()}') return (True, conn) -def select(conn, sql, data={}, fetchone=False, as_dict=True): +def select(conn, sql, data=None, fetchone=False, as_dict=True): """The SELECT statement is used to query the database. The result of a SELECT is zero or more rows of data where each row has a fixed number of columns. A SELECT statement does not make any changes to the database. """ + if data is None: + data = {} c = conn.cursor() try: if data: @@ -171,7 +175,7 @@ def select(conn, sql, data={}, fetchone=False, as_dict=True): return (True, c.fetchone()) return (True, c.fetchall()) except Exception as e: - return(False, 'Query failed: {}, Error: {}, Data: {}'.format(sql, e, data)) + return (False, f'Query failed: {sql}, Error: {e}, Data: {data}') def regexp(expr, item): @@ -180,6 +184,9 @@ def regexp(expr, item): For Python, you have to implement REGEXP using a Python function at runtime. https://stackoverflow.com/questions/5365451/problem-with-regexp-python-and-sqlite/5365533#5365533 """ + if item is None: + # a NULL column value cannot match a regex (and re.search(None) raises) + return False reg = re.compile(expr) return reg.search(item) is not None @@ -215,7 +222,7 @@ def main(): success, conn = connect(path=path, filename=db) if not success: - module.fail_json(msg='Unable to connect to database: {}'.format(conn)) + module.fail_json(msg=f'Unable to connect to database: {conn}') query_result = [] if query_type == 'select': diff --git a/tests/unit/plugins/module_utils/test_ipa_diff.py b/tests/unit/plugins/module_utils/test_ipa_diff.py new file mode 100644 index 00000000..054d57ce --- /dev/null +++ b/tests/unit/plugins/module_utils/test_ipa_diff.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for the ipa_diff module_util (pure --diff helpers). + +The collection import is wired up by tests/conftest.py. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from ansible_collections.linuxfabrik.lfops.plugins.module_utils import ipa_diff + + +class TestCompareKey(unittest.TestCase): + + def test_scalar_equal(self): + self.assertTrue(ipa_diff._compare_key('a', 'a')) + self.assertFalse(ipa_diff._compare_key('a', 'b')) + + def test_list_order_insensitive(self): + self.assertTrue(ipa_diff._compare_key(['a', 'b'], ['b', 'a'])) + + def test_list_length_mismatch(self): + self.assertFalse(ipa_diff._compare_key(['a'], ['a', 'b'])) + + def test_scalar_promoted_to_list(self): + # ipa side is a one-element list, arg is the bare scalar + self.assertTrue(ipa_diff._compare_key('a', ['a'])) + + +class TestGenArgsDiff(unittest.TestCase): + + def test_only_changed_keys(self): + before, after = ipa_diff.gen_args_diff({'a': 'x', 'b': 'y'}, {'a': ['x'], 'b': ['z']}) + self.assertEqual(before, {'b': 'z'}) + self.assertEqual(after, {'b': 'y'}) + + def test_ignore_list(self): + before, after = ipa_diff.gen_args_diff({'a': 'x'}, {'a': ['z']}, ignore=['a']) + self.assertEqual((before, after), ({}, {})) + + def test_empty_args(self): + self.assertEqual(ipa_diff.gen_args_diff({}, {'a': ['x']}), ({}, {})) + + +class TestGenMemberDiff(unittest.TestCase): + + def test_no_change(self): + self.assertEqual(ipa_diff.gen_member_diff('member_user', [], [], ['a']), ({}, {})) + + def test_add_and_delete(self): + before, after = ipa_diff.gen_member_diff('member_user', ['c'], ['a'], ['a', 'b']) + self.assertEqual(before, {'member_user': ['a', 'b']}) + self.assertEqual(after, {'member_user': ['b', 'c']}) + + +class TestMergeDiffs(unittest.TestCase): + + def test_merge(self): + before, after = ipa_diff.merge_diffs(({'a': 1}, {'a': 2}), ({'b': 3}, {'b': 4})) + self.assertEqual(before, {'a': 1, 'b': 3}) + self.assertEqual(after, {'a': 2, 'b': 4}) + + +class TestIPADiffTracker(unittest.TestCase): + + def test_empty_build(self): + self.assertEqual(ipa_diff.IPADiffTracker().build_diff(), {}) + + def test_skips_equal_entries(self): + t = ipa_diff.IPADiffTracker() + t.add_entry_diff('host1', {'x': 1}, {'x': 1}) + self.assertEqual(t.build_diff(), {}) + + def test_records_changed_entry(self): + t = ipa_diff.IPADiffTracker() + t.add_entry_diff('host1', {'x': 1}, {'x': 2}) + diff = t.build_diff() + self.assertEqual(len(diff['diff']), 1) + self.assertEqual(diff['diff'][0]['before_header'], 'host1') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/plugins/modules/test_gpg_key.py b/tests/unit/plugins/modules/test_gpg_key.py new file mode 100644 index 00000000..e7d7ac53 --- /dev/null +++ b/tests/unit/plugins/modules/test_gpg_key.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for the gpg_key module's pure key-matching logic. + +match_key decides whether an existing key satisfies the requested +attributes (used for idempotency); it takes plain dicts and needs no gpg +binary. The collection import is wired up by tests/conftest.py. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import copy +import unittest + +from ansible_collections.linuxfabrik.lfops.plugins.modules import gpg_key + + +_KEY = { + 'algo': '1', # 1 -> RSA + 'length': '2048', + 'uids': ['Test Name (a comment) '], +} + +_PARAMS = { + 'key_type': 'RSA', + 'key_length': 2048, + 'name_real': 'Test Name', + 'name_comment': 'a comment', + 'name_email': 'test@example.com', + 'subkey_type': None, + 'subkey_length': None, +} + + +class TestMatchKey(unittest.TestCase): + + def test_full_match(self): + self.assertTrue(gpg_key.match_key(copy.deepcopy(_KEY), dict(_PARAMS))) + + def test_wrong_length(self): + params = dict(_PARAMS, key_length=4096) + self.assertFalse(gpg_key.match_key(copy.deepcopy(_KEY), params)) + + def test_wrong_type(self): + params = dict(_PARAMS, key_type='DSA') + self.assertFalse(gpg_key.match_key(copy.deepcopy(_KEY), params)) + + def test_wrong_email(self): + params = dict(_PARAMS, name_email='other@example.com') + self.assertFalse(gpg_key.match_key(copy.deepcopy(_KEY), params)) + + def test_wrong_real_name(self): + params = dict(_PARAMS, name_real='Someone Else') + self.assertFalse(gpg_key.match_key(copy.deepcopy(_KEY), params)) + + def test_subkey_match(self): + key = copy.deepcopy(_KEY) + key['subkey_info'] = {'sub1': {'algo': '1', 'length': '2048'}} + params = dict(_PARAMS, subkey_type='RSA', subkey_length=2048) + self.assertTrue(gpg_key.match_key(key, params)) + + def test_subkey_no_match(self): + key = copy.deepcopy(_KEY) + key['subkey_info'] = {'sub1': {'algo': '1', 'length': '1024'}} + params = dict(_PARAMS, subkey_type='RSA', subkey_length=2048) + self.assertFalse(gpg_key.match_key(key, params)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/plugins/modules/test_sqlite_query.py b/tests/unit/plugins/modules/test_sqlite_query.py new file mode 100644 index 00000000..a162c3df --- /dev/null +++ b/tests/unit/plugins/modules/test_sqlite_query.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for the sqlite_query module helpers. + +These exercise connect / select / regexp / close against a real +temporary SQLite database (sqlite3 is in the standard library, so no +mocking is needed). The collection import is wired up by +tests/conftest.py. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import os +import tempfile +import unittest + +from ansible_collections.linuxfabrik.lfops.plugins.modules import sqlite_query as mod + + +class SqliteHelpersTestCase(unittest.TestCase): + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix='lfops_sqlite_test_') + ok, self.conn = mod.connect(path=self.tmpdir, filename='test.db') + self.assertTrue(ok) + cur = self.conn.cursor() + cur.execute('CREATE TABLE t (id INTEGER, name TEXT)') + cur.execute("INSERT INTO t VALUES (1, 'alpha'), (2, 'beta'), (3, NULL)") + self.conn.commit() + + def tearDown(self): + mod.close(self.conn) + + +class TestSelect(SqliteHelpersTestCase): + + def test_as_dict(self): + ok, rows = mod.select(self.conn, 'SELECT id, name FROM t WHERE id = 1') + self.assertTrue(ok) + self.assertEqual(rows, [{'id': 1, 'name': 'alpha'}]) + + def test_as_tuple(self): + ok, rows = mod.select(self.conn, 'SELECT id, name FROM t WHERE id = 1', as_dict=False) + self.assertTrue(ok) + self.assertEqual(tuple(rows[0]), (1, 'alpha')) + + def test_fetch_one(self): + ok, row = mod.select(self.conn, 'SELECT id FROM t ORDER BY id', fetchone=True) + self.assertTrue(ok) + self.assertEqual(row, {'id': 1}) + + def test_fetch_one_empty(self): + ok, row = mod.select(self.conn, 'SELECT id FROM t WHERE id = 999', fetchone=True) + self.assertTrue(ok) + self.assertEqual(row, []) + + def test_named_args(self): + ok, rows = mod.select( + self.conn, 'SELECT name FROM t WHERE id = :wanted', data={'wanted': 2}, + ) + self.assertTrue(ok) + self.assertEqual(rows, [{'name': 'beta'}]) + + def test_bad_query_reports_failure(self): + ok, msg = mod.select(self.conn, 'SELECT * FROM does_not_exist') + self.assertFalse(ok) + self.assertIn('Query failed', msg) + + +class TestRegexp(SqliteHelpersTestCase): + + def test_regexp_in_where(self): + ok, rows = mod.select(self.conn, "SELECT name FROM t WHERE name REGEXP '^al'") + self.assertTrue(ok) + self.assertEqual(rows, [{'name': 'alpha'}]) + + +class TestConnectFailure(unittest.TestCase): + + def test_connect_to_unwritable_path(self): + ok, result = mod.connect(path='/nonexistent/dir/that/should/not/exist', filename='x.db') + self.assertFalse(ok) + self.assertIn('failed', result.lower()) + + +if __name__ == '__main__': + unittest.main() From 3a7e80c1ca104004709d0da8bd07088396656213 Mon Sep 17 00:00:00 2001 From: Markus Frei <31855393+markuslf@users.noreply.github.com> Date: Mon, 25 May 2026 10:14:25 +0200 Subject: [PATCH 56/66] fix(plugins/modules/sqlite_query): fail the task on a failed query (#270) main() ignored the success flag returned by select(), so a failed query exited with changed=false and the error message smuggled into query_result - reporting success for a broken query. Check the flag and call fail_json with the error instead. Add a reusable Ansible module test harness (tests/ansible_harness.py: set_module_args + exit_json/fail_json patching, profile-aware so it works on ansible-core 2.15 through 2.21) and main()-level tests covering both the success and the failure path. --- CHANGELOG.md | 1 + plugins/modules/sqlite_query.py | 4 ++ tests/ansible_harness.py | 60 +++++++++++++++++++ tests/conftest.py | 3 + .../unit/plugins/modules/test_sqlite_query.py | 33 ++++++++++ 5 files changed, 101 insertions(+) create mode 100644 tests/ansible_harness.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4810c4d8..121e9ad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **plugin:sqlite_query**: A failed query now fails the task instead of reporting success with the error text in `query_result`. Playbooks that relied on the previous silent success will now correctly fail. * **plugin:sqlite_query**: A `REGEXP` query against a column that contains NULL values no longer fails; a NULL value simply does not match. * **plugin:uptimerobot_\***: The modules no longer crash when the UptimeRobot API returns a non-list response for a list endpoint; the response is passed through instead. * **plugin:nextcloud_occ_app_config, plugin:nextcloud_occ_system_config, plugin:uptimerobot_monitor, plugin:uptimerobot_psp**: Fixed their documentation so `ansible-doc` renders them again. A unit-test guard now catches this class of error for every in-house plugin. diff --git a/plugins/modules/sqlite_query.py b/plugins/modules/sqlite_query.py index c354b177..4131bd6d 100644 --- a/plugins/modules/sqlite_query.py +++ b/plugins/modules/sqlite_query.py @@ -228,6 +228,10 @@ def main(): if query_type == 'select': success, query_result = select(conn, query, named_args, fetchone=fetch_one, as_dict=as_dict) changed = False + if not success: + close(conn) + # query_result holds the error message when the query failed + module.fail_json(msg=query_result) close(conn) # in the event of a successful module execution, you will want to diff --git a/tests/ansible_harness.py b/tests/ansible_harness.py new file mode 100644 index 00000000..1f946f4e --- /dev/null +++ b/tests/ansible_harness.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Helpers to unit-test an Ansible module's main() function. + +Standard ansible-test pattern: feed arguments via set_module_args(), then +patch AnsibleModule.exit_json / fail_json so they raise instead of calling +sys.exit(), so a test can assert on the outcome. tests/conftest.py puts the +tests/ directory on sys.path so test modules can `import ansible_harness`. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json +import unittest.mock + +from ansible.module_utils import basic +from ansible.module_utils.common.text.converters import to_bytes + + +class AnsibleExitJson(Exception): + """Raised in place of AnsibleModule.exit_json().""" + + +class AnsibleFailJson(Exception): + """Raised in place of AnsibleModule.fail_json().""" + + +def set_module_args(args): + """Prepare module arguments as if passed in by Ansible.""" + basic._ANSIBLE_ARGS = to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': args})) + # ansible-core 2.19+ also requires a serialization profile to decode the args. + if hasattr(basic, '_ANSIBLE_PROFILE'): + basic._ANSIBLE_PROFILE = 'legacy' + + +def _exit_json(self, **kwargs): + kwargs.setdefault('changed', False) + raise AnsibleExitJson(kwargs) + + +def _fail_json(self, **kwargs): + kwargs.setdefault('failed', True) + raise AnsibleFailJson(kwargs) + + +def patch_module(): + """Return a mock.patch.multiple context manager for exit_json / fail_json.""" + return unittest.mock.patch.multiple( + basic.AnsibleModule, + exit_json=_exit_json, + fail_json=_fail_json, + ) diff --git a/tests/conftest.py b/tests/conftest.py index 1e4b9f4c..117ffb1c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,4 +39,7 @@ def _make_collection_importable(): os.environ.setdefault('ANSIBLE_COLLECTIONS_PATH', str(root)) +# make the tests/ directory importable so test modules can `import ansible_harness` +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) + _make_collection_importable() diff --git a/tests/unit/plugins/modules/test_sqlite_query.py b/tests/unit/plugins/modules/test_sqlite_query.py index a162c3df..684892ae 100644 --- a/tests/unit/plugins/modules/test_sqlite_query.py +++ b/tests/unit/plugins/modules/test_sqlite_query.py @@ -22,6 +22,8 @@ import tempfile import unittest +import ansible_harness + from ansible_collections.linuxfabrik.lfops.plugins.modules import sqlite_query as mod @@ -91,5 +93,36 @@ def test_connect_to_unwritable_path(self): self.assertIn('failed', result.lower()) +class TestMain(unittest.TestCase): + """main() must fail the task on a failed query instead of reporting success.""" + + def setUp(self): + self.tmpdir = tempfile.mkdtemp(prefix='lfops_sqlite_main_test_') + ok, conn = mod.connect(path=self.tmpdir, filename='main.db') + conn.execute('CREATE TABLE t (id INTEGER)') + conn.execute('INSERT INTO t VALUES (1)') + conn.commit() + mod.close(conn) + + def test_successful_query_exits_with_result(self): + ansible_harness.set_module_args({ + 'path': self.tmpdir, 'db': 'main.db', 'query': 'SELECT id FROM t', + }) + with ansible_harness.patch_module(): + with self.assertRaises(ansible_harness.AnsibleExitJson) as cm: + mod.main() + self.assertEqual(cm.exception.args[0]['query_result'], [{'id': 1}]) + self.assertFalse(cm.exception.args[0]['changed']) + + def test_failed_query_fails_the_task(self): + ansible_harness.set_module_args({ + 'path': self.tmpdir, 'db': 'main.db', 'query': 'SELECT * FROM does_not_exist', + }) + with ansible_harness.patch_module(): + with self.assertRaises(ansible_harness.AnsibleFailJson) as cm: + mod.main() + self.assertIn('Query failed', cm.exception.args[0]['msg']) + + if __name__ == '__main__': unittest.main() From 2170cc8d94aa04e60fbd8c0751097b182a7b1995 Mon Sep 17 00:00:00 2001 From: Markus Frei <31855393+markuslf@users.noreply.github.com> Date: Mon, 25 May 2026 10:28:08 +0200 Subject: [PATCH 57/66] fix(plugins/modules/bitwarden_item): honor check mode and preserve password on None (#271) Two behavior fixes: - check_mode: the module declared supports_check_mode but wrote to the vault regardless (edit/create/add_attachment). Guard every write behind 'not module.check_mode' and return the predicted item in check mode. - None password: diff_and_update saw target password None vs an existing real password as a change and overwrote it with null. A None password now preserves the existing item's password, matching the documented behavior ('overwritten by every non-None value'). Clarify the DOCUMENTATION accordingly and add main()-level tests (fake Bitwarden client + the ansible module harness) for both paths. The get_item_by_id returns-or-raises contract is left for a separate PR. --- CHANGELOG.md | 2 + plugins/modules/bitwarden_item.py | 22 ++- .../plugins/modules/test_bitwarden_item.py | 138 +++++++++++++++++- 3 files changed, 153 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 121e9ad2..b5896cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **plugin:bitwarden_item**: The module no longer writes to the Bitwarden vault when run in check mode (`--check`); it reports the would-be change instead. +* **plugin:bitwarden_item**: A run without `password` (the default `None`) no longer overwrites an existing item's password; the current password is preserved, matching the documented behavior. * **plugin:sqlite_query**: A failed query now fails the task instead of reporting success with the error text in `query_result`. Playbooks that relied on the previous silent success will now correctly fail. * **plugin:sqlite_query**: A `REGEXP` query against a column that contains NULL values no longer fails; a NULL value simply does not match. * **plugin:uptimerobot_\***: The modules no longer crash when the UptimeRobot API returns a non-list response for a list endpoint; the response is passed through instead. diff --git a/plugins/modules/bitwarden_item.py b/plugins/modules/bitwarden_item.py index 36e6ec13..e0f5b379 100644 --- a/plugins/modules/bitwarden_item.py +++ b/plugins/modules/bitwarden_item.py @@ -28,7 +28,7 @@ - Only login items (Bitwarden type 1) are managed. Cards, secure notes and identities are out of scope. - TOTP secrets are not managed; the C(totp) field is set to an empty string on creation. - I(uris) replaces the URI list on every run. Omitting it on an existing item causes the URI list to be cleared. - - I(password) defaults to C(None), which translates to "do not set a password". Use the C(bitwarden_item) lookup plugin to generate one. + - I(password) defaults to C(None), which leaves the password unmanaged - a new item is created without one, and an existing item keeps its current password. Use the C(bitwarden_item) lookup plugin to generate one. - Attachments are matched by basename only; this module assumes that an attachment with the same basename has the same content. Pre-existing attachments are kept; only missing ones are uploaded. - Organization, collection and folder IDs can be copied from the URL in the Bitwarden web vault. - The cache file lives in C($XDG_RUNTIME_DIR) (falling back to C(/tmp)) and is shared across this module and the C(bitwarden_item) lookup within the same controller session. @@ -80,7 +80,7 @@ required: False type: str password: - description: Password to set on the login item. C(None) (the default) leaves the password field unset; existing passwords on already-existing items are overwritten by every non-C(None) value. + description: Password to set on the login item. C(None) (the default) leaves the password unmanaged - a new item gets no password and an existing item keeps its current one. Any non-C(None) value is written on every run. required: False type: str purpose: @@ -367,18 +367,26 @@ def run_module(): collection_id, folder_id, ) + + # A None password means "do not manage the password": preserve the existing + # item's password instead of overwriting it with null. + if password is None and current_item and current_item.get('login'): + target_item['login']['password'] = current_item['login'].get('password') + if current_item: # check if changed, adjust if necessary changed, updated_item = diff_and_update(current_item, target_item) - if changed: + if changed and not module.check_mode: result = bw.edit_item(updated_item, updated_item['id']) + elif changed: + result = updated_item else: result = current_item else: # generate a new one changed = True - result = bw.create_item(target_item) + result = target_item if module.check_mode else bw.create_item(target_item) if attachments: current_attachments = set(current_attachment['fileName'] for current_attachment in result.get('attachments', [])) @@ -386,12 +394,14 @@ def run_module(): for attachment in attachments: if os.path.basename(attachment) not in current_attachments: attachments_changed = True - bw.add_attachment(result['id'], attachment) + if not module.check_mode: + bw.add_attachment(result['id'], attachment) if attachments_changed: changed = True # we need to fetch the item again, so that it also contains the newly added attachments - result = bw.get_item_by_id(result['id']) + if not module.check_mode: + result = bw.get_item_by_id(result['id']) result['changed'] = changed # move username and password higher for easier access diff --git a/tests/unit/plugins/modules/test_bitwarden_item.py b/tests/unit/plugins/modules/test_bitwarden_item.py index 3fbba453..0089415d 100644 --- a/tests/unit/plugins/modules/test_bitwarden_item.py +++ b/tests/unit/plugins/modules/test_bitwarden_item.py @@ -6,10 +6,12 @@ # https://www.linuxfabrik.ch/ # License: The Unlicense, see LICENSE file. -"""Unit tests for the bitwarden_item module's pure diff helper. +"""Unit tests for the bitwarden_item module. -The module itself runs via AnsiballZ, but `diff_and_update` is a plain -function and is tested in isolation. The collection import is wired up by +`diff_and_update` is a plain function tested in isolation. The main() +behavior (check_mode must not write, a None password must not overwrite +an existing one) is tested with a fake Bitwarden client and the shared +ansible module harness. The collection import is wired up by tests/conftest.py. """ @@ -17,8 +19,13 @@ __metaclass__ = type +import copy import unittest +import unittest.mock +import ansible_harness + +from ansible_collections.linuxfabrik.lfops.plugins.modules import bitwarden_item as mod from ansible_collections.linuxfabrik.lfops.plugins.modules.bitwarden_item import diff_and_update @@ -62,5 +69,130 @@ def test_nested_dict_no_change(self): self.assertFalse(changed) +_EXISTING_ITEM = { + 'id': 'abc', + 'name': 'host - db', + 'login': {'username': 'dba', 'password': 'linuxfabrik-existing', 'totp': '', 'uris': []}, + 'notes': 'Generated by Ansible.', + 'organizationId': None, + 'collectionIds': None, + 'folderId': None, +} + + +class _FakeBitwarden: + """Stand-in for the Bitwarden client; records writes instead of doing them.""" + + items = [] + edited = [] + created = [] + + def __init__(self, *args, **kwargs): + pass + + @property + def is_unlocked(self): + return True + + def sync(self, *args, **kwargs): + pass + + def get_items(self, *args, **kwargs): + return [copy.deepcopy(i) for i in type(self).items] + + def get_item_by_id(self, item_id): + return None + + def get_template_item_login_uri(self, uris): + return list(uris or []) + + def get_template_item_login(self, username=None, password=None, login_uris=None): + return {'username': username, 'password': password, 'totp': '', 'uris': login_uris or []} + + def get_template_item(self, name, login=None, notes=None, organization_id=None, + collection_ids=None, folder_id=None): + return { + 'name': name, 'login': login, 'notes': notes, + 'organizationId': organization_id, 'collectionIds': collection_ids, + 'folderId': folder_id, + } + + @staticmethod + def get_pretty_name(name, hostname=None, purpose=None): + return name or hostname + + def edit_item(self, item, item_id): + type(self).edited.append((copy.deepcopy(item), item_id)) + result = copy.deepcopy(item) + result['id'] = item_id + return result + + def create_item(self, item): + type(self).created.append(copy.deepcopy(item)) + result = copy.deepcopy(item) + result['id'] = 'new-id' + return result + + +class TestMain(unittest.TestCase): + + def setUp(self): + _FakeBitwarden.items = [] + _FakeBitwarden.edited = [] + _FakeBitwarden.created = [] + self._patchers = [ + unittest.mock.patch.object(mod, 'Bitwarden', _FakeBitwarden), + ansible_harness.patch_module(), + ] + for p in self._patchers: + p.start() + + def tearDown(self): + for p in reversed(self._patchers): + p.stop() + + def _run(self, args): + ansible_harness.set_module_args(args) + try: + mod.run_module() + except ansible_harness.AnsibleExitJson as exc: + return exc.args[0] + raise AssertionError('module did not call exit_json') + + def test_check_mode_create_does_not_write(self): + _FakeBitwarden.items = [] # nothing exists -> would create + result = self._run({ + 'name': 'host - db', 'username': 'dba', 'password': 'linuxfabrik-new', + '_ansible_check_mode': True, + }) + self.assertTrue(result['changed']) + self.assertEqual(_FakeBitwarden.created, []) # no write in check mode + + def test_check_mode_edit_does_not_write(self): + _FakeBitwarden.items = [_EXISTING_ITEM] + result = self._run({ + 'name': 'host - db', 'username': 'dba', 'password': 'a-different-password', + '_ansible_check_mode': True, + }) + self.assertTrue(result['changed']) + self.assertEqual(_FakeBitwarden.edited, []) # no write in check mode + + def test_none_password_does_not_overwrite(self): + _FakeBitwarden.items = [_EXISTING_ITEM] + # password omitted entirely -> must preserve the existing one, no change + result = self._run({'name': 'host - db', 'username': 'dba'}) + self.assertFalse(result['changed']) + self.assertEqual(_FakeBitwarden.edited, []) + self.assertEqual(result['password'], 'linuxfabrik-existing') + + def test_changed_password_writes_when_not_check_mode(self): + _FakeBitwarden.items = [_EXISTING_ITEM] + result = self._run({ + 'name': 'host - db', 'username': 'dba', 'password': 'a-different-password', + }) + self.assertTrue(result['changed']) + self.assertEqual(len(_FakeBitwarden.edited), 1) + + if __name__ == '__main__': unittest.main() From 1ddef3d75ef5a2c456f5debcf0698cda59963507 Mon Sep 17 00:00:00 2001 From: Markus Frei <31855393+markuslf@users.noreply.github.com> Date: Mon, 25 May 2026 10:31:22 +0200 Subject: [PATCH 58/66] fix(plugins/modules/nextcloud_occ_app_config): compare array values as JSON (#272) Nextcloud stores an array config value and returns it as a parsed JSON array (verified against Nextcloud 33: config:list yields ["alpha", "beta"]). The module stringified that list with str() (Python repr, single quotes) and compared it against the user's array literal, which never matched - so the module reported a change and re-ran config:app:set on every run. Compare array values as parsed JSON instead (values_match()), and store the cached current value as canonical JSON. Add unit tests for the helper and for the cached (installed_config_json) idempotency path. The occ output formats were verified empirically in a Nextcloud podman container. --- CHANGELOG.md | 1 + plugins/modules/nextcloud_occ_app_config.py | 23 ++++- .../modules/test_nextcloud_occ_app_config.py | 92 +++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 tests/unit/plugins/modules/test_nextcloud_occ_app_config.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b5896cb7..2b730b5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **plugin:nextcloud_occ_app_config**: An `array` config value is now compared as JSON, so a key whose stored value already matches the desired one no longer reports a change (and re-runs `occ config:app:set`) on every run. * **plugin:bitwarden_item**: The module no longer writes to the Bitwarden vault when run in check mode (`--check`); it reports the would-be change instead. * **plugin:bitwarden_item**: A run without `password` (the default `None`) no longer overwrites an existing item's password; the current password is preserved, matching the documented behavior. * **plugin:sqlite_query**: A failed query now fails the task instead of reporting success with the error text in `query_result`. Playbooks that relied on the previous silent success will now correctly fail. diff --git a/plugins/modules/nextcloud_occ_app_config.py b/plugins/modules/nextcloud_occ_app_config.py index ffd583c5..269f4cb9 100644 --- a/plugins/modules/nextcloud_occ_app_config.py +++ b/plugins/modules/nextcloud_occ_app_config.py @@ -117,6 +117,23 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native + +def values_match(current_value, value, value_type): + """Decide whether the stored value already matches the desired one. + + For C(array) values both sides are compared as parsed JSON, so an + array stored by Nextcloud (returned as a JSON array, e.g. via + C(config:list)) compares equal to the user's array literal regardless + of whitespace or key ordering. All other types compare as strings. + """ + if value_type == 'array': + try: + return json.loads(current_value) == json.loads(value) + except (json.JSONDecodeError, ValueError, TypeError): + return False + return current_value == value + + def main(): # define available arguments/parameters a user can pass to this module module_args = dict( @@ -187,7 +204,9 @@ def main(): current_value = str(raw) current_type = 'float' elif isinstance(raw, list): - current_value = str(raw) + # store canonical JSON so it can be compared as JSON against the + # user's array literal (config:list returns an already-parsed list) + current_value = json.dumps(raw) current_type = 'array' else: current_value = str(raw) @@ -225,7 +244,7 @@ def main(): if state == 'present': # check if the current value and type match the desired settings - if current_value == value and current_type == value_type: + if current_type == value_type and values_match(current_value, value, value_type): module.exit_json(**result) # else, the value will be changed diff --git a/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py b/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py new file mode 100644 index 00000000..9871eb69 --- /dev/null +++ b/tests/unit/plugins/modules/test_nextcloud_occ_app_config.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8; py-indent-offset: 4 -*- +# +# Author: Linuxfabrik GmbH, Zurich, Switzerland +# Contact: info (at) linuxfabrik (dot) ch +# https://www.linuxfabrik.ch/ +# License: The Unlicense, see LICENSE file. + +"""Unit tests for nextcloud_occ_app_config array idempotency. + +Nextcloud stores an C(array) value and returns it as a parsed JSON array +via C(config:list) (verified against Nextcloud 33: the value comes back +as C(["alpha", "beta"])). Comparing that as a Python repr string against +the user's array literal never matched, so the module reported a change +on every run. values_match() now compares array values as parsed JSON. +The collection import is wired up by tests/conftest.py. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +import ansible_harness + +from ansible_collections.linuxfabrik.lfops.plugins.modules import nextcloud_occ_app_config as mod + + +class TestValuesMatch(unittest.TestCase): + + def test_array_equal_ignoring_whitespace(self): + # occ returns '["alpha","beta"]'; user passes a spaced literal + self.assertTrue(mod.values_match('["alpha","beta"]', '["alpha", "beta"]', 'array')) + + def test_array_canonical_vs_user(self): + # cached path stores json.dumps(list) -> '["alpha", "beta"]' + self.assertTrue(mod.values_match('["alpha", "beta"]', '["alpha","beta"]', 'array')) + + def test_array_different(self): + self.assertFalse(mod.values_match('["alpha", "beta"]', '["alpha","gamma"]', 'array')) + + def test_array_invalid_json_is_not_a_match(self): + self.assertFalse(mod.values_match("['alpha', 'beta']", '["alpha","beta"]', 'array')) + + def test_non_array_string_compare(self): + self.assertTrue(mod.values_match('90', '90', 'integer')) + self.assertFalse(mod.values_match('90', '91', 'integer')) + + +class TestMainCachedArray(unittest.TestCase): + """Exercise main() via the installed_config_json (cache) path, no occ needed.""" + + def setUp(self): + self._patch = ansible_harness.patch_module() + self._patch.start() + + def tearDown(self): + self._patch.stop() + + def _run(self, args): + ansible_harness.set_module_args(args) + try: + mod.main() + except ansible_harness.AnsibleExitJson as exc: + return exc.args[0] + raise AssertionError('module did not call exit_json') + + def test_array_already_set_is_idempotent(self): + result = self._run({ + 'app': 'core', + 'name': 'test_array', + 'value': '["alpha","beta"]', + 'type': 'array', + 'installed_config_json': {'apps': {'core': {'test_array': ['alpha', 'beta']}}}, + }) + self.assertFalse(result['changed']) + + def test_array_differs_reports_change(self): + result = self._run({ + 'app': 'core', + 'name': 'test_array', + 'value': '["alpha","beta"]', + 'type': 'array', + 'installed_config_json': {'apps': {'core': {'test_array': ['alpha', 'gamma']}}}, + '_ansible_check_mode': True, # avoid the real occ config:app:set call + }) + self.assertTrue(result['changed']) + + +if __name__ == '__main__': + unittest.main() From 1b08dc0fbea2aef47268962b8c9e6962dbe8601d Mon Sep 17 00:00:00 2001 From: Markus Frei Date: Mon, 25 May 2026 10:50:00 +0200 Subject: [PATCH 59/66] fix(plugins/modules/gpg_key): refresh vendored python-gnupg and correct docs - Sync plugins/module_utils/gnupg.py with upstream python-gnupg 0.5.6 (byte-identical), keeping the module working on current Python and GnuPG. - gnupghome is now type=path (expands ~, resolves relative paths). - Drop the misleading "python-gnupg required on the controller" requirement; the library ships with the collection. Document the returned field as uids. - Document the vendored module_util in CONTRIBUTING and exclude it from bandit, consistent with the vendored ipa*.py modules. --- .pre-commit-config.yaml | 5 +- CHANGELOG.md | 2 + CONTRIBUTING.md | 8 +- plugins/module_utils/gnupg.py | 1727 ++++++++++++++------- plugins/module_utils/gnupg.py_LICENSE.txt | 2 +- plugins/modules/gpg_key.py | 8 +- 6 files changed, 1196 insertions(+), 556 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b73007a2..9e90cd3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,9 +42,12 @@ repos: # false-positives on the project's own code style (`shell=dict(...)` # in argument_spec triggers B604, the literal `'on_create'` sentinel # triggers B105). Out of scope for in-tree review. + # `plugins/module_utils/gnupg.py` is vendored python-gnupg kept + # byte-identical with upstream; bandit flags its (expected) subprocess + # use (B404/B603) and asserts (B101), which we do not patch out. # `tests/` holds unit tests whose fixtures use throwaway passwords # (B105/B106); scanning test fixtures for hardcoded secrets is noise. - exclude: '^(plugins/modules/ipa.*|tests/.*)\.py$' + exclude: '^(plugins/modules/ipa.*|plugins/module_utils/gnupg|tests/.*)\.py$' types_or: ['python'] - repo: 'https://github.com/jendrikseipp/vulture' diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b730b5b..d6a754db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* **plugin:gpg_key**: Refresh the bundled GPG helper library so the module keeps working on current Python and GnuPG releases. Existing playbooks are unaffected. The `gnupghome` parameter now expands `~` and resolves relative paths, matching its documentation. * **docs**: All role READMEs now follow a consistent structure that separates the dependencies a playbook sets up for you from what you must provide yourself. Documentation only, no behavior changes. * **role:keycloak**: The role no longer leaves the bootstrap admin credentials lying around in `/etc/sysconfig/keycloak` after the first run. It now writes the credentials, waits for Keycloak to consume them on startup (provisioning the bootstrap admin in the `master` realm), re-renders the sysconfig file with the credentials removed, and stores a state marker at `/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state` so subsequent runs skip the credential render entirely. After the first run, `keycloak__admin_login` can be removed from the inventory. Disaster recovery: delete the marker file, re-add the variable, re-run. Also recommend a `-temp` suffix for the initial admin username (example: `keycloak-admin-temp`) so it is visually obvious in the Keycloak UI which account must be deleted once a permanent admin exists. * **role:redis**: Bump default for `net.core.somaxconn` from `1024` to `4096` to match the RHEL 9 / RHEL 10 kernel default and the current Redis upstream recommendation. Hosts on RHEL 9 or 10 see no effective change (the override was already below the kernel default); RHEL 8 hosts now get `4096` instead of `1024`. @@ -43,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **plugin:gpg_key**: Corrected the module documentation. The GPG helper library ships with the collection, so no separate `python-gnupg` install is required, and the returned key field is documented as `uids` (matching the actual output). * **plugin:nextcloud_occ_app_config**: An `array` config value is now compared as JSON, so a key whose stored value already matches the desired one no longer reports a change (and re-runs `occ config:app:set`) on every run. * **plugin:bitwarden_item**: The module no longer writes to the Bitwarden vault when run in check mode (`--check`); it reports the would-be change instead. * **plugin:bitwarden_item**: A run without `password` (the default `None`) no longer overwrites an existing item's password; the current password is preserved, matching the documented behavior. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5057fb2c..23b5d5f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -739,7 +739,7 @@ The following roles use techniques that are unusual within LFOps. Roles not in t ### Vendored Plugins -Some files under `plugins/modules/` are not authored by Linuxfabrik but vendored from upstream projects, either because we needed local patches or because the upstream version requires a newer ansible-core than LFOps supports. They are kept in lockstep with their upstream and should be re-synced (or removed) when the listed condition is met. +Some files under `plugins/modules/` and `plugins/module_utils/` are not authored by Linuxfabrik but vendored from upstream projects, either because we needed local patches, because the upstream version requires a newer ansible-core than LFOps supports, or because the dependency has to ship with the module to the managed node. They are kept in lockstep with their upstream and should be re-synced (or removed) when the listed condition is met. * `plugins/modules/ipagroup.py`, `ipahbacrule.py`, `ipahostgroup.py`, `ipapwpolicy.py`, `ipasudocmd.py`, `ipasudocmdgroup.py`, `ipasudorule.py`, `ipauser.py` @@ -753,6 +753,12 @@ Some files under `plugins/modules/` are not authored by Linuxfabrik but vendored * Reason: community.general 11.0.0 requires ansible-core >= 2.18, which LFOps does not yet mandate (RHEL 8 / Python 3.6 still supported). * Drop when: LFOps raises its minimum ansible-core to >= 2.18; switch to `community.general.lvm_pv` and update `roles/lvm` accordingly. +* `plugins/module_utils/gnupg.py` (and its `gnupg.py_LICENSE.txt`) + + * Upstream: (`python-gnupg`). The synced revision is recorded in the file's own `__version__`. + * Reason: the `gpg_key` module runs on the managed node and drives the `gpg` binary through this library. Bundling it byte-identical with upstream avoids requiring a `python-gnupg` pip install on every target. The upstream BSD license is kept alongside it. + * Drop when: not expected; re-sync with the upstream release when picking up bug fixes or newer-Python support, keeping the file unmodified. + ### Plugins diff --git a/plugins/module_utils/gnupg.py b/plugins/module_utils/gnupg.py index 8d167397..508ed602 100644 --- a/plugins/module_utils/gnupg.py +++ b/plugins/module_utils/gnupg.py @@ -1,4 +1,4 @@ -""" A wrapper for the 'gpg' command:: +""" A wrapper for the GnuPG `gpg` command. Portions of this module are derived from A.M. Kuchling's well-designed GPG.py, using Richard Jones' updated version 1.3, which can be found @@ -27,31 +27,32 @@ and so does not work on Windows). Renamed to gnupg.py to avoid confusion with the previous versions. -Modifications Copyright (C) 2008-2021 Vinay Sajip. All rights reserved. +Modifications Copyright (C) 2008-2025 Vinay Sajip. All rights reserved. -A unittest harness (test_gnupg.py) has also been added. +For the full documentation, see https://docs.red-dove.com/python-gnupg/ or +https://gnupg.readthedocs.io/ """ -__version__ = "0.4.8" -__author__ = "Vinay Sajip" -__date__ = "$24-Nov-2021 09:13:33$" - -try: - from io import StringIO -except ImportError: # pragma: no cover - from cStringIO import StringIO - import codecs -import locale +from datetime import datetime +from email.utils import parseaddr +from io import StringIO import logging import os +try: + from queue import Queue, Empty +except ImportError: + from Queue import Queue, Empty import re import socket -from subprocess import Popen -from subprocess import PIPE +from subprocess import Popen, PIPE import sys import threading +__version__ = '0.5.6' +__author__ = 'Vinay Sajip' +__date__ = '$31-Dec-2025 16:41:34$' + STARTUPINFO = None if os.name == 'nt': # pragma: no cover try: @@ -59,25 +60,28 @@ except ImportError: STARTUPINFO = None -try: - import logging.NullHandler as NullHandler -except ImportError: - class NullHandler(logging.Handler): - def handle(self, record): - pass try: unicode _py3k = False string_types = basestring text_type = unicode + path_types = (bytes, str) except NameError: _py3k = True string_types = str text_type = str + path_types = (str, ) logger = logging.getLogger(__name__) if not logger.handlers: - logger.addHandler(NullHandler()) + logger.addHandler(logging.NullHandler()) + +# See gh-196: Logging could show sensitive data. It also produces some voluminous +# output. Hence, split into two tiers - stuff that's always logged, and stuff that's +# only logged if log_everything is True. (This is set by the test script.) +# +# For now, only debug logging of chunks falls into the optionally-logged category. +log_everything = False # We use the test below because it works for Jython as well as CPython if os.path.__name__ == 'ntpath': # pragma: no cover @@ -94,22 +98,21 @@ def shell_quote(s): def shell_quote(s): """ - Quote text so that it is safe for Posix command shells. + Quote text so that it is safe for POSIX command shells. - For example, "*.py" would be converted to "'*.py'". If the text is - considered safe it is returned unquoted. + For example, "*.py" would be converted to "'*.py'". If the text is considered safe it is returned unquoted. - :param s: The value to quote - :type s: str (or unicode on 2.x) - :return: A safe version of the input, from the point of view of Posix - command shells - :rtype: The passed-in type + Args: + s (str): The value to quote + Returns: + str: A safe version of the input, from the point of view of POSIX + command shells. """ if not isinstance(s, string_types): # pragma: no cover raise TypeError('Expected string type, got %s' % type(s)) - if not s: + if not s: # pragma: no cover result = "''" - elif not UNSAFE.search(s): + elif not UNSAFE.search(s): # pragma: no cover result = s else: result = "'%s'" % s.replace("'", r"'\''") @@ -128,13 +131,19 @@ def shell_quote(s): # this module attribute appropriately. fsencoding = sys.getfilesystemencoding() + def no_quote(s): + """ + Legacy function which is a no-op on Python 3. + """ if not _py3k and isinstance(s, text_type): s = s.encode(fsencoding) return s -def _copy_data(instream, outstream): + +def _copy_data(instream, outstream, buffer_size, error_queue): # Copy one stream to another + assert buffer_size > 0 sent = 0 if hasattr(sys.stdin, 'encoding'): enc = sys.stdin.encoding @@ -144,45 +153,52 @@ def _copy_data(instream, outstream): # See issue #39: read can fail when e.g. a text stream is provided # for what is actually a binary file try: - data = instream.read(1024) - except UnicodeError: + data = instream.read(buffer_size) + except Exception as e: # pragma: no cover logger.warning('Exception occurred while reading', exc_info=1) + error_queue.put_nowait(e) break if not data: break sent += len(data) - # logger.debug("sending chunk (%d): %r", sent, data[:256]) + # logger.debug('sending chunk (%d): %r', sent, data[:256]) try: outstream.write(data) except UnicodeError: # pragma: no cover outstream.write(data.encode(enc)) - except: + except Exception as e: # pragma: no cover # Can sometimes get 'broken pipe' errors even when the data has all # been sent logger.exception('Error sending data') + error_queue.put_nowait(e) break try: outstream.close() except IOError: # pragma: no cover logger.warning('Exception occurred while closing: ignored', exc_info=1) - logger.debug("closed output, %d bytes sent", sent) + logger.debug('closed output, %d bytes sent', sent) + -def _threaded_copy_data(instream, outstream): - wr = threading.Thread(target=_copy_data, args=(instream, outstream)) - wr.setDaemon(True) +def _threaded_copy_data(instream, outstream, buffer_size, error_queue): + assert buffer_size > 0 + wr = threading.Thread(target=_copy_data, args=(instream, outstream, buffer_size, error_queue)) + wr.daemon = True logger.debug('data copier: %r, %r, %r', wr, instream, outstream) wr.start() return wr + def _write_passphrase(stream, passphrase, encoding): passphrase = '%s\n' % passphrase passphrase = passphrase.encode(encoding) stream.write(passphrase) logger.debug('Wrote passphrase') + def _is_sequence(instance): return isinstance(instance, (list, tuple, set, frozenset)) + def _make_memory_stream(s): try: from io import BytesIO @@ -191,6 +207,7 @@ def _make_memory_stream(s): rv = StringIO(s) return rv + def _make_binary_stream(s, encoding): if _py3k: if isinstance(s, str): @@ -200,21 +217,55 @@ def _make_binary_stream(s, encoding): s = s.encode(encoding) return _make_memory_stream(s) -class Verify(object): - "Handle status messages for --verify" - TRUST_UNDEFINED = 0 - TRUST_NEVER = 1 - TRUST_MARGINAL = 2 - TRUST_FULLY = 3 - TRUST_ULTIMATE = 4 +class StatusHandler(object): + """ + The base class for handling status messages from `gpg`. + """ + + on_data_failure = None # set at instance level when failures occur + + def __init__(self, gpg): + """ + Initialize an instance. + + Args: + gpg (GPG): The :class:`GPG` instance in use. + """ + self.gpg = gpg + + def handle_status(self, key, value): + """ + Handle status messages from the `gpg` child process. These are lines of the format + + [GNUPG:] + + Args: + key (str): Identifies what the status message is. + value (str): Identifies additional data, which differs depending on the key. + """ + raise NotImplementedError + + +class Verify(StatusHandler): + """ + This class handles status messages during signature verificaton. + """ + + TRUST_EXPIRED = 0 + TRUST_UNDEFINED = 1 + TRUST_NEVER = 2 + TRUST_MARGINAL = 3 + TRUST_FULLY = 4 + TRUST_ULTIMATE = 5 TRUST_LEVELS = { - "TRUST_UNDEFINED" : TRUST_UNDEFINED, - "TRUST_NEVER" : TRUST_NEVER, - "TRUST_MARGINAL" : TRUST_MARGINAL, - "TRUST_FULLY" : TRUST_FULLY, - "TRUST_ULTIMATE" : TRUST_ULTIMATE, + 'TRUST_EXPIRED': TRUST_EXPIRED, + 'TRUST_UNDEFINED': TRUST_UNDEFINED, + 'TRUST_NEVER': TRUST_NEVER, + 'TRUST_MARGINAL': TRUST_MARGINAL, + 'TRUST_FULLY': TRUST_FULLY, + 'TRUST_ULTIMATE': TRUST_ULTIMATE, } # for now, just the most common error codes. This can be expanded as and @@ -233,7 +284,7 @@ class Verify(object): returncode = None def __init__(self, gpg): - self.gpg = gpg + StatusHandler.__init__(self, gpg) self.valid = False self.fingerprint = self.creation_date = self.timestamp = None self.signature_id = self.key_id = None @@ -247,8 +298,9 @@ def __init__(self, gpg): self.trust_text = None self.trust_level = None self.sig_info = {} + self.problems = [] - def __nonzero__(self): + def __nonzero__(self): # pragma: no cover return self.valid __bool__ = __nonzero__ @@ -260,92 +312,97 @@ def update_sig_info(**kwargs): if sig_id: info = self.sig_info[sig_id] info.update(kwargs) + else: + logger.debug('Ignored due to missing sig iD: %s', kwargs) if key in self.TRUST_LEVELS: self.trust_text = key self.trust_level = self.TRUST_LEVELS[key] - update_sig_info(trust_level=self.trust_level, - trust_text=self.trust_text) - elif key in ("WARNING", "ERROR"): + update_sig_info(trust_level=self.trust_level, trust_text=self.trust_text) + # See Issue #214. Once we see this, we're done with the signature just seen. + # Zap the signature ID, because we don't see a SIG_ID unless we have a new + # good signature. + self.signature_id = None + elif key in ('WARNING', 'ERROR'): # pragma: no cover logger.warning('potential problem: %s: %s', key, value) - elif key == "BADSIG": # pragma: no cover + elif key == 'BADSIG': # pragma: no cover self.valid = False self.status = 'signature bad' self.key_id, self.username = value.split(None, 1) - update_sig_info(keyid=self.key_id, username=self.username, - status=self.status) - elif key == "ERRSIG": # pragma: no cover + self.problems.append({'status': self.status, 'keyid': self.key_id, 'user': self.username}) + update_sig_info(keyid=self.key_id, username=self.username, status=self.status) + elif key == 'ERRSIG': # pragma: no cover self.valid = False parts = value.split() - (self.key_id, - algo, hash_algo, - cls, - self.timestamp) = parts[:5] + (self.key_id, algo, hash_algo, cls, self.timestamp) = parts[:5] # Since GnuPG 2.2.7, a fingerprint is tacked on if len(parts) >= 7: self.fingerprint = parts[6] self.status = 'signature error' - update_sig_info(keyid=self.key_id, timestamp=self.timestamp, - fingerprint=self.fingerprint, status=self.status) - elif key == "EXPSIG": # pragma: no cover + update_sig_info(keyid=self.key_id, + timestamp=self.timestamp, + fingerprint=self.fingerprint, + status=self.status) + self.problems.append({ + 'status': self.status, + 'keyid': self.key_id, + 'timestamp': self.timestamp, + 'fingerprint': self.fingerprint + }) + elif key == 'EXPSIG': # pragma: no cover self.valid = False self.status = 'signature expired' self.key_id, self.username = value.split(None, 1) - update_sig_info(keyid=self.key_id, username=self.username, - status=self.status) - elif key == "GOODSIG": + update_sig_info(keyid=self.key_id, username=self.username, status=self.status) + self.problems.append({'status': self.status, 'keyid': self.key_id, 'user': self.username}) + elif key == 'GOODSIG': self.valid = True self.status = 'signature good' self.key_id, self.username = value.split(None, 1) - update_sig_info(keyid=self.key_id, username=self.username, - status=self.status) - elif key == "VALIDSIG": - fingerprint, creation_date, sig_ts, expire_ts = value.split()[:4] - (self.fingerprint, - self.creation_date, - self.sig_timestamp, - self.expire_timestamp) = (fingerprint, creation_date, sig_ts, - expire_ts) + update_sig_info(keyid=self.key_id, username=self.username, status=self.status) + elif key == 'VALIDSIG': + parts = value.split() + fingerprint, creation_date, sig_ts, expire_ts = parts[:4] + (self.fingerprint, self.creation_date, self.sig_timestamp, + self.expire_timestamp) = (fingerprint, creation_date, sig_ts, expire_ts) # may be different if signature is made with a subkey - self.pubkey_fingerprint = value.split()[-1] + if len(parts) >= 10: + self.pubkey_fingerprint = parts[9] self.status = 'signature valid' - update_sig_info(fingerprint=fingerprint, creation_date=creation_date, - timestamp=sig_ts, expiry=expire_ts, + update_sig_info(fingerprint=fingerprint, + creation_date=creation_date, + timestamp=sig_ts, + expiry=expire_ts, pubkey_fingerprint=self.pubkey_fingerprint, status=self.status) - elif key == "SIG_ID": + elif key == 'SIG_ID': sig_id, creation_date, timestamp = value.split() - self.sig_info[sig_id] = {'creation_date': creation_date, - 'timestamp': timestamp} - (self.signature_id, - self.creation_date, self.timestamp) = (sig_id, creation_date, - timestamp) - elif key == "DECRYPTION_FAILED": # pragma: no cover - self.valid = False - self.key_id = value - self.status = 'decryption failed' - elif key == "NO_PUBKEY": # pragma: no cover + self.sig_info[sig_id] = {'creation_date': creation_date, 'timestamp': timestamp} + (self.signature_id, self.creation_date, self.timestamp) = (sig_id, creation_date, timestamp) + elif key == 'NO_PUBKEY': # pragma: no cover self.valid = False self.key_id = value self.status = 'no public key' - elif key == "NO_SECKEY": # pragma: no cover + self.problems.append({'status': self.status, 'keyid': self.key_id}) + elif key == 'NO_SECKEY': # pragma: no cover self.valid = False self.key_id = value self.status = 'no secret key' - elif key in ("EXPKEYSIG", "REVKEYSIG"): # pragma: no cover + self.problems.append({'status': self.status, 'keyid': self.key_id}) + elif key in ('EXPKEYSIG', 'REVKEYSIG'): # pragma: no cover # signed with expired or revoked key self.valid = False - self.key_id = value.split()[0] - if key == "EXPKEYSIG": + self.key_id, self.username = value.split(None, 1) + if key == 'EXPKEYSIG': self.key_status = 'signing key has expired' else: self.key_status = 'signing key was revoked' self.status = self.key_status update_sig_info(status=self.status, keyid=self.key_id) - elif key in ("UNEXPECTED", "FAILURE"): # pragma: no cover + self.problems.append({'status': self.status, 'keyid': self.key_id}) + elif key in ('UNEXPECTED', 'FAILURE'): # pragma: no cover self.valid = False - self.key_id = value - if key == "UNEXPECTED": + if key == 'UNEXPECTED': self.status = 'unexpected data' else: # N.B. there might be other reasons. For example, if an output @@ -371,32 +428,38 @@ def update_sig_info(**kwargs): message = '%s: %s' % (operation, mapping[code]) if not self.status: self.status = message - elif key in ("DECRYPTION_INFO", "PLAINTEXT", "PLAINTEXT_LENGTH", - "BEGIN_SIGNING"): + elif key == 'NODATA': # pragma: no cover + # See issue GH-191 + self.valid = False + self.status = 'signature expected but not found' + elif key in ('DECRYPTION_INFO', 'PLAINTEXT', 'PLAINTEXT_LENGTH', 'BEGIN_SIGNING', 'KEY_CONSIDERED'): pass + elif key in ('NEWSIG', ): + # Only sent in gpg2. Clear any signature ID, to be set by a following SIG_ID + self.signature_id = None else: # pragma: no cover - logger.debug('message ignored: %s, %s', key, value) + logger.debug('message ignored: %r, %r', key, value) -class ImportResult(object): - "Handle status messages for --import" - counts = '''count no_user_id imported imported_rsa unchanged - n_uids n_subk n_sigs n_revoc sec_read sec_imported +class ImportResult(StatusHandler): + """ + This class handles status messages during key import. + """ + + counts = '''count no_user_id imported imported_rsa unchanged n_uids n_subk n_sigs n_revoc sec_read sec_imported sec_dups not_imported'''.split() returncode = None def __init__(self, gpg): - self.gpg = gpg + StatusHandler.__init__(self, gpg) self.results = [] self.fingerprints = [] for result in self.counts: setattr(self, result, 0) def __nonzero__(self): - if self.not_imported: return False - if not self.fingerprints: return False - return True + return bool(not self.not_imported and self.fingerprints) __bool__ = __nonzero__ @@ -418,55 +481,53 @@ def __nonzero__(self): } def handle_status(self, key, value): - if key in ("WARNING", "ERROR"): + if key in ('WARNING', 'ERROR'): # pragma: no cover logger.warning('potential problem: %s: %s', key, value) - elif key in ("IMPORTED", "KEY_CONSIDERED"): + elif key in ('IMPORTED', 'KEY_CONSIDERED'): # this duplicates info we already see in import_ok & import_problem pass - elif key == "NODATA": # pragma: no cover - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'No valid data found'}) - elif key == "IMPORT_OK": + elif key == 'NODATA': # pragma: no cover + self.results.append({'fingerprint': None, 'problem': '0', 'text': 'No valid data found'}) + elif key == 'IMPORT_OK': reason, fingerprint = value.split() reasons = [] for code, text in list(self.ok_reason.items()): if int(reason) | int(code) == int(reason): reasons.append(text) - reasontext = '\n'.join(reasons) + "\n" - self.results.append({'fingerprint': fingerprint, - 'ok': reason, 'text': reasontext}) + reasontext = '\n'.join(reasons) + '\n' + self.results.append({'fingerprint': fingerprint, 'ok': reason, 'text': reasontext}) self.fingerprints.append(fingerprint) - elif key == "IMPORT_PROBLEM": # pragma: no cover + elif key == 'IMPORT_PROBLEM': # pragma: no cover try: reason, fingerprint = value.split() - except: + except Exception: reason = value fingerprint = '' - self.results.append({'fingerprint': fingerprint, - 'problem': reason, 'text': self.problem_reason[reason]}) - elif key == "IMPORT_RES": + self.results.append({'fingerprint': fingerprint, 'problem': reason, 'text': self.problem_reason[reason]}) + elif key == 'IMPORT_RES': import_res = value.split() for i, count in enumerate(self.counts): setattr(self, count, int(import_res[i])) - elif key == "KEYEXPIRED": # pragma: no cover - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'Key expired'}) - elif key == "SIGEXPIRED": # pragma: no cover - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'Signature expired'}) - elif key == "FAILURE": # pragma: no cover - self.results.append({'fingerprint': None, - 'problem': '0', 'text': 'Other failure'}) + elif key == 'KEYEXPIRED': # pragma: no cover + self.results.append({'fingerprint': None, 'problem': '0', 'text': 'Key expired'}) + elif key == 'SIGEXPIRED': # pragma: no cover + self.results.append({'fingerprint': None, 'problem': '0', 'text': 'Signature expired'}) + elif key == 'FAILURE': # pragma: no cover + self.results.append({'fingerprint': None, 'problem': '0', 'text': 'Other failure'}) else: # pragma: no cover logger.debug('message ignored: %s, %s', key, value) def summary(self): + """ + Return a summary indicating how many keys were imported and how many were not imported. + """ result = [] result.append('%d imported' % self.imported) if self.not_imported: # pragma: no cover result.append('%d not imported' % self.not_imported) return ', '.join(result) + ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) BASIC_ESCAPES = { r'\n': '\n', @@ -477,16 +538,18 @@ def summary(self): r'\0': '\0', } -class SendResult(object): - returncode = None +class SendResult(StatusHandler): + """ + This class handles status messages during key sending. + """ - def __init__(self, gpg): - self.gpg = gpg + returncode = None def handle_status(self, key, value): logger.debug('SendResult: %s: %s', key, value) + def _set_fields(target, fieldnames, args): for i, var in enumerate(fieldnames): if i < len(args): @@ -494,25 +557,30 @@ def _set_fields(target, fieldnames, args): else: target[var] = 'unavailable' -class SearchKeys(list): - ''' Handle status messages for --search-keys. - Handle pub and uid (relating the latter to the former). +class SearchKeys(StatusHandler, list): + """ + This class handles status messages during key search. + """ - Don't care about the rest - ''' + # Handle pub and uid (relating the latter to the former). + # Don't care about the rest UID_INDEX = 1 FIELDS = 'type keyid algo length date expires'.split() returncode = None def __init__(self, gpg): - self.gpg = gpg + StatusHandler.__init__(self, gpg) self.curkey = None self.fingerprints = [] self.uids = [] + self.uid_map = {} def get_fields(self, args): + """ + Internal method used to update the instance from a `gpg` status message. + """ result = {} _set_fields(result, self.FIELDS, args) result['uids'] = [] @@ -520,39 +588,52 @@ def get_fields(self, args): return result def pub(self, args): + """ + Internal method used to update the instance from a `gpg` status message. + """ self.curkey = curkey = self.get_fields(args) self.append(curkey) def uid(self, args): + """ + Internal method used to update the instance from a `gpg` status message. + """ uid = args[self.UID_INDEX] uid = ESCAPE_PATTERN.sub(lambda m: chr(int(m.group(1), 16)), uid) for k, v in BASIC_ESCAPES.items(): uid = uid.replace(k, v) self.curkey['uids'].append(uid) self.uids.append(uid) + uid_data = {} + self.uid_map[uid] = uid_data + for fn, fv in zip(self.FIELDS, args): + uid_data[fn] = fv def handle_status(self, key, value): # pragma: no cover pass + class ListKeys(SearchKeys): - ''' Handle status messages for --list-keys, --list-sigs. + """ + This class handles status messages during listing keys and signatures. - Handle pub and uid (relating the latter to the former). + Handle pub and uid (relating the latter to the former). - Don't care about (info from src/DETAILS): + We don't care about (info from GnuPG DETAILS file): - crt = X.509 certificate - crs = X.509 certificate and private key available - uat = user attribute (same as user id except for field 10). - sig = signature - rev = revocation signature - pkd = public key data (special field format, see below) - grp = reserved for gpgsm - rvk = revocation key - ''' + crt = X.509 certificate + crs = X.509 certificate and private key available + uat = user attribute (same as user id except for field 10). + sig = signature + rev = revocation signature + pkd = public key data (special field format, see below) + grp = reserved for gpgsm + rvk = revocation key + """ UID_INDEX = 9 - FIELDS = 'type trust length algo keyid date expires dummy ownertrust uid sig cap issuer flag token hash curve compliance updated origin'.split() + FIELDS = ('type trust length algo keyid date expires dummy ownertrust uid sig' + ' cap issuer flag token hash curve compliance updated origin keygrip').split() def __init__(self, gpg): super(ListKeys, self).__init__(gpg) @@ -560,8 +641,11 @@ def __init__(self, gpg): self.key_map = {} def key(self, args): + """ + Internal method used to update the instance from a `gpg` status message. + """ self.curkey = curkey = self.get_fields(args) - if curkey['uid']: + if curkey['uid']: # pragma: no cover curkey['uids'].append(curkey['uid']) del curkey['uid'] curkey['subkeys'] = [] @@ -571,6 +655,9 @@ def key(self, args): pub = sec = key def fpr(self, args): + """ + Internal method used to update the instance from a `gpg` status message. + """ fp = args[9] if fp in self.key_map and self.gpg.check_fingerprint_collisions: # pragma: no cover raise ValueError('Unexpected fingerprint collision: %s' % fp) @@ -579,9 +666,19 @@ def fpr(self, args): self.fingerprints.append(fp) self.key_map[fp] = self.curkey else: - self.curkey['subkeys'][-1].append(fp) + self.curkey['subkeys'][-1][2] = fp self.key_map[fp] = self.curkey + def grp(self, args): + """ + Internal method used to update the instance from a `gpg` status message. + """ + grp = args[9] + if not self.in_subkey: + self.curkey['keygrip'] = grp + else: + self.curkey['subkeys'][-1][3] = grp + def _collect_subkey_info(self, curkey, args): info_map = curkey.setdefault('subkey_info', {}) info = {} @@ -589,36 +686,53 @@ def _collect_subkey_info(self, curkey, args): info_map[args[4]] = info def sub(self, args): + """ + Internal method used to update the instance from a `gpg` status message. + """ # See issue #81. We create a dict with more information about # subkeys, but for backward compatibility reason, have to add it in # as a separate entry 'subkey_info' - subkey = [args[4], args[11]] # keyid, type + subkey = [args[4], args[11], None, None] # keyid, type, fp, grp self.curkey['subkeys'].append(subkey) self._collect_subkey_info(self.curkey, args) self.in_subkey = True def ssb(self, args): - subkey = [args[4], None] # keyid, type + """ + Internal method used to update the instance from a `gpg` status message. + """ + subkey = [args[4], None, None, None] # keyid, type, fp, grp self.curkey['subkeys'].append(subkey) self._collect_subkey_info(self.curkey, args) self.in_subkey = True def sig(self, args): + """ + Internal method used to update the instance from a `gpg` status message. + """ # keyid, uid, sigclass self.curkey['sigs'].append((args[4], args[9], args[10])) + class ScanKeys(ListKeys): - ''' Handle status messages for --with-fingerprint.''' + """ + This class handles status messages during scanning keys. + """ def sub(self, args): + """ + Internal method used to update the instance from a `gpg` status message. + """ # --with-fingerprint --with-colons somehow outputs fewer colons, # use the last value args[-1] instead of args[11] - subkey = [args[4], args[-1]] + subkey = [args[4], args[-1], None, None] self.curkey['subkeys'].append(subkey) self._collect_subkey_info(self.curkey, args) self.in_subkey = True + class TextHandler(object): + def _as_text(self): return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) @@ -631,113 +745,197 @@ def __str__(self): return self.data +_INVALID_KEY_REASONS = { + 0: 'no specific reason given', + 1: 'not found', + 2: 'ambiguous specification', + 3: 'wrong key usage', + 4: 'key revoked', + 5: 'key expired', + 6: 'no crl known', + 7: 'crl too old', + 8: 'policy mismatch', + 9: 'not a secret key', + 10: 'key not trusted', + 11: 'missing certificate', + 12: 'missing issuer certificate', + 13: 'key disabled', + 14: 'syntax error in specification', +} + + +def _determine_invalid_recipient_or_signer(s): # pragma: no cover + parts = s.split() + if len(parts) >= 2: + code, ident = parts[:2] + else: + code = parts[0] + ident = '' + unexpected = 'unexpected return code %r' % code + try: + key = int(code) + result = _INVALID_KEY_REASONS.get(key, unexpected) + except ValueError: + result = unexpected + return '%s:%s' % (result, ident) + + class Crypt(Verify, TextHandler): - "Handle status messages for --encrypt and --decrypt" + """ + This class handles status messages during encryption and decryption. + """ + def __init__(self, gpg): Verify.__init__(self, gpg) self.data = '' self.ok = False self.status = '' + self.status_detail = '' self.key_id = None def __nonzero__(self): - if self.ok: return True - return False + return bool(self.ok) __bool__ = __nonzero__ def handle_status(self, key, value): - if key in ("WARNING", "ERROR"): + if key in ('WARNING', 'ERROR'): logger.warning('potential problem: %s: %s', key, value) - elif key == "NODATA": - if self.status not in ("decryption failed",): - self.status = "no data was provided" - elif key in ("NEED_PASSPHRASE", "BAD_PASSPHRASE", "GOOD_PASSPHRASE", - "MISSING_PASSPHRASE", "KEY_NOT_CREATED", "NEED_PASSPHRASE_PIN"): - self.status = key.replace("_", " ").lower() - elif key == "DECRYPTION_FAILED": + elif key == 'NODATA': + if self.status not in ('decryption failed', ): + self.status = 'no data was provided' + elif key in ('NEED_PASSPHRASE', 'BAD_PASSPHRASE', 'GOOD_PASSPHRASE', 'MISSING_PASSPHRASE', 'KEY_NOT_CREATED', + 'NEED_PASSPHRASE_PIN'): # pragma: no cover + self.status = key.replace('_', ' ').lower() + elif key == 'DECRYPTION_FAILED': # pragma: no cover if self.status != 'no secret key': # don't overwrite more useful message self.status = 'decryption failed' - elif key == "NEED_PASSPHRASE_SYM": + elif key == 'NEED_PASSPHRASE_SYM': self.status = 'need symmetric passphrase' - elif key == "BEGIN_DECRYPTION": + elif key == 'BEGIN_DECRYPTION': if self.status != 'no secret key': # don't overwrite more useful message self.status = 'decryption incomplete' - elif key == "BEGIN_ENCRYPTION": + elif key == 'BEGIN_ENCRYPTION': self.status = 'encryption incomplete' - elif key == "DECRYPTION_OKAY": + elif key == 'DECRYPTION_OKAY': self.status = 'decryption ok' self.ok = True - elif key == "END_ENCRYPTION": + elif key == 'END_ENCRYPTION': self.status = 'encryption ok' self.ok = True - elif key == "INV_RECP": # pragma: no cover - self.status = 'invalid recipient' - elif key == "KEYEXPIRED": # pragma: no cover + elif key == 'INV_RECP': # pragma: no cover + if not self.status: + self.status = 'invalid recipient' + else: + self.status = 'invalid recipient: %s' % self.status + self.status_detail = _determine_invalid_recipient_or_signer(value) + elif key == 'KEYEXPIRED': # pragma: no cover self.status = 'key expired' - elif key == "SIG_CREATED": # pragma: no cover + elif key == 'SIG_CREATED': # pragma: no cover self.status = 'sig created' - elif key == "SIGEXPIRED": # pragma: no cover + elif key == 'SIGEXPIRED': # pragma: no cover self.status = 'sig expired' - elif key == "ENC_TO": # pragma: no cover + elif key == 'ENC_TO': # pragma: no cover # ENC_TO self.key_id = value.split(' ', 1)[0] - elif key in ("USERID_HINT", "GOODMDC", - "END_DECRYPTION", "CARDCTRL", "BADMDC", - "SC_OP_FAILURE", "SC_OP_SUCCESS", - "PINENTRY_LAUNCHED", "KEY_CONSIDERED"): + elif key in ('USERID_HINT', 'GOODMDC', 'END_DECRYPTION', 'CARDCTRL', 'BADMDC', 'SC_OP_FAILURE', + 'SC_OP_SUCCESS', 'PINENTRY_LAUNCHED'): pass else: Verify.handle_status(self, key, value) -class GenKey(object): - "Handle status messages for --gen-key" + +class GenKey(StatusHandler): + """ + This class handles status messages during key generation. + """ returncode = None def __init__(self, gpg): - self.gpg = gpg + StatusHandler.__init__(self, gpg) self.type = None - self.fingerprint = None + self.fingerprint = '' + self.status = None - def __nonzero__(self): - if self.fingerprint: return True - return False + def __nonzero__(self): # pragma: no cover + return bool(self.fingerprint) __bool__ = __nonzero__ - def __str__(self): - return self.fingerprint or '' + def __str__(self): # pragma: no cover + return self.fingerprint def handle_status(self, key, value): - if key in ("WARNING", "ERROR"): # pragma: no cover + if key in ('WARNING', 'ERROR'): # pragma: no cover logger.warning('potential problem: %s: %s', key, value) - elif key == "KEY_CREATED": - (self.type,self.fingerprint) = value.split() - elif key in ("PROGRESS", "GOOD_PASSPHRASE", "KEY_NOT_CREATED"): + elif key == 'KEY_CREATED': + parts = value.split() + (self.type, self.fingerprint) = parts[:2] + self.status = 'ok' + elif key == 'KEY_NOT_CREATED': + self.status = key.replace('_', ' ').lower() + elif key in ('PROGRESS', 'GOOD_PASSPHRASE'): # pragma: no cover pass else: # pragma: no cover logger.debug('message ignored: %s, %s', key, value) -class ExportResult(GenKey): - """Handle status messages for --export[-secret-key]. - For now, just use an existing class to base it on - if needed, we - can override handle_status for more specific message handling. +class AddSubkey(StatusHandler): + """ + This class handles status messages during subkey addition. """ + + returncode = None + + def __init__(self, gpg): + StatusHandler.__init__(self, gpg) + self.type = None + self.fingerprint = '' + self.status = None + + def __nonzero__(self): # pragma: no cover + return bool(self.fingerprint) + + __bool__ = __nonzero__ + + def __str__(self): + return self.fingerprint + def handle_status(self, key, value): - if key in ("EXPORTED", "EXPORT_RES"): + if key in ('WARNING', 'ERROR'): # pragma: no cover + logger.warning('potential problem: %s: %s', key, value) + elif key == 'KEY_CREATED': + (self.type, self.fingerprint) = value.split() + self.status = 'ok' + else: # pragma: no cover + logger.debug('message ignored: %s, %s', key, value) + + +class ExportResult(GenKey): + """ + This class handles status messages during key export. + """ + + # For now, just use an existing class to base it on - if needed, we + # can override handle_status for more specific message handling. + + def handle_status(self, key, value): + if key in ('EXPORTED', 'EXPORT_RES'): pass else: super(ExportResult, self).handle_status(key, value) -class DeleteResult(object): - "Handle status messages for --delete-key and --delete-secret-key" + +class DeleteResult(StatusHandler): + """ + This class handles status messages during key deletion. + """ returncode = None def __init__(self, gpg): - self.gpg = gpg + StatusHandler.__init__(self, gpg) self.status = 'ok' def __str__(self): @@ -750,33 +948,38 @@ def __str__(self): } def handle_status(self, key, value): - if key == "DELETE_PROBLEM": # pragma: no cover - self.status = self.problem_reason.get(value, - "Unknown error: %r" % value) + if key == 'DELETE_PROBLEM': # pragma: no cover + self.status = self.problem_reason.get(value, 'Unknown error: %r' % value) else: # pragma: no cover logger.debug('message ignored: %s, %s', key, value) - def __nonzero__(self): + def __nonzero__(self): # pragma: no cover return self.status == 'ok' __bool__ = __nonzero__ class TrustResult(DeleteResult): + """ + This class handles status messages during key trust setting. + """ pass -class Sign(TextHandler): - "Handle status messages for --sign" +class Sign(StatusHandler, TextHandler): + """ + This class handles status messages during signing. + """ returncode = None def __init__(self, gpg): - self.gpg = gpg + StatusHandler.__init__(self, gpg) self.type = None self.hash_algo = None self.fingerprint = None self.status = None + self.status_detail = None self.key_id = None self.username = None @@ -786,40 +989,101 @@ def __nonzero__(self): __bool__ = __nonzero__ def handle_status(self, key, value): - if key in ("WARNING", "ERROR", "FAILURE"): # pragma: no cover + if key in ('WARNING', 'ERROR', 'FAILURE'): # pragma: no cover logger.warning('potential problem: %s: %s', key, value) - elif key in ("KEYEXPIRED", "SIGEXPIRED"): # pragma: no cover + elif key in ('KEYEXPIRED', 'SIGEXPIRED'): # pragma: no cover self.status = 'key expired' - elif key == "KEYREVOKED": # pragma: no cover + elif key == 'KEYREVOKED': # pragma: no cover self.status = 'key revoked' - elif key == "SIG_CREATED": - (self.type, - algo, self.hash_algo, cls, self.timestamp, self.fingerprint - ) = value.split() + elif key == 'SIG_CREATED': + (self.type, algo, self.hash_algo, cls, self.timestamp, self.fingerprint) = value.split() self.status = 'signature created' - elif key == "USERID_HINT": # pragma: no cover + elif key == 'USERID_HINT': # pragma: no cover self.key_id, self.username = value.split(' ', 1) - elif key == "BAD_PASSPHRASE": + elif key == 'BAD_PASSPHRASE': # pragma: no cover self.status = 'bad passphrase' - elif key in ("NEED_PASSPHRASE", "GOOD_PASSPHRASE", "BEGIN_SIGNING"): + elif key in ('INV_SGNR', 'INV_RECP'): # pragma: no cover + # INV_RECP is returned in older versions + if not self.status: + self.status = 'invalid signer' + else: + self.status = 'invalid signer: %s' % self.status + self.status_detail = _determine_invalid_recipient_or_signer(value) + elif key in ('NEED_PASSPHRASE', 'GOOD_PASSPHRASE', 'BEGIN_SIGNING'): pass else: # pragma: no cover logger.debug('message ignored: %s, %s', key, value) -VERSION_RE = re.compile(r'gpg \(GnuPG(?:/MacGPG2)?\) (\d+(\.\d+)*)'.encode('ascii'), re.I) + +class AutoLocateKey(StatusHandler): + """ + This class handles status messages during key auto-locating. + fingerprint: str + key_length: int + created_at: date + email: str + email_real_name: str + """ + + def __init__(self, gpg): + StatusHandler.__init__(self, gpg) + self.fingerprint = None + self.type = None + self.created_at = None + self.email = None + self.email_real_name = None + + def handle_status(self, key, value): + if key == "IMPORTED": + _, email, display_name = value.split() + + self.email = email + self.email_real_name = display_name[1:-1] + elif key == "KEY_CONSIDERED": + self.fingerprint = value.strip().split()[0] + + def pub(self, args): + """ + Internal method to handle the 'pub' status message. + `pub` message contains the fingerprint of the public key, its type and its creation date. + """ + pass + + def uid(self, args): + self.created_at = datetime.fromtimestamp(int(args[5])) + raw_email_content = args[9] + email, real_name = parseaddr(raw_email_content) + self.email = email + self.email_real_name = real_name + + def sub(self, args): + self.key_length = int(args[2]) + + def fpr(self, args): + # Only store the first fingerprint + self.fingerprint = self.fingerprint or args[9] + + +VERSION_RE = re.compile(r'\bcfg:version:(\d+(\.\d+)*)'.encode('ascii')) HEX_DIGITS_RE = re.compile(r'[0-9a-f]+$', re.I) PUBLIC_KEY_RE = re.compile(r'gpg: public key is (\w+)') -class GPG(object): +class GPG(object): + """ + This class provides a high-level programmatic interface for `gpg`. + """ error_map = None decode_errors = 'strict' + buffer_size = 16384 # override in instance if needed + result_map = { 'crypt': Crypt, 'delete': DeleteResult, 'generate': GenKey, + 'addSubkey': AddSubkey, 'import': ImportResult, 'send': SendResult, 'list': ListKeys, @@ -829,26 +1093,40 @@ class GPG(object): 'trust': TrustResult, 'verify': Verify, 'export': ExportResult, + 'auto-locate-key': AutoLocateKey, } + "A map of GPG operations to result object types." + + def __init__(self, + gpgbinary='gpg', + gnupghome=None, + verbose=False, + use_agent=False, + keyring=None, + options=None, + secret_keyring=None, + env=None): + """Initialize a GPG process wrapper. + + Args: + gpgbinary (str): A pathname for the GPG binary to use. - "Encapsulate access to the gpg executable" - def __init__(self, gpgbinary='gpg', gnupghome=None, verbose=False, - use_agent=False, keyring=None, options=None, - secret_keyring=None): - """Initialize a GPG process wrapper. Options are: + gnupghome (str): A pathname to where we can find the public and private keyrings. The default is + whatever `gpg` defaults to. - gpgbinary -- full pathname for GPG binary. + keyring (str|list): The name of alternative keyring file to use, or a list of such keyring files. If + specified, the default keyring is not used. - gnupghome -- full pathname to where we can find the public and - private keyrings. Default is whatever gpg defaults to. - keyring -- name of alternative keyring file to use, or list of such - keyrings. If specified, the default keyring is not used. - options =-- a list of additional options to pass to the GPG binary. - secret_keyring -- name of alternative secret keyring file to use, or - list of such keyrings. + options (list): A list of additional options to pass to the GPG binary. + + secret_keyring (str|list): The name of an alternative secret keyring file to use, or a list of such + keyring files. + + env (dict): A dict of environment variables to be used for the GPG subprocess. """ self.gpgbinary = gpgbinary self.gnupghome = gnupghome + self.env = env # issue 112: fail if the specified value isn't a directory if gnupghome and not os.path.isdir(gnupghome): raise ValueError('gnupghome should be a directory (it isn\'t): %s' % gnupghome) @@ -858,7 +1136,7 @@ def __init__(self, gpgbinary='gpg', gnupghome=None, verbose=False, if isinstance(keyring, string_types): keyring = [keyring] self.keyring = keyring - if secret_keyring: + if secret_keyring: # pragma: no cover # Allow passing a string or another iterable. Make it uniformly # a list of keyring filenames if isinstance(secret_keyring, string_types): @@ -875,20 +1153,19 @@ def __init__(self, gpgbinary='gpg', gnupghome=None, verbose=False, # falling back to utf-8, because gpg itself uses latin-1 as the default # encoding. self.encoding = 'latin-1' - if gnupghome and not os.path.isdir(self.gnupghome): - os.makedirs(self.gnupghome,0x1C0) + if gnupghome and not os.path.isdir(self.gnupghome): # pragma: no cover + os.makedirs(self.gnupghome, 0o700) try: - p = self._open_subprocess(["--version"]) + p = self._open_subprocess(['--list-config', '--with-colons']) except OSError: msg = 'Unable to run gpg (%s) - it may not be available.' % self.gpgbinary logger.exception(msg) raise OSError(msg) - result = self.result_map['verify'](self) # any result will do for this + result = self.result_map['verify'](self) # any result will do for this self._collect_output(p, result, stdin=p.stdin) if p.returncode != 0: # pragma: no cover - raise ValueError("Error invoking gpg: %s: %s" % (p.returncode, - result.stderr)) - m = VERSION_RE.match(result.data) + raise ValueError('Error invoking gpg: %s: %s' % (p.returncode, result.stderr)) + m = VERSION_RE.search(result.data) if not m: # pragma: no cover self.version = None else: @@ -903,22 +1180,26 @@ def make_args(self, args, passphrase): """ Make a list of command line elements for GPG. The value of ``args`` will be appended. The ``passphrase`` argument needs to be True if - a passphrase will be sent to GPG, else False. + a passphrase will be sent to `gpg`, else False. + + Args: + args (list[str]): A list of arguments. + passphrase (str): The passphrase to use. """ cmd = [self.gpgbinary, '--status-fd', '2', '--no-tty', '--no-verbose'] - if 'DEBUG_IPC' in os.environ: + if 'DEBUG_IPC' in os.environ: # pragma: no cover cmd.extend(['--debug', 'ipc']) if passphrase and hasattr(self, 'version'): if self.version >= (2, 1): cmd[1:1] = ['--pinentry-mode', 'loopback'] cmd.extend(['--fixed-list-mode', '--batch', '--with-colons']) if self.gnupghome: - cmd.extend(['--homedir', no_quote(self.gnupghome)]) + cmd.extend(['--homedir', no_quote(self.gnupghome)]) if self.keyring: cmd.append('--no-default-keyring') for fn in self.keyring: cmd.extend(['--keyring', no_quote(fn)]) - if self.secret_keyring: + if self.secret_keyring: # pragma: no cover for fn in self.secret_keyring: cmd.extend(['--secret-keyring', no_quote(fn)]) if passphrase: @@ -934,19 +1215,6 @@ def _open_subprocess(self, args, passphrase=False): # Internal method: open a pipe to a GPG subprocess and return # the file objects for communicating with it. - # def debug_print(cmd): - # result = [] - # for c in cmd: - # if ' ' not in c: - # result.append(c) - # else: - # if '"' not in c: - # result.append('"%s"' % c) - # elif "'" not in c: - # result.append("'%s'" % c) - # else: - # result.append(c) # give up - # return ' '.join(cmd) from subprocess import list2cmdline as debug_print cmd = self.make_args(args, passphrase) @@ -958,9 +1226,8 @@ def _open_subprocess(self, args, passphrase=False): si = STARTUPINFO() si.dwFlags = STARTF_USESHOWWINDOW si.wShowWindow = SW_HIDE - result = Popen(cmd, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE, - startupinfo=si) - logger.debug("%s: %s", result.pid, debug_print(cmd)) + result = Popen(cmd, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE, startupinfo=si, env=self.env) + logger.debug('%s: %s', result.pid, debug_print(cmd)) return result def _read_response(self, stream, result): @@ -978,7 +1245,7 @@ def _read_response(self, stream, result): line = line.rstrip() if self.verbose: # pragma: no cover print(line) - logger.debug("%s", line) + logger.debug('%s', line) if line[0:9] == '[GNUPG:] ': # Chop off the prefix line = line[9:] @@ -987,23 +1254,35 @@ def _read_response(self, stream, result): if len(L) > 1: value = L[1] else: - value = "" + value = '' result.handle_status(keyword, value) result.stderr = ''.join(lines) - def _read_data(self, stream, result, on_data=None): + def _read_data(self, stream, result, on_data=None, buffer_size=1024): # Read the contents of the file from GPG's stdout + assert buffer_size > 0 chunks = [] + on_data_failure = None while True: - data = stream.read(1024) + data = stream.read(buffer_size) if len(data) == 0: if on_data: - on_data(data) + try: + on_data(data) + except Exception as e: + if on_data_failure is None: + on_data_failure = e break - logger.debug("chunk: %r" % data[:256]) + if log_everything: + logger.debug('chunk: %r' % data[:256]) append = True if on_data: - append = on_data(data) != False + try: + on_data_result = on_data(data) + append = on_data_result is not False + except Exception as e: + if on_data_failure is None: + on_data_failure = e if append: chunks.append(data) if _py3k: @@ -1011,31 +1290,31 @@ def _read_data(self, stream, result, on_data=None): result.data = type(data)().join(chunks) else: result.data = ''.join(chunks) + if on_data_failure: + result.on_data_failure = on_data_failure def _collect_output(self, process, result, writer=None, stdin=None): """ - Drain the subprocesses output streams, writing the collected output - to the result. If a writer thread (writing to the subprocess) is given, - make sure it's joined before returning. If a stdin stream is given, - close it before returning. + Drain the subprocesses output streams, writing the collected output to the result. If a writer thread (writing + to the subprocess) is given, make sure it's joined before returning. If a stdin stream is given, close it + before returning. """ stderr = codecs.getreader(self.encoding)(process.stderr) rr = threading.Thread(target=self._read_response, args=(stderr, result)) - rr.setDaemon(True) + rr.daemon = True logger.debug('stderr reader: %r', rr) rr.start() stdout = process.stdout - dr = threading.Thread(target=self._read_data, args=(stdout, result, - self.on_data)) - dr.setDaemon(True) + dr = threading.Thread(target=self._read_data, args=(stdout, result, self.on_data, self.buffer_size)) + dr.daemon = True logger.debug('stdout reader: %r', dr) dr.start() dr.join() rr.join() if writer is not None: - writer.join() + writer.join(0.01) process.wait() result.returncode = rc = process.returncode if rc != 0: @@ -1051,39 +1330,97 @@ def _collect_output(self, process, result, writer=None, stdin=None): def is_valid_file(self, fileobj): """ - Simplistic check for a file object + A simplistic check for a file-like object. + + Args: + fileobj (object): The object to test. + Returns: + bool: ``True`` if it's a file-like object, else ``False``. """ return hasattr(fileobj, 'read') - def _handle_io(self, args, fileobj, result, passphrase=None, binary=False): + def _get_fileobj(self, fileobj_or_path): + if self.is_valid_file(fileobj_or_path): + result = fileobj_or_path + elif not isinstance(fileobj_or_path, path_types): + raise TypeError('Not a valid file or path: %s' % fileobj_or_path) + elif not os.path.exists(fileobj_or_path): + raise ValueError('No such file: %s' % fileobj_or_path) + else: + result = open(fileobj_or_path, 'rb') + return result + + def _handle_io(self, args, fileobj_or_path, result, passphrase=None, binary=False): "Handle a call to GPG - pass input data, collect output data" # Handle a basic data call - pass data to GPG, handle the output # including status information. Garbage In, Garbage Out :) - if not self.is_valid_file(fileobj): - raise TypeError('Not a valid file: %s' % fileobj) - p = self._open_subprocess(args, passphrase is not None) - if not binary: # pragma: no cover - stdin = codecs.getwriter(self.encoding)(p.stdin) - else: - stdin = p.stdin - if passphrase: - _write_passphrase(stdin, passphrase, self.encoding) - writer = _threaded_copy_data(fileobj, stdin) - self._collect_output(p, result, writer, stdin) - return result + fileobj = self._get_fileobj(fileobj_or_path) + writer = None # See issue #237 + try: + p = self._open_subprocess(args, passphrase is not None) + if not binary: # pragma: no cover + stdin = codecs.getwriter(self.encoding)(p.stdin) + else: + stdin = p.stdin + if passphrase: + _write_passphrase(stdin, passphrase, self.encoding) + error_queue = Queue() + writer = _threaded_copy_data(fileobj, stdin, self.buffer_size, error_queue) + self._collect_output(p, result, writer, stdin) + try: + exc = error_queue.get_nowait() + # if we get here, that means an error occurred in the copying thread + raise exc + except Empty: + pass + return result + finally: + if writer: + writer.join(0.01) + if fileobj is not fileobj_or_path: + fileobj.close() # # SIGNATURE METHODS # + def sign(self, message, **kwargs): - """sign message""" + """ + Sign a message. This method delegates most of the work to the `sign_file()` method. + + Args: + message (str|bytes): The data to sign. + kwargs (dict): Keyword arguments, which are passed to `sign_file()`: + + * keyid (str): The key id of the signer. + + * passphrase (str): The passphrase for the key. + + * clearsign (bool): Whether to use clear signing. + + * detach (bool): Whether to produce a detached signature. + + * binary (bool): Whether to produce a binary signature. + + * output (str): The path to write a detached signature to. + + * extra_args (list[str]): Additional arguments to pass to `gpg`. + """ f = _make_binary_stream(message, self.encoding) result = self.sign_file(f, **kwargs) f.close() return result def set_output_without_confirmation(self, args, output): - "If writing to a file which exists, avoid a confirmation message." + """ + If writing to a file which exists, avoid a confirmation message by + updating the *args* value in place to set the output path and avoid + any cpmfirmation prompt. + + Args: + args (list[str]): A list of arguments. + output (str): The path to the outpur file. + """ if os.path.exists(output): # We need to avoid an overwrite confirmation message args.extend(['--yes']) @@ -1091,19 +1428,49 @@ def set_output_without_confirmation(self, args, output): def is_valid_passphrase(self, passphrase): """ - Confirm that the passphrase doesn't contain newline-type characters - - it is passed in a pipe to gpg, and so not checking could lead to - spoofing attacks by passing arbitrary text after passphrase and newline. + Confirm that the passphrase doesn't contain newline-type characters - it is passed in a pipe to `gpg`, + and so not checking could lead to spoofing attacks by passing arbitrary text after passphrase and newline. + + Args: + passphrase (str): The passphrase to test. + + Returns: + bool: ``True`` if it's a valid passphrase, else ``False``. + """ + return ('\n' not in passphrase and '\r' not in passphrase and '\x00' not in passphrase) + + def sign_file(self, + fileobj_or_path, + keyid=None, + passphrase=None, + clearsign=True, + detach=False, + binary=False, + output=None, + extra_args=None): """ - return ('\n' not in passphrase and '\r' not in passphrase and - '\x00' not in passphrase) + Sign data in a file or file-like object. + + Args: + fileobj_or_path (str|file): The file or file-like object to sign. + + keyid (str): The key id of the signer. + + passphrase (str): The passphrase for the key. + + clearsign (bool): Whether to use clear signing. - def sign_file(self, file, keyid=None, passphrase=None, clearsign=True, - detach=False, binary=False, output=None, extra_args=None): - """sign file""" + detach (bool): Whether to produce a detached signature. + + binary (bool): Whether to produce a binary signature. + + output (str): The path to write a detached signature to. + + extra_args (list[str]): Additional arguments to pass to `gpg`. + """ if passphrase and not self.is_valid_passphrase(passphrase): raise ValueError('Invalid passphrase') - logger.debug("sign_file: %s", file) + logger.debug('sign_file: %s', fileobj_or_path) if binary: # pragma: no cover args = ['-s'] else: @@ -1111,69 +1478,94 @@ def sign_file(self, file, keyid=None, passphrase=None, clearsign=True, # You can't specify detach-sign and clearsign together: gpg ignores # the detach-sign in that case. if detach: - args.append("--detach-sign") + args.append('--detach-sign') elif clearsign: - args.append("--clearsign") + args.append('--clearsign') if keyid: args.extend(['--default-key', no_quote(keyid)]) - if output: # write the output to a file with the specified name + if output: # pragma: no cover + # write the output to a file with the specified name self.set_output_without_confirmation(args, output) - if extra_args: + if extra_args: # pragma: no cover args.extend(extra_args) result = self.result_map['sign'](self) - #We could use _handle_io here except for the fact that if the - #passphrase is bad, gpg bails and you can't write the message. + # We could use _handle_io here except for the fact that if the + # passphrase is bad, gpg bails and you can't write the message. + fileobj = self._get_fileobj(fileobj_or_path) p = self._open_subprocess(args, passphrase is not None) + writer = None try: stdin = p.stdin if passphrase: _write_passphrase(stdin, passphrase, self.encoding) - writer = _threaded_copy_data(file, stdin) + error_queue = Queue() + writer = _threaded_copy_data(fileobj, stdin, self.buffer_size, error_queue) + try: + exc = error_queue.get_nowait() + # if we get here, that means an error occurred in the copying thread + raise exc + except Empty: + pass except IOError: # pragma: no cover - logging.exception("error writing message") - writer = None + logging.exception('error writing message') + finally: + if writer: + writer.join(0.01) + if fileobj is not fileobj_or_path: + fileobj.close() self._collect_output(p, result, writer, stdin) return result def verify(self, data, **kwargs): - """Verify the signature on the contents of the string 'data' - - >>> GPGBINARY = os.environ.get('GPGBINARY', 'gpg') - >>> if not os.path.isdir('keys'): os.mkdir('keys') - >>> gpg = GPG(gpgbinary=GPGBINARY, gnupghome='keys') - >>> input = gpg.gen_key_input(passphrase='foo') - >>> key = gpg.gen_key(input) - >>> assert key - >>> sig = gpg.sign('hello',keyid=key.fingerprint,passphrase='bar') - >>> assert not sig - >>> sig = gpg.sign('hello',keyid=key.fingerprint,passphrase='foo') - >>> assert sig - >>> verify = gpg.verify(sig.data) - >>> assert verify + """ + Verify the signature on the contents of the string *data*. This method delegates most of the work to + `verify_file()`. + + Args: + data (str|bytes): The data to verify. + kwargs (dict): Keyword arguments, which are passed to `verify_file()`: + + * fileobj_or_path (str|file): A path to a signature, or a file-like object containing one. + * data_filename (str): If the signature is a detached one, the path to the data that was signed. + + * close_file (bool): If a file-like object is passed in, whether to close it. + + * extra_args (list[str]): Additional arguments to pass to `gpg`. """ f = _make_binary_stream(data, self.encoding) result = self.verify_file(f, **kwargs) f.close() return result - def verify_file(self, file, data_filename=None, close_file=True, extra_args=None): - "Verify the signature on the contents of the file-like object 'file'" - logger.debug('verify_file: %r, %r', file, data_filename) + def verify_file(self, fileobj_or_path, data_filename=None, close_file=True, extra_args=None): + """ + Verify a signature. + + Args: + fileobj_or_path (str|file): A path to a signature, or a file-like object containing one. + + data_filename (str): If the signature is a detached one, the path to the data that was signed. + + close_file (bool): If a file-like object is passed in, whether to close it. + + extra_args (list[str]): Additional arguments to pass to `gpg`. + """ + logger.debug('verify_file: %r, %r', fileobj_or_path, data_filename) result = self.result_map['verify'](self) args = ['--verify'] - if extra_args: + if extra_args: # pragma: no cover args.extend(extra_args) if data_filename is None: - self._handle_io(args, file, result, binary=True) + self._handle_io(args, fileobj_or_path, result, binary=True) else: logger.debug('Handling detached verification') import tempfile - fd, fn = tempfile.mkstemp(prefix='pygpg') - s = file.read() + fd, fn = tempfile.mkstemp(prefix='pygpg-') + s = fileobj_or_path.read() if close_file: - file.close() + fileobj_or_path.close() logger.debug('Wrote to temp file: %r', s) os.write(fd, s) os.close(fd) @@ -1183,15 +1575,24 @@ def verify_file(self, file, data_filename=None, close_file=True, extra_args=None p = self._open_subprocess(args) self._collect_output(p, result, stdin=p.stdin) finally: - os.unlink(fn) + os.remove(fn) return result def verify_data(self, sig_filename, data, extra_args=None): - "Verify the signature in sig_filename against data in memory" + """ + Verify the signature in sig_filename against data in memory + + Args: + sig_filename (str): The path to a signature. + + data (str|bytes): The data to be verified. + + extra_args (list[str]): Additional arguments to pass to `gpg`. + """ logger.debug('verify_data: %r, %r ...', sig_filename, data[:16]) result = self.result_map['verify'](self) args = ['--verify'] - if extra_args: + if extra_args: # pragma: no cover args.extend(extra_args) args.extend([no_quote(sig_filename), '-']) stream = _make_memory_stream(data) @@ -1205,82 +1606,123 @@ def verify_data(self, sig_filename, data, extra_args=None): def import_keys(self, key_data, extra_args=None, passphrase=None): """ Import the key_data into our keyring. + + Args: + key_data (str|bytes): The key data to import. + + passphrase (str): The passphrase to use. + + extra_args (list[str]): Additional arguments to pass to `gpg`. """ result = self.result_map['import'](self) logger.debug('import_keys: %r', key_data[:256]) data = _make_binary_stream(key_data, self.encoding) args = ['--import'] - if extra_args: + if extra_args: # pragma: no cover args.extend(extra_args) self._handle_io(args, data, result, passphrase=passphrase, binary=True) logger.debug('import_keys result: %r', result.__dict__) data.close() return result - def recv_keys(self, keyserver, *keyids): - """Import a key from a keyserver + def import_keys_file(self, key_path, **kwargs): + """ + Import the key data in key_path into our keyring. - >>> import shutil - >>> shutil.rmtree("keys", ignore_errors=True) - >>> GPGBINARY = os.environ.get('GPGBINARY', 'gpg') - >>> if not os.path.isdir('keys'): os.mkdir('keys') - >>> gpg = GPG(gpgbinary=GPGBINARY, gnupghome='keys') - >>> os.chmod('keys', 0x1C0) - >>> result = gpg.recv_keys('pgp.mit.edu', '92905378') - >>> if 'NO_EXTERNAL_TESTS' not in os.environ: assert result + Args: + key_path (str): A path to the key data to be imported. + """ + with open(key_path, 'rb') as f: + return self.import_keys(f.read(), **kwargs) + def recv_keys(self, keyserver, *keyids, **kwargs): + """ + Import one or more keys from a keyserver. + + Args: + keyserver (str): The key server hostname. + + keyids (str): A list of key ids to receive. """ result = self.result_map['import'](self) logger.debug('recv_keys: %r', keyids) - data = _make_binary_stream("", self.encoding) - #data = "" - args = ['--keyserver', no_quote(keyserver), '--recv-keys'] + data = _make_binary_stream('', self.encoding) + args = ['--keyserver', no_quote(keyserver)] + if 'extra_args' in kwargs: # pragma: no cover + args.extend(kwargs['extra_args']) + args.append('--recv-keys') args.extend([no_quote(k) for k in keyids]) self._handle_io(args, data, result, binary=True) logger.debug('recv_keys result: %r', result.__dict__) data.close() return result - def send_keys(self, keyserver, *keyids): - """Send a key to a keyserver. + # This function isn't exercised by tests, to avoid polluting external + # key servers with test keys + def send_keys(self, keyserver, *keyids, **kwargs): # pragma: no cover + """ + Send one or more keys to a keyserver. + + Args: + keyserver (str): The key server hostname. - Note: it's not practical to test this function without sending - arbitrary data to live keyservers. + keyids (list[str]): A list of key ids to send. """ + + # Note: it's not practical to test this function without sending + # arbitrary data to live keyservers. + result = self.result_map['send'](self) logger.debug('send_keys: %r', keyids) data = _make_binary_stream('', self.encoding) - #data = "" - args = ['--keyserver', no_quote(keyserver), '--send-keys'] + args = ['--keyserver', no_quote(keyserver)] + if 'extra_args' in kwargs: + args.extend(kwargs['extra_args']) + args.append('--send-keys') args.extend([no_quote(k) for k in keyids]) self._handle_io(args, data, result, binary=True) logger.debug('send_keys result: %r', result.__dict__) data.close() return result - def delete_keys(self, fingerprints, secret=False, passphrase=None, - expect_passphrase=True): + def delete_keys(self, fingerprints, secret=False, passphrase=None, expect_passphrase=True, exclamation_mode=False): """ Delete the indicated keys. - Since GnuPG 2.1, you can't delete secret keys without providing a - passphrase. However, if you're expecting the passphrase to go to gpg - via pinentry, you should specify expect_passphrase=False. (It's only - checked for GnuPG >= 2.1). + Args: + fingerprints (str|list[str]): The keys to delete. + + secret (bool): Whether to delete secret keys. + + passphrase (str): The passphrase to use. + + expect_passphrase (bool): Whether a passphrase is expected. + + exclamation_mode (bool): If specified, a `'!'` is appended to each fingerprint. This deletes only a subkey + or an entire key, depending on what the fingerprint refers to. + + .. note:: Passphrases + + Since GnuPG 2.1, you can't delete secret keys without providing a passphrase. However, if you're expecting + the passphrase to go to `gpg` via pinentry, you should specify expect_passphrase=False. (It's only checked + for GnuPG >= 2.1). """ - if passphrase and not self.is_valid_passphrase(passphrase): + if passphrase and not self.is_valid_passphrase(passphrase): # pragma: no cover raise ValueError('Invalid passphrase') - which='key' + which = 'key' if secret: # pragma: no cover - if (self.version >= (2, 1) and passphrase is None and - expect_passphrase): + if self.version >= (2, 1) and passphrase is None and expect_passphrase: raise ValueError('For GnuPG >= 2.1, deleting secret keys ' 'needs a passphrase to be provided') - which='secret-key' + which = 'secret-key' if _is_sequence(fingerprints): # pragma: no cover fingerprints = [no_quote(s) for s in fingerprints] else: fingerprints = [no_quote(fingerprints)] + + if exclamation_mode: + fingerprints = [f + '!' for f in fingerprints] + args = ['--delete-%s' % which] if secret and self.version >= (2, 1): args.insert(0, '--yes') @@ -1293,29 +1735,49 @@ def delete_keys(self, fingerprints, secret=False, passphrase=None, # Need to send in a passphrase. f = _make_binary_stream('', self.encoding) try: - self._handle_io(args, f, result, passphrase=passphrase, - binary=True) + self._handle_io(args, f, result, passphrase=passphrase, binary=True) finally: f.close() return result - def export_keys(self, keyids, secret=False, armor=True, minimal=False, - passphrase=None, expect_passphrase=True): + def export_keys(self, + keyids, + secret=False, + armor=True, + minimal=False, + passphrase=None, + expect_passphrase=True, + output=None): """ - Export the indicated keys. A 'keyid' is anything gpg accepts. + Export the indicated keys. A 'keyid' is anything `gpg` accepts. + + Args: + keyids (str|list[str]): A single keyid or a list of them. + + secret (bool): Whether to export secret keys. + + armor (bool): Whether to ASCII-armor the output. + + minimal (bool): Whether to pass `--export-options export-minimal` to `gpg`. + + passphrase (str): The passphrase to use. - Since GnuPG 2.1, you can't export secret keys without providing a - passphrase. However, if you're expecting the passphrase to go to gpg - via pinentry, you should specify expect_passphrase=False. (It's only - checked for GnuPG >= 2.1). + expect_passphrase (bool): Whether a passphrase is expected. + + output (str): If specified, the path to write the exported key(s) to. + + .. note:: Passphrases + + Since GnuPG 2.1, you can't export secret keys without providing a passphrase. However, if you're expecting + the passphrase to go to `gpg` via pinentry, you should specify expect_passphrase=False. (It's only checked + for GnuPG >= 2.1). """ - if passphrase and not self.is_valid_passphrase(passphrase): + if passphrase and not self.is_valid_passphrase(passphrase): # pragma: no cover raise ValueError('Invalid passphrase') - which='' + which = '' if secret: - which='-secret-key' - if (self.version >= (2, 1) and passphrase is None and - expect_passphrase): + which = '-secret-key' + if self.version >= (2, 1) and passphrase is None and expect_passphrase: # pragma: no cover raise ValueError('For GnuPG >= 2.1, exporting secret keys ' 'needs a passphrase to be provided') if _is_sequence(keyids): @@ -1326,11 +1788,13 @@ def export_keys(self, keyids, secret=False, armor=True, minimal=False, if armor: args.append('--armor') if minimal: # pragma: no cover - args.extend(['--export-options','export-minimal']) + args.extend(['--export-options', 'export-minimal']) + if output: # pragma: no cover + # write the output to a file with the specified name + self.set_output_without_confirmation(args, output) args.extend(keyids) # gpg --export produces no status-fd output; stdout will be # empty in case of failure - #stdout, stderr = p.communicate() result = self.result_map['export'](self) if not secret or self.version < (2, 1): p = self._open_subprocess(args) @@ -1339,8 +1803,7 @@ def export_keys(self, keyids, secret=False, armor=True, minimal=False, # Need to send in a passphrase. f = _make_binary_stream('', self.encoding) try: - self._handle_io(args, f, result, passphrase=passphrase, - binary=True) + self._handle_io(args, f, result, passphrase=passphrase, binary=True) finally: f.close() logger.debug('export_keys result[:100]: %r', result.data[:100]) @@ -1350,17 +1813,13 @@ def export_keys(self, keyids, secret=False, armor=True, minimal=False, result = result.decode(self.encoding, self.decode_errors) return result - def _get_list_output(self, p, kind): - # Get the response information - result = self.result_map[kind](self) - self._collect_output(p, result, stdin=p.stdin) - lines = result.data.decode(self.encoding, - self.decode_errors).splitlines() - valid_keywords = 'pub uid sec fpr sub ssb sig'.split() + def _decode_result(self, result): + lines = result.data.decode(self.encoding, self.decode_errors).splitlines() + valid_keywords = 'pub uid sec fpr sub ssb sig grp'.split() for line in lines: if self.verbose: # pragma: no cover print(line) - logger.debug("line: %r", line.rstrip()) + logger.debug('line: %r', line.rstrip()) if not line: # pragma: no cover break L = line.strip().split(':') @@ -1371,52 +1830,71 @@ def _get_list_output(self, p, kind): getattr(result, keyword)(L) return result + def _get_list_output(self, p, kind): + # Get the response information + result = self.result_map[kind](self) + self._collect_output(p, result, stdin=p.stdin) + return self._decode_result(result) + def list_keys(self, secret=False, keys=None, sigs=False): - """ list the keys currently in the keyring - - >>> import shutil - >>> shutil.rmtree("keys", ignore_errors=True) - >>> GPGBINARY = os.environ.get('GPGBINARY', 'gpg') - >>> if not os.path.isdir('keys'): os.mkdir('keys') - >>> gpg = GPG(gpgbinary=GPGBINARY, gnupghome='keys') - >>> input = gpg.gen_key_input(passphrase='foo') - >>> result = gpg.gen_key(input) - >>> fp1 = result.fingerprint - >>> result = gpg.gen_key(input) - >>> fp2 = result.fingerprint - >>> pubkeys = gpg.list_keys() - >>> assert fp1 in pubkeys.fingerprints - >>> assert fp2 in pubkeys.fingerprints - - """ - - if sigs: - which = 'sigs' - else: - which = 'keys' + """ + List the keys currently in the keyring. + + Args: + secret (bool): Whether to list secret keys. + + keys (str|list[str]): A list of key ids to match. + + sigs (bool): Whether to include signature information. + + Returns: + list[dict]: A list of dictionaries with key information. + """ + if secret: - which='secret-keys' - args = ['--list-%s' % which, - '--fingerprint', '--fingerprint'] # get subkey FPs, too + which = 'secret-keys' + else: + which = 'sigs' if sigs else 'keys' + args = ['--list-%s' % which, '--fingerprint', '--fingerprint'] # get subkey FPs, too + + if self.version >= (2, 1): + args.append('--with-keygrip') + if keys: if isinstance(keys, string_types): keys = [keys] args.extend(keys) p = self._open_subprocess(args) - return self._get_list_output(p, 'list') + result = self._get_list_output(p, 'list') + # Fix up subkey_info with fingerprint and grip values + for key in result: + # import pdb; pdb.set_trace() + subkeys = key['subkeys'] + subkey_info = key.get('subkey_info') + if subkey_info: + for sk in subkeys: + skid, capability, fp, grp = sk + d = subkey_info[skid] + d['capability'] = capability + d['fingerprint'] = fp + d['keygrip'] = grp + return result def scan_keys(self, filename): """ - List details of an ascii armored or binary key file - without first importing it to the local keyring. + List details of an ascii armored or binary key file without first importing it to the local keyring. + + Args: + filename (str): The path to the file containing the key(s). - The function achieves this on modern GnuPG by running: + .. warning:: Warning: + Care is needed. The function works on modern GnuPG by running: - $ gpg --dry-run --import-options import-show --import + $ gpg --dry-run --import-options import-show --import filename - On older versions, it does the *much* riskier: + On older versions, it does the *much* riskier: - $ gpg --with-fingerprint --with-colons filename + $ gpg --with-fingerprint --with-colons filename """ if self.version >= (2, 1): args = ['--dry-run', '--import-options', 'import-show', '--import'] @@ -1428,41 +1906,66 @@ def scan_keys(self, filename): p = self._open_subprocess(args) return self._get_list_output(p, 'scan') - def search_keys(self, query, keyserver='pgp.mit.edu'): - """ search keyserver by query (using --search-keys option) + def scan_keys_mem(self, key_data): + """ + List details of an ascii armored or binary key without first importing it to the local keyring. + + Args: + key_data (str|bytes): The key data to import. + + .. warning:: Warning: + Care is needed. The function works on modern GnuPG by running: - >>> import shutil - >>> shutil.rmtree('keys', ignore_errors=True) - >>> GPGBINARY = os.environ.get('GPGBINARY', 'gpg') - >>> if not os.path.isdir('keys'): os.mkdir('keys') - >>> gpg = GPG(gpgbinary=GPGBINARY, gnupghome='keys') - >>> os.chmod('keys', 0x1C0) - >>> result = gpg.search_keys('') - >>> if 'NO_EXTERNAL_TESTS' not in os.environ: assert result, 'Failed using default keyserver' - >>> #keyserver = 'keyserver.ubuntu.com' - >>> #result = gpg.search_keys('', keyserver) - >>> #assert result, 'Failed using keyserver.ubuntu.com' + $ gpg --dry-run --import-options import-show --import filename + On older versions, it does the *much* riskier: + + $ gpg --with-fingerprint --with-colons filename + """ + result = self.result_map['scan'](self) + logger.debug('scan_keys: %r', key_data[:256]) + data = _make_binary_stream(key_data, self.encoding) + if self.version >= (2, 1): + args = ['--dry-run', '--import-options', 'import-show', '--import'] + else: + logger.warning('Trying to list packets, but if the file is not a ' + 'keyring, might accidentally decrypt') + args = ['--with-fingerprint', '--with-colons', '--fixed-list-mode'] + self._handle_io(args, data, result, binary=True) + logger.debug('scan_keys result: %r', result.__dict__) + data.close() + return self._decode_result(result) + + def search_keys(self, query, keyserver='pgp.mit.edu', extra_args=None): + """ + search a keyserver by query (using the `--search-keys` option). + + Args: + query(str): The query to use. + + keyserver (str): The key server hostname. + + extra_args (list[str]): Additional arguments to pass to `gpg`. """ query = query.strip() if HEX_DIGITS_RE.match(query): query = '0x' + query - args = ['--fingerprint', - '--keyserver', no_quote(keyserver), '--search-keys', - no_quote(query)] + args = ['--fingerprint', '--keyserver', no_quote(keyserver)] + if extra_args: # pragma: no cover + args.extend(extra_args) + args.extend(['--search-keys', no_quote(query)]) p = self._open_subprocess(args) # Get the response information result = self.result_map['search'](self) self._collect_output(p, result, stdin=p.stdin) - lines = result.data.decode(self.encoding, - self.decode_errors).splitlines() + lines = result.data.decode(self.encoding, self.decode_errors).splitlines() valid_keywords = ['pub', 'uid'] for line in lines: if self.verbose: # pragma: no cover print(line) logger.debug('line: %r', line.rstrip()) - if not line: # sometimes get blank lines on Windows + if not line: # sometimes get blank lines on Windows continue L = line.strip().split(':') if not L: # pragma: no cover @@ -1472,21 +1975,38 @@ def search_keys(self, query, keyserver='pgp.mit.edu'): getattr(result, keyword)(L) return result - def gen_key(self, input): - """Generate a key; you might use gen_key_input() to create the - control input. + def auto_locate_key(self, email, mechanisms=None, **kwargs): + """ + Auto locate a public key by `email`. + + Args: + email (str): The email address to search for. + mechanisms (list[str]): A list of mechanisms to use. Valid mechanisms can be found + here https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html + under "--auto-key-locate". Default: ['wkd', 'ntds', 'ldap', 'cert', 'dane', 'local'] + """ + mechanisms = mechanisms or ['wkd', 'ntds', 'ldap', 'cert', 'dane', 'local'] + + args = ['--auto-key-locate', ','.join(mechanisms), '--locate-keys', email] - >>> GPGBINARY = os.environ.get('GPGBINARY', 'gpg') - >>> if not os.path.isdir('keys'): os.mkdir('keys') - >>> gpg = GPG(gpgbinary=GPGBINARY, gnupghome='keys') - >>> input = gpg.gen_key_input(passphrase='foo') - >>> result = gpg.gen_key(input) - >>> assert result - >>> result = gpg.gen_key('foo') - >>> assert not result + result = self.result_map['auto-locate-key'](self) + + if 'extra_args' in kwargs: + args.extend(kwargs['extra_args']) + + process = self._open_subprocess(args) + self._collect_output(process, result, stdin=process.stdin) + self._decode_result(result) + return result + def gen_key(self, input): + """ + Generate a key; you might use `gen_key_input()` to create the input. + + Args: + input (str): The input to the key creation operation. """ - args = ["--gen-key"] + args = ['--gen-key'] result = self.result_map['generate'](self) f = _make_binary_stream(input, self.encoding) self._handle_io(args, f, result, binary=True) @@ -1495,29 +2015,33 @@ def gen_key(self, input): def gen_key_input(self, **kwargs): """ - Generate --gen-key input per gpg doc/DETAILS + Generate `--gen-key` input (see `gpg` documentation in DETAILS). + + Args: + kwargs (dict): A list of keyword arguments. + Returns: + str: A string suitable for passing to the `gen_key()` method. """ + parms = {} no_protection = kwargs.pop('no_protection', False) for key, val in list(kwargs.items()): - key = key.replace('_','-').title() - if str(val).strip(): # skip empty strings + key = key.replace('_', '-').title() + if str(val).strip(): # skip empty strings parms[key] = val - parms.setdefault('Key-Type','RSA') + parms.setdefault('Key-Type', 'RSA') if 'key_curve' not in kwargs: - parms.setdefault('Key-Length',2048) - parms.setdefault('Name-Real', "Autogenerated Key") - logname = (os.environ.get('LOGNAME') or os.environ.get('USERNAME') or - 'unspecified') + parms.setdefault('Key-Length', 2048) + parms.setdefault('Name-Real', 'Autogenerated Key') + logname = (os.environ.get('LOGNAME') or os.environ.get('USERNAME') or 'unspecified') hostname = socket.gethostname() - parms.setdefault('Name-Email', "%s@%s" % (logname.replace(' ', '_'), - hostname)) - out = "Key-Type: %s\n" % parms.pop('Key-Type') + parms.setdefault('Name-Email', '%s@%s' % (logname.replace(' ', '_'), hostname)) + out = 'Key-Type: %s\n' % parms.pop('Key-Type') for key, val in list(parms.items()): - out += "%s: %s\n" % (key, val) - if no_protection: + out += '%s: %s\n' % (key, val) + if no_protection: # pragma: no cover out += '%no-protection\n' - out += "%commit\n" + out += '%commit\n' return out # Key-Type: RSA @@ -1542,13 +2066,73 @@ def gen_key_input(self, **kwargs): # %secring foo.sec # %commit + def add_subkey(self, master_key, master_passphrase=None, algorithm='rsa', usage='encrypt', expire='-'): + """ + Add subkeys to a master key, + + Args: + master_key (str): The master key. + + master_passphrase (str): The passphrase for the master key. + + algorithm (str): The key algorithm to use. + + usage (str): The desired uses for the subkey. + + expire (str): The expiration date of the subkey. + """ + if self.version[0] < 2: + raise NotImplementedError('Not available in GnuPG 1.x') + if not master_key: # pragma: no cover + raise ValueError('No master key fingerprint specified') + + if master_passphrase and not self.is_valid_passphrase(master_passphrase): # pragma: no cover + raise ValueError('Invalid passphrase') + + args = ['--quick-add-key', master_key, algorithm, usage, str(expire)] + + result = self.result_map['addSubkey'](self) + + f = _make_binary_stream('', self.encoding) + self._handle_io(args, f, result, passphrase=master_passphrase, binary=True) + return result + # # ENCRYPTION # - def encrypt_file(self, file, recipients, sign=None, - always_trust=False, passphrase=None, - armor=True, output=None, symmetric=False, extra_args=None): - "Encrypt the message read from the file-like object 'file'" + + def encrypt_file(self, + fileobj_or_path, + recipients, + sign=None, + always_trust=False, + passphrase=None, + armor=True, + output=None, + symmetric=False, + extra_args=None): + """ + Encrypt data in a file or file-like object. + + Args: + fileobj_or_path (str|file): A path to a file or a file-like object containing the data to be encrypted. + + recipients (str|list): A key id of a recipient of the encrypted data, or a list of such key ids. + + sign (str): If specified, the key id of a signer to sign the encrypted data. + + always_trust (bool): Whether to always trust keys. + + passphrase (str): The passphrase to use for a signature. + + armor (bool): Whether to ASCII-armor the output. + + output (str): A path to write the encrypted output to. + + symmetric (bool): Whether to use symmetric encryption, + + extra_args (list[str]): A list of additional arguments to pass to `gpg`. + """ if passphrase and not self.is_valid_passphrase(passphrase): raise ValueError('Invalid passphrase') args = ['--encrypt'] @@ -1564,68 +2148,51 @@ def encrypt_file(self, file, recipients, sign=None, raise ValueError('No recipients specified with asymmetric ' 'encryption') if not _is_sequence(recipients): - recipients = (recipients,) + recipients = (recipients, ) for recipient in recipients: args.extend(['--recipient', no_quote(recipient)]) - if armor: # create ascii-armored output - False for binary output + if armor: # create ascii-armored output - False for binary output args.append('--armor') - if output: # write the output to a file with the specified name + if output: # pragma: no cover + # write the output to a file with the specified name self.set_output_without_confirmation(args, output) if sign is True: # pragma: no cover args.append('--sign') elif sign: # pragma: no cover args.extend(['--sign', '--default-key', no_quote(sign)]) if always_trust: # pragma: no cover - args.append('--always-trust') - if extra_args: + args.extend(['--trust-model', 'always']) + if extra_args: # pragma: no cover args.extend(extra_args) result = self.result_map['crypt'](self) - self._handle_io(args, file, result, passphrase=passphrase, binary=True) + self._handle_io(args, fileobj_or_path, result, passphrase=passphrase, binary=True) logger.debug('encrypt result[:100]: %r', result.data[:100]) return result def encrypt(self, data, recipients, **kwargs): - """Encrypt the message contained in the string 'data' - - >>> import shutil - >>> if os.path.exists("keys"): - ... shutil.rmtree("keys", ignore_errors=True) - >>> GPGBINARY = os.environ.get('GPGBINARY', 'gpg') - >>> if not os.path.isdir('keys'): os.mkdir('keys') - >>> gpg = GPG(gpgbinary=GPGBINARY, gnupghome='keys') - >>> input = gpg.gen_key_input(name_email='user1@test', passphrase='pp1') - >>> result = gpg.gen_key(input) - >>> fp1 = result.fingerprint - >>> input = gpg.gen_key_input(name_email='user2@test', passphrase='pp2') - >>> result = gpg.gen_key(input) - >>> fp2 = result.fingerprint - >>> result = gpg.encrypt("hello",fp2) - >>> message = str(result) - >>> assert message != 'hello' - >>> result = gpg.decrypt(message, passphrase='pp2') - >>> assert result - >>> str(result) - 'hello' - >>> result = gpg.encrypt("hello again", fp1) - >>> message = str(result) - >>> result = gpg.decrypt(message, passphrase='bar') - >>> result.status in ('decryption failed', 'bad passphrase') - True - >>> assert not result - >>> result = gpg.decrypt(message, passphrase='pp1') - >>> result.status == 'decryption ok' - True - >>> str(result) - 'hello again' - >>> result = gpg.encrypt("signed hello", fp2, sign=fp1, passphrase='pp1') - >>> result.status == 'encryption ok' - True - >>> message = str(result) - >>> result = gpg.decrypt(message, passphrase='pp2') - >>> result.status == 'decryption ok' - True - >>> assert result.fingerprint == fp1 + """ + Encrypt the message contained in the string *data* for *recipients*. This method delegates most of the work to + `encrypt_file()`. + + Args: + data (str|bytes): The data to encrypt. + + recipients (str|list[str]): A key id of a recipient of the encrypted data, or a list of such key ids. + + kwargs (dict): Keyword arguments, which are passed to `encrypt_file()`: + * sign (str): If specified, the key id of a signer to sign the encrypted data. + + * always_trust (bool): Whether to always trust keys. + + * passphrase (str): The passphrase to use for a signature. + * armor (bool): Whether to ASCII-armor the output. + + * output (str): A path to write the encrypted output to. + + * symmetric (bool): Whether to use symmetric encryption, + + * extra_args (list[str]): A list of additional arguments to pass to `gpg`. """ data = _make_binary_stream(data, self.encoding) result = self.encrypt_file(data, recipients, **kwargs) @@ -1633,54 +2200,117 @@ def encrypt(self, data, recipients, **kwargs): return result def decrypt(self, message, **kwargs): + """ + Decrypt the data in *message*. This method delegates most of the work to + `decrypt_file()`. + + Args: + message (str|bytes): The data to decrypt. A default key will be used for decryption. + + kwargs (dict): Keyword arguments, which are passed to `decrypt_file()`: + + * always_trust: Whether to always trust keys. + + * passphrase (str): The passphrase to use. + + * output (str): If specified, the path to write the decrypted data to. + + * extra_args (list[str]): A list of extra arguments to pass to `gpg`. + """ data = _make_binary_stream(message, self.encoding) result = self.decrypt_file(data, **kwargs) data.close() return result - def decrypt_file(self, file, always_trust=False, passphrase=None, - output=None, extra_args=None): + def decrypt_file(self, fileobj_or_path, always_trust=False, passphrase=None, output=None, extra_args=None): + """ + Decrypt data in a file or file-like object. + + Args: + fileobj_or_path (str|file): A path to a file or a file-like object containing the data to be decrypted. + + always_trust: Whether to always trust keys. + + passphrase (str): The passphrase to use. + + output (str): If specified, the path to write the decrypted data to. + + extra_args (list[str]): A list of extra arguments to pass to `gpg`. + """ if passphrase and not self.is_valid_passphrase(passphrase): raise ValueError('Invalid passphrase') - args = ["--decrypt"] - if output: # write the output to a file with the specified name + args = ['--decrypt'] + if output: # pragma: no cover + # write the output to a file with the specified name self.set_output_without_confirmation(args, output) if always_trust: # pragma: no cover - args.append("--always-trust") - if extra_args: + args.extend(['--trust-model', 'always']) + if extra_args: # pragma: no cover args.extend(extra_args) result = self.result_map['crypt'](self) - self._handle_io(args, file, result, passphrase, binary=True) - logger.debug('decrypt result[:100]: %r', result.data[:100]) + self._handle_io(args, fileobj_or_path, result, passphrase, binary=True) + # logger.debug('decrypt result[:100]: %r', result.data[:100]) return result def get_recipients(self, message, **kwargs): + """ Get the list of recipients for an encrypted message. This method delegates most of the work to + `get_recipients_file()`. + + Args: + message (str|bytes): The encrypted message. + + kwargs (dict): Keyword arguments, which are passed to `get_recipients_file()`: + + * extra_args (list[str]): A list of extra arguments to pass to `gpg`. + """ data = _make_binary_stream(message, self.encoding) result = self.get_recipients_file(data, **kwargs) data.close() return result - def get_recipients_file(self, file, extra_args=None): + def get_recipients_file(self, fileobj_or_path, extra_args=None): + """ + Get the list of recipients for an encrypted message in a file or file-like object. + + Args: + fileobj_or_path (str|file): A path to a file or file-like object containing the encrypted data. + + extra_args (list[str]): A list of extra arguments to pass to `gpg`. + """ args = ['--decrypt', '--list-only', '-v'] - if extra_args: + if extra_args: # pragma: no cover args.extend(extra_args) result = self.result_map['crypt'](self) - self._handle_io(args, file, result, binary=True) + self._handle_io(args, fileobj_or_path, result, binary=True) ids = [] for m in PUBLIC_KEY_RE.finditer(result.stderr): ids.append(m.group(1)) return ids def trust_keys(self, fingerprints, trustlevel): + """ + Set the trust level for one or more keys. + + Args: + fingerprints (str|list[str]): A key id for which to set the trust level, or a list of such key ids. + + trustlevel (str): The trust level. This is one of the following. + + * ``'TRUST_EXPIRED'`` + * ``'TRUST_UNDEFINED'`` + * ``'TRUST_NEVER'`` + * ``'TRUST_MARGINAL'`` + * ``'TRUST_FULLY'`` + * ``'TRUST_ULTIMATE'`` + """ levels = Verify.TRUST_LEVELS if trustlevel not in levels: poss = ', '.join(sorted(levels)) - raise ValueError('Invalid trust level: "%s" (must be one of %s)' % - (trustlevel, poss)) - trustlevel = levels[trustlevel] + 2 + raise ValueError('Invalid trust level: "%s" (must be one of %s)' % (trustlevel, poss)) + trustlevel = levels[trustlevel] + 1 import tempfile try: - fd, fn = tempfile.mkstemp() + fd, fn = tempfile.mkstemp(prefix='pygpg-') lines = [] if isinstance(fingerprints, string_types): fingerprints = [fingerprints] @@ -1688,15 +2318,14 @@ def trust_keys(self, fingerprints, trustlevel): lines.append('%s:%s:' % (f, trustlevel)) # The trailing newline is required! s = os.linesep.join(lines) + os.linesep - logger.debug('writing ownertrust info: %s', s); + logger.debug('writing ownertrust info: %s', s) os.write(fd, s.encode(self.encoding)) os.close(fd) result = self.result_map['trust'](self) p = self._open_subprocess(['--import-ownertrust', fn]) self._collect_output(p, result, stdin=p.stdin) if p.returncode != 0: - raise ValueError('gpg returned an error - return code %d' % - p.returncode) + raise ValueError('gpg returned an error - return code %d' % p.returncode) finally: os.remove(fn) return result diff --git a/plugins/module_utils/gnupg.py_LICENSE.txt b/plugins/module_utils/gnupg.py_LICENSE.txt index faa0fa5c..3e96b20c 100644 --- a/plugins/module_utils/gnupg.py_LICENSE.txt +++ b/plugins/module_utils/gnupg.py_LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2008-2014 by Vinay Sajip. +Copyright (c) 2008-2022 by Vinay Sajip. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/plugins/modules/gpg_key.py b/plugins/modules/gpg_key.py index 3a1f3c9d..12610d72 100644 --- a/plugins/modules/gpg_key.py +++ b/plugins/modules/gpg_key.py @@ -29,8 +29,8 @@ - In check mode, the module reports C(changed=true) when no matching key is found, but it does not generate one. requirements: - - GNU Privacy Guard command line tool C(gpg). - - Python library C(python-gnupg) on the controller (or wherever the module runs). + - GNU Privacy Guard command line tool C(gpg) on the host where the module runs. + - "No separate C(python-gnupg) install is required: a copy of the library is bundled with the collection and shipped together with the module." author: - Linuxfabrik GmbH, Zurich, Switzerland, https://www.linuxfabrik.ch @@ -229,7 +229,7 @@ returned: success type: str sample: sec - uid: + uids: description: List of user IDs attached to the key. returned: success type: list @@ -339,7 +339,7 @@ def run_module(): # define available arguments/parameters a user can pass to the module module_args = dict( gpgbinary=dict(type='str', required=False, default='gpg'), - gnupghome=dict(type='str', required=False), + gnupghome=dict(type='path', required=False), name_real=dict(type='str', required=False, default='Autogenerated Key'), name_comment=dict(type='str', required=False, default='Generated by Ansible.'), From c69b622c8054edd012c816fb15df5a8e871c7267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20B=C3=BCrki?= Date: Tue, 12 May 2026 19:01:34 +0200 Subject: [PATCH 60/66] feat(roles/php): update template for RedHat-based systems, Docs (partially finished) to allow for multiple PHP-FPM pools to be configured individually --- roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 b/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 index d8fe5c16..37e6da8e 100644 --- a/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 +++ b/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 @@ -108,7 +108,7 @@ listen.allowed_clients = 127.0.0.1 ; pm.process_idle_timeout - The number of seconds after which ; an idle process will be killed. ; Note: This value is mandatory. -pm = {{ php__fpm_pool_conf_pm__combined_var | d('dynamic') }} +pm = {{ item['pm'] | d('dynamic') }} ; The number of child processes to be created when pm is set to 'static' and the ; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. From b54c7fcbae2b64f54ce19835280c5e47b60601ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20B=C3=BCrki?= Date: Wed, 3 Jun 2026 15:12:14 +0200 Subject: [PATCH 61/66] fix(roles/php): remove opcache pool parameters as opcache is shared among php-fpm instances, use one pool.conf template for both RedHat and Debian os families, update docs. --- roles/php/README.md | 79 ++- roles/php/tasks/main.yml | 2 +- .../etc/php-fpm.d/RedHat-pool.conf.j2 | 463 ------------------ .../{Debian-pool.conf.j2 => pool.conf.j2} | 66 +-- roles/php/vars/Debian.yml | 3 + roles/php/vars/RedHat.yml | 3 + 6 files changed, 74 insertions(+), 542 deletions(-) delete mode 100644 roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 rename roles/php/templates/etc/php-fpm.d/{Debian-pool.conf.j2 => pool.conf.j2} (92%) diff --git a/roles/php/README.md b/roles/php/README.md index 1a69de3a..daa642ce 100644 --- a/roles/php/README.md +++ b/roles/php/README.md @@ -372,151 +372,134 @@ Variables for PHP-FPM Pool Config directives and their default values, defined a * `user`: - * Optional. The Unix user running the pool processes. + * Optional. The Unix user running the pool processes. [php.net](https://www.php.net/install.fpm.configuration.php#user) * Type: String. - * Default: `'apache'` + * Default: `'apache'` (RedHat), `www-data` (Debian) * `group`: - * Optional. The Unix group running the pool processes. + * Optional. The Unix group running the pool processes. [php.net](https://www.php.net/install.fpm.configuration.php#group) * Type: String. - * Default: `'apache'` + * Default: `'apache'` (RedHat), `www-data` (Debian) * `pm`: - * Optional. Choose how the process manager will control the number of child processes. + * Optional. Choose how the process manager will control the number of child processes. [php.net](https://www.php.net/install.fpm.configuration.php#pm) * Type: String. * Default: `'dynamic'` * `pm_max_children`: - * Optional. The number of child processes to be created when pm is set to `'static'` and the maximum number of child processes when pm is set to `'dynamic'` or `'ondemand'`. + * Optional. The number of child processes to be created when pm is set to `'static'` and the maximum number of child processes when pm is set to `'dynamic'` or `'ondemand'`. [php.net](https://www.php.net/install.fpm.configuration.php#pm.max-children) * Type: Number. * Default: `50` * `pm_start_servers`: - * Optional. The number of child processes created on startup. Must be greater than `pm_min_spare_servers` but less than `pm_max_spare_servers`. Used only when `pm` is set to `'dynamic`'. + * Optional. The number of child processes created on startup. Must be greater than `pm_min_spare_servers` but less than `pm_max_spare_servers`. Used only when `pm` is set to `'dynamic`'. [php.net](https://www.php.net/install.fpm.configuration.php#pm.start-servers) * Type: Number. * Default: `5` * `pm_min_spare_servers`: - * Optional. The desired minimum number of idle server processes. Used only when `pm` is set to `'dynamic'`. + * Optional. The desired minimum number of idle server processes. Used only when `pm` is set to `'dynamic'`. [php.net](https://www.php.net/install.fpm.configuration.php#pm.min-spare-servers) * Type: Number. * Default: `5` * `pm_max_spare_servers`: - * Optional. The desired maximum number of idle server processes. Used only when `pm` is set to `'dynamic'`. + * Optional. The desired maximum number of idle server processes. Used only when `pm` is set to `'dynamic'`. [php.net](https://www.php.net/install.fpm.configuration.php#pm.max-spare-servers) * Type: Number. * Default: `35` + * `pm_max_spawn_rate`: + + * Optional. The number of rate to spawn child processes at once. Used only when `pm` is set to `'dynamic'`. [php.net](https://www.php.net/install.fpm.configuration.php#pm.max-spawn-rate) + * Type: Number. + * Default: `32` + * `pm_process_idle_timeout`: - * Optional. The number of seconds after which an idle process will be killed. Used only when `pm` is set to `'ondemand'`. Defaults to `'10s'` if unset. Available units: s(econds, default), m(inutes), h(ours), or d(ays). + * Optional. The number of seconds after which an idle process will be killed. Used only when `pm` is set to `'ondemand'`. Available units: s(econds, default), m(inutes), h(ours), or d(ays). [php.net](https://www.php.net/install.fpm.configuration.php#pm.process-idle-timeout) * Type: String. * Default: `'10s'` * `pm_max_requests`: - * Optional. The number of requests each child process should execute before respawning. This can be useful to work around memory leaks in 3rd party libraries. For endless request processing specify `0`. + * Optional. The number of requests each child process should execute before respawning. [php.net](https://www.php.net/install.fpm.configuration.php#pm.max-requests) * Type: Number. * Default: `0` * `pm_status_path`: - * Optional. Path to view FPM status page. + * Optional. Path to view FPM status page. [php.net](https://www.php.net/install.fpm.configuration.php#pm.status-path) * Type: String. * Default: `'/{{ item["name"] }}-fpm-status'` * `ping_path`: - * Optional. The ping path to check if FPM is alive and responding. + * Optional. The ping path to check if FPM is alive and responding. [php.net](https://www.php.net/install.fpm.configuration.php#ping.path) * Type: String. * Default: `'/{{ item["name"] }}-fpm-ping'` * `request_slowlog_timeout`: - * Optional. The timeout for serving a single request after which a PHP backtrace will be dumped to the slowlog file. A value of `0` means off. Available units: s(econds, default), m(inutes), h(ours), or d(ays). + * Optional. The timeout for serving a single request after which a PHP backtrace will be dumped to the slowlog file. A value of `0` means off. Available units: s(econds, default), m(inutes), h(ours), or d(ays). [php.net](https://www.php.net/install.fpm.configuration.php#request-slowlog-timeout) * Type: Number. * Default: `0` * `request_slowlog_trace_depth`: - * Optional. Depth of slow log stack trace. + * Optional. Depth of slow log stack trace. [php.net](https://www.php.net/install.fpm.configuration.php#request-slowlog-trace-depth) * Type: Number. * Default: `20` * `request_terminate_timeout`: - * The timeout for serving a single request after which the worker process will be killed. This option should be used when the `max_execution_time` ini option does not stop script execution for some reason. A value of `0` means off. Available units: s(econds, default), m(inutes), h(ours), or d(ays). + * Optional. The timeout for serving a single request after which the worker process will be killed. This option should be used when the `max_execution_time` ini option does not stop script execution for some reason. A value of `0` means off. Available units: s(econds, default), m(inutes), h(ours), or d(ays). + * [php.net](https://www.php.net/install.fpm.configuration.php#request-terminate-timeout) * Type: Number. * Default: `0` * `php_admin_value_session_save_path`: - * Optional. - * Type: String. - * Default: `'/var/lib/php/session-{{ item["name"] }}'` - - * `php_admin_value_opcache_file_cache`: - - * Optional. + * Optional. **NOTE:** The session save directory is currently not created automatically. [php.net](https://www.php.net/session.save_path) * Type: String. - * Default: `'/var/lib/php/opcache-{{ item["name"] }}'` + * Default: `'/var/lib/php/{{ item["name"] }}-session'` * `php_admin_value_max_execution_time`: - * Optional. + * Optional. [php.net](https://www.php.net/max_execution_time) * Type: Number. * Default: `{{ php__ini_max_execution_time__combined_var }}` * `php_admin_value_max_input_vars`: - * Optional. + * Optional. [php.net](https://www.php.net/max_input_vars) * Type: Number. * Default: `{{ php__ini_max_input_vars__combined_var }}` * `php_admin_value_memory_limit`: - * Optional. + * Optional. [php.net](https://www.php.net/memory_limit) * Type: String. * Default: `'{{ php__ini_memory_limit__combined_var }}'` - * `php_admin_value_opcache_interned_strings_buffer`: - - * Optional. - * Type: Number. - * Default: `{{ php__ini_opcache_interned_strings_buffer__combined_var }}` - - * `php_admin_value_opcache_max_accelerated_files`: - - * Optional. - * Type: Number. - * Default: `{{ php__ini_opcache_max_accelerated_files__combined_var }}` - - * `php_admin_value_opcache_memory_consumption`: - - * Optional. - * Type: Number. - * Default: `{{ php__ini_opcache_memory_consumption__combined_var }}` - * `php_admin_value_open_basedir`: - * Optional. + * Optional. [php.net](https://www.php.net/open_basedir) * Type: String. * Default: unset * `php_admin_value_post_max_size`: - * Optional. + * Optional. [php.net](https://www.php.net/post_max_size) * Type: String. * Default: `'{{ php__ini_post_max_size__combined_var }}'` * `php_admin_value_upload_max_filesize`: - * Optional. + * Optional. [php.net](https://www.php.net/upload_max_filesize) * Type: String. * Default: `'{{ php__ini_upload_max_filesize__combined_var }}'` diff --git a/roles/php/tasks/main.yml b/roles/php/tasks/main.yml index b385fb5c..039e9e98 100644 --- a/roles/php/tasks/main.yml +++ b/roles/php/tasks/main.yml @@ -138,7 +138,7 @@ - name: 'Deploy the pools to {{ php__fpm_pools_path }}' ansible.builtin.template: backup: true - src: 'etc/php-fpm.d/{{ ansible_facts["os_family"] }}-pool.conf.j2' + src: 'etc/php-fpm.d/pool.conf.j2' dest: '{{ php__fpm_pools_path }}/{{ item["name"] }}.conf' owner: 'root' group: 'root' diff --git a/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 b/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 deleted file mode 100644 index 37e6da8e..00000000 --- a/roles/php/templates/etc/php-fpm.d/RedHat-pool.conf.j2 +++ /dev/null @@ -1,463 +0,0 @@ -#jinja2:block_start_string:'[%', block_end_string:'%]' -; {{ ansible_managed }} -; 2026051301 -[% if item['by_role'] | d() %] -; Generated by Ansible role: {{ item['by_role'] }} -[% endif %] - -; Start a new pool named 'www'. -; the variable $pool can be used in any directive and will be replaced by the -; pool name ('www' here) -[{{ item['name'] }}] - -; Per pool prefix -; It only applies on the following directives: -; - 'access.log' -; - 'slowlog' -; - 'listen' (unixsocket) -; - 'chroot' -; - 'chdir' -; - 'php_values' -; - 'php_admin_values' -; When not set, the global prefix (or @php_fpm_prefix@) applies instead. -; Note: This directive can also be relative to the global prefix. -; Default Value: none -;prefix = /path/to/pools/$pool - -; Unix user/group of processes -; Note: The user is mandatory. If the group is not set, the default user's group -; will be used. -; RPM: apache user chosen to provide access to the same directories as httpd -user = {{ item['user'] | d(php__webserver_user) }} -; RPM: Keep a group allowed to write in log dir. -group = {{ item['group'] | d(php__webserver_group) }} - -; The address on which to accept FastCGI requests. -; Valid syntaxes are: -; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on -; a specific port; -; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on -; a specific port; -; 'port' - to listen on a TCP socket to all addresses -; (IPv6 and IPv4-mapped) on a specific port; -; '/path/to/unix/socket' - to listen on a unix socket. -; Note: This value is mandatory. -listen = /run/php-fpm/{{ item['name'] }}.sock - -; Set listen(2) backlog. -; Default Value: 511 -;listen.backlog = 511 - -; Set permissions for unix socket, if one is used. In Linux, read/write -; permissions must be set in order to allow connections from a web server. -; Default Values: user and group are set as the running user -; mode is set to 0660 -;listen.owner = nobody -;listen.group = nobody -;listen.mode = 0660 - -; When POSIX Access Control Lists are supported you can set them using -; these options, value is a comma separated list of user/group names. -; When set, listen.owner and listen.group are ignored -listen.acl_users = apache -;listen.acl_groups = - -; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect. -; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original -; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address -; must be separated by a comma. If this value is left blank, connections will be -; accepted from any ip address. -; Default Value: any -listen.allowed_clients = 127.0.0.1 - -; Specify the nice(2) priority to apply to the pool processes (only if set) -; The value can vary from -19 (highest priority) to 20 (lower priority) -; Note: - It will only work if the FPM master process is launched as root -; - The pool processes will inherit the master process priority -; unless it specified otherwise -; Default Value: no set -; process.priority = -19 - -; Set the process dumpable flag (PR_SET_DUMPABLE prctl) even if the process user -; or group is differrent than the master process user. It allows to create process -; core dump and ptrace the process for the pool user. -; Default Value: no -; process.dumpable = yes - -; Choose how the process manager will control the number of child processes. -; Possible Values: -; static - a fixed number (pm.max_children) of child processes; -; dynamic - the number of child processes are set dynamically based on the -; following directives. With this process management, there will be -; always at least 1 children. -; pm.max_children - the maximum number of children that can -; be alive at the same time. -; pm.start_servers - the number of children created on startup. -; pm.min_spare_servers - the minimum number of children in 'idle' -; state (waiting to process). If the number -; of 'idle' processes is less than this -; number then some children will be created. -; pm.max_spare_servers - the maximum number of children in 'idle' -; state (waiting to process). If the number -; of 'idle' processes is greater than this -; number then some children will be killed. -; ondemand - no children are created at startup. Children will be forked when -; new requests will connect. The following parameter are used: -; pm.max_children - the maximum number of children that -; can be alive at the same time. -; pm.process_idle_timeout - The number of seconds after which -; an idle process will be killed. -; Note: This value is mandatory. -pm = {{ item['pm'] | d('dynamic') }} - -; The number of child processes to be created when pm is set to 'static' and the -; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. -; This value sets the limit on the number of simultaneous requests that will be -; served. Equivalent to the ApacheMaxClients directive with mpm_prefork. -; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP -; CGI. The below defaults are based on a server without much resources. Don't -; forget to tweak pm.* to fit your needs. -; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' -; Note: This value is mandatory. -pm.max_children = {{ item['pm_max_children'] | d(50) }} - -; The number of child processes created on startup. -; Note: Used only when pm is set to 'dynamic' -; Default Value: min_spare_servers + (max_spare_servers - min_spare_servers) / 2 -pm.start_servers = {{ item['pm_start_servers'] | d(5) }} - -; The desired minimum number of idle server processes. -; Note: Used only when pm is set to 'dynamic' -; Note: Mandatory when pm is set to 'dynamic' -pm.min_spare_servers = {{ item['pm_min_spare_servers'] | d(5) }} - -; The desired maximum number of idle server processes. -; Note: Used only when pm is set to 'dynamic' -; Note: Mandatory when pm is set to 'dynamic' -pm.max_spare_servers = {{ item['pm_max_spare_servers'] | d(35) }} - -; The number of seconds after which an idle process will be killed. -; Note: Used only when pm is set to 'ondemand' -; Default Value: 10s -pm.process_idle_timeout = {{ item['pm_process_idle_timeout'] | d('10s') }} - -; The number of requests each child process should execute before respawning. -; This can be useful to work around memory leaks in 3rd party libraries. For -; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS. -; Default Value: 0 -pm.max_requests = {{ item['pm_max_requests'] | d(0) }} - -; The URI to view the FPM status page. If this value is not set, no URI will be -; recognized as a status page. It shows the following informations: -; pool - the name of the pool; -; process manager - static, dynamic or ondemand; -; start time - the date and time FPM has started; -; start since - number of seconds since FPM has started; -; accepted conn - the number of request accepted by the pool; -; listen queue - the number of request in the queue of pending -; connections (see backlog in listen(2)); -; max listen queue - the maximum number of requests in the queue -; of pending connections since FPM has started; -; listen queue len - the size of the socket queue of pending connections; -; idle processes - the number of idle processes; -; active processes - the number of active processes; -; total processes - the number of idle + active processes; -; max active processes - the maximum number of active processes since FPM -; has started; -; max children reached - number of times, the process limit has been reached, -; when pm tries to start more children (works only for -; pm 'dynamic' and 'ondemand'); -; Value are updated in real time. -; Example output: -; pool: www -; process manager: static -; start time: 01/Jul/2011:17:53:49 +0200 -; start since: 62636 -; accepted conn: 190460 -; listen queue: 0 -; max listen queue: 1 -; listen queue len: 42 -; idle processes: 4 -; active processes: 11 -; total processes: 15 -; max active processes: 12 -; max children reached: 0 -; -; By default the status page output is formatted as text/plain. Passing either -; 'html', 'xml' or 'json' in the query string will return the corresponding -; output syntax. Example: -; http://www.foo.bar/status -; http://www.foo.bar/status?json -; http://www.foo.bar/status?html -; http://www.foo.bar/status?xml -; -; By default the status page only outputs short status. Passing 'full' in the -; query string will also return status for each pool process. -; Example: -; http://www.foo.bar/status?full -; http://www.foo.bar/status?json&full -; http://www.foo.bar/status?html&full -; http://www.foo.bar/status?xml&full -; The Full status returns for each process: -; pid - the PID of the process; -; state - the state of the process (Idle, Running, ...); -; start time - the date and time the process has started; -; start since - the number of seconds since the process has started; -; requests - the number of requests the process has served; -; request duration - the duration in µs of the requests; -; request method - the request method (GET, POST, ...); -; request URI - the request URI with the query string; -; content length - the content length of the request (only with POST); -; user - the user (PHP_AUTH_USER) (or '-' if not set); -; script - the main script called (or '-' if not set); -; last request cpu - the %cpu the last request consumed -; it's always 0 if the process is not in Idle state -; because CPU calculation is done when the request -; processing has terminated; -; last request memory - the max amount of memory the last request consumed -; it's always 0 if the process is not in Idle state -; because memory calculation is done when the request -; processing has terminated; -; If the process is in Idle state, then informations are related to the -; last request the process has served. Otherwise informations are related to -; the current request being served. -; Example output: -; ************************ -; pid: 31330 -; state: Running -; start time: 01/Jul/2011:17:53:49 +0200 -; start since: 63087 -; requests: 12808 -; request duration: 1250261 -; request method: GET -; request URI: /test_mem.php?N=10000 -; content length: 0 -; user: - -; script: /home/fat/web/docs/php/test_mem.php -; last request cpu: 0.00 -; last request memory: 0 -; -; Note: There is a real-time FPM status monitoring sample web page available -; It's available in: @EXPANDED_DATADIR@/fpm/status.html -; -; Note: The value must start with a leading slash (/). The value can be -; anything, but it may not be a good idea to use the .php extension or it -; may conflict with a real PHP file. -; Default Value: not set -pm.status_path = {{ item['pm_status_path'] | d('/' ~ item['name'] ~ '-fpm-status') }} - -; The ping URI to call the monitoring page of FPM. If this value is not set, no -; URI will be recognized as a ping page. This could be used to test from outside -; that FPM is alive and responding, or to -; - create a graph of FPM availability (rrd or such); -; - remove a server from a group if it is not responding (load balancing); -; - trigger alerts for the operating team (24/7). -; Note: The value must start with a leading slash (/). The value can be -; anything, but it may not be a good idea to use the .php extension or it -; may conflict with a real PHP file. -; Default Value: not set -ping.path = {{ item['ping_path'] | d('/' ~ item['name'] ~ '-fpm-ping') }} - -; This directive may be used to customize the response of a ping request. The -; response is formatted as text/plain with a 200 response code. -; Default Value: pong -ping.response = pong - -; The access log file -; Default: not set -;access.log = log/$pool.access.log - -; The access log format. -; The following syntax is allowed -; %%: the '%' character -; %C: %CPU used by the request -; it can accept the following format: -; - %{user}C for user CPU only -; - %{system}C for system CPU only -; - %{total}C for user + system CPU (default) -; %d: time taken to serve the request -; it can accept the following format: -; - %{seconds}d (default) -; - %{miliseconds}d -; - %{mili}d -; - %{microseconds}d -; - %{micro}d -; %e: an environment variable (same as $_ENV or $_SERVER) -; it must be associated with embraces to specify the name of the env -; variable. Some exemples: -; - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e -; - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e -; %f: script filename -; %l: content-length of the request (for POST request only) -; %m: request method -; %M: peak of memory allocated by PHP -; it can accept the following format: -; - %{bytes}M (default) -; - %{kilobytes}M -; - %{kilo}M -; - %{megabytes}M -; - %{mega}M -; %n: pool name -; %o: output header -; it must be associated with embraces to specify the name of the header: -; - %{Content-Type}o -; - %{X-Powered-By}o -; - %{Transfert-Encoding}o -; - .... -; %p: PID of the child that serviced the request -; %P: PID of the parent of the child that serviced the request -; %q: the query string -; %Q: the '?' character if query string exists -; %r: the request URI (without the query string, see %q and %Q) -; %R: remote IP address -; %s: status (response code) -; %t: server time the request was received -; it can accept a strftime(3) format: -; %d/%b/%Y:%H:%M:%S %z (default) -; The strftime(3) format must be encapsuled in a %{}t tag -; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t -; %T: time the log has been written (the request has finished) -; it can accept a strftime(3) format: -; %d/%b/%Y:%H:%M:%S %z (default) -; The strftime(3) format must be encapsuled in a %{}t tag -; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t -; %u: remote user -; -; Default: "%R - %u %t \"%m %r\" %s" -;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%" - -; The log file for slow requests -; Default Value: not set -; Note: slowlog is mandatory if request_slowlog_timeout is set -slowlog = /var/log/php-fpm/{{ item['name'] }}-slow.log - -; The timeout for serving a single request after which a PHP backtrace will be -; dumped to the 'slowlog' file. A value of '0s' means 'off'. -; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) -; Default Value: 0 -request_slowlog_timeout = {{ item['request_slowlog_timeout'] | d(0) }} - -; Depth of slow log stack trace. -; Default Value: 20 -request_slowlog_trace_depth = {{ item['request_slowlog_trace_depth'] | d(20) }} - -; The timeout for serving a single request after which the worker process will -; be killed. This option should be used when the 'max_execution_time' ini option -; does not stop script execution for some reason. A value of '0' means 'off'. -; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) -; Default Value: 0 -request_terminate_timeout = {{ item['request_terminate_timeout'] | d(0) }} - -; Set open file descriptor rlimit. -; Default Value: system defined value -;rlimit_files = 1024 - -; Set max core size rlimit. -; Possible Values: 'unlimited' or an integer greater or equal to 0 -; Default Value: system defined value -;rlimit_core = 0 - -; Chroot to this directory at the start. This value must be defined as an -; absolute path. When this value is not set, chroot is not used. -; Note: you can prefix with '$prefix' to chroot to the pool prefix or one -; of its subdirectories. If the pool prefix is not set, the global prefix -; will be used instead. -; Note: chrooting is a great security feature and should be used whenever -; possible. However, all PHP paths will be relative to the chroot -; (error_log, sessions.save_path, ...). -; Default Value: not set -;chroot = - -; Chdir to this directory at the start. -; Note: relative path can be used. -; Default Value: current directory or / when chroot -;chdir = /var/www - -; Redirect worker stdout and stderr into main error log. If not set, stdout and -; stderr will be redirected to /dev/null according to FastCGI specs. -; Note: on highloaded environement, this can cause some delay in the page -; process time (several ms). -; Default Value: no -;catch_workers_output = yes - -; Clear environment in FPM workers -; Prevents arbitrary environment variables from reaching FPM worker processes -; by clearing the environment in workers before env vars specified in this -; pool configuration are added. -; Setting to "no" will make all environment variables available to PHP code -; via getenv(), $_ENV and $_SERVER. -; Default Value: yes -;clear_env = no - -; Limits the extensions of the main script FPM will allow to parse. This can -; prevent configuration mistakes on the web server side. You should only limit -; FPM to .php extensions to prevent malicious users to use other extensions to -; execute php code. -; Note: set an empty value to allow all extensions. -; Default Value: .php -;security.limit_extensions = .php .php3 .php4 .php5 .php7 - -; Pass environment variables like LD_LIBRARY_PATH. All $VARIABLEs are taken from -; the current environment. -; Default Value: clean env -;env[HOSTNAME] = $HOSTNAME -;env[PATH] = /usr/local/bin:/usr/bin:/bin -;env[TMP] = /tmp -;env[TMPDIR] = /tmp -;env[TEMP] = /tmp - -; Additional php.ini defines, specific to this pool of workers. These settings -; overwrite the values previously defined in the php.ini. The directives are the -; same as the PHP SAPI: -; php_value/php_flag - you can set classic ini defines which can -; be overwritten from PHP call 'ini_set'. -; php_admin_value/php_admin_flag - these directives won't be overwritten by -; PHP call 'ini_set' -; For php_*flag, valid values are on, off, 1, 0, true, false, yes or no. - -; Defining 'extension' will load the corresponding shared extension from -; extension_dir. Defining 'disable_functions' or 'disable_classes' will not -; overwrite previously defined php.ini values, but will append the new value -; instead. - -; Note: path INI options can be relative and will be expanded with the prefix -; (pool, global or @prefix@) - -; Default Value: nothing is defined by default except the values in php.ini and -; specified at startup with the -d argument -;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com -;php_flag[display_errors] = off - -php_admin_flag[log_errors] = on -php_admin_value[error_log] = /var/log/php-fpm/{{ item['name'] }}-error.log -php_admin_value[max_execution_time] = {{ item['php_admin_value_max_execution_time'] | d(php__ini_max_execution_time__combined_var) }} -php_admin_value[max_input_vars] = {{ item['php_admin_value_max_input_vars'] | d(php__ini_max_input_vars__combined_var) }} -php_admin_value[memory_limit] = {{ item['php_admin_value_memory_limit'] | d(php__ini_memory_limit__combined_var) }} -php_admin_value[opcache.interned_strings_buffer] = {{ item['php_admin_value_opcache_interned_strings_buffer'] | d(php__ini_opcache_interned_strings_buffer__combined_var) }} -php_admin_value[opcache.max_accelerated_files] = {{ item['php_admin_value_opcache_max_accelerated_files'] | d(php__ini_opcache_max_accelerated_files__combined_var) }} -php_admin_value[opcache.memory_consumption] = {{ item['php_admin_value_opcache_memory_consumption'] | d(php__ini_opcache_memory_consumption__combined_var) }} -[% if item['php_admin_value_open_basedir'] | d() %] -php_admin_value[open_basedir] = {{ item['php_admin_value_open_basedir'] }} -[% else %] -;php_admin_value[open_basedir] = -[% endif %] -php_admin_value[post_max_size] = {{ item['php_admin_value_post_max_size'] | d(php__ini_post_max_size__combined_var) }} -php_admin_value[upload_max_filesize] = {{ item['php_admin_value_upload_max_filesize'] | d(php__ini_upload_max_filesize__combined_var) }} - -; Set the following data paths to directories owned by the FPM process user. -; -; Do not change the ownership of existing system directories, if the process -; user does not have write permission, create dedicated directories for this -; purpose. -; -; See warning about choosing the location of these directories on your system -; at http://php.net/session.save-path -php_admin_value[session.save_handler] = files -php_admin_value[session.save_path] = {{ item['php_admin_value_session_save_path'] | d('/var/lib/php/session-' ~ item['name']) }} -php_admin_value[opcache.file_cache] = {{ item['php_admin_value_opcache_file_cache'] | d('/var/lib/php/opcache-' ~ item['name']) }} -;php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache - -[% if item['raw'] | d() %] -; raw content -{{ item['raw'] }} -[% endif %] diff --git a/roles/php/templates/etc/php-fpm.d/Debian-pool.conf.j2 b/roles/php/templates/etc/php-fpm.d/pool.conf.j2 similarity index 92% rename from roles/php/templates/etc/php-fpm.d/Debian-pool.conf.j2 rename to roles/php/templates/etc/php-fpm.d/pool.conf.j2 index adf3aec2..5febc06f 100644 --- a/roles/php/templates/etc/php-fpm.d/Debian-pool.conf.j2 +++ b/roles/php/templates/etc/php-fpm.d/pool.conf.j2 @@ -1,6 +1,6 @@ #jinja2:block_start_string:'[%', block_end_string:'%]' ; {{ ansible_managed }} -; 2026051301 +; 2026060301 [% if item['by_role'] | d() %] ; Generated by Ansible role: {{ item['by_role'] }} [% endif %] @@ -19,7 +19,7 @@ ; - 'chdir' ; - 'php_values' ; - 'php_admin_values' -; When not set, the global prefix (or /usr) applies instead. +; When not set, the global prefix (or @php_fpm_prefix@) applies instead. ; Note: This directive can also be relative to the global prefix. ; Default Value: none ;prefix = /path/to/pools/$pool @@ -45,26 +45,34 @@ group = {{ item['group'] | d(php__webserver_group) }} ; (IPv6 and IPv4-mapped) on a specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. -listen = /run/php/{{ item['name'] }}.sock +listen = {{ php__fpm_runtime_path ~ '/' ~ item['name']}}.sock ; Set listen(2) backlog. -; Default Value: 511 (-1 on Linux, FreeBSD and OpenBSD) +; Default Value: 511 ;listen.backlog = 511 ; Set permissions for unix socket, if one is used. In Linux, read/write -; permissions must be set in order to allow connections from a web server. Many -; BSD-derived systems allow connections regardless of permissions. The owner -; and group can be specified either by name or by their numeric IDs. +; permissions must be set in order to allow connections from a web server. +; The owner and group can be specified either by name or by their numeric IDs. ; Default Values: Owner is set to the master process running user. If the group ; is not set, the owner's group is used. Mode is set to 0660. +[% if ansible_facts.os_family == 'Debian' %] listen.owner = {{ php__webserver_user }} listen.group = {{ php__webserver_group }} +[% else %] +;listen.owner = nobody +;listen.group = nobody +[% endif %] ;listen.mode = 0660 ; When POSIX Access Control Lists are supported you can set them using ; these options, value is a comma separated list of user/group names. ; When set, listen.owner and listen.group are ignored +[% if ansible_facts.os_family == 'RedHat' %] +listen.acl_users = {{ php__webserver_user }} +[% else %] ;listen.acl_users = +[% endif %] ;listen.acl_groups = ; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect. @@ -75,10 +83,6 @@ listen.group = {{ php__webserver_group }} ; Default Value: any listen.allowed_clients = 127.0.0.1 -; Set the associated the route table (FIB). FreeBSD only -; Default Value: -1 -;listen.setfib = 1 - ; Specify the nice(2) priority to apply to the pool processes (only if set) ; The value can vary from -19 (highest priority) to 20 (lower priority) ; Note: - It will only work if the FPM master process is launched as root @@ -87,8 +91,7 @@ listen.allowed_clients = 127.0.0.1 ; Default Value: no set ; process.priority = -19 -; Set the process dumpable flag (PR_SET_DUMPABLE prctl for Linux or -; PROC_TRACE_CTL procctl for FreeBSD) even if the process user +; Set the process dumpable flag (PR_SET_DUMPABLE prctl) even if the process user ; or group is different than the master process user. It allows to create process ; core dump and ptrace the process for the pool user. ; Default Value: no @@ -120,7 +123,7 @@ listen.allowed_clients = 127.0.0.1 ; pm.process_idle_timeout - The number of seconds after which ; an idle process will be killed. ; Note: This value is mandatory. -pm = {{ php__fpm_pool_conf_pm__combined_var | d('dynamic') }} +pm = {{ item['pm'] | d('dynamic') }} ; The number of child processes to be created when pm is set to 'static' and the ; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. @@ -152,7 +155,7 @@ pm.max_spare_servers = {{ item['pm_max_spare_servers'] | d(35) }} ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' ; Default Value: 32 -;pm.max_spawn_rate = 32 +pm.max_spawn_rate = {{ item['pm_max_spawn_rate'] | d(32) }} ; The number of seconds after which an idle process will be killed. ; Note: Used only when pm is set to 'ondemand' @@ -256,7 +259,7 @@ pm.max_requests = {{ item['pm_max_requests'] | d(500) }} ; last request memory: 0 ; ; Note: There is a real-time FPM status monitoring sample web page available -; It's available in: /usr/share/php/8.4/fpm/status.html +; It's available in: @EXPANDED_DATADIR@/fpm/status.html ; ; Note: The value must start with a leading slash (/). The value can be ; anything, but it may not be a good idea to use the .php extension or it @@ -355,14 +358,14 @@ ping.response = pong ; %d/%b/%Y:%H:%M:%S %z (default) ; The strftime(3) format must be encapsulated in a %{}t tag ; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t -; %u: remote user +; %u: basic auth user if specified in Authorization header ; ; Default: "%R - %u %t \"%m %r\" %s" ;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{milli}d %{kilo}M %C%%" ; A list of request_uri values which should be filtered from the access log. ; -; As a security precuation, this setting will be ignored if: +; As a security precaution, this setting will be ignored if: ; - the request method is not GET or HEAD; or ; - there is a request body; or ; - there are query parameters; or @@ -379,7 +382,7 @@ ping.response = pong ; The log file for slow requests ; Default Value: not set ; Note: slowlog is mandatory if request_slowlog_timeout is set -slowlog = log/{{ item['name'] }}-slow.log +slowlog = /var/log/{{ php__fpm_service_name }}/{{ item['name'] }}-slow.log ; The timeout for serving a single request after which a PHP backtrace will be ; dumped to the 'slowlog' file. A value of '0s' means 'off'. @@ -481,25 +484,22 @@ request_terminate_timeout = {{ item['request_terminate_timeout'] | d(0) }} ; For php_*flag, valid values are on, off, 1, 0, true, false, yes or no. ; Defining 'extension' will load the corresponding shared extension from -; extension_dir. Defining 'disable_functions' or 'disable_classes' will not -; overwrite previously defined php.ini values, but will append the new value -; instead. +; extension_dir. Defining 'disable_functions' will not overwrite previously +; defined php.ini values, but will append the new value instead. ; Note: path INI options can be relative and will be expanded with the prefix -; (pool, global or /usr) +; (pool, global or @prefix@) ; Default Value: nothing is defined by default except the values in php.ini and ; specified at startup with the -d argument ;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com ;php_flag[display_errors] = off + php_admin_flag[log_errors] = on php_admin_value[error_log] = /var/log/php-fpm/{{ item['name'] }}-error.log php_admin_value[max_execution_time] = {{ item['php_admin_value_max_execution_time'] | d(php__ini_max_execution_time__combined_var) }} php_admin_value[max_input_vars] = {{ item['php_admin_value_max_input_vars'] | d(php__ini_max_input_vars__combined_var) }} php_admin_value[memory_limit] = {{ item['php_admin_value_memory_limit'] | d(php__ini_memory_limit__combined_var) }} -php_admin_value[opcache.interned_strings_buffer] = {{ item['php_admin_value_opcache_interned_strings_buffer'] | d(php__ini_opcache_interned_strings_buffer__combined_var) }} -php_admin_value[opcache.max_accelerated_files] = {{ item['php_admin_value_opcache_max_accelerated_files'] | d(php__ini_opcache_max_accelerated_files__combined_var) }} -php_admin_value[opcache.memory_consumption] = {{ item['php_admin_value_opcache_memory_consumption'] | d(php__ini_opcache_memory_consumption__combined_var) }} [% if item['php_admin_value_open_basedir'] | d() %] php_admin_value[open_basedir] = {{ item['php_admin_value_open_basedir'] }} [% else %] @@ -508,12 +508,18 @@ php_admin_value[open_basedir] = {{ item['php_admin_value_open_basedir'] }} php_admin_value[post_max_size] = {{ item['php_admin_value_post_max_size'] | d(php__ini_post_max_size__combined_var) }} php_admin_value[upload_max_filesize] = {{ item['php_admin_value_upload_max_filesize'] | d(php__ini_upload_max_filesize__combined_var) }} +; Set the following data paths to directories owned by the FPM process user. +; +; Do not change the ownership of existing system directories, if the process +; user does not have write permission, create dedicated directories for this +; purpose. +; +; See warning about choosing the location of these directories on your system +; at http://php.net/session.save-path php_admin_value[session.save_handler] = files -php_admin_value[session.save_path] = {{ item['php_admin_value_session_save_path'] | d('/var/lib/php/session-' ~ item['name']) }} -php_admin_value[opcache.file_cache] = {{ item['php_admin_value_opcache_file_cache'] | d('/var/lib/php/opcache-' ~ item['name']) }} -;php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache +php_admin_value[session.save_path] = {{ item['php_admin_value_session_save_path'] | d('/var/lib/php/' ~ item['name'] ~ '-session') }} +[% if item['raw'] | d() %] -[% if item['raw'] | () %] ; raw content {{ item['raw'] }} [% endif %] diff --git a/roles/php/vars/Debian.yml b/roles/php/vars/Debian.yml index d87c73c7..4db9ee51 100644 --- a/roles/php/vars/Debian.yml +++ b/roles/php/vars/Debian.yml @@ -3,4 +3,7 @@ php__conf_dest: - '/etc/php/{{ php__installed_version }}/cli/conf.d/z00-linuxfabrik.ini' - '/etc/php/{{ php__installed_version }}/fpm/conf.d/z00-linuxfabrik.ini' php__fpm_pools_path: '/etc/php/{{ php__installed_version }}/fpm/pool.d' +php__fpm_runtime_path: '/run/php' php__fpm_service_name: 'php{{ php__installed_version }}-fpm' +php__webserver_user: 'www-data' +php__webserver_group: 'www-data' diff --git a/roles/php/vars/RedHat.yml b/roles/php/vars/RedHat.yml index d4c7fdea..00a998dc 100644 --- a/roles/php/vars/RedHat.yml +++ b/roles/php/vars/RedHat.yml @@ -1,4 +1,7 @@ php__conf_dest: - '/etc/php.d/z00-linuxfabrik.ini' php__fpm_pools_path: '/etc/php-fpm.d' +php__fpm_runtime_path: '/run/php-fpm' php__fpm_service_name: 'php-fpm' +php__webserver_user: 'apache' +php__webserver_group: 'apache' From 65b2e9a3489d95349657381f7ae454c17c45a3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20B=C3=BCrki?= Date: Wed, 3 Jun 2026 15:24:17 +0200 Subject: [PATCH 62/66] docs(roles/php): update examples --- roles/php/README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/roles/php/README.md b/roles/php/README.md index daa642ce..01ea421f 100644 --- a/roles/php/README.md +++ b/roles/php/README.md @@ -512,17 +512,18 @@ Variables for PHP-FPM Pool Config directives and their default values, defined a Example: ```yaml # optional -php__fpm_pool_conf_pm__host_var: 'dynamic' -php__fpm_pool_conf_pm_max_children__host_var: 50 -php__fpm_pool_conf_pm_max_spare_servers__host_var: 35 -php__fpm_pool_conf_pm_min_spare_servers__host_var: 5 -php__fpm_pool_conf_pm_start_servers__host_var: 5 -php__fpm_pool_conf_request_slowlog_timeout__host_var: '10s' -php__fpm_pool_conf_request_terminate_timeout__host_var: '60s' php__fpm_pools__host_var: - name: 'librenms' user: 'librenms' group: 'librenms' + pm: 'dynamic' + pm_max_children: 50 + pm_max_spare_servers: 35 + pm_min_spare_servers: 5 + pm_start_servers: 5 + request_slowlog_timeout: '10s' + request_terminate_timeout: '60s' + php_admin_value_session_save_path: '/var/lib/php/session' # use default session save path instead of /var/lib/php/librenms-session raw: |- env[PATH] = /usr/local/bin:/usr/bin:/bin ``` From a29d1325e23ad95aa5664bf6a8be0ecf2202d75d Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 9 Jun 2026 16:57:55 +0200 Subject: [PATCH 63/66] feat(roles/php): create per-pool session/opcache dirs and wire pool defaults Create a per-pool session.save_path directory (owned by the pool user/group, mode 0700) and a shared opcache directory; run restorecon on RedHat so the httpd_var_run_t / httpd_var_lib_t SELinux labels apply. Default the pool process-manager and timeout subkeys to the php__fpm_pool_conf_* combined vars in the unified pool.conf.j2. Rename role-internal variables to the __php__ prefix (__php__conf_dest, __php__fpm_opcache_path, __php__fpm_pools_path, __php__fpm_runtime_path, __php__fpm_service_name, __php__fpm_session_path, __php__installed_version); they live in vars/ and were never inventory-overridable. Fix README defaults to match the code, document repo_sury as the Debian counterpart to repo_remi, and document the breaking changes (per-pool session path, php_admin_value enforcement, pm.max_requests=500, status path, soap.wsdl_cache_dir). --- CHANGELOG.md | 2 + roles/php/README.md | 45 +++++++++------ roles/php/defaults/main.yml | 2 +- roles/php/handlers/main.yml | 2 +- roles/php/tasks/main.yml | 55 +++++++++++++++---- .../php/templates/etc/php-fpm.d/pool.conf.j2 | 38 ++++++------- roles/php/vars/Debian.yml | 18 +++--- roles/php/vars/RedHat.yml | 12 ++-- 8 files changed, 109 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b961ce27..de2b3b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking Changes +* **role:php**: The PHP-FPM pool config changed for existing hosts. Sessions now live in a per-pool directory (the default `www` pool moves from `/var/lib/php/session` to `/var/lib/php/session/www`), so logged-in users are signed out once after the upgrade. `memory_limit`, `max_execution_time`, `max_input_vars`, `post_max_size`, `upload_max_filesize` and `session.save_path` are now enforced as `php_admin_value`, so applications can no longer raise them at runtime via `ini_set()`. The FPM status path moved from `/fpm-status` to `/www-fpm-status`, and `soap.wsdl_cache_dir` is no longer set (PHP default applies). Worker processes now recycle after 500 requests (`pm.max_requests`), where previously they ran indefinitely. * **role:minio_client, role:objectstore_backup**: Both roles and their playbooks (`playbooks/minio_client.yml`, `playbooks/objectstore_backup.yml`) have been removed, along with the corresponding role blocks in `playbooks/setup_nextcloud.yml` and the `setup_nextcloud__skip_minio_client` / `setup_nextcloud__skip_objectstore_backup` variables. MinIO Server has been archived as no-longer-maintained since February 2026, and we are moving away from using object storage for critical data. Users relying on these roles must replace the MinIO-based object-store backup with their own solution (e.g. `rclone`); the `mc` binary, its config under `/etc/mc/`, the `objectstore-backup` systemd timer/service, and `/usr/local/bin/mc-mirror.sh` are no longer managed by lfops and will remain on existing hosts until removed manually ([#241](https://github.com/Linuxfabrik/lfops/issues/241)). * **role:infomaniak_vm**: Always create a managed port for every entry in `infomaniak_vm__networks`, even when no `fixed_ip` is set. Previously only networks with a `fixed_ip` got a managed port; networks without one relied on OpenStack's auto-created port. To avoid creating unused (but billed) managed ports on VMs provisioned under the old behavior, make sure to manually rename the existing port in OpenStack to match the `port_name`. Note that this port will not survive VM deletion / detachment, since it was automatically created and therefore is owned by OpenStack, not the user. ### Added +* **role:php**: PHP-FPM pools are now fully configurable, each with its own user/group, process-manager tuning, timeouts and `php_admin_value` overrides. Every pool gets its own isolated session directory (created automatically, with correct ownership and SELinux labeling on RedHat). * **role:tmux**: Installs tmux and deploys a system-wide `/etc/tmux.conf` with sensible defaults, such as a larger scrollback buffer and mouse support. Selections are copied to the local clipboard over SSH via OSC 52 (where the terminal emulator supports it), and `prefix + P` dumps a pane's whole scrollback buffer to a file. * **role:graylog_server**: Make more HTTP, Elasticsearch, processing/output buffer and message journal settings configurable via `graylog_server__http_external_uri`, `graylog_server__http_enable_cors`, `graylog_server__elasticsearch_max_total_connections`, `graylog_server__elasticsearch_max_total_connections_per_route`, `graylog_server__output_batch_size`, `graylog_server__processbuffer_processors`, `graylog_server__outputbuffer_processors`, `graylog_server__ring_size`, `graylog_server__inputbuffer_ring_size`, `graylog_server__message_journal_max_age` and `graylog_server__message_journal_max_size`. * **role:mariadb_server**: Make `aria_pagecache_buffer_size`, `key_buffer_size` and `sort_buffer_size` configurable via the corresponding `mariadb_server__cnf_*` variables. diff --git a/roles/php/README.md b/roles/php/README.md index 01ea421f..ccf3789d 100644 --- a/roles/php/README.md +++ b/roles/php/README.md @@ -2,7 +2,7 @@ This role installs and configures PHP (and PHP-FPM) on the system, optionally with additional modules. -Note that this role does NOT let you specify a particular PHP version. It simply installs the latest available PHP version from the repos configured in the system. If you want or need to install a specific or the latest PHP version available, use the [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) beforehand. +Note that this role does NOT let you specify a particular PHP version. It simply installs the latest available PHP version from the repos configured in the system. If you want or need to install a specific or the latest PHP version available, use the [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi) (Red Hat family) or [linuxfabrik.lfops.repo_sury](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_sury) (Debian family) beforehand. This role is compatible with the following PHP versions: @@ -33,7 +33,8 @@ This role never exposes to the world that PHP is installed on the server, no mat Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. -* Optional: [Remi's RPM repository](https://rpms.remirepo.net/) (role: [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi)) provides newer PHP versions. +* Optional: [Remi's RPM repository](https://rpms.remirepo.net/) (role: [linuxfabrik.lfops.repo_remi](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_remi)) provides newer PHP versions on the Red Hat family. +* [Sury repository](https://deb.sury.org/) (role: [linuxfabrik.lfops.repo_sury](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_sury)) provides newer PHP versions on the Debian family. ## Tags @@ -45,15 +46,19 @@ Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/RE * Ensure PHP modules are absent. * Ensure PHP modules are present. * Get PHP version. -* Load default values for `{{ php__installed_version }}`. +* Load default values for `{{ __php__installed_version }}`. * Deploy the /etc/php.d/z00-linuxfabrik.ini. * `systemctl {{ php__fpm_service_enabled | bool | ternary("enable", "disable") }} --now php-fpm`. +* Ensure the shared opcache directory exists. +* Create the per-pool session directories. * Remove absent pools from `/etc/php-fpm.d`. * Deploy the pools to `/etc/php-fpm.d/`. * Triggers: php-fpm.service restart. `php:fpm` +* Ensure the shared opcache directory exists. +* Create the per-pool session directories. * Remove absent pools from /etc/php-fpm.d. * Deploy the pools to /etc/php-fpm.d/. * Triggers: php-fpm.service restart. @@ -61,7 +66,7 @@ Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/RE `php:ini` * Get PHP version. -* Load default values for `{{ php__installed_version }}`. +* Load default values for `{{ __php__installed_version }}`. * Deploy the `/etc/php.d/z00-linuxfabrik.ini`. * Triggers: php-fpm.service restart. @@ -153,7 +158,7 @@ Variables for `php.ini` directives and their default values, defined and support * Set the error reporting level. [php.net](https://www.php.net/manual/en/errorfunc.configuration.php) * Type: String. -* Default: `'E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT'` +* Default: 7.2 - 8.4: `'E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT'`, 8.5: `'E_ALL & ~E_NOTICE & ~E_DEPRECATED'` (`E_STRICT` is deprecated as of PHP 8.4) `php__ini_max_execution_time__group_var` / `php__ini_max_execution_time__host_var` @@ -273,7 +278,7 @@ Variables for `php.ini` directives and their default values, defined and support * [php.net](https://www.php.net/manual/en/session.configuration.php) * Type: Number. -* Default: `32` +* Default: 7.2 - 8.4: `32`. Not managed on 8.5, where PHP's built-in default applies. `php__ini_session_trans_sid_tags__group_var` / `php__ini_session_trans_sid_tags__host_var` @@ -309,6 +314,10 @@ php__ini_upload_max_filesize__host_var: '10000M' Variables for PHP-FPM Pool Config directives and their default values, defined and supported by this role. +For every pool the role creates a dedicated session directory below the distribution's session base (`/var/lib/php/session` on RedHat, `/var/lib/php/sessions` on Debian) and a single shared opcache directory (`/var/lib/php/opcache`). On Debian, stale session files are reaped by the packaged `sessionclean` timer, which recurses the session base using the global `session.gc_maxlifetime`. A per-pool `session.gc_maxlifetime` is therefore not honored by the cleanup on Debian, and a session that stays open but idle longer than the lifetime may be removed. + +Each pool listens on its own Unix socket below the FPM runtime directory (`/run/php-fpm/{{ item["name"] }}.sock` on RedHat, `/run/php/{{ item["name"] }}.sock` on Debian). On Debian, the packaged php-fpm systemd unit additionally maintains a version-agnostic `update-alternatives` alias at `/run/php/php-fpm.sock` that points at the socket of the default `www` pool. This alias only ever tracks `www`, not the pools created by this role, so configure your web server with the explicit per-pool socket path rather than the generic `/run/php/php-fpm.sock`. RedHat ships no such alias. + `php__fpm_pool_conf_pm__group_var` / `php__fpm_pool_conf_pm__host_var` * Choose how the process manager will control the number of child processes. @@ -386,31 +395,31 @@ Variables for PHP-FPM Pool Config directives and their default values, defined a * Optional. Choose how the process manager will control the number of child processes. [php.net](https://www.php.net/install.fpm.configuration.php#pm) * Type: String. - * Default: `'dynamic'` + * Default: `{{ php__fpm_pool_conf_pm__combined_var }}` (which defaults to `'dynamic'`) * `pm_max_children`: * Optional. The number of child processes to be created when pm is set to `'static'` and the maximum number of child processes when pm is set to `'dynamic'` or `'ondemand'`. [php.net](https://www.php.net/install.fpm.configuration.php#pm.max-children) * Type: Number. - * Default: `50` + * Default: `{{ php__fpm_pool_conf_pm_max_children__combined_var }}` (which defaults to `50`) * `pm_start_servers`: * Optional. The number of child processes created on startup. Must be greater than `pm_min_spare_servers` but less than `pm_max_spare_servers`. Used only when `pm` is set to `'dynamic`'. [php.net](https://www.php.net/install.fpm.configuration.php#pm.start-servers) * Type: Number. - * Default: `5` + * Default: `{{ php__fpm_pool_conf_pm_start_servers__combined_var }}` (which defaults to `5`) * `pm_min_spare_servers`: * Optional. The desired minimum number of idle server processes. Used only when `pm` is set to `'dynamic'`. [php.net](https://www.php.net/install.fpm.configuration.php#pm.min-spare-servers) * Type: Number. - * Default: `5` + * Default: `{{ php__fpm_pool_conf_pm_min_spare_servers__combined_var }}` (which defaults to `5`) * `pm_max_spare_servers`: * Optional. The desired maximum number of idle server processes. Used only when `pm` is set to `'dynamic'`. [php.net](https://www.php.net/install.fpm.configuration.php#pm.max-spare-servers) * Type: Number. - * Default: `35` + * Default: `{{ php__fpm_pool_conf_pm_max_spare_servers__combined_var }}` (which defaults to `35`) * `pm_max_spawn_rate`: @@ -426,9 +435,9 @@ Variables for PHP-FPM Pool Config directives and their default values, defined a * `pm_max_requests`: - * Optional. The number of requests each child process should execute before respawning. [php.net](https://www.php.net/install.fpm.configuration.php#pm.max-requests) + * Optional. The number of requests each child process should execute before respawning. For endless request processing specify `0`. [php.net](https://www.php.net/install.fpm.configuration.php#pm.max-requests) * Type: Number. - * Default: `0` + * Default: `500` * `pm_status_path`: @@ -446,7 +455,7 @@ Variables for PHP-FPM Pool Config directives and their default values, defined a * Optional. The timeout for serving a single request after which a PHP backtrace will be dumped to the slowlog file. A value of `0` means off. Available units: s(econds, default), m(inutes), h(ours), or d(ays). [php.net](https://www.php.net/install.fpm.configuration.php#request-slowlog-timeout) * Type: Number. - * Default: `0` + * Default: `{{ php__fpm_pool_conf_request_slowlog_timeout__combined_var }}` (which defaults to `0`) * `request_slowlog_trace_depth`: @@ -459,13 +468,13 @@ Variables for PHP-FPM Pool Config directives and their default values, defined a * Optional. The timeout for serving a single request after which the worker process will be killed. This option should be used when the `max_execution_time` ini option does not stop script execution for some reason. A value of `0` means off. Available units: s(econds, default), m(inutes), h(ours), or d(ays). * [php.net](https://www.php.net/install.fpm.configuration.php#request-terminate-timeout) * Type: Number. - * Default: `0` + * Default: `{{ php__fpm_pool_conf_request_terminate_timeout__combined_var }}` (which defaults to `0`) * `php_admin_value_session_save_path`: - * Optional. **NOTE:** The session save directory is currently not created automatically. [php.net](https://www.php.net/session.save_path) + * Optional. The role creates this directory, owned by the pool's `user` / `group` with mode `0700`, so pools cannot read each other's sessions. On RedHat it inherits the `httpd_var_run_t` SELinux type from the session base; if you point it outside the session base, you have to label it yourself. [php.net](https://www.php.net/session.save_path) * Type: String. - * Default: `'/var/lib/php/{{ item["name"] }}-session'` + * Default: `/var/lib/php/session/{{ item["name"] }}` (RedHat), `/var/lib/php/sessions/{{ item["name"] }}` (Debian) * `php_admin_value_max_execution_time`: @@ -523,7 +532,7 @@ php__fpm_pools__host_var: pm_start_servers: 5 request_slowlog_timeout: '10s' request_terminate_timeout: '60s' - php_admin_value_session_save_path: '/var/lib/php/session' # use default session save path instead of /var/lib/php/librenms-session + php_admin_value_session_save_path: '/var/lib/php/session' # use the shared session dir instead of the per-pool default /var/lib/php/session/librenms raw: |- env[PATH] = /usr/local/bin:/usr/bin:/bin ``` diff --git a/roles/php/defaults/main.yml b/roles/php/defaults/main.yml index 9be76bd0..fba33dae 100644 --- a/roles/php/defaults/main.yml +++ b/roles/php/defaults/main.yml @@ -405,7 +405,7 @@ php__modules__group_var: [] php__modules__host_var: [] php__modules__role_var: Debian: - - name: 'php{{ php__installed_version }}-opcache' + - name: 'php{{ __php__installed_version }}-opcache' state: 'present' RedHat: - name: 'php-opcache' diff --git a/roles/php/handlers/main.yml b/roles/php/handlers/main.yml index 59b7a486..d5769d5f 100644 --- a/roles/php/handlers/main.yml +++ b/roles/php/handlers/main.yml @@ -1,4 +1,4 @@ - name: 'php: restart php-fpm' ansible.builtin.service: - name: '{{ php__fpm_service_name }}' + name: '{{ __php__fpm_service_name }}' state: 'restarted' diff --git a/roles/php/tasks/main.yml b/roles/php/tasks/main.yml index 039e9e98..0821b95a 100644 --- a/roles/php/tasks/main.yml +++ b/roles/php/tasks/main.yml @@ -49,7 +49,7 @@ - name: 'Get PHP version' ansible.builtin.set_fact: - php__installed_version: '{{ ansible_facts["packages"]["php"][0]["version"] | regex_search("\d\.\d") }}' + __php__installed_version: '{{ ansible_facts["packages"]["php"][0]["version"] | regex_search("\d\.\d") }}' tags: - 'php' @@ -104,18 +104,18 @@ - block: - - name: 'Load default values for {{ php__installed_version }}' - ansible.builtin.include_vars: 'vars/{{ php__installed_version }}.yml' + - name: 'Load default values for {{ __php__installed_version }}' + ansible.builtin.include_vars: 'vars/{{ __php__installed_version }}.yml' - name: 'Deploy the PHP configs' ansible.builtin.template: backup: true - src: 'etc/php.d/{{ php__installed_version }}-z00-linuxfabrik.ini.j2' + src: 'etc/php.d/{{ __php__installed_version }}-z00-linuxfabrik.ini.j2' dest: '{{ item }}' owner: 'root' group: 'root' mode: 0o644 - loop: '{{ php__conf_dest }}' + loop: '{{ __php__conf_dest }}' notify: 'php: restart php-fpm' tags: @@ -126,20 +126,53 @@ - block: - - name: 'Remove absent pools from {{ php__fpm_pools_path }}' + # opcache is shared among all php-fpm pools, so a single shared directory is + # enough. On RedHat the package ships it; create it for parity on Debian. + - name: 'Ensure the shared opcache directory {{ __php__fpm_opcache_path }} exists' ansible.builtin.file: - path: '{{ php__fpm_pools_path }}/{{ item["name"] }}.conf' + path: '{{ __php__fpm_opcache_path }}' + state: 'directory' + owner: 'root' + group: '{{ __shared__apache_httpd_group }}' + mode: 0o770 + + # each pool gets its own session directory below the distro's session base, so + # the pools cannot read each other's sessions. On RedHat this inherits the + # `httpd_var_run_t` SELinux type from the parent; on Debian the packaged + # `sessionclean` timer recurses the base and reaps stale files. + - name: 'Create the per-pool session directories' + ansible.builtin.file: + path: '{{ item["php_admin_value_session_save_path"] | d(__php__fpm_session_path ~ "/" ~ item["name"]) }}' + state: 'directory' + owner: '{{ item["user"] | d(__shared__apache_httpd_user) }}' + group: '{{ item["group"] | d(__shared__apache_httpd_group) }}' + mode: 0o700 + when: + - 'item["state"] | d("present") != "absent"' + loop: '{{ php__fpm_pools__combined_var }}' + + - name: 'restorecon -Fvr {{ __php__fpm_session_path }} {{ __php__fpm_opcache_path }}' + ansible.builtin.command: 'restorecon -Fvr {{ __php__fpm_session_path }} {{ __php__fpm_opcache_path }}' + register: '__php__restorecon_result' + changed_when: '__php__restorecon_result["stdout"] | length > 0' + when: + - 'ansible_facts["os_family"] == "RedHat"' + - 'ansible_facts["selinux"]["status"] != "disabled"' + + - name: 'Remove absent pools from {{ __php__fpm_pools_path }}' + ansible.builtin.file: + path: '{{ __php__fpm_pools_path }}/{{ item["name"] }}.conf' state: 'absent' when: - 'item["state"] | d("present") == "absent"' loop: '{{ php__fpm_pools__combined_var }}' notify: 'php: restart php-fpm' - - name: 'Deploy the pools to {{ php__fpm_pools_path }}' + - name: 'Deploy the pools to {{ __php__fpm_pools_path }}' ansible.builtin.template: backup: true src: 'etc/php-fpm.d/pool.conf.j2' - dest: '{{ php__fpm_pools_path }}/{{ item["name"] }}.conf' + dest: '{{ __php__fpm_pools_path }}/{{ item["name"] }}.conf' owner: 'root' group: 'root' mode: 0o644 @@ -155,9 +188,9 @@ - block: - - name: 'systemctl {{ php__fpm_service_enabled | bool | ternary("enable", "disable") }} --now {{ php__fpm_service_name }}' + - name: 'systemctl {{ php__fpm_service_enabled | bool | ternary("enable", "disable") }} --now {{ __php__fpm_service_name }}' ansible.builtin.service: - name: '{{ php__fpm_service_name }}' + name: '{{ __php__fpm_service_name }}' enabled: '{{ php__fpm_service_enabled }}' state: '{{ php__fpm_service_enabled | bool | ternary("started", "stopped") }}' diff --git a/roles/php/templates/etc/php-fpm.d/pool.conf.j2 b/roles/php/templates/etc/php-fpm.d/pool.conf.j2 index 5febc06f..ccf2530c 100644 --- a/roles/php/templates/etc/php-fpm.d/pool.conf.j2 +++ b/roles/php/templates/etc/php-fpm.d/pool.conf.j2 @@ -1,6 +1,6 @@ #jinja2:block_start_string:'[%', block_end_string:'%]' ; {{ ansible_managed }} -; 2026060301 +; 2026060402 [% if item['by_role'] | d() %] ; Generated by Ansible role: {{ item['by_role'] }} [% endif %] @@ -32,8 +32,8 @@ ; --allow-to-run-as-root option to work. ; Default Values: The user is set to master process running user by default. ; If the group is not set, the user's group is used. -user = {{ item['user'] | d(php__webserver_user) }} -group = {{ item['group'] | d(php__webserver_group) }} +user = {{ item['user'] | d(__shared__apache_httpd_user) }} +group = {{ item['group'] | d(__shared__apache_httpd_group) }} ; The address on which to accept FastCGI requests. ; Valid syntaxes are: @@ -45,7 +45,7 @@ group = {{ item['group'] | d(php__webserver_group) }} ; (IPv6 and IPv4-mapped) on a specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. -listen = {{ php__fpm_runtime_path ~ '/' ~ item['name']}}.sock +listen = {{ __php__fpm_runtime_path ~ '/' ~ item['name']}}.sock ; Set listen(2) backlog. ; Default Value: 511 @@ -56,9 +56,9 @@ listen = {{ php__fpm_runtime_path ~ '/' ~ item['name']}}.sock ; The owner and group can be specified either by name or by their numeric IDs. ; Default Values: Owner is set to the master process running user. If the group ; is not set, the owner's group is used. Mode is set to 0660. -[% if ansible_facts.os_family == 'Debian' %] -listen.owner = {{ php__webserver_user }} -listen.group = {{ php__webserver_group }} +[% if ansible_facts["os_family"] == 'Debian' %] +listen.owner = {{ __shared__apache_httpd_user }} +listen.group = {{ __shared__apache_httpd_group }} [% else %] ;listen.owner = nobody ;listen.group = nobody @@ -68,8 +68,8 @@ listen.group = {{ php__webserver_group }} ; When POSIX Access Control Lists are supported you can set them using ; these options, value is a comma separated list of user/group names. ; When set, listen.owner and listen.group are ignored -[% if ansible_facts.os_family == 'RedHat' %] -listen.acl_users = {{ php__webserver_user }} +[% if ansible_facts["os_family"] == 'RedHat' %] +listen.acl_users = {{ __shared__apache_httpd_user }} [% else %] ;listen.acl_users = [% endif %] @@ -123,7 +123,7 @@ listen.allowed_clients = 127.0.0.1 ; pm.process_idle_timeout - The number of seconds after which ; an idle process will be killed. ; Note: This value is mandatory. -pm = {{ item['pm'] | d('dynamic') }} +pm = {{ item['pm'] | d(php__fpm_pool_conf_pm__combined_var) }} ; The number of child processes to be created when pm is set to 'static' and the ; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'. @@ -134,22 +134,22 @@ pm = {{ item['pm'] | d('dynamic') }} ; forget to tweak pm.* to fit your needs. ; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand' ; Note: This value is mandatory. -pm.max_children = {{ item['pm_max_children'] | d(50) }} +pm.max_children = {{ item['pm_max_children'] | d(php__fpm_pool_conf_pm_max_children__combined_var) }} ; The number of child processes created on startup. ; Note: Used only when pm is set to 'dynamic' ; Default Value: (min_spare_servers + max_spare_servers) / 2 -pm.start_servers = {{ item['pm_start_servers'] | d(5) }} +pm.start_servers = {{ item['pm_start_servers'] | d(php__fpm_pool_conf_pm_start_servers__combined_var) }} ; The desired minimum number of idle server processes. ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' -pm.min_spare_servers = {{ item['pm_min_spare_servers'] | d(5) }} +pm.min_spare_servers = {{ item['pm_min_spare_servers'] | d(php__fpm_pool_conf_pm_min_spare_servers__combined_var) }} ; The desired maximum number of idle server processes. ; Note: Used only when pm is set to 'dynamic' ; Note: Mandatory when pm is set to 'dynamic' -pm.max_spare_servers = {{ item['pm_max_spare_servers'] | d(35) }} +pm.max_spare_servers = {{ item['pm_max_spare_servers'] | d(php__fpm_pool_conf_pm_max_spare_servers__combined_var) }} ; The number of rate to spawn child processes at once. ; Note: Used only when pm is set to 'dynamic' @@ -382,13 +382,13 @@ ping.response = pong ; The log file for slow requests ; Default Value: not set ; Note: slowlog is mandatory if request_slowlog_timeout is set -slowlog = /var/log/{{ php__fpm_service_name }}/{{ item['name'] }}-slow.log +slowlog = /var/log/{{ __php__fpm_service_name }}/{{ item['name'] }}-slow.log ; The timeout for serving a single request after which a PHP backtrace will be ; dumped to the 'slowlog' file. A value of '0s' means 'off'. ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) ; Default Value: 0 -request_slowlog_timeout = {{ item['request_slowlog_timeout'] | d(0) }} +request_slowlog_timeout = {{ item['request_slowlog_timeout'] | d(php__fpm_pool_conf_request_slowlog_timeout__combined_var) }} ; Depth of slow log stack trace. ; Default Value: 20 @@ -399,7 +399,7 @@ request_slowlog_trace_depth = {{ item['request_slowlog_trace_depth'] | d(20) }} ; does not stop script execution for some reason. A value of '0' means 'off'. ; Available units: s(econds)(default), m(inutes), h(ours), or d(ays) ; Default Value: 0 -request_terminate_timeout = {{ item['request_terminate_timeout'] | d(0) }} +request_terminate_timeout = {{ item['request_terminate_timeout'] | d(php__fpm_pool_conf_request_terminate_timeout__combined_var) }} ; The timeout set by 'request_terminate_timeout' ini option is not engaged after ; application calls 'fastcgi_finish_request' or when application has finished and @@ -496,7 +496,7 @@ request_terminate_timeout = {{ item['request_terminate_timeout'] | d(0) }} ;php_flag[display_errors] = off php_admin_flag[log_errors] = on -php_admin_value[error_log] = /var/log/php-fpm/{{ item['name'] }}-error.log +php_admin_value[error_log] = /var/log/{{ __php__fpm_service_name }}/{{ item['name'] }}-error.log php_admin_value[max_execution_time] = {{ item['php_admin_value_max_execution_time'] | d(php__ini_max_execution_time__combined_var) }} php_admin_value[max_input_vars] = {{ item['php_admin_value_max_input_vars'] | d(php__ini_max_input_vars__combined_var) }} php_admin_value[memory_limit] = {{ item['php_admin_value_memory_limit'] | d(php__ini_memory_limit__combined_var) }} @@ -517,7 +517,7 @@ php_admin_value[upload_max_filesize] = {{ item['php_admin_value_upload_max_files ; See warning about choosing the location of these directories on your system ; at http://php.net/session.save-path php_admin_value[session.save_handler] = files -php_admin_value[session.save_path] = {{ item['php_admin_value_session_save_path'] | d('/var/lib/php/' ~ item['name'] ~ '-session') }} +php_admin_value[session.save_path] = {{ item['php_admin_value_session_save_path'] | d(__php__fpm_session_path ~ '/' ~ item['name']) }} [% if item['raw'] | d() %] ; raw content diff --git a/roles/php/vars/Debian.yml b/roles/php/vars/Debian.yml index 4db9ee51..dd121a49 100644 --- a/roles/php/vars/Debian.yml +++ b/roles/php/vars/Debian.yml @@ -1,9 +1,9 @@ -php__conf_dest: - - '/etc/php/{{ php__installed_version }}/apache2/conf.d/z00-linuxfabrik.ini' - - '/etc/php/{{ php__installed_version }}/cli/conf.d/z00-linuxfabrik.ini' - - '/etc/php/{{ php__installed_version }}/fpm/conf.d/z00-linuxfabrik.ini' -php__fpm_pools_path: '/etc/php/{{ php__installed_version }}/fpm/pool.d' -php__fpm_runtime_path: '/run/php' -php__fpm_service_name: 'php{{ php__installed_version }}-fpm' -php__webserver_user: 'www-data' -php__webserver_group: 'www-data' +__php__conf_dest: + - '/etc/php/{{ __php__installed_version }}/apache2/conf.d/z00-linuxfabrik.ini' + - '/etc/php/{{ __php__installed_version }}/cli/conf.d/z00-linuxfabrik.ini' + - '/etc/php/{{ __php__installed_version }}/fpm/conf.d/z00-linuxfabrik.ini' +__php__fpm_opcache_path: '/var/lib/php/opcache' +__php__fpm_pools_path: '/etc/php/{{ __php__installed_version }}/fpm/pool.d' +__php__fpm_runtime_path: '/run/php' +__php__fpm_service_name: 'php{{ __php__installed_version }}-fpm' +__php__fpm_session_path: '/var/lib/php/sessions' diff --git a/roles/php/vars/RedHat.yml b/roles/php/vars/RedHat.yml index 00a998dc..a84267e7 100644 --- a/roles/php/vars/RedHat.yml +++ b/roles/php/vars/RedHat.yml @@ -1,7 +1,7 @@ -php__conf_dest: +__php__conf_dest: - '/etc/php.d/z00-linuxfabrik.ini' -php__fpm_pools_path: '/etc/php-fpm.d' -php__fpm_runtime_path: '/run/php-fpm' -php__fpm_service_name: 'php-fpm' -php__webserver_user: 'apache' -php__webserver_group: 'apache' +__php__fpm_opcache_path: '/var/lib/php/opcache' +__php__fpm_pools_path: '/etc/php-fpm.d' +__php__fpm_runtime_path: '/run/php-fpm' +__php__fpm_service_name: 'php-fpm' +__php__fpm_session_path: '/var/lib/php/session' From 3d06f928225e4e65d7704feefbb4a18aeb90119b Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 9 Jun 2026 17:00:42 +0200 Subject: [PATCH 64/66] feat(playbooks/php): run repo_sury on the Debian os family --- playbooks/php.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/playbooks/php.yml b/playbooks/php.yml index b569cc22..22662e7f 100644 --- a/playbooks/php.yml +++ b/playbooks/php.yml @@ -29,6 +29,10 @@ - 'ansible_facts["os_family"] == "RedHat"' - 'not php__skip_repo_remi | default(false)' + - role: 'linuxfabrik.lfops.repo_sury' + when: + - 'ansible_facts["os_family"] == "Debian"' + - role: 'linuxfabrik.lfops.php' From 4f46f1b786eaae0001ff5e4a48ccb7c2cd080014 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 9 Jun 2026 17:13:47 +0200 Subject: [PATCH 65/66] refactor(roles/php): use standard Jinja2 delimiters in the pool config template Switch pool.conf.j2 from the custom [% %] block delimiters back to the default {% %} to match roles/example. The custom delimiters were only needed because the access.format comments contain literal %{%Y...%z}t strftime examples; those two lines are now wrapped in {% raw -%} ... {% endraw %} instead. Rendered output is byte-identical. --- .../php/templates/etc/php-fpm.d/pool.conf.j2 | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/roles/php/templates/etc/php-fpm.d/pool.conf.j2 b/roles/php/templates/etc/php-fpm.d/pool.conf.j2 index ccf2530c..e18600a1 100644 --- a/roles/php/templates/etc/php-fpm.d/pool.conf.j2 +++ b/roles/php/templates/etc/php-fpm.d/pool.conf.j2 @@ -1,9 +1,8 @@ -#jinja2:block_start_string:'[%', block_end_string:'%]' ; {{ ansible_managed }} ; 2026060402 -[% if item['by_role'] | d() %] +{% if item['by_role'] | d() %} ; Generated by Ansible role: {{ item['by_role'] }} -[% endif %] +{% endif %} ; Start a new pool named 'www'. ; the variable $pool can be used in any directive and will be replaced by the @@ -56,23 +55,23 @@ listen = {{ __php__fpm_runtime_path ~ '/' ~ item['name']}}.sock ; The owner and group can be specified either by name or by their numeric IDs. ; Default Values: Owner is set to the master process running user. If the group ; is not set, the owner's group is used. Mode is set to 0660. -[% if ansible_facts["os_family"] == 'Debian' %] +{% if ansible_facts["os_family"] == 'Debian' %} listen.owner = {{ __shared__apache_httpd_user }} listen.group = {{ __shared__apache_httpd_group }} -[% else %] +{% else %} ;listen.owner = nobody ;listen.group = nobody -[% endif %] +{% endif %} ;listen.mode = 0660 ; When POSIX Access Control Lists are supported you can set them using ; these options, value is a comma separated list of user/group names. ; When set, listen.owner and listen.group are ignored -[% if ansible_facts["os_family"] == 'RedHat' %] +{% if ansible_facts["os_family"] == 'RedHat' %} listen.acl_users = {{ __shared__apache_httpd_user }} -[% else %] +{% else %} ;listen.acl_users = -[% endif %] +{% endif %} ;listen.acl_groups = ; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect. @@ -352,12 +351,16 @@ ping.response = pong ; it can accept a strftime(3) format: ; %d/%b/%Y:%H:%M:%S %z (default) ; The strftime(3) format must be encapsulated in a %{}t tag +{% raw -%} ; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t +{% endraw %} ; %T: time the log has been written (the request has finished) ; it can accept a strftime(3) format: ; %d/%b/%Y:%H:%M:%S %z (default) ; The strftime(3) format must be encapsulated in a %{}t tag +{% raw -%} ; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t +{% endraw %} ; %u: basic auth user if specified in Authorization header ; ; Default: "%R - %u %t \"%m %r\" %s" @@ -500,11 +503,11 @@ php_admin_value[error_log] = /var/log/{{ __php__fpm_service_name }}/{{ item['nam php_admin_value[max_execution_time] = {{ item['php_admin_value_max_execution_time'] | d(php__ini_max_execution_time__combined_var) }} php_admin_value[max_input_vars] = {{ item['php_admin_value_max_input_vars'] | d(php__ini_max_input_vars__combined_var) }} php_admin_value[memory_limit] = {{ item['php_admin_value_memory_limit'] | d(php__ini_memory_limit__combined_var) }} -[% if item['php_admin_value_open_basedir'] | d() %] +{% if item['php_admin_value_open_basedir'] | d() %} php_admin_value[open_basedir] = {{ item['php_admin_value_open_basedir'] }} -[% else %] +{% else %} ;php_admin_value[open_basedir] = -[% endif %] +{% endif %} php_admin_value[post_max_size] = {{ item['php_admin_value_post_max_size'] | d(php__ini_post_max_size__combined_var) }} php_admin_value[upload_max_filesize] = {{ item['php_admin_value_upload_max_filesize'] | d(php__ini_upload_max_filesize__combined_var) }} @@ -518,8 +521,8 @@ php_admin_value[upload_max_filesize] = {{ item['php_admin_value_upload_max_files ; at http://php.net/session.save-path php_admin_value[session.save_handler] = files php_admin_value[session.save_path] = {{ item['php_admin_value_session_save_path'] | d(__php__fpm_session_path ~ '/' ~ item['name']) }} -[% if item['raw'] | d() %] +{% if item['raw'] | d() %} ; raw content {{ item['raw'] }} -[% endif %] +{% endif %} From 5935f50a18ff7af118bb2f64377b8517ad4a4dbe Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 9 Jun 2026 17:20:45 +0200 Subject: [PATCH 66/66] refactor(roles/php): drop dead default() fallbacks from the .ini templates The php__ini_*__combined_var values always resolve via vars/.yml (loaded with include_vars before the template runs), so the | default(...) / | d(...) fallbacks in the .ini templates never fired. Several of them also contradicted the version defaults, which was misleading. Remove all 251 dead fallbacks; rendered output is unchanged. --- .../etc/php.d/7.2-z00-linuxfabrik.ini.j2 | 56 +++++++++---------- .../etc/php.d/7.3-z00-linuxfabrik.ini.j2 | 56 +++++++++---------- .../etc/php.d/7.4-z00-linuxfabrik.ini.j2 | 56 +++++++++---------- .../etc/php.d/8.0-z00-linuxfabrik.ini.j2 | 56 +++++++++---------- .../etc/php.d/8.1-z00-linuxfabrik.ini.j2 | 56 +++++++++---------- .../etc/php.d/8.2-z00-linuxfabrik.ini.j2 | 56 +++++++++---------- .../etc/php.d/8.3-z00-linuxfabrik.ini.j2 | 56 +++++++++---------- .../etc/php.d/8.4-z00-linuxfabrik.ini.j2 | 56 +++++++++---------- .../etc/php.d/8.5-z00-linuxfabrik.ini.j2 | 54 +++++++++--------- 9 files changed, 251 insertions(+), 251 deletions(-) diff --git a/roles/php/templates/etc/php.d/7.2-z00-linuxfabrik.ini.j2 b/roles/php/templates/etc/php.d/7.2-z00-linuxfabrik.ini.j2 index 6264f52e..05d122d5 100644 --- a/roles/php/templates/etc/php.d/7.2-z00-linuxfabrik.ini.j2 +++ b/roles/php/templates/etc/php.d/7.2-z00-linuxfabrik.ini.j2 @@ -3,43 +3,43 @@ ; php 7.2 [PHP] -date.timezone = {{ php__ini_date_timezone__combined_var | default('Europe/Zurich') }} -default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var | default('60') }} -display_errors = {{ php__ini_display_errors__combined_var | default('off') }} -display_startup_errors = {{ php__ini_display_startup_errors__combined_var | default('off') }} -error_reporting = {{ php__ini_error_reporting__combined_var | default('E_ALL & ~E_DEPRECATED & ~E_STRICT') }} +date.timezone = {{ php__ini_date_timezone__combined_var }} +default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var }} +display_errors = {{ php__ini_display_errors__combined_var }} +display_startup_errors = {{ php__ini_display_startup_errors__combined_var }} +error_reporting = {{ php__ini_error_reporting__combined_var }} expose_php = Off html_errors = Off -max_execution_time = {{ php__ini_max_execution_time__combined_var | default('30') }} -max_file_uploads = {{ php__ini_max_file_uploads__combined_var | default('20') }} -max_input_time = {{ php__ini_max_input_time__combined_var | default('60') }} -max_input_vars = {{ php__ini_max_input_vars__combined_var | default('1000') }} -memory_limit = {{ php__ini_memory_limit__combined_var | default('128M') }} -post_max_size = {{ php__ini_post_max_size__combined_var | default('8M') }} +max_execution_time = {{ php__ini_max_execution_time__combined_var }} +max_file_uploads = {{ php__ini_max_file_uploads__combined_var }} +max_input_time = {{ php__ini_max_input_time__combined_var }} +max_input_vars = {{ php__ini_max_input_vars__combined_var }} +memory_limit = {{ php__ini_memory_limit__combined_var }} +post_max_size = {{ php__ini_post_max_size__combined_var }} realpath_cache_size = 4M realpath_cache_ttl = 120 serialize_precision = -1 -upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var | default('2M') }} +upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var }} -opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var | d('/etc/php.d/opcache*.blacklist') }} -opcache.enable = {{ php__ini_opcache_enable__combined_var | d('1') }} -opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var | d('0') }} -opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var | d('1') }} -opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var | d('8') }} -opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var | d('10000') }} -opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var | d('128') }} -opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var | d('2') }} -opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var | d('1') }} -opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var | d('1') }} +opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var }} +opcache.enable = {{ php__ini_opcache_enable__combined_var }} +opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var }} +opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var }} +opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var }} +opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var }} +opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var }} +opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var }} +opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var }} +opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var }} [mail function] mail.add_x_header = On -SMTP = {{ php__ini_smtp__combined_var | default('localhost') }} +SMTP = {{ php__ini_smtp__combined_var }} smtp_port = 25 [Session] -session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var | default('off') }} -session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var | default('off') }} -session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var | default(1440) }} -session.sid_length = {{ php__ini_session_sid_length__combined_var | default(26) }} -session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var | default("a=href,area=href,frame=src,form=") }}" +session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var }} +session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }} +session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }} +session.sid_length = {{ php__ini_session_sid_length__combined_var }} +session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}" diff --git a/roles/php/templates/etc/php.d/7.3-z00-linuxfabrik.ini.j2 b/roles/php/templates/etc/php.d/7.3-z00-linuxfabrik.ini.j2 index 95f735af..ede55bfe 100644 --- a/roles/php/templates/etc/php.d/7.3-z00-linuxfabrik.ini.j2 +++ b/roles/php/templates/etc/php.d/7.3-z00-linuxfabrik.ini.j2 @@ -3,43 +3,43 @@ ; php 7.3 [PHP] -date.timezone = {{ php__ini_date_timezone__combined_var | default('Europe/Zurich') }} -default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var | default('60') }} -display_errors = {{ php__ini_display_errors__combined_var | default('off') }} -display_startup_errors = {{ php__ini_display_startup_errors__combined_var | default('off') }} -error_reporting = {{ php__ini_error_reporting__combined_var | default('E_ALL & ~E_DEPRECATED & ~E_STRICT') }} +date.timezone = {{ php__ini_date_timezone__combined_var }} +default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var }} +display_errors = {{ php__ini_display_errors__combined_var }} +display_startup_errors = {{ php__ini_display_startup_errors__combined_var }} +error_reporting = {{ php__ini_error_reporting__combined_var }} expose_php = Off html_errors = Off -max_execution_time = {{ php__ini_max_execution_time__combined_var | default('30') }} -max_file_uploads = {{ php__ini_max_file_uploads__combined_var | default('20') }} -max_input_time = {{ php__ini_max_input_time__combined_var | default('60') }} -max_input_vars = {{ php__ini_max_input_vars__combined_var | default('1000') }} -memory_limit = {{ php__ini_memory_limit__combined_var | default('128M') }} -post_max_size = {{ php__ini_post_max_size__combined_var | default('8M') }} +max_execution_time = {{ php__ini_max_execution_time__combined_var }} +max_file_uploads = {{ php__ini_max_file_uploads__combined_var }} +max_input_time = {{ php__ini_max_input_time__combined_var }} +max_input_vars = {{ php__ini_max_input_vars__combined_var }} +memory_limit = {{ php__ini_memory_limit__combined_var }} +post_max_size = {{ php__ini_post_max_size__combined_var }} realpath_cache_size = 4M realpath_cache_ttl = 120 serialize_precision = -1 -upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var | default('2M') }} +upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var }} -opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var | d('/etc/php.d/opcache*.blacklist') }} -opcache.enable = {{ php__ini_opcache_enable__combined_var | d('1') }} -opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var | d('0') }} -opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var | d('1') }} -opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var | d('8') }} -opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var | d('10000') }} -opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var | d('128') }} -opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var | d('2') }} -opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var | d('1') }} -opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var | d('1') }} +opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var }} +opcache.enable = {{ php__ini_opcache_enable__combined_var }} +opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var }} +opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var }} +opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var }} +opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var }} +opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var }} +opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var }} +opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var }} +opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var }} [mail function] mail.add_x_header = On -SMTP = {{ php__ini_smtp__combined_var | default('localhost') }} +SMTP = {{ php__ini_smtp__combined_var }} smtp_port = 25 [Session] -session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var | default('off') }} -session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var | default('off') }} -session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var | default(1440) }} -session.sid_length = {{ php__ini_session_sid_length__combined_var | default(26) }} -session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var | default("a=href,area=href,frame=src,form=") }}" +session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var }} +session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }} +session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }} +session.sid_length = {{ php__ini_session_sid_length__combined_var }} +session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}" diff --git a/roles/php/templates/etc/php.d/7.4-z00-linuxfabrik.ini.j2 b/roles/php/templates/etc/php.d/7.4-z00-linuxfabrik.ini.j2 index 0f6d60db..d7b44c71 100644 --- a/roles/php/templates/etc/php.d/7.4-z00-linuxfabrik.ini.j2 +++ b/roles/php/templates/etc/php.d/7.4-z00-linuxfabrik.ini.j2 @@ -3,43 +3,43 @@ ; php 7.4 [PHP] -date.timezone = {{ php__ini_date_timezone__combined_var | default('Europe/Zurich') }} -default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var | default('60') }} -display_errors = {{ php__ini_display_errors__combined_var | default('off') }} -display_startup_errors = {{ php__ini_display_startup_errors__combined_var | default('off') }} -error_reporting = {{ php__ini_error_reporting__combined_var | default('E_ALL & ~E_DEPRECATED & ~E_STRICT') }} +date.timezone = {{ php__ini_date_timezone__combined_var }} +default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var }} +display_errors = {{ php__ini_display_errors__combined_var }} +display_startup_errors = {{ php__ini_display_startup_errors__combined_var }} +error_reporting = {{ php__ini_error_reporting__combined_var }} expose_php = Off html_errors = Off -max_execution_time = {{ php__ini_max_execution_time__combined_var | default('30') }} -max_file_uploads = {{ php__ini_max_file_uploads__combined_var | default('20') }} -max_input_time = {{ php__ini_max_input_time__combined_var | default('60') }} -max_input_vars = {{ php__ini_max_input_vars__combined_var | default('1000') }} -memory_limit = {{ php__ini_memory_limit__combined_var | default('128M') }} -post_max_size = {{ php__ini_post_max_size__combined_var | default('8M') }} +max_execution_time = {{ php__ini_max_execution_time__combined_var }} +max_file_uploads = {{ php__ini_max_file_uploads__combined_var }} +max_input_time = {{ php__ini_max_input_time__combined_var }} +max_input_vars = {{ php__ini_max_input_vars__combined_var }} +memory_limit = {{ php__ini_memory_limit__combined_var }} +post_max_size = {{ php__ini_post_max_size__combined_var }} realpath_cache_size = 4M realpath_cache_ttl = 120 serialize_precision = -1 -upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var | default('2M') }} +upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var }} -opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var | d('/etc/php.d/opcache*.blacklist') }} -opcache.enable = {{ php__ini_opcache_enable__combined_var | d('1') }} -opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var | d('0') }} -opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var | d('1') }} -opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var | d('8') }} -opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var | d('10000') }} -opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var | d('128') }} -opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var | d('2') }} -opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var | d('1') }} -opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var | d('1') }} +opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var }} +opcache.enable = {{ php__ini_opcache_enable__combined_var }} +opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var }} +opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var }} +opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var }} +opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var }} +opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var }} +opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var }} +opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var }} +opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var }} [mail function] mail.add_x_header = On -SMTP = {{ php__ini_smtp__combined_var | default('localhost') }} +SMTP = {{ php__ini_smtp__combined_var }} smtp_port = 25 [Session] -session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var | default('off') }} -session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var | default('off') }} -session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var | default(1440) }} -session.sid_length = {{ php__ini_session_sid_length__combined_var | default(26) }} -session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var | default("a=href,area=href,frame=src,form=") }}" +session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var }} +session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }} +session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }} +session.sid_length = {{ php__ini_session_sid_length__combined_var }} +session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}" diff --git a/roles/php/templates/etc/php.d/8.0-z00-linuxfabrik.ini.j2 b/roles/php/templates/etc/php.d/8.0-z00-linuxfabrik.ini.j2 index d15e77ba..27403c68 100644 --- a/roles/php/templates/etc/php.d/8.0-z00-linuxfabrik.ini.j2 +++ b/roles/php/templates/etc/php.d/8.0-z00-linuxfabrik.ini.j2 @@ -3,43 +3,43 @@ ; php 8.0 [PHP] -date.timezone = {{ php__ini_date_timezone__combined_var | default('Europe/Zurich') }} -default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var | default('60') }} -display_errors = {{ php__ini_display_errors__combined_var | default('off') }} -display_startup_errors = {{ php__ini_display_startup_errors__combined_var | default('off') }} -error_reporting = {{ php__ini_error_reporting__combined_var | default('E_ALL & ~E_DEPRECATED & ~E_STRICT') }} +date.timezone = {{ php__ini_date_timezone__combined_var }} +default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var }} +display_errors = {{ php__ini_display_errors__combined_var }} +display_startup_errors = {{ php__ini_display_startup_errors__combined_var }} +error_reporting = {{ php__ini_error_reporting__combined_var }} expose_php = Off html_errors = Off -max_execution_time = {{ php__ini_max_execution_time__combined_var | default('30') }} -max_file_uploads = {{ php__ini_max_file_uploads__combined_var | default('20') }} -max_input_time = {{ php__ini_max_input_time__combined_var | default('60') }} -max_input_vars = {{ php__ini_max_input_vars__combined_var | default('1000') }} -memory_limit = {{ php__ini_memory_limit__combined_var | default('128M') }} -post_max_size = {{ php__ini_post_max_size__combined_var | default('8M') }} +max_execution_time = {{ php__ini_max_execution_time__combined_var }} +max_file_uploads = {{ php__ini_max_file_uploads__combined_var }} +max_input_time = {{ php__ini_max_input_time__combined_var }} +max_input_vars = {{ php__ini_max_input_vars__combined_var }} +memory_limit = {{ php__ini_memory_limit__combined_var }} +post_max_size = {{ php__ini_post_max_size__combined_var }} realpath_cache_size = 4M realpath_cache_ttl = 120 serialize_precision = -1 -upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var | default('2M') }} +upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var }} -opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var | d('/etc/php.d/opcache*.blacklist') }} -opcache.enable = {{ php__ini_opcache_enable__combined_var | d('1') }} -opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var | d('0') }} -opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var | d('1') }} -opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var | d('8') }} -opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var | d('10000') }} -opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var | d('128') }} -opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var | d('2') }} -opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var | d('1') }} -opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var | d('1') }} +opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var }} +opcache.enable = {{ php__ini_opcache_enable__combined_var }} +opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var }} +opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var }} +opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var }} +opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var }} +opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var }} +opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var }} +opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var }} +opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var }} [mail function] mail.add_x_header = On -SMTP = {{ php__ini_smtp__combined_var | default('localhost') }} +SMTP = {{ php__ini_smtp__combined_var }} smtp_port = 25 [Session] -session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var | default('off') }} -session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var | default('off') }} -session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var | default(1440) }} -session.sid_length = {{ php__ini_session_sid_length__combined_var | default(26) }} -session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var | default("a=href,area=href,frame=src,form=") }}" +session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var }} +session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }} +session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }} +session.sid_length = {{ php__ini_session_sid_length__combined_var }} +session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}" diff --git a/roles/php/templates/etc/php.d/8.1-z00-linuxfabrik.ini.j2 b/roles/php/templates/etc/php.d/8.1-z00-linuxfabrik.ini.j2 index c502c5d8..5d2c33be 100644 --- a/roles/php/templates/etc/php.d/8.1-z00-linuxfabrik.ini.j2 +++ b/roles/php/templates/etc/php.d/8.1-z00-linuxfabrik.ini.j2 @@ -3,43 +3,43 @@ ; php 8.1 [PHP] -date.timezone = {{ php__ini_date_timezone__combined_var | default('Europe/Zurich') }} -default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var | default('60') }} -display_errors = {{ php__ini_display_errors__combined_var | default('off') }} -display_startup_errors = {{ php__ini_display_startup_errors__combined_var | default('off') }} -error_reporting = {{ php__ini_error_reporting__combined_var | default('E_ALL & ~E_DEPRECATED & ~E_STRICT') }} +date.timezone = {{ php__ini_date_timezone__combined_var }} +default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var }} +display_errors = {{ php__ini_display_errors__combined_var }} +display_startup_errors = {{ php__ini_display_startup_errors__combined_var }} +error_reporting = {{ php__ini_error_reporting__combined_var }} expose_php = Off html_errors = Off -max_execution_time = {{ php__ini_max_execution_time__combined_var | default('30') }} -max_file_uploads = {{ php__ini_max_file_uploads__combined_var | default('20') }} -max_input_time = {{ php__ini_max_input_time__combined_var | default('60') }} -max_input_vars = {{ php__ini_max_input_vars__combined_var | default('1000') }} -memory_limit = {{ php__ini_memory_limit__combined_var | default('128M') }} -post_max_size = {{ php__ini_post_max_size__combined_var | default('8M') }} +max_execution_time = {{ php__ini_max_execution_time__combined_var }} +max_file_uploads = {{ php__ini_max_file_uploads__combined_var }} +max_input_time = {{ php__ini_max_input_time__combined_var }} +max_input_vars = {{ php__ini_max_input_vars__combined_var }} +memory_limit = {{ php__ini_memory_limit__combined_var }} +post_max_size = {{ php__ini_post_max_size__combined_var }} realpath_cache_size = 4M realpath_cache_ttl = 120 serialize_precision = -1 -upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var | default('2M') }} +upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var }} -opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var | d('/etc/php.d/opcache*.blacklist') }} -opcache.enable = {{ php__ini_opcache_enable__combined_var | d('1') }} -opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var | d('0') }} -opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var | d('1') }} -opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var | d('8') }} -opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var | d('10000') }} -opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var | d('128') }} -opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var | d('2') }} -opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var | d('1') }} -opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var | d('1') }} +opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var }} +opcache.enable = {{ php__ini_opcache_enable__combined_var }} +opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var }} +opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var }} +opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var }} +opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var }} +opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var }} +opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var }} +opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var }} +opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var }} [mail function] mail.add_x_header = On -SMTP = {{ php__ini_smtp__combined_var | default('localhost') }} +SMTP = {{ php__ini_smtp__combined_var }} smtp_port = 25 [Session] -session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var | default('off') }} -session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var | default('off') }} -session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var | default(1440) }} -session.sid_length = {{ php__ini_session_sid_length__combined_var | default(26) }} -session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var | default("a=href,area=href,frame=src,form=") }}" +session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var }} +session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }} +session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }} +session.sid_length = {{ php__ini_session_sid_length__combined_var }} +session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}" diff --git a/roles/php/templates/etc/php.d/8.2-z00-linuxfabrik.ini.j2 b/roles/php/templates/etc/php.d/8.2-z00-linuxfabrik.ini.j2 index 985fa425..c437d033 100644 --- a/roles/php/templates/etc/php.d/8.2-z00-linuxfabrik.ini.j2 +++ b/roles/php/templates/etc/php.d/8.2-z00-linuxfabrik.ini.j2 @@ -3,43 +3,43 @@ ; php 8.2 [PHP] -date.timezone = {{ php__ini_date_timezone__combined_var | default('Europe/Zurich') }} -default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var | default('60') }} -display_errors = {{ php__ini_display_errors__combined_var | default('off') }} -display_startup_errors = {{ php__ini_display_startup_errors__combined_var | default('off') }} -error_reporting = {{ php__ini_error_reporting__combined_var | default('E_ALL & ~E_DEPRECATED & ~E_STRICT') }} +date.timezone = {{ php__ini_date_timezone__combined_var }} +default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var }} +display_errors = {{ php__ini_display_errors__combined_var }} +display_startup_errors = {{ php__ini_display_startup_errors__combined_var }} +error_reporting = {{ php__ini_error_reporting__combined_var }} expose_php = Off html_errors = Off -max_execution_time = {{ php__ini_max_execution_time__combined_var | default('30') }} -max_file_uploads = {{ php__ini_max_file_uploads__combined_var | default('20') }} -max_input_time = {{ php__ini_max_input_time__combined_var | default('60') }} -max_input_vars = {{ php__ini_max_input_vars__combined_var | default('1000') }} -memory_limit = {{ php__ini_memory_limit__combined_var | default('128M') }} -post_max_size = {{ php__ini_post_max_size__combined_var | default('8M') }} +max_execution_time = {{ php__ini_max_execution_time__combined_var }} +max_file_uploads = {{ php__ini_max_file_uploads__combined_var }} +max_input_time = {{ php__ini_max_input_time__combined_var }} +max_input_vars = {{ php__ini_max_input_vars__combined_var }} +memory_limit = {{ php__ini_memory_limit__combined_var }} +post_max_size = {{ php__ini_post_max_size__combined_var }} realpath_cache_size = 4M realpath_cache_ttl = 120 serialize_precision = -1 -upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var | default('2M') }} +upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var }} -opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var | d('/etc/php.d/opcache*.blacklist') }} -opcache.enable = {{ php__ini_opcache_enable__combined_var | d('1') }} -opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var | d('0') }} -opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var | d('1') }} -opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var | d('8') }} -opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var | d('10000') }} -opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var | d('128') }} -opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var | d('2') }} -opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var | d('1') }} -opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var | d('1') }} +opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var }} +opcache.enable = {{ php__ini_opcache_enable__combined_var }} +opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var }} +opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var }} +opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var }} +opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var }} +opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var }} +opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var }} +opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var }} +opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var }} [mail function] mail.add_x_header = On -SMTP = {{ php__ini_smtp__combined_var | default('localhost') }} +SMTP = {{ php__ini_smtp__combined_var }} smtp_port = 25 [Session] -session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var | default('off') }} -session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var | default('off') }} -session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var | default(1440) }} -session.sid_length = {{ php__ini_session_sid_length__combined_var | default(26) }} -session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var | default("a=href,area=href,frame=src,form=") }}" +session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var }} +session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }} +session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }} +session.sid_length = {{ php__ini_session_sid_length__combined_var }} +session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}" diff --git a/roles/php/templates/etc/php.d/8.3-z00-linuxfabrik.ini.j2 b/roles/php/templates/etc/php.d/8.3-z00-linuxfabrik.ini.j2 index d9691c7b..d22dc0f3 100644 --- a/roles/php/templates/etc/php.d/8.3-z00-linuxfabrik.ini.j2 +++ b/roles/php/templates/etc/php.d/8.3-z00-linuxfabrik.ini.j2 @@ -3,43 +3,43 @@ ; php 8.3 [PHP] -date.timezone = {{ php__ini_date_timezone__combined_var | default('Europe/Zurich') }} -default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var | default('60') }} -display_errors = {{ php__ini_display_errors__combined_var | default('off') }} -display_startup_errors = {{ php__ini_display_startup_errors__combined_var | default('off') }} -error_reporting = {{ php__ini_error_reporting__combined_var | default('E_ALL & ~E_DEPRECATED & ~E_STRICT') }} +date.timezone = {{ php__ini_date_timezone__combined_var }} +default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var }} +display_errors = {{ php__ini_display_errors__combined_var }} +display_startup_errors = {{ php__ini_display_startup_errors__combined_var }} +error_reporting = {{ php__ini_error_reporting__combined_var }} expose_php = Off html_errors = Off -max_execution_time = {{ php__ini_max_execution_time__combined_var | default('30') }} -max_file_uploads = {{ php__ini_max_file_uploads__combined_var | default('20') }} -max_input_time = {{ php__ini_max_input_time__combined_var | default('60') }} -max_input_vars = {{ php__ini_max_input_vars__combined_var | default('1000') }} -memory_limit = {{ php__ini_memory_limit__combined_var | default('128M') }} -post_max_size = {{ php__ini_post_max_size__combined_var | default('8M') }} +max_execution_time = {{ php__ini_max_execution_time__combined_var }} +max_file_uploads = {{ php__ini_max_file_uploads__combined_var }} +max_input_time = {{ php__ini_max_input_time__combined_var }} +max_input_vars = {{ php__ini_max_input_vars__combined_var }} +memory_limit = {{ php__ini_memory_limit__combined_var }} +post_max_size = {{ php__ini_post_max_size__combined_var }} realpath_cache_size = 4M realpath_cache_ttl = 120 serialize_precision = -1 -upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var | default('2M') }} +upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var }} -opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var | d('/etc/php.d/opcache*.blacklist') }} -opcache.enable = {{ php__ini_opcache_enable__combined_var | d('1') }} -opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var | d('1') }} -opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var | d('1') }} -opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var | d('8') }} -opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var | d('10000') }} -opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var | d('128') }} -opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var | d('2') }} -opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var | d('1') }} -opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var | d('1') }} +opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var }} +opcache.enable = {{ php__ini_opcache_enable__combined_var }} +opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var }} +opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var }} +opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var }} +opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var }} +opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var }} +opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var }} +opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var }} +opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var }} [mail function] mail.add_x_header = Off -SMTP = {{ php__ini_smtp__combined_var | default('localhost') }} +SMTP = {{ php__ini_smtp__combined_var }} smtp_port = 25 [Session] -session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var | default('off') }} -session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var | default('off') }} -session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var | default(1440) }} -session.sid_length = {{ php__ini_session_sid_length__combined_var | default(26) }} -session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var | default("a=href,area=href,frame=src,form=") }}" +session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var }} +session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }} +session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }} +session.sid_length = {{ php__ini_session_sid_length__combined_var }} +session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}" diff --git a/roles/php/templates/etc/php.d/8.4-z00-linuxfabrik.ini.j2 b/roles/php/templates/etc/php.d/8.4-z00-linuxfabrik.ini.j2 index c20723f6..78802e83 100644 --- a/roles/php/templates/etc/php.d/8.4-z00-linuxfabrik.ini.j2 +++ b/roles/php/templates/etc/php.d/8.4-z00-linuxfabrik.ini.j2 @@ -3,43 +3,43 @@ ; php 8.4 [PHP] -date.timezone = {{ php__ini_date_timezone__combined_var | default('Europe/Zurich') }} -default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var | default('60') }} -display_errors = {{ php__ini_display_errors__combined_var | default('off') }} -display_startup_errors = {{ php__ini_display_startup_errors__combined_var | default('off') }} -error_reporting = {{ php__ini_error_reporting__combined_var | default('E_ALL & ~E_DEPRECATED & ~E_STRICT') }} +date.timezone = {{ php__ini_date_timezone__combined_var }} +default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var }} +display_errors = {{ php__ini_display_errors__combined_var }} +display_startup_errors = {{ php__ini_display_startup_errors__combined_var }} +error_reporting = {{ php__ini_error_reporting__combined_var }} expose_php = Off html_errors = Off -max_execution_time = {{ php__ini_max_execution_time__combined_var | default('30') }} -max_file_uploads = {{ php__ini_max_file_uploads__combined_var | default('20') }} -max_input_time = {{ php__ini_max_input_time__combined_var | default('60') }} -max_input_vars = {{ php__ini_max_input_vars__combined_var | default('1000') }} -memory_limit = {{ php__ini_memory_limit__combined_var | default('128M') }} -post_max_size = {{ php__ini_post_max_size__combined_var | default('8M') }} +max_execution_time = {{ php__ini_max_execution_time__combined_var }} +max_file_uploads = {{ php__ini_max_file_uploads__combined_var }} +max_input_time = {{ php__ini_max_input_time__combined_var }} +max_input_vars = {{ php__ini_max_input_vars__combined_var }} +memory_limit = {{ php__ini_memory_limit__combined_var }} +post_max_size = {{ php__ini_post_max_size__combined_var }} realpath_cache_size = 4M realpath_cache_ttl = 120 serialize_precision = -1 -upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var | default('2M') }} +upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var }} -opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var | d('/etc/php.d/opcache*.blacklist') }} -opcache.enable = {{ php__ini_opcache_enable__combined_var | d('1') }} -opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var | d('1') }} -opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var | d('1') }} -opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var | d('8') }} -opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var | d('10000') }} -opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var | d('128') }} -opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var | d('2') }} -opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var | d('1') }} -opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var | d('1') }} +opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var }} +opcache.enable = {{ php__ini_opcache_enable__combined_var }} +opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var }} +opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var }} +opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var }} +opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var }} +opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var }} +opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var }} +opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var }} +opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var }} [mail function] mail.add_x_header = Off -SMTP = {{ php__ini_smtp__combined_var | default('localhost') }} +SMTP = {{ php__ini_smtp__combined_var }} smtp_port = 25 [Session] -session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var | default('off') }} -session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var | default('off') }} -session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var | default(1440) }} -session.sid_length = {{ php__ini_session_sid_length__combined_var | default(26) }} -session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var | default("a=href,area=href,frame=src,form=") }}" +session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var }} +session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }} +session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }} +session.sid_length = {{ php__ini_session_sid_length__combined_var }} +session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}" diff --git a/roles/php/templates/etc/php.d/8.5-z00-linuxfabrik.ini.j2 b/roles/php/templates/etc/php.d/8.5-z00-linuxfabrik.ini.j2 index 620143f3..e83f4378 100644 --- a/roles/php/templates/etc/php.d/8.5-z00-linuxfabrik.ini.j2 +++ b/roles/php/templates/etc/php.d/8.5-z00-linuxfabrik.ini.j2 @@ -3,42 +3,42 @@ ; php 8.5 [PHP] -date.timezone = {{ php__ini_date_timezone__combined_var | default('Europe/Zurich') }} -default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var | default('60') }} -display_errors = {{ php__ini_display_errors__combined_var | default('On') }} -display_startup_errors = {{ php__ini_display_startup_errors__combined_var | default('On') }} -error_reporting = {{ php__ini_error_reporting__combined_var | default('E_ALL') }} +date.timezone = {{ php__ini_date_timezone__combined_var }} +default_socket_timeout = {{ php__ini_default_socket_timeout__combined_var }} +display_errors = {{ php__ini_display_errors__combined_var }} +display_startup_errors = {{ php__ini_display_startup_errors__combined_var }} +error_reporting = {{ php__ini_error_reporting__combined_var }} expose_php = Off ; differs from default on purpose due to security reasons html_errors = Off ; differs from default on purpose due to security reasons -max_execution_time = {{ php__ini_max_execution_time__combined_var | default('30') }} -max_file_uploads = {{ php__ini_max_file_uploads__combined_var | default('20') }} -max_input_time = {{ php__ini_max_input_time__combined_var | default('-1') }} -max_input_vars = {{ php__ini_max_input_vars__combined_var | default('1000') }} -memory_limit = {{ php__ini_memory_limit__combined_var | default('128M') }} -post_max_size = {{ php__ini_post_max_size__combined_var | default('8M') }} +max_execution_time = {{ php__ini_max_execution_time__combined_var }} +max_file_uploads = {{ php__ini_max_file_uploads__combined_var }} +max_input_time = {{ php__ini_max_input_time__combined_var }} +max_input_vars = {{ php__ini_max_input_vars__combined_var }} +memory_limit = {{ php__ini_memory_limit__combined_var }} +post_max_size = {{ php__ini_post_max_size__combined_var }} realpath_cache_size = 4M realpath_cache_ttl = 120 serialize_precision = -1 -upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var | default('2M') }} +upload_max_filesize = {{ php__ini_upload_max_filesize__combined_var }} -opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var | d('/etc/php.d/opcache*.blacklist') }} -opcache.enable = {{ php__ini_opcache_enable__combined_var | d('1') }} -opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var | d('0') }} -opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var | d('0') }} -opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var | d('8') }} -opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var | d('10000') }} -opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var | d('128') }} -opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var | d('2') }} -opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var | d('1') }} -opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var | d('1') }} +opcache.blacklist_filename = {{ php__ini_opcache_blacklist_filename__combined_var }} +opcache.enable = {{ php__ini_opcache_enable__combined_var }} +opcache.enable_cli = {{ php__ini_opcache_enable_cli__combined_var }} +opcache.huge_code_pages = {{ php__ini_opcache_huge_code_pages__combined_var }} +opcache.interned_strings_buffer = {{ php__ini_opcache_interned_strings_buffer__combined_var }} +opcache.max_accelerated_files = {{ php__ini_opcache_max_accelerated_files__combined_var }} +opcache.memory_consumption = {{ php__ini_opcache_memory_consumption__combined_var }} +opcache.revalidate_freq = {{ php__ini_opcache_revalidate_freq__combined_var }} +opcache.save_comments = {{ php__ini_opcache_save_comments__combined_var }} +opcache.validate_timestamps = {{ php__ini_opcache_validate_timestamps__combined_var }} [mail function] mail.add_x_header = Off -SMTP = {{ php__ini_smtp__combined_var | default('localhost') }} +SMTP = {{ php__ini_smtp__combined_var }} smtp_port = 25 [Session] -session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var | default('off') }} -session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var | default('off') }} -session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var | default(1440) }} -session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var | default("a=href,area=href,frame=src,form=") }}" +session.cookie_httponly = {{ php__ini_session_cookie_httponly__combined_var }} +session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }} +session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }} +session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}"