Task to enable dnf repos only if they are not enabled

I have a task like this:

    - name: Enable repos
      ansible.builtin.command:
        cmd: "dnf config-manager --enable {{ item }}"
      loop:
      - codeready-builder-for-rhel-9-x86_64-rpms
      - ansible-automation-platform-2.4-for-rhel-9-x86_64-rpms
      - rhel-9-for-x86_64-supplementary-rpms
      - ...
      become: true

This works well to enable the listed repos, however obviously this task always is reported as changed, even when the repos are already enabled.

How can I rewrite the task so that it executes the command only if the respective repo is not enabled already? (Then I would get the desired ok result instead of a changed result for already enabled repos.)

I’m not sure if you have on rhel-9 the equivalent of dnf5 repo list --json --disabled as in the first task below, but if you do then you should be able to get this to work. (The commands below work on Fedora 41, although of course the repo IDs are different.)

    - name: List disabled dnf repos
      ansible.builtin.command: dnf5 repo list --json --disabled
      register: repos_disabled

    - name: Enable only the repos of interest
      ansible.builtin.command:
        cmd: "dnf config-manager --enable {{ item }}"
      become: true
      vars:
        to_enable:
          - codeready-builder-for-rhel-9-x86_64-rpms
          - ansible-automation-platform-2.4-for-rhel-9-x86_64-rpms
          - rhel-9-for-x86_64-supplementary-rpms
      loop: '{{ repos_disabled.stdout
                | from_json
                | map(attribute="id")
                | intersect(to_enable) }}'

Thanks for chiming in!

dnf5 is not available, but dnf repolist --disabled works, so I might cut the repo names out of this…

Then you have two problems: (1) parsing the results from dnf repolist --disabled, and (2) parsing the registered result from the “repolist” task in the “config-manager” task.

You could just write the IDs of those repos to stdout, which you would register, and the 2nd task could use result.stdout_lines which, conveniently enough, is a list version of result.stdout. In fact, in this case, you probably should.

However, parsing “stuff” and generating JSON from bash not hard, and the technique may be useful especially if you have more complicated datums than single strings to return. So here’s the “create the list as JSON even though dnf repolist doesn’t support --json” version.

    - name: List disabled dnf repos
      args:
        executable: /bin/bash
      ansible.builtin.shell: |
        seen_header=no
        declare -a repos
        while read -r -a repo ; do
          if [ "${repo[0]} ${repo[1]}" = "repo id" ] ; then
            seen_header=yes
            continue
          fi
          [ "${seen_header}" = "yes" ] || continue
          repos+=( "\"${repo[0]}\"" ) # double-quoted!
        done < <( dnf repolist --disabled )
        # Print JSON to stdout
        IFS=',';
        printf "[%s]\n" "${repos[*]}"
      become: true
      changed_when: false
      register: repos_disabled

    - name: Enable only the repos of interest
      ansible.builtin.command:
        cmd: "dnf config-manager --enable {{ item }}"
      become: true
      register: result
      changed_when: result.rc == 0
      vars:
        to_enable:
          - codeready-builder-for-rhel-9-x86_64-rpms
          - ansible-automation-platform-2.4-for-rhel-9-x86_64-rpms
          - rhel-9-for-x86_64-supplementary-rpms
      loop: '{{ repos_disabled.stdout
                | from_json
                | intersect(to_enable) }}'

A couple of things worth pointing out.

First, because JSON likes double-quoted strings, go ahead and add the enclosing quotes at the time the strings are added to the array. That leaves only the problem of getting commas between the entries when we get around to producing the JSON.

Second, "${repos[*]}" will separate all the strings in the repos array with the first character of $IFS, so set that to a comma. (It’s right at the end of the script; otherwise we’d set it back to something sane, like $' \t\n'.)

Third, because our JSON is just a list of strings, we don’t have to use
“| map(attribute="id")” in the second task.

There you go - a much fancier hammer than this little nail requires, but people seem to have this thing about “don’t use the shell” when they would be perfectly happy using the same logic if it were wrapped in Python and hidden away in a plugin or module. Go figure.

1 Like

If you’re comfortable with using the community.general collection, there is a dnf_config_manager module specifically for enabling/disabling repositories. There’s also the rhsm_repository module for managing repositories through the subscription manager on entitled RHEL hosts. This might be preferred since you appear to be using RHEL 9, not CentOS 9 or other equivalent.

    - name: Enable repos
      community.general.dnf_config_manager:
        name:
          - codeready-builder-for-rhel-9-x86_64-rpms
          - ansible-automation-platform-2.4-for-rhel-9-x86_64-rpms
          - rhel-9-for-x86_64-supplementary-rpms
        state: enabled
      when: ansible_distribution_major_version == 9

    - name: Enable repos
      community.general.rhsm_repository:
        name:
          - codeready-builder-for-rhel-9-x86_64-rpms
          - ansible-automation-platform-2.4-for-rhel-9-x86_64-rpms
          - rhel-9-for-x86_64-supplementary-rpms
        state: enabled
      when: ansible_distribution_major_version == 9

    # This module appears to support wildcards
    - name: Enable repos 
      community.general.rhsm_repository:
        name:
          - codeready-builder-for-rhel-*-x86_64-rpms
          - ansible-automation-platform-2.4-for-rhel-*-x86_64-rpms
          - rhel-*-for-x86_64-supplementary-rpms
        state: enabled

1 Like

Here is another alternative.

Since repo files in yum.repos.d directory are just .ini files, you can also use community.general.ini_file module. It’s a bit low level but should work as expected. Here is an example:

- name: "Configure YUM repos"
  community.general.ini_file:
    path: "{{ item['file'] }}"
    section: "{{ item['name'] }}"
    option: "enabled"
    value: "1"
    no_extra_spaces: true
    create: false
  loop:
  - file: "/etc/yum.repos.d/redhat.repo"
    name: "codeready-builder-for-rhel-9-x86_64-rpms"
  - ...

The unfortunate thing is that in RHEL, subscription-manager likes to make a mess and overwrite any manual modification you have done to the repos.

This approach (1stly constructing a JSON list via a shell script, then 2ndly using intersect to get the repos to be enabled and then 3rdly feed them to dnf) works for me. Thanks a lot.

A few syntactically changes to make it run for me:

    - name: Record disabled dnf repos
      ansible.builtin.shell: |
        seen_header=no
        declare -a repos
        while read -r -a repo ; do
          if [ "${repo[0]} ${repo[1]}" = "repo id" ] ; then
            seen_header=yes
            continue
          fi
          [ "${seen_header}" = "yes" ] || continue
          repos+=( "\"${repo[0]}\"" ) # double-quoted!
        done < <( dnf repolist --disabled )
        # Print JSON to stdout
        IFS=',';
        printf "[%s]\n" "${repos[*]}"
      args:
        executable: /bin/bash  # <-- this needs to be in the args section
      become: true
      changed_when: false
      register: repos_disabled

and

    - name: Enable necessary repos only if not yet enabled
      ansible.builtin.command:
        cmd: "dnf config-manager --enable {{ item }}"
      register: act_res
      changed_when: act_res.rc == 0
      vars:
        to_enable:
          - codeready-builder-for-rhel-9-x86_64-rpms
          - ansible-automation-platform-2.4-for-rhel-9-x86_64-rpms
          - rhel-9-for-x86_64-supplementary-rpms
      loop: '{{ repos_disabled.stdout
                | from_json
                | intersect(to_enable) }}'
      become: true  # <-- this needs sudo

Thanks again! :slight_smile:

I think I would be comfortable with it, but the collection doesn;t seem to be installed. (I get a syntax error on community.general.dnf_config_manager:) And on an air-gapped machine I don’t want to add stuff if not absolutely necessary.

1 Like

That’s perfectly fine to keep dependencies to a minimum if need be. Depending on how you installed ansible-core, you most likely could install ansible as well, which is the “batteries included” version that includes the community.general collection among many others.

I installed ansible-core via dnf from available local mirrors of the RHEL9 repos.

I cannot see a plain ansible package. When I do a search via dnf I get:

ansible-automation-platform-common
ansible-automation-platform-installer
ansible-automation-platform-selinux
ansible-builder
ansible-collection-microsoft-sql
ansible-collection-redhat-rhel_mgmt
ansible-core
ansible-dev-tools
ansible-freeipa
ansible-freeipa-collection
ansible-freeipa-tests
ansible-lint
ansible-navigator
ansible-pcp
ansible-rulebook
ansible-runner
ansible-sign
ansible-test

I don’t think the communty stuff is in theer.

For RHEL9 and other rpm based distros, the ansible rpm package is available from EPEL.

You could also install via pip if you have pypi.org or local mirrors available.

While I stand by my proposed two-task solution above as a valid answer to the very specific original ask (with the corrections which you pointed out; thanks @halloleo), It’s always a good idea to step back, look at the larger picture, and question how we got into a situation that needed such an extraordinary solution in the first place.

For one thing, some package management modules allow temporarily enabling repositories during their operation, so it may not be necessary to enable them as a separate step anyway. It depends on why you want them enabled. If it’s just to install packages from them, you can skip enabling them first.

From a bigger picture perspective, even if you determine you don’t need other collections right now, it’s extremely unlikely that you never will. So you’d probably do yourself a favor to work out how to use the suggestions @Denney-tech is recommending now in your air-gapped environment so you’ll have less to struggle through in a genuine crunch later.

1 Like