I have a simple playbook referencing an inventory hosts.yml file. I want to be able to query a group, and retrieve the IP address for hosts in that group.
Simple hosts.yml
---
all:
hosts:
w2k16-std:
ansible_host: 10.1.1.12
in_play_ip: 10.200.2.12
w2k12-x64:
ansible_host: 10.1.1.13
in_play_ip: 10.200.2.13
lin-test:
ansible_host: 10.1.1.20
in_play_ip: 10.200.2.20
children:
windows:
vars:
ansible_connection: winrm
ansible_winrm_transport: certificate
ansible_winrm_cert_pem: /home/user/.ssh/windows.pem
ansible_winrm_cert_key_pem: /home/user/.ssh/windows_key.pem
ansible_port: 5986
ansible_winrm_scheme: https
ansible_winrm_server_cert_validation: ignore
hosts:
w2k12-x64:
w2k16-std:
linux:
hosts:
lin-test:
targets:
hosts:
w2k12-x64:
w2k16-std:
Here's the simple playbook:
---
- hosts: kali
vars:
tasks:
- name: Create comma delimited list of target names from targets group.
set_fact:
targets: "{{ groups.targets | list | join(',') }}"
- name: List targets from Inventory
debug:
var: targets
Produces the following output:
TASK [List targets from Inventory] ***************************************
ok: [lin-test] => {
"targets": "w2k12-x64,w2k16-std"
I want be able do the following:
Retrieve the members of the targets group (w2k12-x64 & w2k16-std)
Get the IP address listed in ansible_host values.
Create a comma delimited list of the target's IPs (10.1.1.12,10.1.1.13).
So far I can:
Retrieve the members of the targets group (w2k12-x64 & w2k16-std)
Create a comma delimited list of the target names.
I'm not sure if my hosts file is correctly laid out, or if I just need help with the query?
Desired output:
Produce the following output:
TASK [List target IPs from Inventory] ***************************************
ok: [lin-test] => {
"targets": "10.1.1.12,10.1.1.13"
You can extract the value you are looking for from the hostvars special variable.
You can also filter the items of the dictionary hostvars contained in the group targets thanks to the filters dict2items, which will create a key/value list from the dictionary, and selectattr to get only the items of the created list having a key matching in the targets group.
Then, you just need to reduce the list of dictionaries to a simple list via map, because you are only interested in the ansible_host attribute, and join the whole thing together.
So, this end up with this single task:
- debug:
msg: >-
{{
hostvars
| dict2items
| selectattr('key', 'in', groups.targets)
| map(attribute="value.ansible_host")
| join(',')
}}
Which yields your expected:
TASK [debug] **************************************************************
ok: [localhost] =>
msg: 10.1.1.12,10.1.1.13
Related
I need some help here in determining the best way to do this and how to setup my playbook to appropriately pull variables for each node.
Lets say I have 3 hosts
/etc/ansible/hosts:
host1
host2
host3
I have a variable file with multiple entries in it
vars/IPs.yaml:
---
IP: ['192.168.77.35', '192.167.77.36', '192.168.77.37']
I am running this playbook:
network_change.yaml:
---
- hosts: all
vars_files:
- vars/IPs.yaml
tasks:
- name: Check 10G Interface
stat:
path: /etc/sysconfig/network-scripts/ifcfg-card-10Gb-1
register: teng
- name: Change 10G Interface Settings
lineinfile:
path: /etc/sysconfig/network-scripts/ifcfg-card-10Gb-1
regexp: '{{item.From}}'
line: '{{item.To}}'
when: teng.stat.exists
with_items:
- { From: 'IPADDR=', To: 'IPADDR={{IP}}'}
I have this working for a single host just fine...but when I have multiple hosts I'm not sure how to loop through the IPs.yaml variables and pull the next value the next time the loop runs. Is there also a way for me to not use a dictionary .yaml variable, can I just use a raw text file that has the IPs on newlines?
Essentially I want to loop through and have the hosts show this in each of the respective hosts /etc/sysconfig/network-scripts/ifcfg-card-10Gb-1.
host1 = 'IPADDR=192.168.77.35'
host2 = 'IPADDR=192.168.77.36'
host3 = 'IPADDR=192.168.77.37'
The end game is to be able to do this over 100+ hosts with simple text files, rather than dictionary yaml files and will be including multiple variables, etc. The systems will all be able to be hit via dhcp/hostnames/IP.
Create a dictionary with the data, e.g.
- set_fact:
_dict: '{{ dict(ansible_play_hosts|zip(IP)) }}'
run_once: true
should give
_dict:
host1: 192.168.77.35
host2: 192.167.77.36
host3: 192.168.77.37
Then use this dictionary and select the IP, e.g.
- name: Change 10G Interface Settings
lineinfile:
path: /etc/sysconfig/network-scripts/ifcfg-card-10Gb-1
regexp: 'IPADDR='
line: 'IPADDR={{ _dict[inventory_hostname] }}'
when: teng.stat.exists
You might want to test it first, e.g.
- name: Change 10G Interface Settings
debug:
msg: 'IPADDR={{ _dict[inventory_hostname] }}'
should give
ok: [host2] =>
msg: IPADDR=192.167.77.36
ok: [host1] =>
msg: IPADDR=192.168.77.35
ok: [host3] =>
msg: IPADDR=192.168.77.37
The next option, instead of creating the dictionary, is to calculate the index in the list, e.g. should give the same result
- name: Change 10G Interface Settings
debug:
msg: 'IPADDR={{ IP[_index|int] }}'
vars:
_index: '{{ ansible_play_hosts.index(inventory_hostname) }}'
Q: "Use a raw text file that has the IPs on newlines"
A: Create the file, e.g.
shell> cat IP.txt
192.168.77.35
192.167.77.36
192.168.77.37
Create the list on the fly, e.g.
- set_fact:
IP: "{{ lookup('file', 'IP.txt').split('\n') }}"
run_once: true
should give
IP:
- 192.168.77.35
- 192.167.77.36
- 192.168.77.37
The more robust solution would be to put the hashes into the file, e.g.
shell> cat IP.txt
host1: 192.168.77.35
host2: 192.167.77.36
host3: 192.168.77.37
Then the task
- include_vars:
file: IP.txt
name: _dict
should create the dictionary
_dict:
host1: 192.168.77.35
host2: 192.167.77.36
host3: 192.168.77.37
But, the most simple solution would be to store this dictionary in IP.yml and put it into the directory group_vars/all.
I want to create multi-level data in an Ansible .ini file, and then loop over it.
For example (non-working psuedo code):
ini file:
hosts=[ host1=10.1.2.3, host2=10.4.5.6, ...]
task file:
- name: Test hosts loop
debug:
msg: "item {{item}} = {{item.key}} + {{ item.value }}"
loop: "{{ hosts }}"
That doesn't work, and I can't see a good way to do it using Ansible data structures.
I don't mind if the task file gets a little bit complicated, but I need to keep the .ini file simple enough for people who aren't Ansible experts to be able to edit it.
Is there a good and simple way to do this?
There are various solutions around, but they all seem brainbreakingly complicated.
The best I've been able to come up with is:
ini file:
hosts=["host1=1.2.3.4","host2=4.5.6.7",...]
task file:
- name: Test hosts loop
debug:
msg: "item {{item}} = {{ item | regex_replace('=.*') }} + {{ item | regex_replace('.*=') }}"
loop: "{{ hosts }}"
And it works:
TASK [Test playbook: Test hosts loop] **********************************************
ok: [testhost] => (item=host1=1.2.3.4) => {
"msg": "item host1=1.2.3.4 = host1 + 1.2.3.4"
}
ok: [testhost] => (item=host2=4.5.6.7) => {
"msg": "item host2=4.5.6.7 = host2 + 4.5.6.7"
}
But is there a better way to do it?
Base reference for the below examples: How to build your inventory
Switch those vars declarations to yaml, ini format is not handy at all for that.
I am voluntarily explicit declaring the hosts below. You can omit redundant declarations if you wish.
In a nutshell:
All-in-one yaml inventory
inventories/example/hosts.yaml
---
all:
# These will be applied to all hosts in inventory
# except implicit localhost
vars:
my_dict1:
key1: toto
key2:
complicated1: yeah
complicated2: bah
hosts:
host1:
# These will be applied to host1 only
a_specific_dict:
this:
can:
go:
very: deep
# The remaining host to not have specific vars here
host2:
host3:
some_group:
# These will be applied to the group `some_group` only
vars:
do_something_list:
- name: laundry
activate: yes
- name: clean-up
activate: false
hosts:
# No need to recap previous vars if defined elsewhere
# I prefer declaring all my host with their vars
# explicitly in the `all` group for clarity.
host1:
host3:
Tidy-up: structured inventory
Same as above using good practices. We modify the host file to contain only the hosts/groups information:
inventories/example/hosts.yaml
---
all:
hosts:
host1:
host2:
host3:
some_group:
hosts:
host1:
host3:
Note that in this case, an ini file would still be very legible. Here is the alternative inventories/example/hosts.ini
host1
host2
host3
[some_group]
host1
host3
Now place the vars in dedicated yaml files.
In inventories/example/group_vars/all.yml
---
my_dict1:
key1: toto
key2:
complicated1: yeah
complicated2: bah
In inventories/example/group_vars/some_group.yml
---
do_something_list:
- name: laundry
activate: yes
- name: clean-up
activate: false
And in inventories/example/host_vars/host1.yml
---
a_specific_dict:
this:
can:
go:
very: deep
I need to pass a specific host from my inventory as a parameter into a role. The host is part of a group but is demarcated by a variable that none of the other hosts have.
snippet of: hosts.yml
dbservers:
hosts:
pg01:
ansible_host: pg01.domain.com
master_slave: master
pg02:
ansible_host: pg02.domain.com
master_slave: slave
I want to be able to resolve pg01 based on the fact that the variable master_slave is set to 'master' such that I can call into a role like this:
- name: Do something
include_role:
name: a.database.role.to.run.on.master
vars:
master_database_host: {{ something that resolves to pg01 }}
How can I resolve the appropriate host from inventory?
You can use a mix of filters to extract the host you need:
tasks:
- debug:
msg: '{{groups["group_name"] | map("extract", hostvars) | selectattr("master_slave", "equalto", "master") | map(attribute="inventory_hostname") | list}}'
Step by step:
groups["group_name"] is a list of all the hosts in the group group_name.
map("extract", hostvars) takes hostvars, a dictionary mapping the host to their variables, and extracts the hosts that are in group_name (i.e. groups["group_name"]). This results in a list containing the hosts in group_name mapped to their variables.
selectattr("master_slave", "equalto", "master") selects all hosts who have an attribute master_slave that equals to master. This result in a list with all the hosts that are masters mapped to their variables.
map(attribute="inventory_hostname") takes a list as input and returns the inventory_hostname attribute of every item. This result in a list with all the hosts that are masters.
The play below (with json_query)
- hosts: dbservers
tasks:
- set_fact:
master_database_host: "{{ groups['dbservers']|
map('extract',hostvars)|
list|
json_query('[?master_slave==`master`].inventory_hostname')|
first }}"
- debug:
var: master_database_host
gives
ok: [pg02] => {
"master_database_host": "pg01"
}
ok: [pg01] => {
"master_database_host": "pg01"
}
You can use if else condition in vars to assign the values.
So your play should be Something like this.
- name: Do something
include_role:
name: a.database.role.to.run.on.master
vars:
master_database_host: "{{ hostvars['pg01']['ansible_host'] if \"{{ hostvars['pg01']['master_slave'] }}\" == 'master' else 'default value goes here'}}"
Make sure to use proper escaping to make the conditional statements work.
The reason this works is Since ansible internally uses python to do stuff it is a way to use ternary operator in python.
You could also generate dynamic groups based on the master/slave status:
---
- name: Play to create dynamic groups
hosts: dbservers
gather_facts: false
tasks:
- name: Create groups based on variable master_slave
group_by:
key: "database-{{ hostvars[inventory_hostname]['master_slave'] }}"
- name: Play to use the dynamic group database-master
hosts: database-master
gather_facts: false
tasks:
- name: Show hosts in group
debug:
msg: "This is {{ inventory_hostname }} from the dynamic database-master group."
The first play uses all dbservers and creates the dynamic groups based on the master_slave variable.
The dynamic groups are:
database-master containing pg01
database-slave containing pg02
The second play uses one of the dynamic created groups.
To use group_by the used variable has to exist for all hosts used.
This concept works best on automatic variable gathered by ansibles setup e.g. ansible_distribution to create dynamic groups based on the Distribution (Debian, Redhat, Ubuntu ...) or distribution versions using ansible_distribution_version.
host1: abc
host2: xyz
host1 and host2 are listed under test-hosts
[test-hosts]
abc
xyz
When i debug for inventory_hostnames, i see them like below
> TASK [debug inventory_hostname]
> *************************************************************************************************************************************************************************
ok: [abc] => {
> "inventory_hostname": "abc" }
ok: [xyz] => {
> "inventory_hostname": "xyz" }
Is there any way we can gather inventory_hostname's like a list by assigning it to a variable.
Expected result:
exp_result: [abc, xyz]
You can use groups['test-hosts'] to get those hosts as a list.
For example:
---
- hosts: all
gather_facts: false
tasks:
- set_fact:
host_list: "{{ groups['test-hosts'] }}"
- debug:
msg: "{{host_list}}"
You can also use the variable inventory_hostnames if you want to select multiple groups or exclude groups with patterns.
In these examples, we're selecting all hosts except ones in the www group.
Saving the list of hosts as a variable for a task:
- debug:
var: hostnames
vars:
hostnames: "{{ query('inventory_hostnames', 'all:!www') }}"
You can also use the lookup plugin to loop over the hosts that match the pattern. From the docs:
- name: show all the hosts matching the pattern, i.e. all but the group www
debug:
msg: "{{ item }}"
with_inventory_hostnames:
- all:!www
I have a task in my Ansible playbook that I'm wanting iterate over each host in the group that I have and for each host I would like to assign a name from the hostname list that I've created in the vars folder.
I'm familiar with looping through inventory already by writing loop: "{{ groups['mygroup'] }}" and I have a list of hostnames I would like to assign each IP in 'mygroup' within the host file.
# In tasks file - roles/company/tasks/main.yml
- name: change hostname
win_hostname:
name: "{{ item }}"
loop: "{{ hostname }}"
register: res
# In the Inventory file
[company]
10.0.10.128
10.0.10.166
10.0.10.200
# In vars - roles/company/vars/main.yml
hostname:
- GL-WKS-18
- GL-WKS-19
- GL-WKS-20
# site.yml file located under /etc/ansible
- hosts: company
roles:
- common
- company #This is where the loop exists mentioned above.
# Command to run playbook
ansible-playbook -i hosts company.yml
I seem to have the individual pieces down or know about it, but how can I combine iterating over hosts from an inventory group and assign names that I have in an already created list (in roles vars folder) already?
UPDATE
the task mentioned above has been updated to reflect changes mentioned in answer:
- name: change hostname
win_hostname:
name: "{{ item.1 }}"
loop: {{ groups.company|zip(hostname)|list }}"
register: res
However the output I'm getting is incorrect, this should not run 9 times rather only three times, once per IP in the [company] group in the inventory. Also there are only three hostnames in a list that need to be assigned to each of the hosts in the inventory sheet.
changed: [10.0.10.128] => (item=[u'10.0.10.128', u'GL-WKS-18'])
changed: [10.0.10.166] => (item=[u'10.0.10.128', u'GL-WKS-18'])
changed: [10.0.10.200] => (item=[u'10.0.10.128', u'GL-WKS-18'])
changed: [10.0.10.128] => (item=[u'10.0.10.166', u'GL-WKS-19'])
changed: [10.0.10.166] => (item=[u'10.0.10.166', u'GL-WKS-19'])
changed: [10.0.10.200] => (item=[u'10.0.10.166', u'GL-WKS-19'])
ok: [10.0.10.128] => (item=[u'10.0.10.200', u'GL-WKS-20'])
ok: [10.0.10.166] => (item=[u'10.0.10.200', u'GL-WKS-20'])
ok: [10.0.10.200] => (item=[u'10.0.10.200', u'GL-WKS-20'])
Whenever I have a question about looping in Ansible I also go visit the Loops documentation. It sounds like you want to iterate over two lists in parallel, pairing an item from the list of hosts in your inventory with an item from the list of hostnames. In previous versions of Ansible that would suggest using the with_together loop, while with more recent versions of Ansible that suggests the zip filter (there's an example in the docs here).
To demonstrate this for your use case, I started with an inventory file that has three hosts:
[mygroup]
hostA ansible_host=localhost
hostB ansible_host=localhost
hostC ansible_host=localhost
And the following playbook:
---
- hosts: all
- hosts: localhost
gather_facts: false
vars:
hostnames:
- hostname01
- hostname02
- hostname03
tasks:
- name: change hostname
debug:
msg:
win_hostname:
name: "{{ item }}"
loop: "{{ groups.mygroup|zip(hostnames)|list }}"
Here I'm using a debug task instead of actually running the win_hostname task. The output of running:
ansible-playbook -i hosts playbook.yml
Looks like:
TASK [change hostname] ********************************************************************************************************************************
ok: [localhost] => (item=[u'hostA', u'hostname01']) => {
"msg": {
"win_hostname": {
"name": [
"hostA",
"hostname01"
]
}
}
}
ok: [localhost] => (item=[u'hostB', u'hostname02']) => {
"msg": {
"win_hostname": {
"name": [
"hostB",
"hostname02"
]
}
}
}
ok: [localhost] => (item=[u'hostC', u'hostname03']) => {
"msg": {
"win_hostname": {
"name": [
"hostC",
"hostname03"
]
}
}
}
As you can see, it's paired each host from the inventory with a hostname from the hostnames list.
Update
Based on the additional information you've provided, I think what you
actually want is this:
- name: change hostname
win_hostname:
name: "{{ hostnames[group.company.index(inventory_hostname) }}"
This will assign one value from hostname to each host in your
inventory. We're looking up the position of the current
inventory_hostname in your group, and then using that to index into
the hostnames list.