Repository: ansible/ansible
Issue category: Bug Report

Issue:
DNF5 state: latest fails when installing
### Summary

When using `ansible.builtin.dnf` with `state: latest` the previous behavior was to install and update all packages.
Fedora 41 uses DNF5 which does not install the package but instead fails.
A Bug report is filed on the DNF5 repo: https://github.com/rpm-software-management/dnf5/issues/1824
But this may not be reverted because this may be intended behavior.

### Issue Type

Bug Report

### Component Name

dnf

### Ansible Version

```console
$ ansible --version
ansible [core 2.17.5]
  config file = ...
  configured module search path = ['...', '/usr/share/ansible/plugins/modules']
  ansible python module location = ...
  ansible collection location = ...:/usr/share/ansible/collections
  executable location = ...
  python version = 3.12.7 (main, Oct  1 2024, 02:05:46) [GCC 13.3.0] (...)
  jinja version = 3.1.4
  libyaml = True
```


### Configuration

```console
# if using a version older than ansible-core 2.12 you should omit the '-t all'
$ ansible-config dump --only-changed -t all
ansible-config [core 2.17.5]
  config file = [...]/ansible.cfg
  configured module search path = ['[...]/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = [...]/python3.12/site-packages/ansible
  ansible collection location = [...]/.ansible/collections:/usr/share/ansible/collections
  executable location = [...]/bin/ansible-config
  python version = 3.12.7 (main, Oct  1 2024, 02:05:46) [GCC 13.3.0] ([...]/bin/python3.12)
  jinja version = 3.1.4
  libyaml = True
Using [...]/inventory/ansible.cfg as config file
CONFIG_FILE() = [...]/ansible.cfg
DEFAULT_HOST_LIST([...]/ansible.cfg) = ['[...]/hosts.yml']
DEFAULT_VERBOSITY(/[...]/ansible.cfg) = 2
EDITOR(env: EDITOR) = /usr/bin/nano
PERSISTENT_COMMAND_TIMEOUT([...]/ansible.cfg) = 30
PERSISTENT_CONNECT_TIMEOUT([...]/ansible.cfg) = 30
```


### OS / Environment

Fedora 41

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)

```
- name: Install sssd
  ansible.builtin.dnf:
    name:
      - sssd
    state: latest

### Expected Results

SSSD is expected to be installed.
Documentation: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/dnf_module.html#parameter-state
> Whether to install (present, latest), or remove (absent) a package.

`latest` should install and update the packages

### Actual Results

```console
{
  "failures": [
    "Packages for argument 'sssd' available, but not installed."
  ],
  "rc": 1,
  "msg": "Failed to install some of the specified packages",
  "invocation": {
    "module_args": {
      "name": [
        "sssd"
      ],
      "state": "latest",
      "allow_downgrade": false,
      "autoremove": false,
      "bugfix": false,
      "cacheonly": false,
      "disable_gpg_check": false,
      "disable_plugin": [],
      "disablerepo": [],
      "download_only": false,
      "enable_plugin": [],
      "enablerepo": [],
      "exclude": [],
      "installroot": "/",
      "install_repoquery": true,
      "install_weak_deps": true,
      "security": false,
      "skip_broken": false,
      "update_cache": false,
      "update_only": false,
      "validate_certs": true,
      "sslverify": true,
      "lock_timeout": 30,
      "allowerasing": false,
      "nobest": false,
      "conf_file": null,
      "disable_excludes": null,
      "download_dir": null,
      "list": null,
      "releasever": null
    }
  },
  "_ansible_no_log": false,
  "changed": false
}
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Hints:
Files identified in the description:

* [`lib/ansible/plugins/action/dnf.py`](https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/action/dnf.py)
* [`lib/ansible/modules/dnf.py`](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/dnf.py)

If these files are incorrect, please update the `component name` section of the description or use the [component](https://github.com/ansible/ansibotmini#commands) bot command.

<!--- boilerplate: components_banner --->

I don't seem to be able to reproduce this.

Preparing a target host:
```
% podman run --name container --rm -it --entrypoint bash fedora:41
[root@2049d5586296 /]# dnf install python3-libdnf5
...
```

Running the task provided in the reproducer:
```
% ansible container -m dnf -a "name=sssd state=latest"
container | CHANGED => {
    "ansible_facts": {
        "pkg_mgr": "dnf5"
    },
    "changed": true,
    "msg": "",
    "rc": 0,
    "results": [
        "Installed: sssd-2.10.0-1.fc41.aarch64",
        "Installed: sssd-ad-2.10.0-1.fc41.aarch64",
        "Installed: sssd-common-2.10.0-1.fc41.aarch64",
        "Installed: sssd-ipa-2.10.0-1.fc41.aarch64",
        "Installed: sssd-krb5-2.10.0-1.fc41.aarch64",
        "Installed: sssd-ldap-2.10.0-1.fc41.aarch64",
        "Installed: sssd-proxy-2.10.0-1.fc41.aarch64",
        "Installed: dbus-libs-1:1.14.10-4.fc41.aarch64",
        "Installed: libbasicobjects-0.1.1-57.fc41.aarch64",
        "Installed: libcollection-0.7.0-57.fc41.aarch64",
        "Installed: libdhash-0.5.0-57.fc41.aarch64",
        "Installed: libini_config-1.3.1-57.fc41.aarch64",
        "Installed: libref_array-0.1.5-57.fc41.aarch64",
        "Installed: libsss_certmap-2.10.0-1.fc41.aarch64",
        "Installed: libsss_idmap-2.10.0-1.fc41.aarch64",
        "Installed: libtalloc-2.4.2-4.fc41.aarch64",
        "Installed: libtdb-1.4.12-3.fc41.aarch64",
        "Installed: libtevent-0.16.1-4.fc41.aarch64",
        "Installed: sssd-common-pac-2.10.0-1.fc41.aarch64",
        "Installed: sssd-krb5-common-2.10.0-1.fc41.aarch64",
        "Installed: c-ares-1.33.0-1.fc41.aarch64",
        "Installed: sssd-client-2.10.0-1.fc41.aarch64",
        "Installed: systemd-256.7-1.fc41.aarch64",
        "Installed: libipa_hbac-2.10.0-1.fc41.aarch64",
        "Installed: libpath_utils-0.2.1-57.fc41.aarch64",
        "Installed: cyrus-sasl-gssapi-2.1.28-27.fc41.aarch64",
        "Installed: jansson-2.13.1-10.fc41.aarch64",
        "Installed: libsss_nss_idmap-2.10.0-1.fc41.aarch64",
        "Installed: dbus-1:1.14.10-4.fc41.aarch64",
        "Installed: libfdisk-2.40.2-4.fc41.aarch64",
        "Installed: libseccomp-2.5.5-2.fc41.aarch64",
        "Installed: systemd-pam-256.7-1.fc41.aarch64",
        "Installed: dbus-broker-36-4.fc41.aarch64",
        "Installed: dbus-common-1:1.14.10-4.fc41.noarch",
        "Installed: libldb-2:4.21.1-7.fc41.aarch64",
        "Installed: lmdb-libs-0.9.33-2.fc41.aarch64",
        "Installed: samba-client-libs-2:4.21.1-7.fc41.aarch64",
        "Installed: avahi-libs-0.8-29.fc41.aarch64",
        "Installed: libicu-74.2-2.fc41.aarch64",
        "Installed: libwbclient-2:4.21.1-7.fc41.aarch64",
        "Installed: samba-common-2:4.21.1-7.fc41.noarch",
        "Installed: samba-common-libs-2:4.21.1-7.fc41.aarch64",
        "Installed: libsmbclient-2:4.21.1-7.fc41.aarch64",
        "Installed: libnl3-3.11.0-1.fc41.aarch64",
        "Installed: glibc-gconv-extra-2.40-9.fc41.aarch64",
        "Installed: libsss_sudo-2.10.0-1.fc41.aarch64",
        "Installed: bind-utils-32:9.18.30-1.fc41.aarch64",
        "Installed: bind-libs-32:9.18.30-1.fc41.aarch64",
        "Installed: jemalloc-5.3.0-7.fc41.aarch64",
        "Installed: protobuf-c-1.5.0-4.fc41.aarch64",
        "Installed: fstrm-0.6.1-11.fc41.aarch64",
        "Installed: libmaxminddb-1.11.0-1.fc41.aarch64",
        "Installed: adcli-0.9.2-7.fc41.aarch64",
        "Installed: libxkbcommon-1.7.0-4.fc41.aarch64",
        "Installed: xkeyboard-config-2.42-2.fc41.noarch",
        "Installed: libbpf-2:1.4.7-1.fc41.aarch64",
        "Installed: diffutils-3.10-8.fc41.aarch64",
        "Installed: kmod-libs-33-1.fc41.aarch64",
        "Installed: cryptsetup-libs-2.7.5-1.fc41.aarch64",
        "Installed: device-mapper-libs-1.02.199-4.fc41.aarch64",
        "Installed: device-mapper-1.02.199-4.fc41.aarch64",
        "Installed: qrencode-libs-4.1.1-8.fc41.aarch64",
        "Installed: systemd-networkd-256.7-1.fc41.aarch64",
        "Installed: systemd-resolved-256.7-1.fc41.aarch64",
        "Installed: logrotate-3.22.0-2.fc41.aarch64",
        "Installed: libuv-1:1.49.2-1.fc41.aarch64",
        "Installed: systemd-udev-256.7-1.fc41.aarch64",
        "Installed: kbd-2.6.4-4.fc41.aarch64",
        "Installed: kmod-33-1.fc41.aarch64",
        "Installed: kbd-legacy-2.6.4-4.fc41.noarch",
        "Installed: kbd-misc-2.6.4-4.fc41.noarch",
        "Installed: pkgconf-pkg-config-2.3.0-1.fc41.aarch64",
        "Installed: pkgconf-2.3.0-1.fc41.aarch64",
        "Installed: pkgconf-m4-2.3.0-1.fc41.noarch",
        "Installed: libpkgconf-2.3.0-1.fc41.aarch64",
        "Installed: tpm2-tss-4.1.3-3.fc41.aarch64",
        "Installed: libfido2-1.15.0-2.fc41.aarch64",
        "Installed: libcbor-0.11.0-2.fc41.aarch64",
        "Installed: glibc-2.40-9.fc41.aarch64",
        "Installed: glibc-common-2.40-9.fc41.aarch64",
        "Installed: glibc-minimal-langpack-2.40-9.fc41.aarch64",
        "Removed: glibc-2.40-3.fc41.aarch64",
        "Removed: glibc-common-2.40-3.fc41.aarch64",
        "Removed: glibc-minimal-langpack-2.40-3.fc41.aarch64"
    ]
}
```

I suspect that the cause of your issue is certain unknown set of packages being installed on the target system that somehow trips the `is_installed` check into thinking `sssd` is already installed and trying to upgrade it instead of installing it. What is the output of `# rpm -qa sssd-*` on the target system prior to running your playbook?
Thank you for your help. `rpm -qa sssd-*` yields:
```
sssd-nfs-idmap-2.10.0-1.fc41.x86_64
sssd-client-2.10.0-1.fc41.x86_64
sssd-common-2.10.0-1.fc41.x86_64
sssd-krb5-common-2.10.0-1.fc41.x86_64
sssd-kcm-2.10.0-1.fc41.x86_64
```

This is on a fresh Fedora 41 _Workstation_ Installation.
What are my options to circumvent this problem? Is this an ansible or a dnf bug?

Patch:
diff --git a/lib/ansible/modules/dnf5.py b/lib/ansible/modules/dnf5.py
index df4ee206748716..b157158514ffab 100644
--- a/lib/ansible/modules/dnf5.py
+++ b/lib/ansible/modules/dnf5.py
@@ -358,6 +358,15 @@
 
 def is_installed(base, spec):
     settings = libdnf5.base.ResolveSpecSettings()
+    # Disable checking whether SPEC is a binary -> `/usr/(s)bin/<SPEC>`,
+    # this prevents scenarios like the following:
+    #   * the `sssd-common` package is installed and provides `/usr/sbin/sssd` binary
+    #   * the `sssd` package is NOT installed
+    #   * due to `set_with_binaries(True)` being default `is_installed(base, "sssd")` would "unexpectedly" return True
+    # If users wish to target the `sssd` binary they can by specifying the full path `name=/usr/sbin/sssd` explicitly
+    # due to settings.set_with_filenames(True) being default.
+    settings.set_with_binaries(False)
+
     installed_query = libdnf5.rpm.PackageQuery(base)
     installed_query.filter_installed()
     match, nevra = installed_query.resolve_pkg_spec(spec, settings, True)


Test patch:
diff --git a/changelogs/fragments/84259-dnf5-latest-fix.yml b/changelogs/fragments/84259-dnf5-latest-fix.yml
new file mode 100644
index 00000000000000..40f6ddb740860f
--- /dev/null
+++ b/changelogs/fragments/84259-dnf5-latest-fix.yml
@@ -0,0 +1,2 @@
+bugfixes:
+  - "dnf5 - fix installing a package using ``state=latest`` when a binary of the same name as the package is already installed (https://github.com/ansible/ansible/issues/84259)"
diff --git a/test/integration/targets/dnf/tasks/repo.yml b/test/integration/targets/dnf/tasks/repo.yml
index 6ab8fa1a3b3cdd..ec31fe4a4ae39e 100644
--- a/test/integration/targets/dnf/tasks/repo.yml
+++ b/test/integration/targets/dnf/tasks/repo.yml
@@ -517,3 +517,50 @@
       dnf:
         name: provides_foo*
         state: absent
+
+# https://github.com/ansible/ansible/issues/84259
+- name: test installing a package named `package-name` while a package providing `/usr/sbin/package-name` is installed
+  block:
+    - dnf:
+        name: package-name
+        state: absent
+
+    - dnf:
+        name: provides-binary
+        state: present
+
+    - dnf:
+        name: package-name
+        state: latest
+      register: dnf_result
+
+    - assert:
+        that:
+          - dnf_result is changed
+  always:
+    - name: Clean up
+      dnf:
+        name:
+          - provides-binary
+          - package-name
+        state: absent
+
+- name: test installing a package that provides a binary by specifying the binary name
+  block:
+    - dnf:
+        name: provides-binary
+        state: absent
+
+    - dnf:
+        name: /usr/sbin/package-name
+        state: present
+      register: dnf_result
+
+    - assert:
+        that:
+          - dnf_result is changed
+  always:
+    - name: Clean up
+      dnf:
+        name: provides-binary
+        state: absent
diff --git a/test/integration/targets/setup_rpm_repo/library/create_repo.py b/test/integration/targets/setup_rpm_repo/library/create_repo.py
index 5acf2397195754..6fffe5ad90b487 100644
--- a/test/integration/targets/setup_rpm_repo/library/create_repo.py
+++ b/test/integration/targets/setup_rpm_repo/library/create_repo.py
@@ -15,8 +15,10 @@
     from rpmfluff.make import make_gif
     from rpmfluff.sourcefile import GeneratedSourceFile
     from rpmfluff.rpmbuild import SimpleRpmBuild
+    from rpmfluff.utils import expectedArch
     from rpmfluff.yumrepobuild import YumRepoBuild
 except ImportError:
+    expectedArch = None  # define here to avoid NameError as it is used on top level in SPECS
     HAS_RPMFLUFF = False
 
 
@@ -30,6 +32,7 @@ class RPM:
     recommends: list[str] | None = None
     requires: list[str] | None = None
     file: str | None = None
+    binary: str | None = None
 
 
 SPECS = [
@@ -58,6 +61,8 @@ class RPM:
     RPM(name='broken-b', version='1.0', requires=['broken-a = 1.2.3-1']),
     RPM(name='broken-c', version='1.0', requires=['broken-c = 1.2.4-1']),
     RPM(name='broken-d', version='1.0', requires=['broken-a']),
+    RPM(name='provides-binary', version='1.0', arch=[expectedArch], binary='/usr/sbin/package-name'),
+    RPM(name='package-name', version='1.0'),
 ]
 
 
@@ -81,10 +86,13 @@ def create_repo():
                 )
             )
 
+        if spec.binary:
+            pkg.add_simple_compilation(installPath=spec.binary)
+
         pkgs.append(pkg)
 
     repo = YumRepoBuild(pkgs)
-    repo.make('noarch', 'i686', 'x86_64')
+    repo.make('noarch', 'i686', 'x86_64', expectedArch)
 
     for pkg in pkgs:
         pkg.clean()