How to sort complex version numbers in Ansible - sorting

I'm building an Ansible playbook in which I want to retrieve the latest version of a software. For this I used "sort" filter in Ansible. That, however, becomes a bit harder, when using version numbers, that are more complex and are not really numbers, e.g. 0.2.1, 0.10.1.
This is what I'm doing right now:
- name: Set version to compare
set_fact:
versions:
- "0.1.0"
- "0.1.5"
- "0.11.11"
- "0.9.11"
- "0.9.3"
- "0.10.2"
- "0.6.1"
- "0.6.0"
- "0.11.0"
- "0.6.5"
- name: Sorted list
debug:
msg: "{{ versions | sort }}"
- name: Show the latest element
debug:
msg: "{{ versions | sort | last }}"
The playbook above works, as long as version numbers stay underneath the number 10 (e.g. 0.9.3, but not 0.10.2).
To show the issue:
TASK [Set version to compare] ***************************************************************************************************************
ok: [localhost]
TASK [Sorted list] **************************************************************************************************************************
ok: [localhost] => {
"msg": [
"0.1.0",
"0.1.5",
"0.10.2",
"0.11.0",
"0.11.11",
"0.6.0",
"0.6.1",
"0.6.5",
"0.9.11",
"0.9.3"
]
}
TASK [Show the latest element] **************************************************************************************************************
ok: [localhost] => {
"msg": "0.9.3"
}
In this example the value the desired value is 0.11.11
Does anyone know a good way to sort complex version numbers in Ansible?
Any help would be appreciated. Thanks.

An option would be to write a filter plugin, e.g.
shell> cat filter_plugins/sort_versions.py
from distutils.version import LooseVersion
def filter_sort_versions(value):
return sorted(value, key=LooseVersion)
class FilterModule(object):
filter_sort = {
'sort_versions': filter_sort_versions,
}
def filters(self):
return self.filter_sort
Then the task below
- debug:
msg: "{{ versions | sort_versions }}"
gives
msg:
- 0.1.0
- 0.1.5
- 0.6.0
- 0.6.1
- 0.6.5
- 0.9.3
- 0.9.11
- 0.10.2
- 0.11.0
- 0.11.11
You don't have to write the filter if you can install the collection community.general. Use the filter community.general.version_sort, e.g. the task below gives the same result
- debug:
msg: "{{ versions | community.general.version_sort }}"

You can use Jinja2 compare version instead of installing a filter plugin
- name: test
set_fact:
max_number: "{{ item }}"
when: max_number |default('0') is version(item, '<')
loop: "{{ master_version }}"
just follow Playbook Test Comparing versions.

Part of what you are looking for is the version test, however, it is built on the idea that a user just wants one comparison. So, you'll need to do some glue to find the "latest" one:
- set_fact:
max_version: >-
{%- set vmax = {} -%}
{%- for v_1 in versions -%}
{%- for v_2 in versions -%}
{%- if v_1 is version(v_2, ">") and v_1 is version(vmax.get("max", "0.0.0"), ">") -%}
{%- set _ = vmax.update({"max": v_1}) -%}
{%- endif -%}
{%- endfor -%}
{%- endfor -%}
{{ vmax.max }}
(I don't pretend that's the optimal solution, as it is very likely comparing versions against each other more than once, but it should work fine for small version lists)

I had a similar use case where I had a version and needed to obtain the previous version from a list of versions.
---
- name: previous_version_filter
hosts: localhost
gather_facts: false
vars:
versions:
- "21.7.1"
- "21.13.0"
- "21.7.2"
- "21.13.1"
- "21.8.0"
- "21.7.0"
version: "21.13.0"
newer_versions: []
tasks:
- name: Create newer_versions list
set_fact:
newer_versions: "{{ newer_versions + [item] }}"
when: item is version(version, '>')
loop: "{{ versions }}"
- name: Create previous_versions list
set_fact:
previous_versions: "{{ versions | difference(newer_versions) | difference([ version ]) }}"
- name: Obtain previous_version
set_fact:
previous_version: "{{ item }}"
when: previous_version | default('0') is version(item, '<')
loop: "{{ previous_versions }}"
- debug:
msg: "{{ previous_version }}"

Related

Get name and version from a dictionary formatted facts

Below ansible tasks display pip packages in dictionary format.
- name: Ganther pip packages
pip_package_info:
clients: pip3
register: pip_pkgs
And the output is:
{
"packages": {
pip3: {
"xxx": [
{
name: "xxx"
source: "pip3"
version: "1.0.0"
}
],
"yyy": [
{
name: "yyy"
source: "pip3"
version: "2.0.0"
}
]
}
}
}
I tried to loop through the registered variable, but, I am getting errors about undefined variable. How to get the name and version from the above dictionary?
You can use the values method of the Python dictionary in order to have something that you could loop on:
- debug:
msg: "{{ item.name }} is in version {{ item.version }}"
loop: "{{ pip_pkgs.packages.pip3.values() | flatten }}"
Alternatively, it can also be achieved with dict2items, but makes the loop syntax a little bit longer, since you then have to map the attribute you are interested in — here, the value:
- debug:
msg: "{{ item.name }} is in version {{ item.version }}"
loop: >-
{{
pip_pkgs.packages.pip3
| dict2items
| map(attribute='value')
| flatten
}}
If you want to create a dictionary, this would be the easiest:
- set_fact:
pip_packages: >-
{{
dict(
_pip_pkg | map(attribute='name')
| zip(_pip_pkg | map(attribute='version'))
)
}}
vars:
_pip_pkg: "{{ pip_pkgs.packages.pip3.values() | flatten }}"
The dictionary would look like
pip_packages:
ansible: 5.6.0
ansible-compat: 2.0.2
ansible-core: 2.12.4
ansible-lint: 6.0.2
Given the two tasks:
- pip_package_info:
clients: pip3
register: pip_pkgs
- debug:
msg: "{{ item.name }} is in version {{ item.version }}"
loop: "{{ pip_pkgs.packages.pip3.values() | flatten }}"
loop_control:
label: "{{ item.name }}"
This gives:
ok: [localhost] => (item=ansible) =>
msg: ansible is in version 5.6.0
ok: [localhost] => (item=ansible-compat) =>
msg: ansible-compat is in version 2.0.2
ok: [localhost] => (item=ansible-core) =>
msg: ansible-core is in version 2.12.5
ok: [localhost] => (item=ansible-lint) =>
msg: ansible-lint is in version 6.0.2
... list goes on

Create a new list from an existing list in Ansible

This has got to be a simple question but I can't seem to find the answer anywhere.
I have the following list:
yum_repo_ip_addrs: ['172.16.130.4', '172.16.130.1']
I want to dynamically create a list called yum_baseurls where I copy in these values into the list along with the rest of the url. The list should ultimately look like this when run successfully:
yum_baseurls:
- "http://172.16.130.4/repos/elrepo-8-x86_64"
- "http://172.16.130.1/repos/elrepo-8-x86_64"
Instead, I'm finding that after the first iteration of my loop it's pasting in the variables literally.
Here's my playbook:
---
- name: Print the list of baseurl IP addresses.
debug:
msg: "{{ yum_repo_ip_addrs }}"
- name: Create the list of baseurls.
set_fact:
yum_baseurls: "{{ yum_baseurls + ['http://{{ item }}/repos/elrepo-{{ ansible_distribution_major_version }}-{{ ansible_userspace_architecture }}'] }}"
with_items:
- "{{ yum_repo_ip_addrs }}"
- name: print the list of baseurls.
debug:
msg: "{{ yum_baseurls }}"
And here's the output I get when I run it:
TASK [yum : Print the list of baseurl IP addresses.] ***************************************************************************************
ok: [ansibletarget3.jnk.sys] => {
"msg": [
"172.16.130.4",
"172.16.130.1"
]
}
TASK [yum : Create the list of baseurls.] **************************************************************************************************
ok: [ansibletarget3.jnk.sys] => (item=172.16.130.4)
ok: [ansibletarget3.jnk.sys] => (item=172.16.130.1)
TASK [yum : print the list of baseurls.] ***************************************************************************************************
ok: [ansibletarget3.jnk.sys] => {
"msg": [
"http://172.16.130.1/repos/elrepo-8-x86_64",
"http://{{ item }}/repos/elrepo-{{ ansible_distribution_major_version }}-{{ ansible_userspace_architecture }}"
]
}
Is there a better way to generate my list?
I'd remove it from the code and put it somewhere into the vars, e.g.
yum_repo_ip_addrs: [172.16.130.4, 172.16.130.1]
version: 8
architecture: x86_64
yum_baseurls_str: |
{% for ip in yum_repo_ip_addrs %}
- http://{{ ip }}/repos/elrepo-{{ version }}-{{ architecture }}
{% endfor %}
yum_baseurls: "{{ yum_baseurls_str|from_yaml }}"

Ansible merge dictionaries using with_items and vars stores only last item

Trying to create a dictionary per item and merge them
---
- name: TestingLab
hosts: localhost
gather_facts: False
tasks:
- name: Hello Vars
set_fact:
two_nums:
- 1
- 2
- name: create empty dict
set_fact:
ids: {}
- name: Merge all
vars:
single_entry: "{ '{{ item }}': {{ item }} }"
set_fact:
ids: "{{ ids | combine(single_entry) }}"
with_items: "{{ two_nums }}"
- name: Print Result
debug:
msg: "{{ ids }}"
I thought I followed the right guidelines but I seem to be getting only the last item afterwards:
ok: [localhost] => {
"msg": {
"2": 2
} }
I tried replacing the single_entry with the expression in vars but it does not run.
Is there a different syntax to get this done?
EDIT: version info
ansible-playbook 2.5.1
python version = 2.7.17 [GCC 7.5.0]
Try the filters dict and zip. The zip is available since 2.3, e.g.
- set_fact:
d2: "{{ dict(two_nums|zip(two_nums)) }}"
- debug:
var: d2
- debug:
var: d2|type_debug
gives
d2:
1: 1
2: 2
d2|type_debug: dict
If this does not work try Jinja and the filter from_yaml, e.g.
- hosts: localhost
vars:
two_nums:
- 1
- 2
l1: |-
{% for i in two_nums %}
{{ i }}: {{ i }}
{% endfor %}
tasks:
- set_fact:
d1: "{{ l1|from_yaml }}"
- debug:
var: d1
- debug:
var: d1|type_debug
gives the same result
d1:
1: 1
2: 2
d1|type_debug: dict
If you need the keys to be strings quote it, e.g.
l1: |-
{% for i in two_nums %}
"{{ i }}": {{ i }}
{% endfor %}
gives
d1:
'1': 1
'2': 2
In the first case, map the list's items to string, e.g.
- set_fact:
d2: "{{ dict(two_nums|map('string')|zip(two_nums)) }}"
gives the same result
d2:
'1': 1
'2': 2
I can't reproduce the behavior you're describing. Running your
playbook verbatim, I get as output:
TASK [Print Result] **************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"1": 1,
"2": 2
}
}
I'm using Ansible core 2.11.2, but I've also tested your playbook with Ansible 2.9.20 and I get the same output.
I would probably drop the set_fact task, and also change how you're
setting single_entry:
- name: TestingLab
hosts: localhost
gather_facts: False
tasks:
- name: Hello Vars
set_fact:
two_nums:
- 1
- 2
- name: Merge all
vars:
ids: {}
single_entry: "{{ {item: item} }}"
set_fact:
ids: "{{ ids | combine(single_entry) }}"
with_items: "{{ two_nums }}"
- name: Print Result
debug:
msg: "{{ ids }}"
In this version, the template expression is returning a dictionary,
and only requires a single set of Jinja template markers. I'm curious
if this version behaves any differently for you.

Sorting list of version numbers in Jinja2 [duplicate]

I'm building an Ansible playbook in which I want to make a backup of a database in case I need to upgrade the software. For this I want to compare the highest version number that is available to the version number that is installed. In case the latest version is hight than the installed version I'll back up the database.
The problem however is that I cannot find a good way to sort version numbers in Ansible. The standard sort filter sorts on strings instead of numbers/versions.
This is what I'm doing right now:
- name: Get package version
yum:
list: package
register: software_version
- name: Read which version is installed and available
set_fact:
software_version_installed: "{{ software_version | json_query(\"results[?yumstate=='installed'].version\") | sort | last }}"
software_version_available: "{{ software_version | json_query(\"results[?yumstate=='available'].version\") | sort | last }}"
- name: Backup old database file on remote host
copy:
src: "{{ software.database_path }}"
dest: "{{ software.database_path }}_{{ ansible_date_time.date }}_v{{ software_version_installed }}"
remote_src: yes
when: software_version_installed is version(software_version_available, "<")
The playbook above works, as long as version numbers stay underneath the number 10 (e.g. 1.2.3, but not 1.10.1) since sorting is performed like a string. When the version number has to sort e.g. 1.2.3 and 1.10.1, it will take 1.2.3 as latest version.
To show the issue:
- name: Read which version is installed and available
set_fact:
software_versions: [ "2.5.0", "2.9.0", "2.10.0", "2.11.0" ]
- name: Debug
debug:
var: software_versions | sort
TASK [grafana : Debug]**********************************
ok: [localhost] => {
"software_versions | sort": [
"2.10.0",
"2.11.0",
"2.5.0",
"2.9.0"
]
}
Does anyone know a good way to sort version numbers in Ansible?
Q: Does anyone know a good way to sort version numbers in Ansible?
A: Use filter_plugin. For example the filter
shell> cat filter_plugins/version_sort.py
from distutils.version import LooseVersion
def version_sort(l):
return sorted(l, key=LooseVersion)
class FilterModule(object):
def filters(self):
return {
'version_sort' : version_sort
}
with the playbook
shell> cat test-versions.yml
- name: Sort versions
hosts: localhost
vars:
versions:
- "0.1.0"
- "0.1.5"
- "0.11.11"
- "0.9.11"
- "0.9.3"
- "0.10.2"
- "0.6.1"
- "0.6.0"
- "0.11.0"
- "0.6.5"
tasks:
- debug:
msg: "{{ versions|version_sort }}"
gives
"msg": [
"0.1.0",
"0.1.5",
"0.6.0",
"0.6.1",
"0.6.5",
"0.9.3",
"0.9.11",
"0.10.2",
"0.11.0",
"0.11.11"
]
For your convenience, the filter is available at Github ansible-plugins.
Version comparison does the job to iterate the list and compare items. See the example below
shell> cat test-versions.yml
- hosts: localhost
vars:
version_installed: "1.10.1"
versions:
- "1.1.3"
- "1.2.3"
- "1.7.5"
- "1.10.7"
tasks:
- debug: msg="{{ item }} is newer than {{ version_installed }}"
loop: "{{ versions }}"
when: item is version(version_installed, '>')
shell> ansible-playbook test-versions.yml | grep msg
"msg": "1.10.7 is newer than 1.10.1"
It's now solved in another way. Instead of sorting the versions I compared the current version to all available versions.
I've started by setting an update variable to false
Next I compared the installed version to every available version
If installed version > current version, set the update variable to true
The task performing the backup will only be performed when the update variable is true.
- name: Get package version
yum:
list: package
register: software_version
- name: Read which version is installed and available
set_fact:
software_update: false
software_version_installed: "{{ software_version | json_query(\"results[?yumstate=='installed'].version\") | last }}"
software_version_available: "{{ software_version | json_query(\"results[?yumstate=='available'].version\") }}"
- name: Check if upgrade is needed
set_fact:
software_update: true
when: software_version_installed is version(item, "<")
with_items: "{{ software_version_available }}"
- name: Backup old database file on remote host
copy:
src: "{{ software.database_path }}"
dest: "{{ software.database_path }}_{{ ansible_date_time.date }}_v{{ software_version_installed }}"
remote_src: yes
when: software_update
All the answers here provide a custom way of sorting versions, I'd like to point out that stock ansible can do this already:
- name: Sort list by version number
debug:
var: ansible_versions | community.general.version_sort
vars:
ansible_versions:
- '2.8.0'
- '2.11.0'
- '2.7.0'
- '2.10.0'
- '2.9.0'
https://docs.ansible.com/ansible/latest/collections/community/general/docsite/filter_guide_working_with_versions.html
The current accepted answer offers a very nice solution, using a filter_plugin. Unfortunately, the distutils Python package that is uses appears to be deprecated. Some googling led me to the packaging package, which offers a similar Version class. Here's an updated filter_plugin that doesn't use distutils:
from packaging.version import Version
def version_sort(l):
return sorted(l, key=Version)
class FilterModule(object):
def filters(self):
return {
'version_sort': version_sort
}
It's working well for us, but I don't want to promise that the behavior will be exactly the same in every situation.

How to sort version numbers in Ansible

I'm building an Ansible playbook in which I want to make a backup of a database in case I need to upgrade the software. For this I want to compare the highest version number that is available to the version number that is installed. In case the latest version is hight than the installed version I'll back up the database.
The problem however is that I cannot find a good way to sort version numbers in Ansible. The standard sort filter sorts on strings instead of numbers/versions.
This is what I'm doing right now:
- name: Get package version
yum:
list: package
register: software_version
- name: Read which version is installed and available
set_fact:
software_version_installed: "{{ software_version | json_query(\"results[?yumstate=='installed'].version\") | sort | last }}"
software_version_available: "{{ software_version | json_query(\"results[?yumstate=='available'].version\") | sort | last }}"
- name: Backup old database file on remote host
copy:
src: "{{ software.database_path }}"
dest: "{{ software.database_path }}_{{ ansible_date_time.date }}_v{{ software_version_installed }}"
remote_src: yes
when: software_version_installed is version(software_version_available, "<")
The playbook above works, as long as version numbers stay underneath the number 10 (e.g. 1.2.3, but not 1.10.1) since sorting is performed like a string. When the version number has to sort e.g. 1.2.3 and 1.10.1, it will take 1.2.3 as latest version.
To show the issue:
- name: Read which version is installed and available
set_fact:
software_versions: [ "2.5.0", "2.9.0", "2.10.0", "2.11.0" ]
- name: Debug
debug:
var: software_versions | sort
TASK [grafana : Debug]**********************************
ok: [localhost] => {
"software_versions | sort": [
"2.10.0",
"2.11.0",
"2.5.0",
"2.9.0"
]
}
Does anyone know a good way to sort version numbers in Ansible?
Q: Does anyone know a good way to sort version numbers in Ansible?
A: Use filter_plugin. For example the filter
shell> cat filter_plugins/version_sort.py
from distutils.version import LooseVersion
def version_sort(l):
return sorted(l, key=LooseVersion)
class FilterModule(object):
def filters(self):
return {
'version_sort' : version_sort
}
with the playbook
shell> cat test-versions.yml
- name: Sort versions
hosts: localhost
vars:
versions:
- "0.1.0"
- "0.1.5"
- "0.11.11"
- "0.9.11"
- "0.9.3"
- "0.10.2"
- "0.6.1"
- "0.6.0"
- "0.11.0"
- "0.6.5"
tasks:
- debug:
msg: "{{ versions|version_sort }}"
gives
"msg": [
"0.1.0",
"0.1.5",
"0.6.0",
"0.6.1",
"0.6.5",
"0.9.3",
"0.9.11",
"0.10.2",
"0.11.0",
"0.11.11"
]
For your convenience, the filter is available at Github ansible-plugins.
Version comparison does the job to iterate the list and compare items. See the example below
shell> cat test-versions.yml
- hosts: localhost
vars:
version_installed: "1.10.1"
versions:
- "1.1.3"
- "1.2.3"
- "1.7.5"
- "1.10.7"
tasks:
- debug: msg="{{ item }} is newer than {{ version_installed }}"
loop: "{{ versions }}"
when: item is version(version_installed, '>')
shell> ansible-playbook test-versions.yml | grep msg
"msg": "1.10.7 is newer than 1.10.1"
It's now solved in another way. Instead of sorting the versions I compared the current version to all available versions.
I've started by setting an update variable to false
Next I compared the installed version to every available version
If installed version > current version, set the update variable to true
The task performing the backup will only be performed when the update variable is true.
- name: Get package version
yum:
list: package
register: software_version
- name: Read which version is installed and available
set_fact:
software_update: false
software_version_installed: "{{ software_version | json_query(\"results[?yumstate=='installed'].version\") | last }}"
software_version_available: "{{ software_version | json_query(\"results[?yumstate=='available'].version\") }}"
- name: Check if upgrade is needed
set_fact:
software_update: true
when: software_version_installed is version(item, "<")
with_items: "{{ software_version_available }}"
- name: Backup old database file on remote host
copy:
src: "{{ software.database_path }}"
dest: "{{ software.database_path }}_{{ ansible_date_time.date }}_v{{ software_version_installed }}"
remote_src: yes
when: software_update
All the answers here provide a custom way of sorting versions, I'd like to point out that stock ansible can do this already:
- name: Sort list by version number
debug:
var: ansible_versions | community.general.version_sort
vars:
ansible_versions:
- '2.8.0'
- '2.11.0'
- '2.7.0'
- '2.10.0'
- '2.9.0'
https://docs.ansible.com/ansible/latest/collections/community/general/docsite/filter_guide_working_with_versions.html
The current accepted answer offers a very nice solution, using a filter_plugin. Unfortunately, the distutils Python package that is uses appears to be deprecated. Some googling led me to the packaging package, which offers a similar Version class. Here's an updated filter_plugin that doesn't use distutils:
from packaging.version import Version
def version_sort(l):
return sorted(l, key=Version)
class FilterModule(object):
def filters(self):
return {
'version_sort': version_sort
}
It's working well for us, but I don't want to promise that the behavior will be exactly the same in every situation.

Resources