Ansible - Get groups specific subgroups - ansible

I have the following groups defined in my inventory:
[webservers]
[server_storage]
[server_ws1]
[webservers:children]
server_storage
server_ws1
During my play, I'm in need of getting the names of the children groups of group['webservers'] ONLY (I think of theese as 'subgroups')
So, let's say I would need to set_fact a variable that contains in this case, the list of strings:
- server_storage
- server_ws1
This would have to be dynamic, so if I add group 'server_ws2' to group['webservers'], this should return
- server_storage
- server_ws1
- server_ws2
I've been playing with the use of group_names, group['webservers'] (which doesn't return subgroups, but hostnames)
Basically, I need a simple way of getting a the list of subgroups of a specific group. Is this possible without the use of black magic?
UPDATE: The idea, is that the hosts could belong to more groups, but I need only the subgroups or children of webservers group. It's constant, no matter the host, the output should allways be the same.
By the way, this one didn't work How can I get a list of child groups in Ansible?, because it retuns all groups for the current host, I need only subgroups of the specified group.

Q: "Get the names of the children groups of group['webservers']"
A: For example, the inventory
shell> cat hosts
[webservers]
[server_storage]
host_01
[server_ws1]
host_02
[webservers:children]
server_storage
server_ws1
and the playbook
shell> cat playbook.yml
- hosts: webservers
tasks:
- set_fact:
subgroups: "{{ subgroups|default([]) + hostvars[item].group_names }}"
loop: "{{ groups.webservers }}"
run_once: true
- debug:
msg: "{{ subgroups|unique|difference(['webservers']) }}"
run_once: true
give (abridged)
shell> ansible-playbook -i hosts playbook.yml
msg:
- server_storage
- server_ws1
As a workaround, it's possible to use ansible-inventory to list the structure of the inventory, store the output in a file, and use this file as needed, e.g. include_vars in a playbook. See the example below
shell> cat hosts
host_01
host_02
[webservers]
[server_storage]
[server_ws1]
[webservers:children]
server_storage
server_ws1
shell> ansible-inventory -i hosts --list --output hosts.json
shell> cat hosts.json
{
"_meta": {
"hostvars": {}
},
"all": {
"children": [
"ungrouped",
"webservers"
]
},
"ungrouped": {
"hosts": [
"host_01",
"host_02"
]
},
"webservers": {
"children": [
"server_storage",
"server_ws1"
]
}

Related

Ansible: Listing inventory group members

I have what I'm hoping is a simple question to answer, we have multiple groups of servers that patch monthly with Ansible Tower. The morning prior to a group being patched I have Ansible send out an email to the respective groups that would work on those servers so they are aware prior to the patches.
Currently, I have the playbooks written manually where I have typed out the name of every server in each group, but as groups grow and change, this becomes a pain as I'm having to edit these playbooks each time.
How can I have Ansible populate the names of servers in a group without me having to type them out? Here's my example playbook:
- name: Alert for pending updates
hosts: localhost
tasks:
- name: Send email to groups
community.general.mail:
to:
- 'email address 1'
- 'email address 2'
- 'email address 3'
cc:
- 'email address 1'
- 'email address 2'
subject: Scheduled Update Alert
body: "Monthly OS patches will begin tonight at 9pm CST on the following systems: Server 1, Server 2, Server 3, Server 4, Server 5, and Server 6"
sender: "ansible.updates#domain.com"
headers: 'Reply-To=email#domain.com'
delegate_to: localhost
I'm not sure if this is relevant, but we do not use static inventory files, our inventories are synced with vCenter and the groups are created using tags in vCenter.
Thanks to anyone who takes time to help!
Edit
Using #U880D playbook:
---
- name: Test output of hosts in a group
hosts: localhost
gather_facts: false
tasks:
- name: Show Facts
debug:
msg: "{{ groups }}"
...
I'm using Ansible Tower, so the inventory in my original question is dynamically created from VMware and I use tags on the VMs in vCenter to create groups in my Ansible inventory, so rather than using the dynamically created inventory and having a massive output of groups and servers, I used a static "test" inventory with one server in it. I created a few different test groups and left some of them empty and populated others with the test server.
Here's the output of that:
ok: [localhost] => {
"msg": {
"Test_Group_1": [
"test server 1"
],
"Test_Group_2": [],
"Test_Group_3": [
"test server 1"
],
"all": [
"test server 1"
],
"ungrouped": []
}
}
How do you now filter this output down to a single group? Currently using this method it just prints all groups and their members. I want to be able to tell Ansible to select, for example Test_Group_3 and list it's members in my email body.
Ansible provides Specical Variables like groups_names and groups
A dictionary/map with all the groups in inventory and each group has the list of hosts that belong to it
An inventory like
[test]
test.example.com
ansible-inventory --graph
#all:
|--#test:
| |--test.example.com
|--#ungrouped:
ansible-inventory --list
{
"_meta": {
"hostvars": {}
},
"all": {
"children": [
"test",
"ungrouped"
]
},
"test": {
"hosts": [
"test.example.com"
]
}
}
has Default groups and a sample playbook like
---
- hosts: test
become: false
gather_facts: false
tasks:
- name: Show Facts
debug:
msg: "{{ groups }}"
will result into an output of
TASK [Show Facts] *******
ok: [test.example.com] =>
msg:
all:
- test.example.com
test:
- test.example.com
ungrouped: []
How can I have Ansible populate the names of servers in a group without me having to type them out?
The above mentioned approach could be the first step to get a list of hostnames for an email body.
How do you now filter this output down to a single group? Currently using this method it just prints all groups and their members. I want to be able to tell Ansible to select, for example test group and list it's members in my email body.
For this have a look into the following approach
---
- hosts: localhost
become: false
gather_facts: false
vars:
PATCHGROUP: 'test'
tasks:
- name: Show Facts
debug:
msg: "{{ groups[PATCHGROUP] | join(', ') }}"
which will result into an output of
TASK [Show Facts] **********************************************************************************************************************************
ok: [localhost] =>
msg: test.example.com, added.example.com
In other words, one need just to define the group dynamically and make a string out of the list by simply joining the elements and in the case shown probably end up with something like
body: "Monthly OS patches will begin tonight at 9pm CST on the following systems: {{ groups[PATCHGROUP] | join(', ') }}"
Here it was used Referencing nested variables from Dictionary variables and Manipulating strings.

Get IP address from hosts in a Group

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

How can I resolve an inventory member who has a certain attribute?

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.

Get a list of inventory hostnames

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

How to loop through inventory and assign value in Ansible

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.

Resources