Repository: ansible/ansible-modules-extras

Issue:
DNF module not able to install existing group 
<!--- Verify first that your issue/request is not already reported in GitHub -->
##### ISSUE TYPE

<!--- Pick one below and delete the rest: -->
- Bug Report
##### COMPONENT NAME

<!--- Name of the plugin/module/task -->

dnf module
##### ANSIBLE VERSION

<!--- Paste verbatim output from “ansible --version” between quotes -->

```
"ansible 2.0.2.0"
```
##### CONFIGURATION

<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

no changes to default
##### OS / ENVIRONMENT

<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->

fedora 23
##### SUMMARY

<!--- Explain the problem briefly -->

when trying to install a package group "Xfce desktop" using ansible it fails, while it runs normally using the dnf groupinstall "Xfce desktop" command.
##### STEPS TO REPRODUCE

<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

run command:

ansible-playbook -i inventory -b --ask-become-pass testserver.yml

task for installing Xfce desktop:

```
   - name: Ensure XFCE is installed
      dnf:
       name="@Xfce desktop"
       state=present
```

reproducible: always

see above

<!--- You can also paste gist.github.com links for larger files -->
##### EXPECTED RESULTS

<!--- What did you expect to happen when running the steps above? -->

That the package group "Xfce desktop" is installed without errors using the ansible dnf module.
##### ACTUAL RESULTS

<!--- What actually happened? If possible run with high verbosity (-vvvv) -->

Fatal error.

```
"fatal: [testserver]: FAILED! => {"changed": false, "failed": true, "invocation": {"module_args": {"conf_file": null, "disable_gpg_check": false, "disablerepo": [], "enablerepo": [], "list": null, "name": ["Xfce desktop"], "state": "present"}, "module_name": "dnf"}, "msg": "No package Xfce desktop available."}"
```



Discussion:
the actual executed task:

"    - name: Ensure XFCE is installed
         dnf:
           name="Xfce desktop"
           state=present
"

EDIT: I cleaned up some quoting in the original report so it formats better.

Looks like dnf expects groups like "@Xfce Desktop" to be install via 'dnf install "@Xfce Desktop'  vs 'dnf groupinstall "@Xfce Desktop".

The dnf modules doesn't seem to support that currently.

I'm not sure if this is a new feature which isn't implemented yet, since installing package groups is explicitly given as an example with the ansible dnf module:

From the examples on the dnf module page:
- name: install the 'Development tools' package group
  
  dnf: name="@Development tools" state=present

Source: http://docs.ansible.com/ansible/dnf_module.html

Or am I perhaps now misunderstanding your words?

Have you tried using the true yaml syntax?

```
dnf:
  name: @Xfce Desktop
```

You don't need to specify `state=present` because that's default.

@alikins, I ran into this as well...  Attempting to install the Fedora Workstation group. 

In the yum module, package groups vs environment groups are designated via @ or @^, respectively.  So I am assuming the implementation will be the same for both modules somehow... 

I ran into this as well.
It only affects Environment groups, other groups (like "@Administration Tools") do get installed.

@ignatenkobrain, @DJMuggs, ping. This issue is still waiting on your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_renotify_maintainer --->

@ignatenkobrain, @DJMuggs, ping. This issue is still waiting on your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_renotify_maintainer --->

@ignatenkobrain, @DJMuggs, ping. This issue is still waiting on your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_renotify_maintainer --->

needs_contributor

@ansibot needs_contributor

@ansibot bot_broken


Resolving patch:
diff --git a/packaging/os/dnf.py b/packaging/os/dnf.py
--- a/packaging/os/dnf.py
+++ b/packaging/os/dnf.py
@@ -205,7 +205,7 @@ def _base(module, conf_file, disable_gpg_check, disablerepo, enablerepo):
     base = dnf.Base()
     _configure_base(module, base, conf_file, disable_gpg_check)
     _specify_repositories(base, disablerepo, enablerepo)
-    base.fill_sack()
+    base.fill_sack(load_system_repo='auto')
     return base
 
 
@@ -282,6 +282,9 @@ def _install_remote_rpms(base, filenames):
 
 
 def ensure(module, base, state, names):
+    # Accumulate failures.  Package management modules install what they can
+    # and fail with a message about what they can't.
+    failures = []
     allow_erasing = False
     if names == ['*'] and state == 'latest':
         base.upgrade_all()
@@ -290,34 +293,70 @@ def ensure(module, base, state, names):
         if group_specs:
             base.read_comps()
 
+        pkg_specs = [p.strip() for p in pkg_specs]
+        filenames = [f.strip() for f in filenames]
         groups = []
+        environments = []
         for group_spec in group_specs:
             group = base.comps.group_by_pattern(group_spec)
             if group:
-                groups.append(group)
+                groups.append(group.strip())
             else:
-                module.fail_json(
-                    msg="No group {} available.".format(group_spec))
+                environment = base.comps.environments_by_pattern(group_spec)
+                if environment:
+                    environments.extend((e.id.strip() for e in environment))
+                else:
+                    module.fail_json(
+                        msg="No group {} available.".format(group_spec))
 
         if state in ['installed', 'present']:
             # Install files.
-            _install_remote_rpms(base, (f.strip() for f in filenames))
+            _install_remote_rpms(base, filenames)
+
             # Install groups.
-            for group in (g.strip() for g in groups):
-                base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES)
+            for group in groups:
+                try:
+                    base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES)
+                except dnf.exceptions.Error as e:
+                    # In dnf 2.0 if all the mandatory packages in a group do
+                    # not install, an error is raised.  We want to capture
+                    # this but still install as much as possible.
+                    failures.append((group, e))
+
+            for environment in environments:
+                try:
+                    base.environment_install(environment, dnf.const.GROUP_PACKAGE_TYPES)
+                except dnf.exceptions.Error as e:
+                    failures.append((group, e))
+
             # Install packages.
-            for pkg_spec in (p.strip() for p in pkg_specs):
+            for pkg_spec in pkg_specs:
                 _mark_package_install(module, base, pkg_spec)
 
         elif state == 'latest':
             # "latest" is same as "installed" for filenames.
             _install_remote_rpms(base, filenames)
+
             for group in groups:
                 try:
-                    base.group_upgrade(group)
-                except dnf.exceptions.CompsError:
-                    # If not already installed, try to install.
-                    base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES)
+                    try:
+                        base.group_upgrade(group)
+                    except dnf.exceptions.CompsError:
+                        # If not already installed, try to install.
+                        base.group_install(group, dnf.const.GROUP_PACKAGE_TYPES)
+                except dnf.exceptions.Error as e:
+                    failures.append((group, e))
+
+            for environment in environments:
+                try:
+                    try:
+                        base.environment_upgrade(environment)
+                    except dnf.exceptions.CompsError:
+                        # If not already installed, try to install.
+                        base.environment_install(group, dnf.const.GROUP_PACKAGE_TYPES)
+                except dnf.exceptions.Error as e:
+                    failures.append((group, e))
+
             for pkg_spec in pkg_specs:
                 # best effort causes to install the latest package
                 # even if not previously installed
@@ -334,18 +373,27 @@ def ensure(module, base, state, names):
             for group in groups:
                 if installed.filter(name=group.name):
                     base.group_remove(group)
+
             for pkg_spec in pkg_specs:
                 if installed.filter(name=pkg_spec):
                     base.remove(pkg_spec)
+
             # Like the dnf CLI we want to allow recursive removal of dependent
             # packages
             allow_erasing = True
 
     if not base.resolve(allow_erasing=allow_erasing):
+        if failures:
+            module.fail_json(msg='Failed to install some of the specified packages',
+                    failures=failures)
         module.exit_json(msg="Nothing to do")
     else:
         if module.check_mode:
+            if failures:
+                module.fail_json(msg='Failed to install some of the specified packages',
+                        failures=failures)
             module.exit_json(changed=True)
+
         base.download_packages(base.transaction.install_set)
         base.do_transaction()
         response = {'changed': True, 'results': []}
@@ -354,6 +402,9 @@ def ensure(module, base, state, names):
         for package in base.transaction.remove_set:
             response['results'].append("Removed: {0}".format(package))
 
+        if failures:
+            module.fail_json(msg='Failed to install some of the specified packages',
+                    failures=failures)
         module.exit_json(**response)
 
 


Test patch: