Merging multiple tasks to reduce the output - ansible

We are trying to get lesser output while executing a playbook on multiple OS flavours. But unable to find a solution hence posting is here for a better answer.
As we get multiple task executed, is it possible to merge into one. We are collecting the output in a file & then will veryfy the same with different tags.
- name: verify hostname
block:
- name: read hostname [PRE]
shell: hostname
register: hostname
- name: set fact [hostname]
set_fact:
results_pre: "{{ results_pre | combine({'hostname': hostname.stdout.replace(\"'\", '\"')|quote }) }}"
- name: write hostname
copy:
dest: "{{ remote_logs_path }}/{{ ansible_ssh_host }}/pre/hostname"
content: "{{ hostname.stdout }}"
tags:
- pre
Current output
TASK [role : read hostname [PRE]] ***************************************************************************
changed: [ip]
TASK [role : set fact [hostname]] ***************************************************************************
ok: [ip]
TASK [role : write hostname] ********************************************************************************
changed: [ip]
Required Output
TASK [role : Hostname Collected] ********************************************************************************
changed: [ip]

Generally, it's a bad idea to parse Ansible output. You may get some runtime warnings or unexpected additional lines.
If you really want to stick to Ansible output, there are a so-called callback plugins, you may try to implement your own if you want.
If you need some report from Ansible playbook, the common pattern is to have a separate task, which reports into a file (usually, on a controller host, using delegate: localhost).
Finally, if you want to check for idempotence, Molecule provides this feature.

Related

Get list of filesystems and mountpoints using Ansible

I'm struggling to get a solution for this:
I need to create a CSV file with all the filesystems and respective mountpoints on every server, using Ansible.
I managed to create a Python script that displays the information I want:
hostname,filesystem,mountpoint
Works beautifully on every server EXCEPT those where Python is not installed :)
- name: Get filesystem data
shell: python /tmp/filesystem.py
register: get_fs_data
become: yes
become_method: sudo
become_user: root
- set_fact:
filesystem_data: "{{ get_fs_data.stdout }}"
when: get_fs_data.rc == 0
So, my question is ...
How can this be achieved without the usage of that Python script?
I basically want to build a similar list like the one above (hostname, filesystem, mountpoint).
I could execute something like:
df --type=xfs -h --output=source,target
But the output has several lines, one for each file system, and I'm not sure how to handle it on Ansible directly.
There are more options.
See Ansible collect df output and convert to dictionary from set_fact how to parse the output of df
The simplest option is to use the setup module parameter gather_subset: mounts. Unfortunately, this doesn't work properly for all OS. There is no problem with Linux. The playbook
- hosts: localhost
gather_facts: false
tasks:
- setup:
gather_subset:
- distribution
- mounts
- debug:
var: ansible_distribution
- debug:
var: ansible_mounts
gives (abridged)
TASK [debug] *********************************************************************************
ok: [localhost] =>
ansible_distribution: Ubuntu
TASK [debug] *********************************************************************************
ok: [localhost] =>
ansible_mounts:
- block_available: 2117913
block_size: 4096
block_total: 10013510
block_used: 7895597
device: /dev/nvme0n1p6
fstype: ext4
inode_available: 1750968
inode_total: 2564096
inode_used: 813128
mount: /
options: rw,relatime,errors=remount-ro
size_available: 8674971648
size_total: 41015336960
uuid: 505b60e7-509f-46e6-b833-f388df6bb9f0
...
But the same playbook on FreeBSD shows nothing
TASK [debug] *********************************************************************************
ok: [test_11] =>
ansible_distribution: FreeBSD
TASK [debug] *********************************************************************************
ok: [test_11] =>
ansible_mounts: []
If gather_subset: mounts works on your systems the report is simple. For example, the task below
- copy:
dest: /tmp/ansible_df_all.csv
content: |
hostname,filesystem,mountpoint
{% for hostname in ansible_play_hosts %}
{% for mount in hostvars[hostname]['ansible_mounts'] %}
{{ hostname }},{{ mount.device }},{{ mount.mount}}
{% endfor %}
{% endfor %}
run_once: true
delegate_to: localhost
will create the file at the controller
shell> cat /tmp/ansible_df_all.csv
hostname,filesystem,mountpoint
localhost,/dev/nvme0n1p6,/
localhost,/dev/nvme0n1p7,/export
localhost,/dev/nvme0n1p2,/boot/efi
Q: Get list of filesystems and mountpoints using Ansible and df without Python installed
As already mentioned within the other answer, there are many options possible.
the output has several lines, one for each file system, and I'm not sure how to handle it on Ansible directly
The data processing can be done on Remote Nodes (data pre-processing, data cleansing), Control Node (data post-processing) or partially on each of them.
Whereby I prefer the answer of #Vladimir Botka and recommend to use it since the data processing is done on the Control Node and in Python
How can this be achieved without the usage of that Python script? (annot.: or any Python installed on the Remote Node)
a lazy approach could be
---
- hosts: test
become: false
gather_facts: false # is necessary because setup.py depends on Python too
tasks:
- name: Gather raw 'df' output with pre-processing
raw: "df --type=ext4 -h --output=source,target | tail -n +2 | sed 's/ */,/g'"
register: result
- name: Show result as CSV
debug:
msg: "{{ inventory_hostname }},{{ item }}"
loop_control:
extended: true
label: "{{ ansible_loop.index0 }}"
loop: "{{ result.stdout_lines }}"
resulting into an output of
TASK [Show result as CSV] *************
ok: [test.example.com] => (item=0) =>
msg: test.example.com,/dev/sda3,/
ok: [test.example.com] => (item=1) =>
msg: test.example.com,/dev/sda4,/tmp
ok: [test.example.com] => (item=2) =>
msg: test.example.com,/dev/sdc1,/var
ok: [test.example.com] => (item=3) =>
msg: test.example.com,/dev/sda2,/boot
As noted before, the data pre-processing parts
Remove first line from output via | tail -n +2
Remove multiple whitepspaces and replace via | sed 's/ */,/g'
could be processed in Ansible and Python on the Control Node, in example
Remove first line in stdout_lines (annot.: and as a general approach for --no-headers even for commands which do not provide such)
Simply split item on space and re-join all elements of the list with comma (,) via {{ item | split | join(',') }}
- name: Gather raw 'df' output without pre-processing
raw: "df --type=ext4 -h --output=source,target"
register: result
- name: Show result as CSV
debug:
msg: "{{ inventory_hostname }},{{ item | split | join(',')}}"
loop_control:
extended: true
label: "{{ ansible_loop.index0 }}"
loop: "{{ result.stdout_lines[1:] }}" # with --no-headers
resulting into the same output as before.
Documentation Links
for Ansible
raw module - Executes a low-down and dirty command
This is useful and should only be done in a few cases ... speaking to any devices such as routers that do not have any Python installed. Arguments given to raw are run directly through the configured remote shell. Standard output, error output and return code are returned when available.
Whats the difference between Ansible raw, shell and command?
The command and shell module, as well gather_facts (annot.: setup.py) depend on a properly installed Python interpreter on the Remote Node(s). If that requirement isn't fulfilled one may experience errors
Extended loop variables
split filter – split a string into a list
Using filters to manipulate data - Manipulating strings, see split and join
Information about Ansible: magic variables
You can use the magic variable inventory_hostname, the name of the host as configured in your inventory, as an alternative to ansible_hostname when fact-gathering is disabled. If you have a long FQDN, you can use inventory_hostname_short, which contains the part up to the first period, without the rest of the domain.
How Ansible gather_facts and sets variables?
and Linux commands
How to omit heading in df command?
How to strip multiple spaces to one using sed?
Further Reading
cli_parse module – Parse cli output or text using a variety of parsers
Re: Wishlist: --no-header option for df

Why does the ansible find module work differently in a role when on localhost?

I have a role, called newrole. It sits in my ansible directory:
ansible
- roles
- newrole
- tasks
main.yml
- templates
template.j2
playbook1.yml
My playbook is defined quite simply:
---
- hosts: host1
roles:
- newrole
And the main.yml in tasks for the newrole:
- name: Find templates in the role
find:
path: "templates"
pattern: "*.j2"
register: result
- debug:
msg: "{{ result }}"
I then invoke the playbook from the ansible directory: ansible-playbook playbook1.yml
This works as expected: it runs on host1 and matches the template.j2 file. However, if I change the playbook ever so slightly to run on localhost instead of host1 (and specify connection: local), things go off the rail. It says it can't find the path templates. If I instead put in the path from the ansible directory (that I am running from), e.g. roles/newrole/templates, then it matches the template.j2 file. However, I can't figure out why that difference is occurring.
Aren't roles supposed to be providing a insulation from the directory structure I am running them from?
Even more confusing is that even on localhost, the template module (ansible.builtin.template) will search for templates only in the templates directory of the newrole (as I would originally expect). Can someone explain this behavior difference?
Is there a way to get the find module in my example to start searching at the root of the role regardless of localhost or not invocation?
When Ansible runs on nodes, it does not ships the whole playbook, nor the whole role on the said node, it packages a Python script that get send to the node and executed there.
So, if you do not send your files over to a node, a task executed on a node is not going to find the said file.
Now, for what you do need, you don't need a find task, you need the fileglob lookup, as lookups do tend to execute on the controller, rather than on the nodes.
Here is an example of usage:
- debug:
var: lookup('fileglob', 'templates/*.j2')
If we go beyond your localised issue, then, you can also use a delegation to the controller, in order to achieve the same.
In your case, that would translate in:
- find:
path: "templates"
pattern: "*.j2"
delegate_to: localhost
run_once: true
register: result
Given the playbook:
- hosts: node1
gather_facts: no
tasks:
- debug:
var: lookup('fileglob', 'templates/*.j2')
- find:
path: "templates"
pattern: "*.j2"
delegate_to: localhost
run_once: true
register: result
- debug:
var: result.files.0.path
This yields:
PLAY [node1] ******************************************************************
TASK [debug] ******************************************************************
ok: [node1] =>
lookup('fileglob', 'templates/*.j2'): /absolute/path/to/templates/template.j2
TASK [find] *******************************************************************
ok: [node1 -> localhost]
TASK [debug] ******************************************************************
ok: [node1] =>
result.files.0.path: templates/template.j2

Trying to get a task having delegate_to to show inventory alias instead of hostname in the output of a playbook

I have a task in my playbook to take backups of a directory on each remote host itself, as :
- name: Copy files to backup
synchronize:
src: /opt/myDir/
dest: /opt/myBackupDir/
archive: yes
ignore_errors: no
delegate_to: "{{ inventory_hostname }}"
register: sync_out
And my inventory is like :
myWeb1 ansible_host=prodvm1 ansible_user=testuser
myApp1 ansible_host=prodvm2 ansible_user=testuser
myApp2 ansible_host=prodvm3 ansible_user=testuser
The issue is that the output shown is like below :
TASK [Copy files to backup] **********************************************************************************************************************************************************
ok: [myWeb1 -> prodvm1]
ok: [myApp1 -> prodvm2]
ok: [myApp2 -> prodvm3]
Since I am delegating the task there are two fields shown in the output, separated by the arrow mark, but I would like to have it show the name / alias specified in the inventory , i.e., myApp1/myWeb1 instead of the actual hostname ( prodvm* ) after the arrows ( to avoid showing the hostname / IPs ).
I tried to use debug module to see what ansible is evaulating inventory_hostname to , but it gives the expected result, i.e., myWeb1.
How can I get similar behaviour when using the delegate_to module ?
It's really annoying behavior for Ansible. Delegation always 'cuts to the point' of where things happens.
If it's really annoy you, you can try disable delegation and use ansible_host trick, but at my opinion, delegation is better (even with junk in output).
This is the ansible_host trick:
- name: Copy files to backup
synchronize:
src: /opt/myDir/
dest: /opt/myBackupDir/
archive: true
var:
ansible_host: '{{ hostvars[name_where_its_delegated].ansible_host }}'
register: sync_out
I warn you again, delegation is better than this.

Reformat ip from ansible_facts without filter

I'm trying to dynamically update and reformat a host IP address for a template to use. I need to get the host IP, and reformat it from typical format into a format without a supporting filter.
I'm very new to ansible. Very new.
However, from what I've gathered...
I should be able to get the host ip "fact" from my playbook. I'm struggling with the syntax to get it to run, but I'm pretty confident that's something I will be able to do.
Once I have the ip, I need to reformat it from 11.22.33.44 into 11\\.22\\.33\\.44 .
So far, I can't even get the syntax right for grabbing the ip fact, let alone reformat it. With regard to the formatting, I've scoured the filters, but didn't find any (explicit) descriptions that would indicate a filter method that would format in the manner I need (though... I'd suspect it exists).
name: "test gathering a fact"
debug: var= "{{ hostvars[groups['webservers'][0]]['ansible_eth0']['ipv4']['address'] }}"
I expected something along the lines of...
11.22.33.44
But I'm getting:
ERROR! Syntax Error while loading YAML.
did not find expected key
The error appears to have been in '/home/my_playbook.yml': line 14, column 13, but may be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: "test gathering a fact"
debug: var= "{{ hostvars[groups['webservers'][0]]['ansible_eth0']['ipv4']['address'] }}"
^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes. Always quote template expression brackets when they
start a value. For instance:
with_items:
- {{ foo }}
Should be written as:
with_items:
- "{{ foo }}""
Once I have the ip, I need to reformat it from 11.22.33.44 into 11\\.22\\.33\\.44 .
- debug:
msg: >-
{{ the_host.ansible_default_ipv4.address | regex_replace('\.', '\\\\.') }}
# watch out if you choose not to use the `>-` syntax
# as then yaml quoting will become a real PITA
vars:
the_host: '{{ hostvars[groups.webservers[0]] }}'
You also might be happier using ansible_default_ipv4.address instead of ansible_eth0.ipv4.address for machines that don't always use eth0 (which is some modern ubuntu flavors, freebsd, etc). That is to say, you likely care about the machine's network identity, and not -- specifically -- what address is bound to a NIC named eth0
indentation is wrong. Instead of
- name: "test gathering a fact"
debug: var= ...
The correct syntax is
- name: "test gathering a fact"
debug: var= ...
I prefer to use the dot notation:
- name: first debug
debug:
msg: "{{ hostvars[groups['webservers'][0]].ansible_enp0s25.ipv4.address }}"
And that code returns this in the output:
TASK [first debug] **********************************************************************
ok: [localhost] => {
"msg": "192.168.65.105"
}
So combining these together into this playbook:
---
- hosts: all
gather_facts: true
tasks:
- name: first debug
debug:
msg: "{{ hostvars[groups['webservers'][0]].ansible_enp0s25.ipv4.address }}"
- name: second debug
debug:
msg: >-
{{ hostvars[groups['webservers'][0]].ansible_enp0s25.ipv4.address | regex_replace('\.', '\\\\.') }}
Gives this output:
PLAY [all] ******************************************************************************
TASK [Gathering Facts] ******************************************************************
ok: [localhost]
TASK [first debug] **********************************************************************
ok: [localhost] => {
"msg": "192.168.65.105"
}
TASK [second debug] *********************************************************************
ok: [localhost] => {
"msg": "192\\\\.168\\\\.65\\\\.105"
}
PLAY RECAP ******************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0

Improving use of add_host in ansible [duplicate]

This question already has answers here:
Ansible - Write multiple line output to file
(2 answers)
local_action: shell error concatenate files
(1 answer)
Closed 4 years ago.
I want to put hosts based on info I found on it in a in-memory group that I'll use on a later play.
I use this way based on writing the hostname of the target on the control node and then using this list to loop over with add_host, as add_host "bypasses the host loop and only runs once for all the hosts in the play" (I quote the documentation).
simplified example:
---
- hosts: servers
tasks:
- name: check if this server has foo role based on file presence
stat:
path: /tmp/foo
register: role
- name: write server name in file on control node
lineinfile:
path: /tmp/servers_foo.txt
state: present
line: "{{ inventory_hostname }}"
delegate_to: 127.0.0.1
when: role.stat.isfile is defined and role.stat.isfile
- name: assign target to group
add_host:
name: "{{ item }}"
groups:
- foo
with_lines: cat /tmp/servers_foo.txt
- hosts: foo
tasks:
- name: test which servers are part of foo
ping:
...
The problem of this playbook is the lineinfile task seems buggy because not all servers are written in the control node file.
…
TASK [lineinfile] ******************************************************************************
changed: [toto.foo -> 127.0.0.1]
changed: [tata.foo -> 127.0.0.1]
changed: [titi.foo -> 127.0.0.1]
changed: [tutu.foo -> 127.0.0.1]
TASK [assign target to group]
changed: [toto.foo] => (item=tata.foo)
changed: [toto.foo] => (item=tutu.foo)
…
this is confirmed by checking the file content on the control node
cat /tmp/servers_foo.txt
tata.foo
tutu.foo
I'm not sure why I have this problem (race condition on file access?) but to workaround this I added a serial: 1 to the first play, so this way it works but this is terribly slow if I have tens of servers and different check to implement.
Do you have a better and faster implementation of such use case or how could I fix the lineinfile task to not having the problem described.

Resources