ansible to perform a task based on hostname value - ansible

Hi I am trying below.
task:
- name: Perform on primary server
blockinfile:
path: '/home/conf'
marker: "#-- {mark} Adding Values --"
block: |
{{ conf }}
when: "'host01' in inventory_hostname"
- name: Perform on stdby server
blockinfile:
path: '/home/conf'
marker: "#-- {mark} Adding Values --"
block: |
{{ conf_stdby }}
when: "'host02' in inventory_hostname"
As every task is performed simultaneously on all hosts, i was expecting it would change host01 in 1st task and skip host02 and vice-versa in second task, However it changed in both hosts in both tasks and when i checked the servers both had conf_stdby.
Also there are many more tasks in my playbook which are common to both the hosts.
inventory_hostname wouldn't work as in inventory file of playbook there is ip no hostname, so is there a way i can use actual host's hostname in when condition?
Even tried this
vars:
my_conf:
host01: "{{ conf }}"
host02: "{{ conf_stdby }}"
task:
- name: Perform on primary server
blockinfile:
path: '/home/conf'
marker: "#-- {mark} Adding Values --"
block: |
{{ conf }}
when: ""{{hostvars[inventory_hostname].ansible_hostname}} in myconf"
Still both host add the same block

You're doing all right. The code should work. To test the strings are equal, usually "==" is used instead of the inclusion "in". But, you can reference the configuration data in a dictionary and simplify the code, e.g.
vars:
my_conf:
host01: "{{ conf }}"
host02: "{{ conf_stdby }}"
task:
- blockinfile:
path: /home/conf
marker: "#-- {mark} Adding Values --"
block: |
{{ my_conf[inventory_hostname] }}
when: "inventory_hostname in my_conf"
Q: "In the inventory, there is no hostname but ip."
A: Whatever you have got in the inventory put it into the dictionary. There are no restrictions on keys in the YAML dictionaries, e.g. the inventory
shell> cat hosts
10.1.0.61
10.1.0.62
and the playbook
shell> cat playbook.yml
- hosts: all
gather_facts: false
vars:
my_conf:
10.1.0.61: conf
10.1.0.62: conf_sdtdby
tasks:
- debug:
msg: "{{ my_conf[inventory_hostname] }}"
gives
ok: [10.1.0.61] =>
msg: conf
ok: [10.1.0.62] =>
msg: conf_sdtdby

Related

Check if a file has certain strings

I have some files (file1), in some servers (group: myservers), which should look like this:
search www.mysebsite.com
nameserver 1.2.3.4
nameserver 1.2.3.5
This is an example of what this file should look like:
The first line is mandatory ("search www.mysebsite.com").
The second and the third lines are mandatory as well, but the ips can change (although they should all be like this: ...).
I've being researching to implement some tasks using Ansible to check if the files are properly configured. I don't want to change any file, only check and output if the files are not ok or not.
I know I can use ansible.builtin.lineinfile to check it, but I still haven't managed to find out how to achieve this.
Can you help please?
For example, given the inventory
shell> cat hosts
[myservers]
test_11
test_13
Create a dictionary of what you want to audit
audit:
files:
/etc/resolv.conf:
patterns:
- '^search example.com$'
- '^nameserver \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
/etc/rc.conf:
patterns:
- '^sshd_enable="YES"$'
- '^syslogd_flags="-ss"$'
Declare the directory at the controller where the files will be stored
my_dest: /tmp/ansible/myservers
fetch the files
- fetch:
src: "{{ item.key }}"
dest: "{{ my_dest }}"
loop: "{{ audit.files|dict2items }}"
Take a look at the fetched files
shell> tree /tmp/ansible/myservers
/tmp/ansible/myservers
├── test_11
│   └── etc
│   ├── rc.conf
│   └── resolv.conf
└── test_13
└── etc
├── rc.conf
└── resolv.conf
4 directories, 4 files
Audit the files. Create the dictionary host_files_results in the loop
- set_fact:
host_files_results: "{{ host_files_results|default({})|
combine(host_file_dict|from_yaml) }}"
loop: "{{ audit.files|dict2items }}"
loop_control:
label: "{{ item.key }}"
vars:
host_file_path: "{{ my_dest }}/{{ inventory_hostname }}/{{ item.key }}"
host_file_lines: "{{ lookup('file', host_file_path).splitlines() }}"
host_file_result: |
[{% for pattern in item.value.patterns %}
{{ host_file_lines[loop.index0] is regex pattern }},
{% endfor %}]
host_file_dict: "{ {{ item.key }}: {{ host_file_result|from_yaml is all }} }"
gives
ok: [test_11] =>
host_files_results:
/etc/rc.conf: true
/etc/resolv.conf: true
ok: [test_13] =>
host_files_results:
/etc/rc.conf: true
/etc/resolv.conf: true
Declare the dictionary audit_files that aggregates host_files_results
audit_files: "{{ dict(ansible_play_hosts|
zip(ansible_play_hosts|
map('extract', hostvars, 'host_files_results'))) }}"
gives
audit_files:
test_11:
/etc/rc.conf: true
/etc/resolv.conf: true
test_13:
/etc/rc.conf: true
/etc/resolv.conf: true
Evaluate the audit results
- block:
- debug:
var: audit_files
- assert:
that: "{{ audit_files|json_query('*.*')|flatten is all }}"
fail_msg: "[ERR] Audit of files failed. [TODO: list failed]"
success_msg: "[OK] Audit of files passed."
run_once: true
gives
msg: '[OK] Audit of files passed.'
Example of a complete playbook for testing
- hosts: myservers
vars:
my_dest: /tmp/ansible/myservers
audit:
files:
/etc/resolv.conf:
patterns:
- '^search example.com$'
- '^nameserver \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
/etc/rc.conf:
patterns:
- '^sshd_enable="YES"$'
- '^syslogd_flags="-ss"$'
audit_files: "{{ dict(ansible_play_hosts|
zip(ansible_play_hosts|
map('extract', hostvars, 'host_files_results'))) }}"
tasks:
- fetch:
src: "{{ item.key }}"
dest: "{{ my_dest }}"
loop: "{{ audit.files|dict2items }}"
loop_control:
label: "{{ item.key }}"
- set_fact:
host_files_results: "{{ host_files_results|default({})|
combine(host_file_dict|from_yaml) }}"
loop: "{{ audit.files|dict2items }}"
loop_control:
label: "{{ item.key }}"
vars:
host_file_path: "{{ my_dest }}/{{ inventory_hostname }}/{{ item.key }}"
host_file_lines: "{{ lookup('file', host_file_path).splitlines() }}"
host_file_result: |
[{% for pattern in item.value.patterns %}
{{ host_file_lines[loop.index0] is regex pattern }},
{% endfor %}]
host_file_dict: "{ {{ item.key }}: {{ host_file_result|from_yaml is all }} }"
- debug:
var: host_files_results
- block:
- debug:
var: audit_files
- assert:
that: "{{ audit_files|json_query('*.*')|flatten is all }}"
fail_msg: "[ERR] Audit of files failed. [TODO: list failed]"
success_msg: "[OK] Audit of files passed."
run_once: true
... implement some tasks using Ansible to check if the files are properly configured. I don't want to change any file, only check and output if the files are not ok or not.
Since Ansible is mostly used as Configuration Management Tool there is no need to check (before) if a file is properly configured. Just declare the Desired State and make sure that the file is in that state. As this is approach is working with Validating: check_mode too, if interested in a Configuration Check or an Audit it could be implemented simply as follow:
resolv.conf as is it should be
# Generated by NetworkManager
search example.com
nameserver 192.0.2.1
hosts.ini
[test]
test.example.com NS_IP=192.0.2.1
resolv.conf.j2 template
# Generated by NetworkManager
search {{ DOMAIN }}
nameserver {{ NS_IP }}
A minimal example playbook for Configuration Check in order to audit the config
---
- hosts: test
become: false
gather_facts: false
vars:
# Ansible v2.9 and later
DOMAIN: "{{ inventory_hostname.split('.', 1) | last }}"
tasks:
- name: Check configuration (file)
template:
src: resolv.conf.j2
dest: resolv.conf
check_mode: true # will never change existing config
register: result
- name: Config change
debug:
msg: "{{ result.changed }}"
will result for no changes into an output of
TASK [Check configuration (file)] ******
ok: [test.example.com]
TASK [Config change] *******************
ok: [test.example.com] =>
msg: false
or for changes into
TASK [Check configuration (file)] ******
changed: [test.example.com]
TASK [Config change] *******************
ok: [test.example.com] =>
msg: true
and depending on what's in the config file.
If one is interested in an other message text and need to invert the output therefore, just use msg: "{{ not result.changed }}" as it will report an false if true and true if false.
Further Reading
Using Ansible inventory, variables in inventory, the template module (to) Template a file out to a target host and Enforcing check_mode on tasks makes it extremely simply to prevent Configuration Drift.
And as a reference for getting the search domain, Ansible: How to get hostname without domain name?.

List name server from resolv.conf with hostname in one line per host

I need to get the DNS server(s) from my network, I tried using:
- hosts: localhost
gather_facts: no
tasks:
- name: check resolv.conf exists
stat:
path: /etc/resolv.conf
register: resolv_conf
- name: check nameservers list in resolv.conf
debug:
msg: "{{ contents }}"
vars:
contents: "{{ lookup('file', '/etc/resolv.conf') | regex_findall('\\s*nameserver\\s*(.*)') }}"
when: resolv_conf.stat.exists == True
But this does not quite gives the result I need.
Will it be possible to write a playbook in such a way that the result looks like the below?
hostname;dns1;dns2;dnsN
The declaration below gives the list of nameservers
nameservers: "{{ lookup('file', '/etc/resolv.conf').splitlines()|
select('match', '^nameserver.*$')|
map('split', ' ')|
map('last')|list }}"
You can join the hostname and the items on the list
msg: "{{ inventory_hostname }};{{ nameservers|join(';') }}"
Notes
Example of a complete playbook for testing
- hosts: localhost
vars:
nameservers: "{{ lookup('file', '/etc/resolv.conf').splitlines()|
select('match', '^nameserver.*$')|
map('split', ' ')|
map('last')|list }}"
tasks:
- debug:
var: nameservers
- debug:
msg: |
{{ inventory_hostname }};{{ nameservers|join(';') }}
The simplified declaration below works fine if there is no nameserver.* in the comments
nameservers: "{{ lookup('file', '/etc/resolv.conf')|
regex_findall('\\s*nameserver\\s*(.*)') }}"
Unfortunately, the Linux default file /etc/resolv.conf contains the comment:
| # run "systemd-resolve --status" to see details about the actual nameservers.
This regex will match nameservers.
nameservers:
- s.
You can solve this problem by putting at least one space behind the keyword nameserver.
regex_findall('\\s*nameserver\\s+(.*)') }}"
However, this won't help if there is the keyword nameserver in the comment.
Q: "No filter named 'split'"
A: There is no filter split in Ansible less than 2.11. Use regex_replace instead
nameservers: "{{ lookup('file', '/etc/resolv.conf').splitlines()|
select('match', '^nameserver.*$')|
map('regex_replace', '^(.*) (.*)$', '\\2')|list }}"
Since your regex_findall already creates you a list with all DNS servers, you just need to add the hostname to that list and join the whole list with a semicolon.
- name: check nameservers list in resolv.conf
debug:
msg: >-
{{
(
[ ansible_hostname ] +
lookup('file', '/etc/resolv.conf', errors='ignore')
| regex_findall('\s*nameserver\s*(.*)')
) | join(';')
}}
Which will result in something like (b176263884e6 being the actual hostname of a container):
TASK [check nameservers list in resolv.conf] *****************************
ok: [localhost] =>
msg: b176263884e6;1.1.1.1;4.4.4.4;8.8.8.8
Note that you don't even need the stat task, as you can ignore errors of the lookup with errors='ignore'.
This will, then, give you only the hostname, along with a warning:
TASK [check nameservers list in resolv.conf] *****************************
[WARNING]: Unable to find '/etc/resolv.conf' in expected paths
(use -vvvvv to see paths)
ok: [localhost] =>
msg: b176263884e6

Ansible nested braces or nested quotes

Using ansible 2.9 I set a variable like this to store part of a group name
- name: set group
set_fact:
ansible_group: aaaa
I then want to use this variable in the following with_items clause:
- name: get
uri:
url: "http://{{ item }}:5324/kjhfg"
with_items: "{{ groups['thisgroup_{{ ansible_group }}'] }}"
However, using nested curly braces gives me the following error:
FAILED! => {"msg": "'dict object' has no attribute 'thisgroup_{{ ansible_group }}'"}
I also tried the following syntax variations
with_items: "{{ groups['thisgroup_ansible_group'] }}"
with_items: "groups['thisgroup_{{ ansible_group }}']"
with_items: "{{ groups['thisgroup_hostvars['localhost']['ansible_group']'] }}"
with_items: "{{ groups['thisgroup_hostvars[''localhost''][''ansible_group'']'] }}"
with_items: "{{ groups['thisgroup_hostvars[`localhost`][`ansible_group`]'] }}"
and probably one hundred other variations which all of them produced various errors
Can someone help me figure out the right syntax?
Simply concatenate the name of the group, e.g. given the inventory
shell> cat hosts
[thisgroup_aaaa]
host1
host2
host3
the playbook
- hosts: all
gather_facts: false
vars:
ansible_group: aaaa
_group: "{{ groups['thisgroup_' ~ ansible_group] }}"
tasks:
- debug:
var: item
loop: "{{ _group }}"
run_once: true
gives
item: host1
item: host2
item: host3

ansible assign shell results specific hosts in hostvars using with_items

Basically i want modify an existing hostvars.
I have a dynamically generated array of hosts named "flash_hosts"
flash_hosts = ['host1', 'host2']
and a shell script which looks up a value for each host.
In following non-working code i try to assign each host the specific result of the script
- name: Assign values to to host var
shell: "get-assigned-value.sh {{ item }}"
register: "{{ hostvars[item].mac=stdout }}"
with_items: "{{ flash_hosts }}"
How can i make this work in in ansible? Basically i understand that register will not allow me to assign the value to hostvars directly, but how can this be solved then, as i need to iterate over the hosts?
Use set_fact to debug your register results on host wise..
- name: Assign values to to host var
shell: "/path/to/get-assigned-value.sh {{ item }}"
register: fileout
with_items:
- host1
- host2
- set_fact:
firstHost: "{{ fileout.results[0] }}"
secondHost: "{{ fileout.results[1] }}"
- debug:
var: firstHost.stdout
- debug:
var: secondHost.stdout
in the above example, firstHost is whole result of shell script running on first host, and firstHost.stdout gives the output of corresponding host shell script result.
It's possible set_facts and delegate_to the flash_hosts with delegate_facts.
But, to create a variable in hostvars the host must be declared in the inventory(static or dynamic). It's not necessary the host is accessible. For example
$ cat hosts
host1
host2
The play below
- hosts: localhost
vars:
flash_hosts: ['host1', 'host2']
tasks:
- name: Assign values to to host var
command: "{{ playbook_dir }}/get-assigned-value.sh {{ item }}"
register: result
loop: "{{ flash_hosts }}"
- set_fact:
mac: "{{ item.stdout }}"
loop: "{{ result.results }}"
delegate_to: "{{ item.item }}"
delegate_facts: true
- debug:
msg: "{{ hostvars[item].mac }}"
loop: "{{ flash_hosts }}"
gives
ok: [localhost] => (item=host1) =>
msg: mac address of host1
ok: [localhost] => (item=host2) =>
msg: mac address of host2
with the script
$ cat get-assigned-value.sh
#!/bin/sh
case $1 in
host1)
printf "mac address of host1"
;;
host2)
printf "mac address of host2"
;;
*)
printf "unknown host"
exit 1
;;
esac
exit 0

How to set variables for all subsequent hosts in one place without redundant code?

TL/DR: I have "src_host" and "dest_host" variables that I want to use to set the "- hosts:" object in a play. However, I have to set them again for each play under "vars:" of each "- hosts:" section e.g. src_host="{{ hostvars['localhost']['src_host'] }}" how do I set these two variables at the beginning and not have to reset them?
My hosts file looks like this
[wordpress]
localhost ansible_user=user ansible_port=22 ansible_ssh_private_key_file=/home/user/.ssh/id_rsa
root_localhost ansible_user=root ansible_port=22 ansible_ssh_private_key_file=/home/user/.ssh/id_rsa
---snip---
server2.net ansible_host="server2.net" ansible_user=user ansible_port=22 ansible_ssh_private_key_file=/home/user/.ssh/id_rsa
root_server2.net ansible_host="server2.net" ansible_user=root ansible_port=22 ansible_ssh_private_key_file=/home/user/.ssh/id_rsa
The beginning of my playbook looks like this:
- hosts: localhost, server2.net, root_server2.net #always include "localhost" in this list because it is needed to store the variables for the src_host and dest_host
vars:
src_host: localhost #modify these and the host will be changed for all subsequent plays/tasks
dest_host: server2.net #modify these and the host will be changed for all subsequent plays/tasks
src_dump_path: /home/user/cvrt9_dump.sql #set vars for copying file
roles:
- set_facts_for_db_copy
- hosts: "{{ src_host }}"
vars:
src_host: "{{ hostvars['localhost']['src_host'] }}"
dest_host: "{{ hostvars['localhost']['dest_host'] }}"
---snip---
roles:
- dump_db
- copy_file
etc . . .
for "- set_facts_for_db_copy" I have "main.yml" as this where I set the "src_host" and "dest_host" variables:
---
# tasks file for set_facts_for_db_copy
- name: create variables that equal src_dump_path and set src_host/dest_host
set_fact:
---snip---
src_host: "{{ src_host }}"
dest_host: "{{ dest_host }}"
So I need to set the "src_host" and "dest_host" for all subsequent "- hosts:" that use them by getting the values from one of the host variables that "set_fact_for_db_copy" set. I randomly picked "localhost" as you may have noticed:
src_host: "{{ hostvars['localhost']['src_host'] }}"
dest_host: "{{ hostvars['localhost']['dest_host'] }}"
If I don't have that line there I get:
user#localhost:/home/maintainer/ansible-play$ ansible-playbook -i hosts_tat-kay playbook.yml
PLAY [localhost, server2.net, root_server2.net] **************
TASK [setup] *******************************************************************
ok: [server2.net]
ok: [root_server2.net]
ok: [localhost]
TASK [set_facts_for_db_copy : create variables that equal src_dump_path] *******
ok: [localhost]
ok: [server2.net]
ok: [root_server2.net]
ERROR! the field 'hosts' has an invalid value, which appears to include a variable that is undefined. The error was: 'src_host' is undefined
The error appears to have been in '/home/maintainer/ansible-play/playbook.yml': line 14, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- hosts: "{{ src_host }}"
^ here
. . .
Now I can set the these variables in my host file:
[wordpress:vars]
src_host=localhost
dest_host=server2.net
But then I still have to reference them from the subsquent "-hosts:" objects in my playbook with "{{ hostvars['localhost']['src_host'] }}" etc . . . So my question is how do I get rid of this redundant code in all my subsequent "-hosts:" objects (shown below) while still letting me change the "src_host" and "dest_host" variables once at the beginning and have those changes affect the rest of the plays? Thanks.
src_host: "{{ hostvars['localhost']['src_host'] }}"
dest_host: "{{ hostvars['localhost']['dest_host'] }}"
For this use your inventory file, make a parent group of the host you need the variable as follow.
[desireenv:children]
wordpress
otherhost
etc
and then assigne th vars value to the new parent group created
[desireenv:vars]
src_host: "{{ hostvars['localhost']['src_host'] }}"
dest_host: "{{ hostvars['localhost']['dest_host'] }}"
One solution I found with the help of https://stackoverflow.com/users/4716639/bryan-calvo-benoit is to put this in my hosts file (inventory file)
[wordpress]
localhost
server2.net
[testenv:children]
wordpress
[testenv:vars]
src_host=localhost
dest_host=server2.net
And then in the ansible playbook and the roles that it calls I had to replace
"{{ src_host }}"
with
"{{ hostvars['localhost']['src_host'] }}"
and likewise for "{{ dest_host }}"
However, I could delete this redundant code in my ansible playbook:
src_host: "{{ hostvars['localhost']['src_host'] }}"
dest_host: "{{ hostvars['localhost']['dest_host'] }}"
It would be nice if I didn't have to change the src_host and dest_host to hostvars['localhost']... because it seems arbitrary to use localhost and also what if I want to run several ansible scripts one right after the other with different src_host and dest_host? Using the inventory file locks it down so this is not ideal. If no one else answers I'll accept this answer because it is the only one that works and it technically does what my question asked.

Resources