From fab21a690051cff2446dcb16936901ae8f340bf0 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Fri, 18 Sep 2026 14:06:50 +0200 Subject: [PATCH 1/3] feat(roles/wordpress): activate or deactivate plugins via wordpress__plugins Entries in wordpress__plugins accept the new `enabled` subkey (default true). With `enabled: false`, the plugin is installed without --activate and then deactivated with `wp plugin deactivate`. WP-CLI prints "Plugin already deactivated." for an inactive plugin, which keeps the task idempotent. --- CHANGELOG.md | 1 + .../group_vars/systems_under_test.yml | 8 ++++++-- extensions/molecule/setup_wordpress/verify.yml | 12 +++++++++++- roles/wordpress/README.md | 10 +++++++++- roles/wordpress/meta/argument_specs.yml | 2 +- roles/wordpress/tasks/main.yml | 18 ++++++++++++++++-- 6 files changed, 44 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1297c42..a3fab7e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:wordpress**: Entries in `wordpress__plugins` accept `enabled: false`, which keeps a plugin installed but deactivated. * **role:system_update**: The role's inventory variables are type-checked when it starts, so a mistyped value fails the run right away instead of surfacing further in as a confusing error. * **role:wordpress**: Several WordPress instances can share a host as pseudo hosts in the inventory, under different host names as well as under different paths of one host name, such as `https://example.com/blog`. * **role:fail2ban**: The `wordpress-login` filter and `z10-wordpress-login` jail ban IPs with too many failed WordPress logins, on the host whose Apache logs the visitor's address. diff --git a/extensions/molecule/setup_wordpress/inventory/group_vars/systems_under_test.yml b/extensions/molecule/setup_wordpress/inventory/group_vars/systems_under_test.yml index c6150804..c40675b1 100644 --- a/extensions/molecule/setup_wordpress/inventory/group_vars/systems_under_test.yml +++ b/extensions/molecule/setup_wordpress/inventory/group_vars/systems_under_test.yml @@ -16,13 +16,17 @@ wordpress__admin_user: wordpress__database_user: username: 'wordpress' password: 'linuxfabrik' -# One plugin to install and one that is absent and was never installed, so the install, the -# uninstall and their change detection all run, together with a theme. +# One plugin to install, one that is absent and was never installed, and one installed but +# deactivated, so the install, the uninstall, the deactivation and their change detection all +# run, together with a theme. wordpress__plugins: - name: 'hello-dolly' state: 'present' - name: 'akismet' state: 'absent' + - name: 'classic-editor' + state: 'present' + enabled: false wordpress__site_title: 'Molecule' wordpress__theme: 'twentytwentyfive' # 127.0.0.1 stands in for the reverse proxy: verify.yml sends requests with X-Forwarded-* headers diff --git a/extensions/molecule/setup_wordpress/verify.yml b/extensions/molecule/setup_wordpress/verify.yml index ef2fe055..e94720d4 100644 --- a/extensions/molecule/setup_wordpress/verify.yml +++ b/extensions/molecule/setup_wordpress/verify.yml @@ -44,6 +44,14 @@ register: '__molecule__wordpress_plugins_result' changed_when: false + - name: 'wp plugin list --status=inactive --field=name' + ansible.builtin.command: '/usr/local/bin/wp plugin list --status=inactive --field=name --path={{ __molecule__wordpress_dir }}' + args: + chdir: '{{ __molecule__wordpress_dir }}' + become_user: 'apache' + register: '__molecule__wordpress_inactive_plugins_result' + changed_when: false + - name: 'wp theme list --status=active --field=name' ansible.builtin.command: '/usr/local/bin/wp theme list --status=active --field=name --path={{ __molecule__wordpress_dir }}' args: @@ -59,8 +67,10 @@ - '"disable-json-api" not in __molecule__wordpress_plugins_result["stdout_lines"]' - '"hello-dolly" in __molecule__wordpress_plugins_result["stdout_lines"]' - '"akismet" not in __molecule__wordpress_plugins_result["stdout_lines"]' + - '"akismet" not in __molecule__wordpress_inactive_plugins_result["stdout_lines"]' + - '"classic-editor" in __molecule__wordpress_inactive_plugins_result["stdout_lines"]' - '__molecule__wordpress_theme_result["stdout_lines"] == [wordpress__theme]' - fail_msg: 'active plugins: {{ __molecule__wordpress_plugins_result["stdout_lines"] }}, active theme: {{ __molecule__wordpress_theme_result["stdout_lines"] }}' + fail_msg: 'active plugins: {{ __molecule__wordpress_plugins_result["stdout_lines"] }}, inactive plugins: {{ __molecule__wordpress_inactive_plugins_result["stdout_lines"] }}, active theme: {{ __molecule__wordpress_theme_result["stdout_lines"] }}' # --url sets HTTPS for WP-CLI, which application passwords require before any filter applies - name: 'wp eval wp_is_application_passwords_available()' diff --git a/roles/wordpress/README.md b/roles/wordpress/README.md index c4055644..1d56960f 100644 --- a/roles/wordpress/README.md +++ b/roles/wordpress/README.md @@ -201,11 +201,17 @@ wordpress__url: 'https://wordpress.example.com' `wordpress__plugins` -* List of WordPress plugin slugs. To get a list of already installed plugins, use the WordPress CLI `sudo -u apache /usr/local/bin/wp plugin list --status=active`. +* List of WordPress plugin slugs. To get a list of already installed plugins and whether they are active, use the WordPress CLI `sudo -u apache /usr/local/bin/wp plugin list`. * Type: List of dictionaries. * Default: `[]` * Subkeys: + * `enabled`: + + * Optional. Activates the plugin (`true`) or keeps it installed but deactivated (`false`). With `false`, `name` has to be the plugin slug, since WP-CLI cannot deactivate a plugin by the path or URL of its zip file. + * Type: Bool. + * Default: `true` + * `name`: * Mandatory. Plugin slug, path to a local zip file, or URL to a remote zip file. @@ -262,6 +268,8 @@ wordpress__plugins: state: 'present' - name: 'Akismet' state: 'absent' + - name: 'classic-editor' + enabled: false wordpress__theme: 'twentysixteen' wordpress__timer_core_minor_update_enabled: true wordpress__trusted_proxies: diff --git a/roles/wordpress/meta/argument_specs.yml b/roles/wordpress/meta/argument_specs.yml index 0cb7044c..c689c0ac 100644 --- a/roles/wordpress/meta/argument_specs.yml +++ b/roles/wordpress/meta/argument_specs.yml @@ -60,7 +60,7 @@ argument_specs: elements: 'dict' required: false default: [] - description: 'WordPress plugins to install or remove.' + description: 'WordPress plugins to install, remove, activate or deactivate.' wordpress__site_title: type: 'str' diff --git a/roles/wordpress/tasks/main.yml b/roles/wordpress/tasks/main.yml index 8d336b79..2e3f5e3c 100644 --- a/roles/wordpress/tasks/main.yml +++ b/roles/wordpress/tasks/main.yml @@ -140,8 +140,8 @@ when: 'item["state"] | d("present") == "absent"' loop: '{{ wordpress__plugins }}' - - name: 'wp plugin install --url={{ wordpress__url | quote }} {{ item["name"] | lower | quote }} --activate' - ansible.builtin.command: '/usr/local/bin/wp plugin install --url={{ wordpress__url | quote }} {{ item["name"] | lower | quote }} --activate' + - name: 'wp plugin install --url={{ wordpress__url | quote }} {{ item["name"] | lower | quote }}{{ (item["enabled"] | d(true) | bool) | ternary(" --activate", "") }}' + ansible.builtin.command: '/usr/local/bin/wp plugin install --url={{ wordpress__url | quote }} {{ item["name"] | lower | quote }}{{ (item["enabled"] | d(true) | bool) | ternary(" --activate", "") }}' args: chdir: '{{ wordpress__install_dir }}' become: true @@ -152,6 +152,20 @@ when: 'item["state"] | d("present") != "absent"' loop: '{{ wordpress__plugins }}' + - name: 'wp plugin deactivate --url={{ wordpress__url | quote }} {{ item["name"] | lower | quote }}' + ansible.builtin.command: '/usr/local/bin/wp plugin deactivate --url={{ wordpress__url | quote }} {{ item["name"] | lower | quote }}' + args: + chdir: '{{ wordpress__install_dir }}' + become: true + become_user: 'apache' + register: '__wordpress__plugin_deactivate_result' + # WP-CLI prints "Plugin already deactivated." for a plugin that is not active + changed_when: '"Plugin already deactivated." not in __wordpress__plugin_deactivate_result["stdout"]' + when: + - 'item["state"] | d("present") != "absent"' + - 'not item["enabled"] | d(true) | bool' + loop: '{{ wordpress__plugins }}' + - name: 'wp theme install --url={{ wordpress__url | quote }} {{ wordpress__theme | quote }} --activate' ansible.builtin.command: '/usr/local/bin/wp theme install --url={{ wordpress__url | quote }} {{ wordpress__theme | quote }} --activate' args: From 1efe28a75d7ce0a8067ba2ffd50b9d45f224bb7e Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Fri, 18 Sep 2026 14:22:04 +0200 Subject: [PATCH 2/3] style(roles/wordpress): label plugin loops and sort plugin subkeys Label the wordpress__plugins loops with the plugin name, as in roles/example, instead of printing the whole dictionary. Put the subkeys of the Molecule plugin entry in the usual order, the identifying key first and the others alphabetically. --- .../inventory/group_vars/systems_under_test.yml | 2 +- roles/wordpress/tasks/main.yml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/extensions/molecule/setup_wordpress/inventory/group_vars/systems_under_test.yml b/extensions/molecule/setup_wordpress/inventory/group_vars/systems_under_test.yml index c40675b1..dd9f3d49 100644 --- a/extensions/molecule/setup_wordpress/inventory/group_vars/systems_under_test.yml +++ b/extensions/molecule/setup_wordpress/inventory/group_vars/systems_under_test.yml @@ -25,8 +25,8 @@ wordpress__plugins: - name: 'akismet' state: 'absent' - name: 'classic-editor' - state: 'present' enabled: false + state: 'present' wordpress__site_title: 'Molecule' wordpress__theme: 'twentytwentyfive' # 127.0.0.1 stands in for the reverse proxy: verify.yml sends requests with X-Forwarded-* headers diff --git a/roles/wordpress/tasks/main.yml b/roles/wordpress/tasks/main.yml index 2e3f5e3c..9a8642b5 100644 --- a/roles/wordpress/tasks/main.yml +++ b/roles/wordpress/tasks/main.yml @@ -139,6 +139,8 @@ changed_when: '__wordpress__plugin_uninstall_result["rc"] == 0' when: 'item["state"] | d("present") == "absent"' loop: '{{ wordpress__plugins }}' + loop_control: + label: '{{ item["name"] }}' - name: 'wp plugin install --url={{ wordpress__url | quote }} {{ item["name"] | lower | quote }}{{ (item["enabled"] | d(true) | bool) | ternary(" --activate", "") }}' ansible.builtin.command: '/usr/local/bin/wp plugin install --url={{ wordpress__url | quote }} {{ item["name"] | lower | quote }}{{ (item["enabled"] | d(true) | bool) | ternary(" --activate", "") }}' @@ -151,6 +153,8 @@ or "activated." in __wordpress__plugin_install_result["stdout"]' when: 'item["state"] | d("present") != "absent"' loop: '{{ wordpress__plugins }}' + loop_control: + label: '{{ item["name"] }}' - name: 'wp plugin deactivate --url={{ wordpress__url | quote }} {{ item["name"] | lower | quote }}' ansible.builtin.command: '/usr/local/bin/wp plugin deactivate --url={{ wordpress__url | quote }} {{ item["name"] | lower | quote }}' @@ -165,6 +169,8 @@ - 'item["state"] | d("present") != "absent"' - 'not item["enabled"] | d(true) | bool' loop: '{{ wordpress__plugins }}' + loop_control: + label: '{{ item["name"] }}' - name: 'wp theme install --url={{ wordpress__url | quote }} {{ wordpress__theme | quote }} --activate' ansible.builtin.command: '/usr/local/bin/wp theme install --url={{ wordpress__url | quote }} {{ wordpress__theme | quote }} --activate' From 1f996ff4986c2a819cabf52b241e6c008c2fc5f8 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Fri, 18 Sep 2026 14:28:02 +0200 Subject: [PATCH 3/3] docs(roles/wordpress): list the plugin name subkey first Put the identifying name subkey of wordpress__plugins first in the README, as in roles/example, followed by the others alphabetically. --- roles/wordpress/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/roles/wordpress/README.md b/roles/wordpress/README.md index 1d56960f..bfc5f571 100644 --- a/roles/wordpress/README.md +++ b/roles/wordpress/README.md @@ -206,17 +206,17 @@ wordpress__url: 'https://wordpress.example.com' * Default: `[]` * Subkeys: + * `name`: + + * Mandatory. Plugin slug, path to a local zip file, or URL to a remote zip file. + * Type: String. + * `enabled`: * Optional. Activates the plugin (`true`) or keeps it installed but deactivated (`false`). With `false`, `name` has to be the plugin slug, since WP-CLI cannot deactivate a plugin by the path or URL of its zip file. * Type: Bool. * Default: `true` - * `name`: - - * Mandatory. Plugin slug, path to a local zip file, or URL to a remote zip file. - * Type: String. - * `state`: * Optional. Either `'present'` or `'absent'`.