Repository: ansible/ansible

Issue:
Support --releasever (and other variables/substitutions?) in dnf module
##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
dnf

##### ANSIBLE VERSION
```
ansible 2.4.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/pjanes/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.14 (default, Nov  3 2017, 10:55:25) [GCC 7.2.1 20170915 (Red Hat 7.2.1-2)]
```

##### CONFIGURATION
Default configuration

##### OS / ENVIRONMENT
Fedora 27 (host); various Fedora (managing)

##### SUMMARY
I'd like to be able to provide the `--releasever` parameter to `dnf` (which overrides the value determined from files in the OS) rather than dropping to a raw command. Ideally I'd also be able to set custom substitutions (normally added to `/etc/dnf/vars`).

##### STEPS TO REPRODUCE
```yaml
- dnf:
    name: foo
    releasever: 27
    state: latest
```

```yaml
- dnf:
    name: foo
    mydist: 1.2.3
    state: latest
```

##### EXPECTED RESULTS
First case: `dnf update -y --releasever=27 foo` updates to the Fedora 27 version of `foo`.

Second case:
Suppose `/etc/yum/repos.d/foo.conf` contained

```
[mydist]
name=My Distribution - $basearch
baseurl=https://example.com/mydist/$mydist/$basearch/
```

and `/etc/dnf/vars/mydist` contained `1.2.3`; then `dnf update -y foo` would update to the MyDist 1.2.3 version of `foo`.

##### ACTUAL RESULTS
N/A

For now I've copied the `dnf` module into my project's `library` directory and added the (simple) code to support `releasever`. I don't know how widely useful this is to the Ansible community; as there's no way to extend modules, though, it seems adding it to core is a better solution than trying to maintain a branch or local copy.


Discussion:
Files identified in the description:
* [lib/ansible/modules/packaging/os/dnf.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/packaging/os/dnf.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @DJMuggs @berenddeschouwer @ignatenkobrain
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I'd think the extent of its usefulness to the Ansible community is similar to `--installroot` support (#30105).
@peterjanes, if I understand you correctly, you've got some code to make this work. Could you make a merge request on which we can vote?
Sure. Didn't want to go through the process if it wasn't something that would be more widely useful.
If you mention this issue (#33314), I'll test and upvote your merge request.

Resolving patch:
diff --git a/lib/ansible/modules/packaging/os/dnf.py b/lib/ansible/modules/packaging/os/dnf.py
--- a/lib/ansible/modules/packaging/os/dnf.py
+++ b/lib/ansible/modules/packaging/os/dnf.py
@@ -87,6 +87,14 @@
     version_added: "2.3"
     default: "/"
 
+  releasever:
+    description:
+      - Specifies an alternative release from which all packages will be
+        installed.
+    required: false
+    version_added: "2.6"
+    default: null
+
   autoremove:
     description:
       - If C(yes), removes all "leaf" packages from the system that were originally
@@ -198,7 +206,7 @@ def _ensure_dnf(module):
                                  "Please install `{0}` package.".format(package))
 
 
-def _configure_base(module, base, conf_file, disable_gpg_check, installroot='/'):
+def _configure_base(module, base, conf_file, disable_gpg_check, installroot='/', releasever=None):
     """Configure the dnf Base object."""
     conf = base.conf
 
@@ -214,6 +222,10 @@ def _configure_base(module, base, conf_file, disable_gpg_check, installroot='/')
     # Set installroot
     conf.installroot = installroot
 
+    # Set releasever
+    if releasever is not None:
+        conf.substitutions['releasever'] = releasever
+
     # Change the configuration file path if provided
     if conf_file:
         # Fail if we can't read the configuration file.
@@ -243,10 +255,10 @@ def _specify_repositories(base, disablerepo, enablerepo):
             repo.enable()
 
 
-def _base(module, conf_file, disable_gpg_check, disablerepo, enablerepo, installroot):
+def _base(module, conf_file, disable_gpg_check, disablerepo, enablerepo, installroot, releasever):
     """Return a fully configured dnf Base object."""
     base = dnf.Base()
-    _configure_base(module, base, conf_file, disable_gpg_check, installroot)
+    _configure_base(module, base, conf_file, disable_gpg_check, installroot, releasever)
     _specify_repositories(base, disablerepo, enablerepo)
     base.fill_sack(load_system_repo='auto')
     return base
@@ -492,6 +504,7 @@ def main():
             disable_gpg_check=dict(default=False, type='bool'),
             installroot=dict(default='/', type='path'),
             autoremove=dict(type='bool'),
+            releasever=dict(default=None),
         ),
         required_one_of=[['name', 'list', 'autoremove']],
         mutually_exclusive=[['name', 'list'], ['autoremove', 'list']],
@@ -516,7 +529,8 @@ def main():
     if params['list']:
         base = _base(
             module, params['conf_file'], params['disable_gpg_check'],
-            params['disablerepo'], params['enablerepo'], params['installroot'])
+            params['disablerepo'], params['enablerepo'], params['installroot'],
+            params['releasever'])
         list_items(module, base, params['list'])
     else:
         # Note: base takes a long time to run so we want to check for failure
@@ -525,7 +539,8 @@ def main():
             module.fail_json(msg="This command has to be run under the root user.")
         base = _base(
             module, params['conf_file'], params['disable_gpg_check'],
-            params['disablerepo'], params['enablerepo'], params['installroot'])
+            params['disablerepo'], params['enablerepo'], params['installroot'],
+            params['releasever'])
 
         ensure(module, base, params['state'], params['name'], params['autoremove'])
 


Test patch:
diff --git a/test/integration/targets/dnf/tasks/dnfreleasever.yml b/test/integration/targets/dnf/tasks/dnfreleasever.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/dnf/tasks/dnfreleasever.yml
@@ -0,0 +1,48 @@
+# make an installroot
+- name: Create installroot
+  local_action:
+    module: command mktemp -d "{{lookup('env', 'TMPDIR') | default('/tmp', true)}}/ansible.test.XXXXXX"
+  register: dnfroot
+
+- name: Make a necessary directory
+  file:
+    path: "/{{dnfroot.stdout}}/etc/dnf/vars"
+    state: directory
+    mode: 0755
+
+- name: Populate directory
+  copy:
+    content: "{{ansible_distribution_version}}\n"
+    dest: "/{{dnfroot.stdout}}/etc/dnf/vars/releasever"
+
+- name: attempt releasever to the installroot
+  dnf:
+    name: filesystem
+    installroot: '/{{dnfroot.stdout}}'
+    releasever: 22
+  register: dnf_result
+
+- name: check filesystem version
+  shell: rpm -q filesystem --root="/{{dnfroot.stdout}}/"
+  failed_when: False
+  register: rpm_result
+
+- debug: var=dnf_result
+- debug: var=rpm_result
+
+- name: verify installation was done
+  assert:
+    that:
+      - "not dnf_result.failed | default(False)"
+      - "dnf_result.changed"
+      - "rpm_result.rc == 0"
+
+- name: verify the version
+  assert:
+    that:
+      - "rpm_result.stdout.find('fc22') != -1"
+
+- name: cleanup installroot
+  file:
+    path: "/{{dnfroot.stdout}}/"
+    state: absent
diff --git a/test/integration/targets/dnf/tasks/main.yml b/test/integration/targets/dnf/tasks/main.yml
--- a/test/integration/targets/dnf/tasks/main.yml
+++ b/test/integration/targets/dnf/tasks/main.yml
@@ -33,3 +33,8 @@
   when:
     - ansible_distribution == 'Fedora'
     - ansible_distribution_major_version|int >= 23
+
+- include: 'dnfreleasever.yml'
+  when:
+    - ansible_distribution == 'Fedora'
+    - ansible_distribution_major_version|int >= 23