You do not need Ansible Galaxy, or even the ansible-galaxy command, to use this role. A role is a directory: Ansible looks for libre_devops.python in a roles/ directory next to your playbook, then in each directory on roles_path. Getting the directory there is the whole job, from GitHub or from a copy inside your own network.
Tested: methods 1 to 3, the offline collections and the git server copy were run on 11 November 2026 with ansible-core 2.16 and 2.21 while Galaxy was unreachable, using
libre_devops.mde_linux1.0.0. The steps are the same for every libre_devops role. The AWX, Automation Controller, execution environment and automation hub notes follow those products' documentation and were not tested here.
- Before you start
- Choose a method
- Method 1: clone it with git
- Method 2: unpack a release archive
- Method 3: a requirements file
- Collections
- Publishing it inside your organisation
- Checking what you install
- Python and ansible-core versions
Pick a release. Releases are git tags such as 1.0.0, listed on the releases page. Pin one. main is where changes land before they are released.
Keep the name. The README and every example call the role libre_devops.python, so the directory must have that name: roles/libre_devops.python. If you give it another name, use that name in your playbooks instead, including in any include_role.
| Method | Needs on the control node | Good for |
|---|---|---|
| 1. Clone it with git | git |
Keeping the role in your playbook repository as a submodule, or updating with git. |
| 2. Unpack a release archive | curl (or a browser) and tar |
Control nodes without git, and carrying the role into a network with no internet access. |
| 3. A requirements file | ansible-galaxy, which comes with ansible-core |
Installing several roles and collections in one step, from GitHub or your own servers, as CI, AWX, Automation Controller and execution environment builds do. It never contacts Galaxy for an entry that names its own source. |
mkdir -p roles
git clone --branch 1.0.0 https://github.com/libre-devops/ansible-role-python.git roles/libre_devops.python--branch takes a tag too. Git says the checkout is a detached HEAD, which is what you want for a fixed release. To move to a later release:
git -C roles/libre_devops.python fetch --tags
git -C roles/libre_devops.python checkout 1.1.0If your playbooks are in a git repository, a submodule records the exact commit in it, so everyone who clones the repository gets the same role:
git submodule add https://github.com/libre-devops/ansible-role-python.git roles/libre_devops.python
git -C roles/libre_devops.python checkout 1.0.0
git add .gitmodules roles/libre_devops.python
git commit -m "Add libre_devops.python 1.0.0"Anyone who clones your repository then runs git submodule update --init.
GitHub serves every release tag as an archive. No git is needed, and the file can be copied anywhere.
curl -fsSL -o ansible-role-python-1.0.0.tar.gz https://github.com/libre-devops/ansible-role-python/archive/refs/tags/1.0.0.tar.gz
sha256sum ansible-role-python-1.0.0.tar.gz
mkdir -p roles/libre_devops.python
tar -xzf ansible-role-python-1.0.0.tar.gz --strip-components=1 -C roles/libre_devops.pythonKeep the SHA-256 from the first download and compare it whenever you install the same file again. --strip-components=1 drops the archive's top directory, which is named after the repository and version, so the files land straight in the role's directory. To update, delete the directory and unpack the new release.
ansible-galaxy ships with ansible-core. Despite its name, it only contacts Galaxy for an entry that is just a name. Give each entry a source and it fetches from there:
---
roles:
# From GitHub, or from your own git server (see Publishing it inside your organisation).
- name: libre_devops.python
src: https://github.com/libre-devops/ansible-role-python.git
scm: git
version: 1.0.0
# Or from an archive on any HTTPS server, such as an artifact store:
# - name: libre_devops.python
# src: https://artifacts.example.internal/ansible/ansible-role-python-1.0.0.tar.gzansible-galaxy role install -r requirements.yml -p rolesAdd --force after changing version, or the copy already installed stays. With scm: git, version also takes a full commit hash, the strictest pin there is.
This role needs no collections, so installing the role is all there is to it.
Copy the repository with its tags, then point method 1 or 3 at your copy:
git clone --bare https://github.com/libre-devops/ansible-role-python.git
cd ansible-role-python.git
git push --all https://git.example.internal/ansible/ansible-role-python.git
git push --tags https://git.example.internal/ansible/ansible-role-python.gitTo pick up new releases later, run in the same directory:
git fetch --prune --tags origin '+refs/heads/*:refs/heads/*'
git push --all https://git.example.internal/ansible/ansible-role-python.git
git push --tags https://git.example.internal/ansible/ansible-role-python.gitRun that on a schedule, or use your git server's own pull mirroring where it has one. Your requirements file then names your server:
roles:
- name: libre_devops.python
src: https://git.example.internal/ansible/ansible-role-python.git
scm: git
version: 1.0.0Download the release archive and record its SHA-256 as in method 2, then upload it to anything that serves files over HTTPS: a generic repository in Artifactory or Nexus, or an internal web server. Install it with method 2 from your URL, or with the archive form of method 3.
Red Hat's private automation hub holds collections and execution environments, not standalone roles like this one. Keep the role on a git server or in an artifact store, and use the hub for the collections.
When they sync a project, AWX and Automation Controller install the roles listed in the project's roles/requirements.yml, from the sources it names. Point those at your git server or artifact store. Or commit the role into the project's roles/ directory with method 1 or 2, and nothing is fetched at all.
ansible-builder installs the roles and collections in the requirements file that the definition names under dependencies.galaxy, from the sources that file gives. Use the same requirements file as method 3.
-
Pin the commit, not only the tag. This shows the commit a release tag points at:
git ls-remote https://github.com/libre-devops/ansible-role-python.git refs/tags/1.0.0
Use that hash as
versionin a requirements file, or withgit checkoutin method 1, and the role cannot change under you. -
Signed commits. Every commit on
mainis signed, and GitHub shows it as Verified. -
Archives. Compare the SHA-256 you recorded every time you install an archive.
ansible --version shows both: the first line is the ansible-core version, and python version is the Python that ansible-core itself runs on, on the control node. That is not your servers' Python.
This role needs ansible-core 2.16 or later. Distribution packages are often older, and older still on long-lived control nodes, such as Ansible 2.9. A current ansible-core can sit beside them without touching the system's Python:
python3.12 -m venv ~/ansible
~/ansible/bin/pip install ansible-core
~/ansible/bin/ansible --versionRHEL 8 and RHEL 9 both have python3.12 in AppStream (dnf install python3.12).
ansible-core runs its modules with the Python on each managed node, and each release drops older versions of Python:
| ansible-core | Oldest Python it can run modules with |
|---|---|
| 2.16 (out of support since May 2025) | 3.6 |
| 2.17 | 3.7 |
| 2.18 and 2.19 | 3.8 |
| 2.20 to 2.22 | 3.9 |
| Distribution | Its own Python | With a current ansible-core |
|---|---|---|
| RHEL 8 and its rebuilds | 3.6 (/usr/libexec/platform-python) |
Does not work until you add a newer Python. |
| RHEL 9 and its rebuilds | 3.9 | Works. |
| RHEL 10 and its rebuilds | 3.12 | Works. |
| SLES 15 | 3.6 | Does not work until you add a newer Python. |
| Debian 12 and 13, Ubuntu 24.04 and later | 3.11 or later | Works. |
With ansible-core 2.17 or later, a RHEL 8 host fails on its first task, usually Gathering Facts, before this role runs at all. The message is The module interpreter '/usr/bin/python3' was not found, because RHEL 8 has no /usr/bin/python3 unless you install one. Pointing ansible_python_interpreter at /usr/libexec/platform-python gives Module result deserialization failed instead. Every module fails the same way, not only this role's.
There are three ways round it.
Use this role (recommended). That is what it is for. Put it first in a play with gather_facts: false, and it finds or installs a Python the running ansible-core accepts, from the host's repositories (Satellite included), package files pushed from the control node, or a self-contained build, then sets ansible_python_interpreter for the rest of the run:
- name: Give the hosts a usable Python
hosts: all
become: true
gather_facts: false
roles:
- role: libre_devops.pythonSee the README's Where Python comes from for the sources. Two other ways follow, for completeness.
Or install one with a play of your own. raw runs a plain command and needs no Python on the host, so it can install one:
---
- name: Give RHEL 8 hosts a Python that current ansible-core supports
hosts: rhel8
become: true
gather_facts: false
tasks:
- name: Install Python 3.12 from AppStream
ansible.builtin.raw: test -x /usr/bin/python3.12 || dnf -y install python3.12
args:
executable: /bin/sh
register: bootstrap_python
changed_when: "'Installed:' in bootstrap_python.stdout"Then tell Ansible to use it, for example in group_vars/rhel8.yml:
ansible_python_interpreter: /usr/bin/python3.12On SLES 15, install python311 (zypper --non-interactive install python311) and use /usr/bin/python3.11.
Or run ansible-core 2.16 on the control node. It is the last release that can manage Python 3.6, but it has had no fixes, security ones included, since May 2025:
python3.12 -m venv ~/ansible-2.16
~/ansible-2.16/bin/pip install 'ansible-core>=2.16,<2.17'This role's CI tests current ansible-core only, not 2.16, so prefer the first way.