How can I get the installed YUM packages with Ansible? - ansible

I am trying to get all the installed YUM packages on an RHEL machine. I can easily get it through using shell commands which is not idempotent and would like to use the YUM command instead.
The shell command works fine:
- name: yum list packages
shell: yum list installed > build_server_info.config
But when I try to use the YUM command, it just executes, but it does not give any results:
- name: yum_command
action: yum list=${pkg} list=available

I can easily get it through using shell commands which is not idempotent
You can't really talk about idempotence, when you are querying the current state of a machine.
"Idempontent" means that the task will ensure the machine is in the desired state no matter how many times you run a certain task.
When you query current state, you don't describe the desired state. No matter what you do, what method you use, the term "idempotent" is just not applicable.
Regarding your example, which does not give you results - you have repeated twice the same argument list and the task should fail (it doesn't, which looks like an Ansible quirk).
To get a list of installed packages, you should use:
- name: yum_command
yum:
list=installed
register: yum_packages
- debug:
var: yum_packages
It saves a list of dictionaries describing each package to a variable yum_packages.
You can then use a JSON Query Filter to get a single package (tar):
- debug: var=item
with_items: "{{yum_packages|json_query(jsonquery)}}"
vars:
jsonquery: "results[?name=='tar']"
to get a result like this:
"item": {
"arch": "x86_64",
"epoch": "2",
"name": "tar",
"nevra": "2:tar-1.26-31.el7.x86_64",
"release": "31.el7",
"repo": "installed",
"version": "1.26",
"yumstate": "installed"
}
Or only its version:
- debug: var=item
with_items: "{{yum_packages|json_query(jsonquery)}}"
vars:
jsonquery: "results[?name=='tar'].version"
"item": "1.26"

Since Ansible 2.5, you can also use the package_facts module: it will gather the list of installed packages as Ansible facts.
Example from the CPU:
- name: get the rpm package facts
package_facts:
manager: rpm
- name: show them
debug: var=ansible_facts.packages

Well, the official Ansible documentation for the yum module describes list as:
"Various (non-idempotent) commands for usage with /usr/bin/ansible and not playbooks."
so you're going to be out of luck with finding an idempotent list invocation.
If you just want to suppress the changed output, set the changed_when parameter to False.
(Also, having the duplicate list parameter is suspicious.)

If your requirement is to check for only one package, and based on that you want to execute another task, you can use package_facts module as
- name: install docker community edition in rhel8
hosts: localhost
connection: local
tasks:
- name: Gather the rpm package facts
package_facts:
manager: auto
- name: check if docker-ce is already installed
debug:
var: ansible_facts.packages['docker-ce']
- name: using command module to install docker-ce
command: yum install docker-ce --nobest -y
when: "'docker-ce' not in ansible_facts.packages"
register: install_res
- name: whether docker is installed now
debug:
var: install_res

Related

How to get latest package version with Ansible

I'm trying to replicate the command yum check-update package_name preferably with the Ansible yum module.
It provides an information to what version package would be updated after yum update execution (or ansible equivalent). For example:
root#host: $ yum check-update kernel
[...]
kernel.x86_64 3.10.0-1160.80.1.el7
[...]
root#host: $
I tried various combination of
- name: Xyz
ansible.builtin.yum:
list: updates
update_cache: true
But I can't limit it to a single package or pattern (like java*).
What I ended up with is ugly and slow (because of the download) workaround:
- name: Check latest available xyz version
yum:
name: xyz
state: latest
download_only: true
become: true
register: _result
- name: Register xyz version
set_fact:
latestXyz: "{{ _result.changes.updated[0][1] | regex_search('xyz-(.+).x86_64.*', '\\1') }}"
Is there any better way to achieve this?
One pretty easy way to achieve this is to run the yum module as you would to update the package, but enforce a dry run on it, with the check_mode option, and register the result of this task.
Inspecting the result, you'll realise that the module will list you the current version of the package and the version it would have been updated to, would it not have been run in dry run.
Given, for example
- yum:
name: expat
state: latest
check_mode: true
register: yum_latest_versions
Will populate the variable yum_latest_versions on a fedora:35 container with:
yum_latest_versions:
ansible_facts:
pkg_mgr: dnf
changed: true
failed: false
msg: 'Check mode: No changes made, but would have if not in check mode'
rc: 0
results:
- 'Installed: expat-2.5.0-1.fc35.x86_64'
- 'Removed: expat-2.4.9-1.fc35.x86_64'
Then to extract, you can indeed use a regex to search in the result:
- debug:
msg: >-
{{
(
yum_latest_versions.results
| map('regex_search', '^Installed: expat\-(.*)\.x86_64$', '\1')
).0.0
}}
Finally yields:
msg: 2.5.0-1.fc37

Getting a list of differences between list of installed packages and list of desired packages in ansible

(See update2 below for my final word on this.)
I'd like to have ansible generate a list of missing packages by comparing the installed packages with a list of desired packages. (The list of missing packages would then be passed to yum for installation.) It seems like this can be done using some combination of package_facts and the "difference" filter, but I can't come up with the right syntax.
For example, the following gives information about installed packages:
- name: get the rpm package facts
package_facts:
- name: show them
debug: var=ansible_facts.packages
and lists can be compared like this:
- name: Find difference of two lists
gather_facts: no
hosts: localhost
vars:
missing: "{{ input1|difference(input2) }}"
input1:
- john
- mary
input2:
- john
- alice
tasks:
- name: 'Print them out'
debug: var=missing
How can I plug these two techniques together to make a list that's usable later by yum?
Update
Zigarn's answer below gets me very close to what I want (with the caveat that it's "set_fact:" instead of "set_facts:" and "package:" instead of "packages:"). Here's what I'm trying now:
- name: Retrieve the installed packages
package_facts:
- name: Get only list of name of installed packages
set_fact:
installed_packages: "{{ ansible_facts.packages.keys() }}"
- name: Specify list of desired packages
set_fact:
desired_packages:
- emacs
- antiword
- gimp
- junk
- otherstuff
- name: Compute missing packages
set_fact:
missing_packages: "{{ desired_packages | difference (installed_packages) }}"
- name: Print list of missing packages
debug: var=missing_packages
- name: Print list of installed packages
debug: var=installed_packages
- name: Install missing packages
ignore_errors: yes
yum:
skip_broken: yes
name: "{{ item }}"
state: present
loop: "{{ missing_packages }}"
This does indeed try to install missing packages, with the odd exception of "emacs" which isn't currently installed, but also isn't put into the missing_packages list by ansible. This might just be something odd about this particular package, so I'm going to go ahead and mark zigarn's post as the answer but continue to look into the problem.
Regarding why I'm doing it this way, two reasons: First, I'm using a loop and a list instead of passing all the packages to yum at once because I want to avoid the case where a typo in the list of desired packages, or a named package that's not in the repositories, makes yum fail to install any of the listed packages. "skip_broken" doesn't help in these cases. Second, I'm installing a lot of packages (thousands) and yum is very slow, so whittling down the list to just the missing packages can save an hour on the time it takes to update a computer.
Update2
After experimenting with this for a while, I've decided that I'm not confident that I understand the data structure returned by ansible_facts.packages and the way "difference" interprets it, so the following is what I'm going to stick with for now.
- name: Get list of installed packages without arch
command: "rpm -q --qf '%{NAME}\n' -a"
args:
warn: false
register: installed_packages_noarch
- name: Get list of installed packages with arch
command: "rpm -q --qf '%{NAME}.%{ARCH}\n' -a"
args:
warn: false
register: installed_packages_arch
- name: Combine the lists of packages with and without arch
set_fact:
installed_packages: "{{ installed_packages_noarch.stdout_lines + installed_packages_arch.stdout_lines }}"
- name: Install missing packages
ignore_errors: yes
yum:
skip_broken: yes
state: present
name: "{{ item }}"
loop: "{{ desired_packages | difference (installed_packages) }}"
vars:
desired_packages:
- emacs
- gimp
- somethingelse
- anotherthing
Note that I've fallen back to just using rpm to get the list of installed packages, and that I actually have it generate two lists and combine them: A list of bare package names, and a list of packagename.architecture. This lets me specify things like libsomething.i686 or libother.x86_64 in the list of desired packages in cases where I want packages of multiple architectures installed.
First, you need to know that you don't need to compute yourself the list of missing packages as the package module (or platform-specific module yum, apt, ...) is idempotent and will anyway check itself the missing packages to install them.
But, if you need it for another purpose, here is how to do it.
package_facts is populating the ansible_facts.packages variables with a specific structured documented in https://docs.ansible.com/ansible/latest/collections/ansible/builtin/package_facts_module.html#returned-facts
If you want to compare it with a simple list of package names, you need to only use the keys of the package_facts. Then you can get the difference between the two lists:
- name: Retrieve the installed packages
package_facts:
- name: Get only list of name of installed packages
set_fact:
installed_packages: "{{ ansible_facts.packages.keys() }}"
- name: Compute missing packages
set_fact:
missing_packages: "{{ package_list_you_want | difference (installed_packages) }}"
- name: Install missing packages
package:
name: "{{ missing_packages }}"
state: present

how can I specify the version of Python to use in an Ansible playbook?

I'm working on a project that is still running Python 2. I'm trying to use Ansible to set up new test servers. The base Linux installation that I start with only has Python 3, so I need my very first "bootstrap" playbook to use Python 3, but then I want subsequent playbooks to use Python 2.
I can specify the version of python in my inventory file like this:
[test_server:vars]
ansible_python_interpreter=/usr/bin/python3
[test_server]
test_server.example.com
But then I have to go edit the inventory file to make sure that I'm using Python 3 for the bootstrap playbook, and then edit it again for the rest of my playbooks. That seems odd. I've tried a few different versions of changing ansible_python_interpreter within my playbooks, like
- hosts: test_server
ansible_python_interpreter: /usr/bin/python
and
- hosts: test_server
tasks:
- name: install pip
ansible_python_interpreter: /usr/bin/python
apt:
name: python-pip
but ansible complains that
ERROR! 'ansible_python_interpreter' is not a valid attribute for a Task
Even though https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html says that
You can still set ansible_python_interpreter to a specific path at any variable level (for example, in host_vars, in vars files, in playbooks, etc.).
What's the invocation to do this correctly?
Q: "What's the invocation to do this correctly?"
- hosts: test_server
tasks:
- name: install pip
ansible_python_interpreter: /usr/bin/python
apt:
name: python-pip
ERROR! 'ansible_python_interpreter' is not a valid attribute for a Task
A: ansible_python_interpreter is not a Playbook keyword. It is a variable and must be declared as such. For example, in the scope of a task
- hosts: test_server
tasks:
- name: install pip
apt:
name: python-pip
vars:
ansible_python_interpreter: /usr/bin/python
, or in the scope of a playbook
- hosts: test_server
vars:
ansible_python_interpreter: /usr/bin/python
tasks:
- name: install pip
apt:
name: python-pip
, or in any other suitable place. See Variable precedence: Where should I put a variable?
See:
INTERPRETER_PYTHON
INTERPRETER_PYTHON_FALLBACK
Notes:
Quoting from Changelog core 2.14:
configuration entry INTERPRETER_PYTHON_DISTRO_MAP is now 'private' and won't show up in normal configuration queries and docs, since it is not 'settable' this avoids user confusion.
Search _INTERPRETER_PYTHON_DISTRO_MAP in ansible/lib/ansible/config/base.yml to see current value of INTERPRETER_PYTHON_DISTRO_MAP

package installation not considered in playbook

I got some trouble with automating an installation using ansible.
I use this role (https://github.com/elastic/ansible-elasticsearch) to install elasticsearch on my ubuntu 16.04 server.
The role depends on the package python-jmespath, as mentioned in the documentation.
The role DOES NOT install the package itsself, so i try to install it before role execution.
- hosts: elasticsearch_master_servers
become: yes
tasks:
- name: preinstall jmespath
command: "apt-get install python-jmespath"
- name: Run the equivalent of "apt-get update" as a separate step
apt:
update_cache: yes
- hosts: elasticsearch_master_servers
become: yes
roles:
- role: elastic.elasticsearch
vars:
...
When running the playbook i expect the python-jmespath package to be installed before execuction of role takes place, but role execution fails with
You need to install \"jmespath\" prior to running json_query filter"
When i check if the package is installed manually using dpkg -s python-jmespath i can see the package is installed correctly.
A second run of the playbook (with the package already installed) doesnt fail.
Do I miss an ansible configuration, that updates the list of installed packages during the playbook run ?
Am I doing something wrong in general ?
Thanks in advance
FWIW. It's possible to tag installation tasks and install the packages in the first step. For example
- name: install packages
package:
name: "{{ item.name }}"
state: "{{ item.state|default('present') }}"
state: present
loop: "{{ packages_needed_by_this_role }}"
tags: manage_packages
Install packages first
shell> ansible_playbook my-playbook.yml -t manage_packages
and then run the playbook
shell> ansible_playbook my-playbook.yml
Notes
This approach makes checking of the playbooks with "--check" much easier.
Checking idempotency is also easier.
With tags: [manage_packages, never] the package task will be skipped when not explicitly selected. This will speed up the playbook.

Install local RPM with regex or glob filename using Ansible

I am trying to write an ansible playbook which installs some RPMs for me after they have been copied to a known location by a Jenkins job. The problem is, I'm not sure how to get the name of the RPM to install without hard coding it.
Here is what I have now:
- hosts: localhost
roles:
- { role: some_role, artifacts: "{{ rpm_path }}/prefix_.*.rpm" }
In this case, rpm_path would be something like:
"/home/jenkins/workspace/rpm_install/artifacts"
The role that is called in this example handles the yum install part:
- name: Install RPMs
yum: name={{item}} state=present
with_items:
- "{{ artifacts }}"
I'd rather not have to hard code RPM names since they come from Jenkins and they are always different. But is there a way either through the yum module, or when I call the role where the regular expression or glob can be interpreted so the full path (rpm name included) is handed to yum?
You should use with_fileglob insted of with_items, something like
- name: Install RPMs
yum: name="{{item}}" state=present
with_fileglob:
- "{{ artifacts }}"

Resources