I'm really struggling to get my head around loops within Ansible when it comes to wanting more than one loop in a task.
Right now I'm writing a task to create some registry entries within Ansible and have the following which does work,
- name: Windows SSL/TLS Configuration
ansible.windows.win_regedit:
path: HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\{{ item.type }}
name: '{{ item.property }}'
data: '{{ item.value }}'
type: dword
state: present
loop:
- type: Server
property: Enabled
value: 0
- type: Server
property: DisabledByDefault
value: 1
- type: Client
property: Enabled
value: 0
- type: Client
property: DisabledByDefault
value: 1
However what I want to do is replace "SSL 3.0" with something like "{{ item.protocol }}" and be able to have it replaced with "SSL 2.0", "SSL 3.0" and "TLS 1.0", then creating the same entries within each of those.
I'd have liked to do them all within the same task, rather than having to run the exact same task 3 times with just one difference in the path.
Is what I want to do even possible?
Use loop_control to set loop_var. Otherwise the inner and outer loop use the same loop variable, which does not work.
See the documentation: Defining inner and outer variable names with loop_var
The documentation explains, that you have to put the inner loop into a different file in order to include it with include_tasks.
If you do not want that, you can add the protocols to each item in order to use with_subelements. Example:
- hosts: localhost
connection: local
vars:
protocols:
- "SSL 2.0"
- "SSL 3.0"
- "TLS 1.0"
keys:
- type: Server
property: Enabled
value: 0
protocols: "{{ protocols }}"
- type: Server
property: DisabledByDefault
value: 1
protocols: "{{ protocols }}"
- type: Client
property: Enabled
value: 0
protocols: "{{ protocols }}"
- type: Client
property: DisabledByDefault
value: 1
protocols: "{{ protocols }}"
tasks:
- debug: msg="{{item.0.type}} {{item.0.property}} {{item.0.value}} {{item.1}}"
with_subelements:
- "{{ keys }}"
- protocols
This will generate 12 keys:
$ ansible-playbook example.yaml
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does
not match 'all'
PLAY [localhost] **********************************************************************************************
TASK [Gathering Facts] ****************************************************************************************
ok: [localhost]
TASK [debug] **************************************************************************************************
ok: [localhost] => (item=[{'type': 'Server', 'property': 'Enabled', 'value': 0}, 'SSL 2.0']) => {
"msg": "Server Enabled 0 SSL 2.0"
}
ok: [localhost] => (item=[{'type': 'Server', 'property': 'Enabled', 'value': 0}, 'SSL 3.0']) => {
"msg": "Server Enabled 0 SSL 3.0"
}
ok: [localhost] => (item=[{'type': 'Server', 'property': 'Enabled', 'value': 0}, 'TLS 1.0']) => {
"msg": "Server Enabled 0 TLS 1.0"
}
ok: [localhost] => (item=[{'type': 'Server', 'property': 'DisabledByDefault', 'value': 1}, 'SSL 2.0']) => {
"msg": "Server DisabledByDefault 1 SSL 2.0"
}
ok: [localhost] => (item=[{'type': 'Server', 'property': 'DisabledByDefault', 'value': 1}, 'SSL 3.0']) => {
"msg": "Server DisabledByDefault 1 SSL 3.0"
}
ok: [localhost] => (item=[{'type': 'Server', 'property': 'DisabledByDefault', 'value': 1}, 'TLS 1.0']) => {
"msg": "Server DisabledByDefault 1 TLS 1.0"
}
ok: [localhost] => (item=[{'type': 'Client', 'property': 'Enabled', 'value': 0}, 'SSL 2.0']) => {
"msg": "Client Enabled 0 SSL 2.0"
}
ok: [localhost] => (item=[{'type': 'Client', 'property': 'Enabled', 'value': 0}, 'SSL 3.0']) => {
"msg": "Client Enabled 0 SSL 3.0"
}
ok: [localhost] => (item=[{'type': 'Client', 'property': 'Enabled', 'value': 0}, 'TLS 1.0']) => {
"msg": "Client Enabled 0 TLS 1.0"
}
ok: [localhost] => (item=[{'type': 'Client', 'property': 'DisabledByDefault', 'value': 1}, 'SSL 2.0']) => {
"msg": "Client DisabledByDefault 1 SSL 2.0"
}
ok: [localhost] => (item=[{'type': 'Client', 'property': 'DisabledByDefault', 'value': 1}, 'SSL 3.0']) => {
"msg": "Client DisabledByDefault 1 SSL 3.0"
}
ok: [localhost] => (item=[{'type': 'Client', 'property': 'DisabledByDefault', 'value': 1}, 'TLS 1.0']) => {
"msg": "Client DisabledByDefault 1 TLS 1.0"
}
PLAY RECAP ****************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0
Related
I have a dict like this:
distr:
Alice:
- Avocado
- Cherries
Bob:
- Apple
David:
- Pineapple
- Watermelon
John:
- Banana
- Mango
Shara:
- Orange
- Peach
Now I want to execute a task with each combination.
Like this:
Alice eats Avocado
Alice eats Cherries
Bob eats Apple
David eats Pineapple
David eats Watermelon
...
My ansible is like that:
- name: Debug
debug:
msg: {{ item.key }} eats {{ item }}
with_items: "{{ lookup('dict', distr) }}"
The result:
TASK [Debug] *******************************************************************************************************************************************************************************************************************************************
ok: [localhost] => (item={'key': 'Alice ', 'value': ['Avocado', 'Cherries']}) => {
"msg": "Alice eats ['Avocado', 'Cherries']"
}
ok: [localhost] => (item={'key': 'Bob ', 'value': ['Apple']}) => {
"msg": "Bob eats ['Apple']"
}
ok: [localhost] => (item={'key': 'David ', 'value': ['Pineapple', 'Watermelon']}) => {
"msg": "David eats ['Pineapple', 'Watermelon']"
}
I tried with subelements and other type of loops without luck. I think this is really simple.
We'll need to process your data to get it into a more useful structure. We want a flat list consisting of (person, food) tuples, and we can get there using the product filter, which returns the cross product of two lists. For example, the expression ["Alice"]|product(["Avocado", "Cherries"]) evalutes to [["Alice", "Avocado"], ["Alice", "Cherries"]].
We can use a looping set_fact task to create our list:
- set_fact:
cross: "{{ cross + [item[0]]|product(item[1])|list }}"
loop: "{{ distr.items()|list }}"
vars:
cross: []
The above tasks iterates through each (key, values) tuple of your
dictionary and appends the cross product of the key and values to a
new list, cross. The result looks like this:
"cross": [
[
"Alice",
"Avocado"
],
[
"Alice",
"Cherries"
],
[
"Bob",
"Apple"
],
[
"David",
"Pineapple"
],
[
"David",
"Watermelon"
],
[
"John",
"Banana"
],
[
"John",
"Mango"
],
[
"Shara",
"Orange"
],
[
"Shara",
"Peach"
]
]
Putting that all together, we get:
- hosts: localhost
gather_facts: false
vars:
distr:
Alice:
- Avocado
- Cherries
Bob:
- Apple
David:
- Pineapple
- Watermelon
John:
- Banana
- Mango
Shara:
- Orange
- Peach
tasks:
- set_fact:
cross: "{{ cross + [item[0]]|product(item[1])|list }}"
loop: "{{ distr.items()|list }}"
vars:
cross: []
- debug:
msg: "{{ item[0] }} eats {{ item[1] }}"
loop: "{{ cross }}"
And the output of running the above playbook is:
PLAY [localhost] *****************************************************************************************************************************************************************************
TASK [set_fact] ******************************************************************************************************************************************************************************
ok: [localhost] => (item=['Alice', ['Avocado', 'Cherries']])
ok: [localhost] => (item=['Bob', ['Apple']])
ok: [localhost] => (item=['David', ['Pineapple', 'Watermelon']])
ok: [localhost] => (item=['John', ['Banana', 'Mango']])
ok: [localhost] => (item=['Shara', ['Orange', 'Peach']])
TASK [debug] *********************************************************************************************************************************************************************************
ok: [localhost] => (item=['Alice', 'Avocado']) => {
"msg": "Alice eats Avocado"
}
ok: [localhost] => (item=['Alice', 'Cherries']) => {
"msg": "Alice eats Cherries"
}
ok: [localhost] => (item=['Bob', 'Apple']) => {
"msg": "Bob eats Apple"
}
ok: [localhost] => (item=['David', 'Pineapple']) => {
"msg": "David eats Pineapple"
}
ok: [localhost] => (item=['David', 'Watermelon']) => {
"msg": "David eats Watermelon"
}
ok: [localhost] => (item=['John', 'Banana']) => {
"msg": "John eats Banana"
}
ok: [localhost] => (item=['John', 'Mango']) => {
"msg": "John eats Mango"
}
ok: [localhost] => (item=['Shara', 'Orange']) => {
"msg": "Shara eats Orange"
}
ok: [localhost] => (item=['Shara', 'Peach']) => {
"msg": "Shara eats Peach"
}
PLAY RECAP ***********************************************************************************************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
I'm having an issue ansible condition and loops:
I would like to get the output of top 5 servers only but the following playbook giving the output of all hosts instead of 2 hosts.
---
- name:
hosts: all
tasks:
- name:
command: hostname -i
register: out
- set_fact: my_ip = "{{out.stdout_lines}}"
with_items: [1,2]
when: item <= 2
output:
TASK [set_fact] *********************************************************************************************************************************************
task path: /home/ec2-user/ansible/mm.yml:9
ok: [s1] => (item=1) => {"ansible_facts": {"_raw_params": "my_ip = \"[u'172.31.45.164']\""}, "ansible_loop_var": "item", "changed": false, "item": 1}
ok: [s1] => (item=2) => {"ansible_facts": {"_raw_params": "my_ip = \"[u'172.31.45.164']\""}, "ansible_loop_var": "item", "changed": false, "item": 2}
ok: [ansible] => (item=1) => {"ansible_facts": {"_raw_params": "my_ip = \"[u'172.31.39.107']\""}, "ansible_loop_var": "item", "changed": false, "item": 1}
ok: [ansible] => (item=2) => {"ansible_facts": {"_raw_params": "my_ip = \"[u'172.31.39.107']\""}, "ansible_loop_var": "item", "changed": false, "item": 2}
ok: [s2] => (item=1) => {"ansible_facts": {"_raw_params": "my_ip = \"[u'172.31.37.172']\""}, "ansible_loop_var": "item", "changed": false, "item": 1}
ok: [s2] => (item=2) => {"ansible_facts": {"_raw_params": "my_ip = \"[u'172.31.37.172']\""}, "ansible_loop_var": "item", "changed": false, "item": 2}
I think you may not fully understand how Ansible works. When your play starts with this:
- hosts: all
This means that Ansible will run the tasks in your play on each of the hosts in your inventory. So if you have, say, five hosts in your inventory, then you're running these tasks...
- name:
command: hostname -i
register: out
- set_fact: my_ip = "{{out.stdout_lines}}"
with_items: [1,2]
when: item <= 2
...five times (so that second task will actually execute 10 times...five hosts, and for each a host a loop with two items).
Furthermore, it's not clear what you're trying to accomplish with that second task. Given your loop (with_items: [1,2]), your conditional (when: item <= 2) will always be true, so it's not serving any purpose. And you're not use the value of item in the task.
If you want the hostnames of the first two hosts in your inventory, you could write:
- hosts: all[0:1]
gather_facts: false
tasks:
- command: echo {{ inventory_hostname }}
register: out
- debug:
msg: "{{ out.stdout }}"
This would run those two tasks on the first two hosts in your inventory. Given an inventory that look like this:
host1 ansible_host=localhost
host2 ansible_host=localhost
host3 ansible_host=localhost
host4 ansible_host=localhost
The above playboook would produce:
PLAY [all[0:1]] ******************************************************************************
TASK [command] *******************************************************************************
changed: [host1]
changed: [host2]
TASK [debug] *********************************************************************************
ok: [host1] => {
"msg": "host1"
}
ok: [host2] => {
"msg": "host2"
}
PLAY RECAP ***********************************************************************************
host1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
host2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
I am using the module csv-source-of-truth (https://github.com/joelwking/csv-source-of-truth) to get the IP and OS information from a csv file. I was able to register these info into a vsheet and using debug, I can see that I can loop through the contents of the vsheet.
However, when I use ios_command and try to loop through the vsheet, it seems that it gets stuck at the first entry of the vsheet.
This are the contents of the Inventory.csv file:
192.168.68.201,ios
192.168.68.202,ios
Code:
---
- hosts: localhost
gather_facts: false
tasks:
- name: Block
block:
- name: Use CSV
csv_to_facts:
src: '{{playbook_dir}}/NEW/Inventory.csv'
vsheets:
- INFO:
- IP
- OS
- debug:
msg: '{{item.IP}}'
loop: '{{INFO}}'
- name: Show Version
vars:
ansible_host: '{{item.IP}}'
ansible_network_os: '{{item.OS}}'
ansible_user: cisco
ansible_ssh_pass: cisco
ansible_connection: network_cli
ansible_become: yes
ansible_become_method: enable
ios_command:
commands: show version
register: output
loop: '{{INFO}}'
- name: Show the output of looped Show Version
debug:
var: output
- name: Show just the stdout_lines
debug:
var: output.results.{{item}}.stdout_lines
with_sequence: "0-{{output|length - 2}}"
You will notice on the output that it only has results for R1 when you look at the uptime information. i.e. R1 has an uptime of such and such.
PLAY [localhost] **********************************************************************************************************************************************
TASK [Use CSV] ************************************************************************************************************************************************
ok: [localhost]
TASK [debug] **************************************************************************************************************************************************
ok: [localhost] => (item={u'IP': u'192.168.68.201', u'OS': u'ios'}) => {
"msg": "192.168.68.201"
}
ok: [localhost] => (item={u'IP': u'192.168.68.202', u'OS': u'ios'}) => {
"msg": "192.168.68.202"
}
TASK [Show Version] *******************************************************************************************************************************************
ok: [localhost] => (item={u'IP': u'192.168.68.201', u'OS': u'ios'})
ok: [localhost] => (item={u'IP': u'192.168.68.202', u'OS': u'ios'})
TASK [Show the output of looped Show Version] *****************************************************************************************************************
ok: [localhost] => {
"output": {
"changed": false,
"msg": "All items completed",
"results": [
{
"ansible_loop_var": "item",
"changed": false,
"failed": false,
"invocation": {
"module_args": {
"auth_pass": null,
"authorize": null,
"commands": [
"show version"
],
"host": null,
"interval": 1,
"match": "all",
"password": null,
"port": null,
"provider": null,
"retries": 10,
"ssh_keyfile": null,
"timeout": null,
"username": null,
"wait_for": null
}
},
"item": {
"IP": "192.168.68.201",
"OS": "ios"
},
"stdout": [
-- Output removed for brevity
],
"stdout_lines": [
[
"-- Output removed for brevity
"R1 uptime is 1 hour, 34 minutes",
]
]
},
{
"ansible_loop_var": "item",
"changed": false,
"failed": false,
"invocation": {
"module_args": {
"auth_pass": null,
"authorize": null,
"commands": [
"show version"
],
"host": null,
"interval": 1,
"match": "all",
"password": null,
"port": null,
"provider": null,
"retries": 10,
"ssh_keyfile": null,
"timeout": null,
"username": null,
"wait_for": null
}
},
"item": {
"IP": "192.168.68.202",
"OS": "ios"
},
"stdout": [
-- Output removed for brevity
],
"stdout_lines": [
[
-- Output removed for brevity
"R1 uptime is 1 hour, 34 minutes",
]
]
}
]
}
}
TASK [Show just the stdout_lines] *****************************************************************************************************************************
ok: [localhost] => (item=0) => {
"ansible_loop_var": "item",
"item": "0",
"output.results.0.stdout_lines": [
[
-- Output removed for brevity
"R1 uptime is 1 hour, 34 minutes",
]
]
}
ok: [localhost] => (item=1) => {
"ansible_loop_var": "item",
"item": "1",
"output.results.1.stdout_lines": [
[
-- Output removed for brevity
"R1 uptime is 1 hour, 34 minutes",
]
]
}
PLAY RECAP ****************************************************************************************************************************************************
localhost : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Try to create an inventory
- name: Create inventory
add_host:
hostname: '{{ item.IP }}'
groups: temp_group_01
ansible_network_os: '{{ item.OS }}'
ansible_user: cisco
ansible_ssh_pass: cisco
ansible_connection: network_cli
ansible_become: yes
ansible_become_method: enable
loop: '{{ INFO }}'
and delegate to the hosts
- name: Show Version
ios_command:
commands: show version
register: output
delegate_to: '{{ item }}'
loop: '{{ groups['temp_group_01'] }}'
Explanation
From the play below can be seen that the connection does not obey the changed ansible_host and keeps using the first item in the loop.
- hosts: test_01
tasks:
- command: hostname
register: result
vars:
ansible_host: "{{ item }}"
loop:
- test_02
- test_03
- debug:
msg: "{{ result.results|map(attribute='stdout')|list }}"
gives
TASK [command] ******************************************************************************
changed: [test_01] => (item=test_02)
changed: [test_01] => (item=test_03)
TASK [debug] ********************************************************************************
ok: [test_01] => {
"msg": [
"test_02",
"test_02"
]
}
This behavior is very probably caused by the connection plugin because vars works as expected. The play below
- hosts: test_01
tasks:
- command: echo "{{ ansible_host }}"
register: result
vars:
ansible_host: "{{ item }}"
loop:
- test_02
- test_03
- debug:
msg: "{{ result.results|map(attribute='stdout')|list }}"
gives
TASK [command] ******************************************************************************
changed: [test_01] => (item=test_02)
changed: [test_01] => (item=test_03)
TASK [debug] ********************************************************************************
ok: [test_01] => {
"msg": [
"test_02",
"test_03"
]
}
As a result, it's not possible to loop ansible_host. Instead, delegate_to shall be used.
I'm getting different results when using loop vs with_items when trying to iterate over a list of dictionaries.
I've tried using loop|dict2items (the structure isn't a dictionary, & it tells me as much. heh) and loop with the flatten filter.
Here is the list of dictionaries:
"msg": [
{
"id": "id1",
"ip": "ip1",
"name": "name1"
},
{
"id": "id2",
"ip": "ip2",
"name": "name2"
},
{
"id": "id3",
"ip": "ip3",
"name": "name3"
},
{
"id": "id4",
"ip": "ip4",
"name": "name4"
}
]
}
Here is the task in the playbook:
- name: Add privateIp windows_instances to inventory
add_host:
name: "{{ item.ip }}"
aws_name: "{{ item.name }}"
groups: windows_instances
aws_instanceid: "{{ item.id }}"
ansible_user: "{{ windows_user }}"
ansible_password: "{{ windows_password }}"
ansible_port: 5985
ansible_connection: winrm
ansible_winrm_server_cert_validation: ignore
loop:
- "{{ list1 | flatten(levels=1) }}"
When attempting to run the above code, I get the "list object has no attribute" error. I've tried different flatten levels to no avail.
HOWEVER...
If I simply replace the loop above with:
with_items:
- "{{ list1 }}"
Everything works perfectly. I'm missing something in the with_items > loop translation here...
Don't put a - before your list.
And here, you have a list of dicts, so you don't need to flatten neither.
This playbook:
- hosts: localhost
gather_facts: no
vars:
demo_list:
- ip: 1.2.3.4
id: 1
name: demo1
- ip: 2.2.3.4
id: 2
name: demo2
- ip: 3.2.3.4
id: 3
name: demo3
tasks:
- name: the list
debug:
msg: "{{ demo_list }}"
- name: unflattened list
debug:
msg: "{{ item.id }} {{ item.ip }} {{ item.name }}"
loop:
"{{ demo_list }}"
- name: flattened list == unflattened list in this case
debug:
msg: "{{ item.id }} {{ item.ip }} {{ item.name }}"
loop:
"{{ demo_list | flatten(levels=1) }}"
gives this result:
PLAY [localhost] ***************************************************************************************
TASK [the list] ****************************************************************************************
ok: [localhost] => {
"msg": [
{
"id": 1,
"ip": "1.2.3.4",
"name": "demo1"
},
{
"id": 2,
"ip": "2.2.3.4",
"name": "demo2"
},
{
"id": 3,
"ip": "3.2.3.4",
"name": "demo3"
}
]
}
TASK [unflattened list] ********************************************************************************
ok: [localhost] => (item=None) => {
"msg": "1 1.2.3.4 demo1"
}
ok: [localhost] => (item=None) => {
"msg": "2 2.2.3.4 demo2"
}
ok: [localhost] => (item=None) => {
"msg": "3 3.2.3.4 demo3"
}
TASK [flattened list == unflattened list in this case] *************************************************
ok: [localhost] => (item=None) => {
"msg": "1 1.2.3.4 demo1"
}
ok: [localhost] => (item=None) => {
"msg": "2 2.2.3.4 demo2"
}
ok: [localhost] => (item=None) => {
"msg": "3 3.2.3.4 demo3"
}
PLAY RECAP *********************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0
I'm trying to use vars to perform loops in ansible but I'm struggling to get them to work. The documentation talks about the loop attribute but as far as I can tell, this only works if you have one loop in a task.
Is it possible to use different loops in a task or must it be split into different tasks?
- name: "Configure ufw"
ufw:
policy: "{{ defaults.policy }}"
direction: "{{ defaults.direction }}"
rule: allow
src: "{{ rules }}"
logging: on
state: enabled
vars:
defaults:
- { direction: incoming, policy: deny }
- { direction: outgoing, policy: allow }
rules:
- 80/tcp
- 443/tcp
When I run the code I get the error
"msg": "The task includes an option with an undefined variable. The error was: 'list object' has no attribute 'direction'
What you need is with_nested. This:
---
- hosts: localhost
become: no
connection: local
vars:
defaults:
- { direction: incoming, policy: deny }
- { direction: outgoing, policy: allow }
rules:
- 80/tcp
- 443/tcp
tasks:
- name: Change connect file to bulk mode
debug:
msg: "{{ item.0.direction }}: {{ item.1 }}"
with_nested:
- "{{ defaults }}"
- "{{ rules }}"
Gives this output:
TASK [Change connect file to bulk mode] *********************************************************************************************
ok: [localhost] => (item=None) => {
"msg": "incoming: 80/tcp"
}
ok: [localhost] => (item=None) => {
"msg": "incoming: 443/tcp"
}
ok: [localhost] => (item=None) => {
"msg": "outgoing: 80/tcp"
}
ok: [localhost] => (item=None) => {
"msg": "outgoing: 443/tcp"
}
with_nested may be an option, as per Jack's answer.
Alternately, and possibly the currently preferred mechanism, is to use loop: "{{ your_first_list | product (your_second_list) | list }}" to combine your multiple lists into a single list.
For example, this:
- name: test
ansible.builtin.debug:
msg: "{{ item.0.0 }} - {{ item.0.1}} - {{ item.1 }}"
loop: "{{ [ ['incoming','deny'], ['outgoing','allow']] | product( [ '80/tcp', '443/tcp' ] ) | list }}"
produces this output:
ok: [examplehost.my.example.com] => (item=[[u'incoming', u'deny'], u'80/tcp']) => {
"msg": "incoming - deny - 80/tcp"
}
ok: [examplehost.my.example.com] => (item=[[u'incoming', u'deny'], u'443/tcp']) => {
"msg": "incoming - deny - 443/tcp"
}
ok: [examplehost.my.example.com] => (item=[[u'outgoing', u'allow'], u'80/tcp']) => {
"msg": "outgoing - allow - 80/tcp"
}
ok: [examplehost.my.example.com] => (item=[[u'outgoing', u'allow'], u'443/tcp']) => {
"msg": "outgoing - allow - 443/tcp"
}