Skip current host and print the rest in a config file ansible - linux

Folks, I'm trying to achieve the below while running my task to edit an XML
Requirement:
Skip current host in which config file is created and print the rest of host from the inventory group to the config file
<ManagerNode ip = "**node IP**" port = "**node port**"/> <haNodes>IP2:port,IP3:port</haNodes> <!-- Comma Seperated Node IPs with port, ,Except the Same Node -->
Can anyone help with achieving this goal?

Q: "Skip current host ... and print the rest of hosts from the inventory group."
A: Create a list of all IPs and use filter difference to remove the current IP. For example the inventory
shell> cat hosts
[ha]
test_01 IP=10.1.0.11
test_02 IP=10.1.0.12
test_03 IP=10.1.0.13
[ha:vars]
port=4567
and the playbook
shell> cat playbook.yml
- hosts: ha
tasks:
- set_fact:
all_IP: "{{ groups.ha|map('extract', hostvars, 'IP')|list }}"
run_once: true
- debug:
msg: "{{ all_IP|difference([IP])|
product([port])|
map('join', ':')|
list }}"
give (abridged)
shell> ansible-playbook -i hosts playbook.yml
ok: [test_01] =>
msg:
- 10.1.0.12:4567
- 10.1.0.13:4567
ok: [test_02] =>
msg:
- 10.1.0.11:4567
- 10.1.0.13:4567
ok: [test_03] =>
msg:
- 10.1.0.11:4567
- 10.1.0.12:4567
Limit the play to test_01 gives abridged
shell> ansible-playbook -i hosts -l test_01 playbook.yml
ok: [test_01] =>
msg:
- 10.1.0.12:4567
- 10.1.0.13:4567

defined a variable
tg_hosts: : "{{ groups['tgzone']|map('extract',hostvars,'ansible_host')|list }}"
used template as:
{{ tg_hosts | difference([ansible_host])| list | join(':port,') + ':port' }}

Related

How can I pass a varialbe set by the set_fact module to the Jinja2 template?

I have a role to setup NATS cluster,, I've used host_vars to define which node is the master node like below:
is_master: true
Then in the setup-nats.yml task, I used the following to extract the master node's IP address based on the host_var I've set and then used it as a variable for the Jinja2 template, however, the variable doesn't get passed down to the template and I get the `variable 'master_ip' is undefined.
- name: Set master IP
set_fact:
set_master_ip: "{{ ansible_facts['default_ipv4']['address'] }}"
cacheable: yes
when: is_master
- name: debug
debug:
msg: "{{ set_master_ip }}"
run_once: true
- name: generate nats-server.conf for the slave nodes
template:
src: nats-server-slave.conf.j2
dest: /etc/nats-server.conf
owner: nats
group: nats
mode: 0644
when:
- is_master == false
vars:
master_ip: "{{ set_master_ip }}"
notify: nats-server
The variable is used like below in the Jinja2 template:
routes = [
nats-route://ruser:{{ nats_server_password }}#{{ master_ip }}:6222
]
}
Questions:
Is this approach according to the best practices?
What is the correct way of doing the above so the variable is passed down to the template?
Test Output:
I'm using Molecule to test my Ansible and even though in the debug task the IP address is visible, it doesn't get passed down to the template:
TASK [nats : Set master IP] ****************************************************
ok: [target1]
skipping: [target2]
skipping: [target3]
TASK [nats : debug] ************************************************************
ok: [target1] =>
msg: 10.0.2.15
TASK [nats : generate nats-server.conf for the slave nodes] ********************
skipping: [target1]
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ansible.errors.AnsibleUndefinedVariable: {{ set_master_ip }}: 'set_master_ip' is undefined
fatal: [target2]: FAILED! => changed=false
msg: 'AnsibleUndefinedVariable: {{ set_master_ip }}: ''set_master_ip'' is undefined'
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ansible.errors.AnsibleUndefinedVariable: {{ set_master_ip }}: 'set_master_ip' is undefined
fatal: [target3]: FAILED! => changed=false
msg: 'AnsibleUndefinedVariable: {{ set_master_ip }}: ''set_master_ip'' is undefined'
Any help is appreciated, thanks in advance.
UPDATE: I suspect the issue has something to do with the variable scope being in the host context but cannot find a way to fix it ( I might be wrong though).
Far from being best practice IMO but answering your direct question. Your problem is not passing the variable to your template but the fact it is not assigned to all hosts in your play loop (and hence is undefined on any non master node). The following (untested) addresses that issue keeping the same task structure.
- name: Set master IP for all nodes
ansible.builtin.set_fact:
master_ip: "{{ hostvars | dict2items | map(attribute='value'
| selectattr('is_master', 'defined') | selectattr('is_master')
| map(attribute='ansible_facts.default_ipv4.address') | first }}"
cacheable: yes
run_once: true
- name: Show calculated master IP (making sure it is assigned everywhere)
ansible.builtin.debug:
msg: "{{ master_ip }}"
- name: generate nats-server.conf for the slave nodes
ansible.builtin.template:
src: nats-server-slave.conf.j2
dest: /etc/nats-server.conf
owner: nats
group: nats
mode: 0644
when: not is_master | bool
notify: nats-server
Ideas for enhancement (non exhaustive):
Select your master based on a group membership in the inventory rather than on a host attribute. This makes gathering the ip easier (e.g. master_ip: "{{ hostvars[groups.master | first].ansible_facts.default_ipv4.address }}"
Set the ip as a play var or directly inside the inventory for the node group rather than in a set_fact task.

how to traverse through each line of a text file in ansible

I have to traverse through package list file which contains list of packages with their architecture. How can I feed those input to my playbook file? I found a way to get the package names alone but architecture version is not coming. This is my package_list file
nginx | x86_64
telnet| x86_64
openssh | i386
This is my playbook
- name: get contents of package.txt
command: cat "/root/packages.txt"
register: _packages
- name: get contents of architecture from packages.txt
command: cat "/root/packages.txt" | awk '{print $3}'
register: _arch
- name: Filter
theforeman.foreman.content_view_filter:
username: "admin"
password: "mypass"
server_url: "myhost"
name: "myfilter"
organization: "COT"
content_view: "M_view"
filter_type: "rpm"
architecture: "{{ _arch }}"
package_name: "{{ item }}"
inclusion: True
loop: "{{ _packages.stdout_lines }}"
loop: "{{ _arch.stdout_lines }}"
Any help would be appreciated
The required output is package name and architecture should be read from packages.txt file through ansible-playbook
try this playbook:
- name: Reproduce issue
hosts: localhost
gather_facts: no
tasks:
- name: get contents of package.txt
command: cat "/root/packages.txt"
register: _packages
- debug:
msg: "package: {{ line.0 }}, arch: {{ line.1 }}"
loop: "{{ _packages.stdout_lines }}"
vars:
line: "{{ item.split('|')|list }}"
result:
ok: [localhost] => (item=nginx | x86_64) => {
"msg": "package: nginx , arch: x86_64 "
}
ok: [localhost] => (item=telnet| x86_64) => {
"msg": "package: telnet, arch: x86_64 "
}
ok: [localhost] => (item=openssh | i386) => {
"msg": "package: openssh , arch: i386 "
}
for your case:
- name: Filter
theforeman.foreman.content_view_filter:
:
:
architecture: "{{ line.1 }}"
package_name: "{{ line.0 }}"
inclusion: True
loop: "{{ _packages.stdout_lines }}"
vars:
line: "{{ item.split('|')|list }}"
following the version of ansible you could write too line: "{{ item | split('|') | list }}"
You need to split up the line into the necessary values by filtering.
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Gahter package list
shell:
cmd: cat package.txt
register: _packages
- name: Show packages
debug:
msg: "Name: {{ item.split('|')[0] }}, Arch: {{ item.split('|')[1] }}"
loop: "{{ _packages.stdout_lines }}"
Further Documentation
Playbook filters - Manipulating strings
Jinja Template Designer - Filters
Further Q&A
Split string into list in Jinja?

Ansible - load multiplne local yml files and merge their content

my host_vars file has about 5k lines of yml code. So I would like to have separate yml files - one file per one service.
Simplified example:
user#test $ cat production/split_configs/a.example.net.yml
my_array:
- a.example.net
user#test $ cat production/split_configs/b.example.net.yml
my_array:
- b.example.net
user#test $ cat webhosts.yml
- hosts: myservers
pre_tasks:
- name: merge ansible arrays
tags: always
delegate_to: localhost
block:
- name: find config files
find:
paths: production/configs/
patterns: '*.yml'
register: find_results
- name: aaa
debug:
msg: "{{ find_results.files }}"
- name: bbb
debug:
msg: "{{ item.path }}"
with_items: "{{ find_results.files }}"
- name: ccc
debug:
msg: "{{ lookup('file', 'production/configs/a.example.net.yml') }}"
- name: ddd
debug:
msg: "{{ lookup('file', item.path) }}"
loop: "{{ find_results.files }}"
tasks:
- name: eee
debug:
msg: "{{ my_array }}"
The goal is to merge content of both arrays an print the merged content in task eee:
my_array:
- a.example.net
- b.example.net
Task aaa print informations about files (path, mode, uid, ...) - it works.
Tasks bbb, and ddd print nothing. I do not understand why.
Task ccc print content of file. But the path is written in playbook :-(
After load files I need to merge them. My idea is to use something like set_fact: my_array="{{ my_array + my_array }}" in task with with_items: "{{ find_results.files }}". Is it good idea? Or how better I can do it?
For example, the tasks below do the job
- include_vars:
file: "{{ item }}"
name: "my_prefix_{{ item|regex_replace('\\W', '_') }}"
with_fileglob: production/split_configs/*
- set_fact:
my_vars: "{{ my_vars|d({})|
combine(lookup('vars', item),
recursive=True,
list_merge='append') }}"
loop: "{{ q('varnames', 'my_prefix_.*') }}"
give
my_vars:
my_array:
- a.example.net
- b.example.net
You can use the simple cat commnd to merge the files into the one file and later include var file for example -
- raw: "cat production/split_configs/* >my_vars.yml"
- include_vars: file=my_vars.yml name=my_vars
will give you the result -
my_vars:
my_array:
- a.example.net
- b.example.net

How ansible can call multiple files under path option as a variable

I am just learning ansible and trying to understand how can i include multiple file in the path option in ansible replace module.
I have three files where i need to replace a old hostname with new hostanme.
Files are :
- /etc/hosts
- /etc/hosts.custom
- /etc/hosts-backup
Below Simple Play works fine:
- name: Replace string in hosts file
hosts: all
gather_facts: false
tasks:
- name: Replace string in host file
replace:
path: /etc/hosts
regexp: "171.20.20.16 fostrain.example.com"
replace: "171.20.20.16 dbfoxtrain.example.com"
backup: yes
However, after lot of googling i see this can be done as follows, but in case i have multiple files and those needs to be called as a variable in different modules, How we can define then in such a way so as to call them by variable name.
Below is Just what i am trying to understand..
- name: Replace string in hosts file
hosts: all
gather_facts: false
tasks:
- name: Checking file contents
slurp:
path: "{{ ?? }}" <-- How to check these three files here
register: fileCheck.out
- debug:
msg: "{{ (fileCheck.out.content | b64decode).split('\n') }}"
- name: Replace string in host file
replace:
path: "{{ item.path }}"
regexp: "{{ item:from }}"
replace: "{{ item:to }}"
backup: yes
loop:
- { path: "/etc/hosts", From: "171.20.20.16 fostrain.example.com", To: "171.20.20.16 dbfoxtrain.example.com"}
- { Path: "/etc/hosts.custom", From: "171.20.20.16 fostrain.example.com", To: "171.20.20.16 dbfoxtrain.example.com"}
- { Path: "/etc/hosts-backup", From: "171.20.20.16 fostrain.example.com", To: "171.20.20.16 dbfoxtrain.example.com"}
I will appreciate any help.
Create couple of variables; a list with all the files, from and to replacement strings or divide them by ip and domain. Then loop over all the files using the file list variable and use from and to replacement variables for each file. If multiple ip and domain mapping is required then you need to adjust the structure further. So recommend going through ansible documentation on using variables and loops for more details.
Playbook may look like below. Have used a minor regex and you can adjust as required.
- name: Replace string in hosts file
hosts: all
gather_facts: false
vars:
files:
- /etc/hosts
- /etc/hosts.custom
- /etc/hosts-backup
from_ip: "171.20.20.16"
from_dn: "fostrain.example.com"
to_ip: "171.20.20.16"
to_dn: "dbfoxtrain.example.com"
tasks:
- name: Replace string in host file
replace:
path: "{{ item }}"
regexp: "{{ from_ip }}\\s+{{ from_dn }}"
replace: "{{ to_ip }} {{ to_dn }}"
loop: "{{ files }}"
If you want to see the contents of each file then slurp and debug modules can be used like below:
- slurp:
path: "{{ item }}"
loop: "{{ files }}"
register: contents
- debug:
msg: "{{ (item.content | b64decode).split('\n') }}"
loop: "{{ contents.results }}"

Ansible loops for assigning sequential integers as hostnames

I'm new to Ansible. I have the following playbook that changes the hostname for a remote server:
---
- hosts: dbservers
remote_user: testuser1
become: yes
become_method: sudo
vars:
LOCAL_HOSTNAME: 'db01'
LOCAL_DOMAIN_NAME: 'ansibletest.com'
tasks:
# Checks and removed the existing occurences of <IP hostname FQDN> from /etc/hosts
- name: Remove occurences of the existing IP
lineinfile: dest=/etc/hosts
regexp='{{ hostvars[item].ansible_default_ipv4.address }}'
state=absent
when: hostvars[item].ansible_default_ipv4.address is defined
with_items: "{{ groups['dbservers'] }}"
# Adds the IP in the format <IP hostname FQDN> to /etc/hosts
- name: Add the IP and hostname to the hosts file
lineinfile: dest=/etc/hosts
regexp='.*{{ item }}$'
line="{{ hostvars[item].ansible_default_ipv4.address }} {{ LOCAL_HOSTNAME }} {{ LOCAL_HOSTNAME }}.{{ LOCAL_DOMAIN_NAME }}"
state=present
when: hostvars[item].ansible_default_ipv4.address is defined
with_items: "{{ groups['dbservers'] }}"
- name: Remove HOSTNAME occurences from /etc/sysconfig/network
lineinfile: dest=/etc/sysconfig/network
regexp='^HOSTNAME'
state=absent
when: hostvars[item].ansible_default_ipv4.address is defined
with_items: "{{ groups['dbservers'] }}"
- name: Add new HOSTNAME to /etc/sysconfig/network
lineinfile: dest=/etc/sysconfig/network
regexp='^HOSTNAME='
line="HOSTNAME={{ LOCAL_HOSTNAME }}.{{ LOCAL_DOMAIN_NAME }}"
state=present
when: hostvars[item].ansible_default_ipv4.address is defined
with_items: "{{ groups['dbservers'] }}"
- name: Set up the hostname
hostname: name={{ LOCAL_HOSTNAME }}.{{ LOCAL_DOMAIN_NAME }}
In this example, LOCAL_HOSTNAME is already assigned a value of db01. And in this scenario, the dbservers group has only one server:
[dbservers]
192.168.1.93
However, I also have 2 other servers that are designated to be webservers:
[webservers]
192.168.1.95
192.168.1.96
[dbservers]
192.168.1.93
The aim is to name them as web01.domain, web02.domain and so on.
As per the docs it seems that this could be achieved by using with_sequence.
My question is, is it possible (in Ansible) to use 2 variables in loops? Something along the lines of the pseudo-code below:
i=1
for host in webservers:
open host(/etc/hosts):
add "IP<space>HOSTNAME{i}<space>"<space>"HOSTNAME{i}.FQDN"
i++
Could this be achieved using playbooks or am I approaching the issue in an wrong way?
Generate indexed hostname first, define it as hostfact and use it later to fill other servers' hosts files.
- hosts: webservers
gather_facts: no
tasks:
- set_fact:
indexed_hostname: "{{ 'web{0:02d}'.format(play_hosts.index(inventory_hostname)+1) }}"
- hosts: dbservers
gather_facts: no
tasks:
- debug:
msg: "{{ hostvars[item].indexed_hostname }}"
with_items: "{{ groups['webservers'] }}"
Also there is such thing as with_indexed_items.

Resources