Repository: ansible-collections/community.general
Pull request #9973: Add zypper skip post errors feature
Author: login: mk2km
Labels: name: bug
description: This issue/PR relates to a bug
name: module
description: module
name: tests
description: tests
name: integration
description: tests/integration
name: new_contributor
description: Help guide this first time contributor
name: plugins
description: plugin (any type)
name: backport-10
description: Automatically create a backport for the stable-10 branch
Created: 2025-04-07T13:34:30Z | Merged: 2025-04-19T07:00:01Z

- Add feature to handle zypper return code 107 with skip_post_errors (default: false).
- Add integration test to verify the skip_post_errors flag.

Issue: #9972
Issue-Ref: https://github.com/ansible-collections/community.general/issues/9972

##### SUMMARY
<!--- Describe the change below, including rationale and design decisions -->

<!--- HINT: Include "Fixes #nnn" if you are fixing an existing issue -->

<!--- Please do not forget to include a changelog fragment:
      https://docs.ansible.com/ansible/devel/community/collection_development_process.html#creating-changelog-fragments
      No need to include one for docs-only or test-only PR, and for new plugin/module PRs.
      Read about more details in CONTRIBUTING.md.
      -->

##### ISSUE TYPE
<!--- Pick one or more below and delete the rest.
      'Test Pull Request' is for PRs that add/extend tests without code changes. -->
- Bugfix Pull Request
- Docs Pull Request
- Feature Pull Request
- New Module/Plugin Pull Request
- Refactoring Pull Request
- Test Pull Request

##### COMPONENT NAME
<!--- Write the SHORT NAME of the module, plugin, task or feature below. -->

##### ADDITIONAL INFORMATION
<!--- Include additional information to help people understand the change here -->
<!--- A step-by-step reproduction of the problem is helpful if there is no related issue -->

<!--- Paste verbatim command output below, e.g. before and after your change -->
```paste below

```



Diff:
diff --git a/changelogs/fragments/9972-zypper-skip-post-errors.yml b/changelogs/fragments/9972-zypper-skip-post-errors.yml
new file mode 100644
index 00000000000..097ec67df0b
--- /dev/null
+++ b/changelogs/fragments/9972-zypper-skip-post-errors.yml
@@ -0,0 +1,2 @@
+minor_changes:
+  - zypper - adds ``skip_post_errors`` that allows to skip RPM post-install errors (Zypper return code 107) (https://github.com/ansible-collections/community.general/issues/9972).
diff --git a/plugins/modules/zypper.py b/plugins/modules/zypper.py
index fb3be3a5d02..8ed359d32eb 100644
--- a/plugins/modules/zypper.py
+++ b/plugins/modules/zypper.py
@@ -158,6 +158,13 @@
     description:
       - Adds C(--quiet) option to I(zypper) install/update command.
     version_added: '10.2.0'
+  skip_post_errors:
+    type: bool
+    required: false
+    default: false
+    description:
+      - When set to V(true), ignore I(zypper) return code 107 (post install script errors).
+    version_added: '10.6.0'
 notes:
   - When used with a C(loop:) each package is processed individually, it is much more efficient to pass the list directly
     to the O(name) option.
@@ -248,6 +255,12 @@
     state: present
   environment:
     ZYPP_LOCK_TIMEOUT: 20
+
+- name: Install the package with post-install error without failing
+  community.general.zypper:
+    name: <package_with_post_install_error>
+    state: present
+    skip_post_errors: true
 """
 
 import os.path
@@ -344,12 +357,13 @@ def parse_zypper_xml(m, cmd, fail_not_found=True, packages=None):
             m.fail_json(msg=errmsg, rc=rc, stdout=stdout, stderr=stderr, cmd=cmd)
         else:
             return {}, rc, stdout, stderr
-    elif rc in [0, 102, 103, 106]:
+    elif rc in [0, 102, 103, 106, 107]:
         # zypper exit codes
         # 0: success
         # 106: signature verification failed
         # 102: ZYPPER_EXIT_INF_REBOOT_NEEDED - Returned after a successful installation of a patch which requires reboot of computer.
         # 103: zypper was upgraded, run same command again
+        # 107: ZYPPER_EXIT_INF_RPM_SCRIPT_FAILED -  Some of the packages %post install scripts returned an error, but package is installed.
         if packages is None:
             firstrun = True
             packages = {}
@@ -368,14 +382,18 @@ def parse_zypper_xml(m, cmd, fail_not_found=True, packages=None):
             # if this was the first run and it failed with 103
             # run zypper again with the same command to complete update
             return parse_zypper_xml(m, cmd, fail_not_found=fail_not_found, packages=packages)
+        if rc == 107 and m.params['skip_post_errors'] and firstrun:
+            # if this was the first run and it failed with 107 with skip_post_errors flag
+            # run zypper again with the same command to complete update
+            return parse_zypper_xml(m, cmd, fail_not_found=fail_not_found, packages=packages)
 
-        # apply simple_errors logic to rc 0,102,103,106
+        # apply simple_errors logic to rc 0,102,103,106,107
         if m.params['simple_errors']:
             stdout = get_simple_errors(dom) or stdout
 
         return packages, rc, stdout, stderr
 
-    # apply simple_errors logic to rc other than 0,102,103,106
+    # apply simple_errors logic to rc other than 0,102,103,106,107
     if m.params['simple_errors']:
         stdout = get_simple_errors(dom) or stdout
 
@@ -602,6 +620,7 @@ def main():
             clean_deps=dict(required=False, default=False, type='bool'),
             simple_errors=dict(required=False, default=False, type='bool'),
             quiet=dict(required=False, default=True, type='bool'),
+            skip_post_errors=dict(required=False, default=False, type='bool'),
         ),
         supports_check_mode=True
     )
diff --git a/tests/integration/targets/zypper/files/post_error.spec b/tests/integration/targets/zypper/files/post_error.spec
new file mode 100644
index 00000000000..af5bc031084
--- /dev/null
+++ b/tests/integration/targets/zypper/files/post_error.spec
@@ -0,0 +1,15 @@
+Summary:   Post error RPM
+Name:      post_error
+Version:   1
+Release:   0
+License:   GPLv3
+Group:     Applications/System
+BuildArch: noarch
+
+%description
+Post error RPM
+
+%files
+
+%post
+exit 1
diff --git a/tests/integration/targets/zypper/files/post_error.spec.license b/tests/integration/targets/zypper/files/post_error.spec.license
new file mode 100644
index 00000000000..edff8c7685d
--- /dev/null
+++ b/tests/integration/targets/zypper/files/post_error.spec.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/tests/integration/targets/zypper/tasks/zypper.yml b/tests/integration/targets/zypper/tasks/zypper.yml
index 3eefddbdfc7..818bdd9f429 100644
--- a/tests/integration/targets/zypper/tasks/zypper.yml
+++ b/tests/integration/targets/zypper/tasks/zypper.yml
@@ -255,6 +255,91 @@
     that:
      - "stat_result.stat.exists == true"
 
+# Build and install an empty rpm with error in post script
+- name: uninstall post_error
+  zypper:
+    name: post_error
+    state: removed
+
+- name: install rpmbuild
+  zypper:
+    name: rpmbuild
+    state: present
+
+- name: clean zypper RPM cache
+  file: 
+    name: /var/cache/zypper/RPMS
+    state: absent
+
+- name: create directory
+  file:
+    path: "{{ remote_tmp_dir | expanduser }}/zypper2"
+    state: directory
+
+- name: copy spec file
+  copy:
+    src: post_error.spec
+    dest: "{{ remote_tmp_dir | expanduser }}/zypper2/post_error.spec"
+
+- name: build rpm
+  command: |
+    rpmbuild -bb \
+    --define "_topdir {{remote_tmp_dir | expanduser }}/zypper2/rpm-build"
+    --define "_builddir %{_topdir}" \
+    --define "_rpmdir %{_topdir}" \
+    --define "_srcrpmdir %{_topdir}" \
+    --define "_specdir {{remote_tmp_dir | expanduser}}/zypper2" \
+    --define "_sourcedir %{_topdir}" \
+    {{ remote_tmp_dir }}/zypper2/post_error.spec
+  register: rpm_build_result
+
+- name: install post_error rpm with skip_post_errors
+  zypper:
+    name: "{{ remote_tmp_dir | expanduser }}/zypper2/rpm-build/noarch/post_error-1-0.noarch.rpm"
+    disable_gpg_check: true
+    skip_post_errors: true
+  register: zypper_result
+
+- name: check post_error rpm
+  shell: rpm -q post_error
+  failed_when: false
+  register: rpm_result
+
+- name: verify installation of post_error
+  assert:
+    that:
+        - "zypper_result.rc == 0"
+        - "zypper_result.changed"
+        - "rpm_result.rc == 0"
+
+- name: uninstall post_error
+  zypper:
+    name: post_error
+    state: removed
+
+- name: install post_error rpm without skip_post_errors
+  zypper:
+    name: "{{ remote_tmp_dir | expanduser }}/zypper2/rpm-build/noarch/post_error-1-0.noarch.rpm"
+    disable_gpg_check: true
+  register: zypper_result
+  ignore_errors: true
+
+- name: check post_error rpm
+  shell: rpm -q post_error
+  failed_when: false
+  register: rpm_result
+
+- name: verify installation of post_error
+  assert:
+    that:
+        - "zypper_result.rc == 107"
+        - "not zypper_result.changed"
+        - "rpm_result.rc == 0"
+
+- name: uninstall post_error
+  zypper:
+    name: post_error
+    state: removed
 
 # test simultaneous remove and install using +- prefixes
 


Reviews:
user: login: felixfontein
body: Thanks for your contribution! Can you please add a [changelog fragment](https://docs.ansible.com/ansible/devel/community/collection_development_process.html#creating-a-changelog-fragment)? Thanks.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T05:13:21Z
user: login: mk2km
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T07:38:00Z
user: login: mk2km
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T07:38:05Z
user: login: felixfontein
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T18:35:27Z
user: login: russoz
body: Minor nit, other than that, LGTM
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-16T21:40:00Z
user: login: russoz
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-18T07:51:12Z

Review comments:
diff_hunk: @@ -158,6 +158,13 @@
     description:
       - Adds C(--quiet) option to I(zypper) install/update command.
     version_added: '10.2.0'
+  skip_post_errors:
+    type: bool
+    required: false
+    default: false
+    description:
+      - When set to V(true), ignore zypper return code 107 (post install script errors).
+    version_added: '10.2.0'
path: plugins/modules/zypper.py
user: login: felixfontein
body: The next feature release will be 10.6.0:
```suggestion
    version_added: '10.6.0'
```
created_at: 2025-04-10T05:12:24Z
author_association: COLLABORATOR
diff_hunk: @@ -158,6 +158,13 @@
     description:
       - Adds C(--quiet) option to I(zypper) install/update command.
     version_added: '10.2.0'
+  skip_post_errors:
+    type: bool
+    required: false
+    default: false
+    description:
+      - When set to V(true), ignore zypper return code 107 (post install script errors).
path: plugins/modules/zypper.py
user: login: felixfontein
body: ```suggestion
      - When set to V(true), ignore I(zypper) return code 107 (post install script errors).
```
created_at: 2025-04-10T05:12:50Z
author_association: COLLABORATOR
diff_hunk: @@ -158,6 +158,13 @@
     description:
       - Adds C(--quiet) option to I(zypper) install/update command.
     version_added: '10.2.0'
+  skip_post_errors:
+    type: bool
+    required: false
+    default: false
+    description:
+      - When set to V(true), ignore zypper return code 107 (post install script errors).
+    version_added: '10.2.0'
path: plugins/modules/zypper.py
user: login: mk2km
body: Done
created_at: 2025-04-15T07:38:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -158,6 +158,13 @@
     description:
       - Adds C(--quiet) option to I(zypper) install/update command.
     version_added: '10.2.0'
+  skip_post_errors:
+    type: bool
+    required: false
+    default: false
+    description:
+      - When set to V(true), ignore zypper return code 107 (post install script errors).
path: plugins/modules/zypper.py
user: login: mk2km
body: Done
created_at: 2025-04-15T07:38:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,2 @@
+minor_changes:
+  - zypper - adds ``skip_post_errors`` that allows to skip rpm post-install errors (zypper return code 107) (https://github.com/ansible-collections/community.general/issues/9972).
path: changelogs/fragments/9972-zypper-skip-post-errors.yml
user: login: felixfontein
body: ```suggestion
  - zypper - adds ``skip_post_errors`` that allows to skip RPM post-install errors (Zypper return code 107) (https://github.com/ansible-collections/community.general/issues/9972).
```
created_at: 2025-04-15T18:35:20Z
author_association: COLLABORATOR
diff_hunk: @@ -255,6 +255,91 @@
     that:
      - "stat_result.stat.exists == true"
 
+# Build and install an empty rpm with error in post script
+- name: uninstall post_error
+  zypper:
+    name: post_error
+    state: removed
+
+- name: install rpmbuild
+  zypper:
+    name: rpmbuild
+    state: present
+
+- name: clean zypper RPM cache
+  file: 
+    name: /var/cache/zypper/RPMS
+    state: absent
+
+- name: create directory
+  file:
+    path: "{{remote_tmp_dir | expanduser}}/zypper2"
path: tests/integration/targets/zypper/tasks/zypper.yml
user: login: russoz
body: Nit picking:
```suggestion
    path: "{{ remote_tmp_dir | expanduser }}/zypper2"
```
This would be a recommended convention to follow, but more importantly, we should try to be consistent throughout the file - see lines 282 and 287 and this one. Each line is expressing the same thing in a slightly different way.
created_at: 2025-04-16T21:39:22Z
author_association: COLLABORATOR
diff_hunk: @@ -158,6 +158,13 @@
     description:
       - Adds C(--quiet) option to I(zypper) install/update command.
     version_added: '10.2.0'
+  skip_post_errors:
+    type: bool
+    required: false
path: plugins/modules/zypper.py
user: login: russoz
body: Not a _problem_ but we usually omit `required=false` from the parameters, that is already implicit. Feel free to keep it or remove it, this is not a show-stopper.
created_at: 2025-04-18T07:49:01Z
author_association: COLLABORATOR
diff_hunk: @@ -255,6 +255,91 @@
     that:
      - "stat_result.stat.exists == true"
 
+# Build and install an empty rpm with error in post script
+- name: uninstall post_error
+  zypper:
+    name: post_error
+    state: removed
+
+- name: install rpmbuild
+  zypper:
+    name: rpmbuild
+    state: present
+
+- name: clean zypper RPM cache
+  file: 
+    name: /var/cache/zypper/RPMS
+    state: absent
+
+- name: create directory
+  file:
+    path: "{{ remote_tmp_dir | expanduser }}/zypper2"
+    state: directory
+
+- name: copy spec file
+  copy:
+    src: post_error.spec
+    dest: "{{ remote_tmp_dir | expanduser }}/zypper2/post_error.spec"
+
+- name: build rpm
+  command: |
+    rpmbuild -bb \
+    --define "_topdir {{remote_tmp_dir | expanduser }}/zypper2/rpm-build"
path: tests/integration/targets/zypper/tasks/zypper.yml
user: login: russoz
body: This one...
created_at: 2025-04-18T07:50:04Z
author_association: COLLABORATOR
diff_hunk: @@ -255,6 +255,91 @@
     that:
      - "stat_result.stat.exists == true"
 
+# Build and install an empty rpm with error in post script
+- name: uninstall post_error
+  zypper:
+    name: post_error
+    state: removed
+
+- name: install rpmbuild
+  zypper:
+    name: rpmbuild
+    state: present
+
+- name: clean zypper RPM cache
+  file: 
+    name: /var/cache/zypper/RPMS
+    state: absent
+
+- name: create directory
+  file:
+    path: "{{ remote_tmp_dir | expanduser }}/zypper2"
+    state: directory
+
+- name: copy spec file
+  copy:
+    src: post_error.spec
+    dest: "{{ remote_tmp_dir | expanduser }}/zypper2/post_error.spec"
+
+- name: build rpm
+  command: |
+    rpmbuild -bb \
+    --define "_topdir {{remote_tmp_dir | expanduser }}/zypper2/rpm-build"
+    --define "_builddir %{_topdir}" \
+    --define "_rpmdir %{_topdir}" \
+    --define "_srcrpmdir %{_topdir}" \
+    --define "_specdir {{remote_tmp_dir | expanduser}}/zypper2" \
path: tests/integration/targets/zypper/tasks/zypper.yml
user: login: russoz
body: ... and this one could also be adjusted. Again, this is nit picking, so not holding the PR back because of it.
created_at: 2025-04-18T07:51:05Z
author_association: COLLABORATOR

PR comments:
user: login: ansibullbot
created_at: 2025-04-07T13:37:45Z
body: cc @AnderEnder @alxgu @andytom @commel @evrardjp @lrupp @sealor @toabctl
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
author_association: COLLABORATOR
user: login: ansibullbot
created_at: 2025-04-07T13:44:10Z
body: The test `ansible-test sanity --test pep8` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/pep8.html)] failed with 1 error:
```
plugins/modules/zypper.py:385:11: E271: multiple spaces after keyword
```
<!-- job_id: 142237 -->
<!-- job_id: 1552d9b8bfe5bcac8dd022f7d8a7e8f5 -->
<!-- job_id: None -->
The test `ansible-test sanity --test pep8` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/pep8.html)] failed with 1 error:
```
plugins/modules/zypper.py:385:11: E271: multiple spaces after keyword
```
<!-- job_id: 142237 -->
<!-- job_id: 1552d9b8bfe5bcac8dd022f7d8a7e8f5 -->
<!-- job_id: None -->
The test `ansible-test sanity --test pep8` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/pep8.html)] failed with 1 error:
```
plugins/modules/zypper.py:385:11: E271: multiple spaces after keyword
```
<!-- job_id: 142237 -->
<!-- job_id: 1552d9b8bfe5bcac8dd022f7d8a7e8f5 -->
<!-- job_id: None -->
The test `ansible-test sanity --test pep8` [[explain](https://docs.ansible.com/ansible-core/devel/dev_guide/testing/sanity/pep8.html)] failed with 1 error:
```
plugins/modules/zypper.py:385:11: E271: multiple spaces after keyword
```
<!-- job_id: 142237 -->
<!-- job_id: 1552d9b8bfe5bcac8dd022f7d8a7e8f5 -->
<!-- job_id: None -->

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: shippable_test_result --->
author_association: COLLABORATOR
user: login: felixfontein
created_at: 2025-04-15T18:55:48Z
body: If nobody objects, I'll merge this on the upcoming weekend.
author_association: COLLABORATOR
user: login: patchback[bot]
created_at: 2025-04-19T07:00:04Z
body: 
### Backport to stable-10: 💚 backport PR created

✅ Backport PR branch: `patchback/backports/stable-10/80252b29f8e795a25db0d290d64354cfe4b5357a/pr-9973`

Backported as https://github.com/ansible-collections/community.general/pull/10016



<sub><sup>
--
🤖 @patchback
I'm built with [octomachinery](https://octomachinery.dev) and
my source is open — https://github.com/sanitizers/patchback-github-app.
</sup></sub>


author_association: NONE
user: login: felixfontein
created_at: 2025-04-19T07:00:17Z
body: @mk2km thanks for your contribution!
@russoz thanks for reviewing!
author_association: COLLABORATOR