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"
}
Related
I am using the following data structure in Ansible:
datacenters:
- name: Datacenter1
clusters:
- name: ClusterA
hosts:
- 192.168.0.1
- 192.168.0.2
- name: ClusterB
hosts:
- 192.168.1.1
- 192.168.1.2
- name: Datacenter2
clusters:
- name: ClusterC
hosts:
- 192.168.2.1
- 192.168.2.2
In a task, I want to iterate over each host while having access to the data of all the parent layers. If there is only one nesting level, it can easily be done with the subelements filter:
loop: '{{ datacenters | subelements(''clusters'') }}'
This will give me access to the data like this:
'Datacenter: {{ item.0.name }}, Cluster: {{ item.1.name }}'
I was hoping to be able to extend this concept like this:
loop: '{{ datacenters | subelements(''clusters'') | subelements(''hosts'') }}'
And being able to access the data like this:
'Datacenter: {{ item.0.name }}, Cluster: {{ item.1.name }}, Host: {{ item.2 }}'
But that does not work and I get the following error message instead:
Unexpected templating type error occurred on ({{ datacenters | subelements('clusters') | subelements('hosts') }}): the key hosts should point to a dictionary, got ...(the result of the first layer)
I found this question, which solves a similar problem, but relies on having distinct dict keys on all nesting levels, which I don't, because datacenters and clusters have the same name key.
So, how can I iterate over subelements of subelements in Ansible?
A bit far-fetched but the following playbook will achieve your goal:
---
- hosts: localhost
gather_facts: false
vars:
datacenters:
- name: Datacenter1
clusters:
- name: ClusterA
hosts:
- 192.168.0.1
- 192.168.0.2
- name: ClusterB
hosts:
- 192.168.1.1
- 192.168.1.2
- name: Datacenter2
clusters:
- name: ClusterC
hosts:
- 192.168.2.1
- 192.168.2.2
# Get the list of datacenters
_dcs: "{{ datacenters | map(attribute='name') }}"
# Get the corresponding list of clusters with subelements on hosts
_clusters: "{{ datacenters | map(attribute='clusters') | map('subelements', 'hosts') }}"
# Recreate a list with the sublisted hosts per clusters and create subelements on that final result
_overall: "{{ dict(_dcs | zip(_clusters)) | dict2items(key_name='datacenter', value_name='clusters') | subelements('clusters') }}"
tasks:
- name: Loop on the result
debug:
msg:
- "DC: {{ item.0.datacenter }}"
- "Cluster: {{ item.1.0.name }}"
- "Host: {{ item.1.1 }}"
loop: "{{ _overall }}"
loop_control:
label: "{{ item.0.datacenter }} - {{ item.1.0.name }}"
This gives:
PLAY [localhost] **************************************************************************************************************************************************
TASK [Loop on the result] *****************************************************************************************************************************************
ok: [localhost] => (item=Datacenter1 - ClusterA) => {
"msg": [
"DC: Datacenter1",
"Cluster: ClusterA",
"Host: 192.168.0.1"
]
}
ok: [localhost] => (item=Datacenter1 - ClusterA) => {
"msg": [
"DC: Datacenter1",
"Cluster: ClusterA",
"Host: 192.168.0.2"
]
}
ok: [localhost] => (item=Datacenter1 - ClusterB) => {
"msg": [
"DC: Datacenter1",
"Cluster: ClusterB",
"Host: 192.168.1.1"
]
}
ok: [localhost] => (item=Datacenter1 - ClusterB) => {
"msg": [
"DC: Datacenter1",
"Cluster: ClusterB",
"Host: 192.168.1.2"
]
}
ok: [localhost] => (item=Datacenter2 - ClusterC) => {
"msg": [
"DC: Datacenter2",
"Cluster: ClusterC",
"Host: 192.168.2.1"
]
}
ok: [localhost] => (item=Datacenter2 - ClusterC) => {
"msg": [
"DC: Datacenter2",
"Cluster: ClusterC",
"Host: 192.168.2.2"
]
}
PLAY RECAP ********************************************************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
I'm unsure if the title matches to what I'm trying to ask but I didn't know what other title to use.
So my question:
What I'm trying to do is gather disk info from VMWare and post the output of capacity_in_kb for EACH disk in a file (but currently only debug).
- name: Gather disk info from virtual machine
community.vmware.vmware_guest_disk_info:
hostname: "{{ lookup('ansible.builtin.env', 'VMWARE_HOST') }}"
username: "{{ lookup('ansible.builtin.env', 'VMWARE_USER') }}"
password: "{{ lookup('ansible.builtin.env', 'VMWARE_PASSWORD') }}"
validate_certs: no
name: "{{ ansible_hostname }}"
datacenter: "{{ lookup('ansible.builtin.env', 'VMWARE_HOST') }}"
delegate_to: 127.0.0.1
register: disk_info
- debug:
var: disk_info
This outputs the following:
{
"disk_info": {
"guest_disk_info": {
"0": {
"key": 2000,
"label": "Hard disk 1",
"summary": "16,777,216 KB",
"backing_filename": "[xxx] xxx.vmdk",
"backing_datastore": "xxx_log",
"controller_key": 1000,
"unit_number": 0,
"capacity_in_kb": 16777216,
"capacity_in_bytes": 17179869184,
"backing_type": "FlatVer2",
"backing_writethrough": false,
"backing_thinprovisioned": false,
"backing_eagerlyscrub": false,
"backing_diskmode": "persistent",
"backing_disk_mode": "persistent",
"backing_uuid": "xxxxxxxxxxxxxxxx",
"controller_bus_number": 0,
"controller_type": "paravirtual"
},
"1": {
"key": 2001,
"label": "Hard disk 2",
"summary": "262,144,000 KB",
"backing_filename": "[xxx] xxx.vmdk",
"backing_datastore": "xxx_log",
"controller_key": 1000,
"unit_number": 1,
"capacity_in_kb": 262144000,
"capacity_in_bytes": 268435456000,
"backing_type": "FlatVer2",
"backing_writethrough": false,
"backing_thinprovisioned": false,
"backing_eagerlyscrub": false,
"backing_diskmode": "persistent",
"backing_disk_mode": "persistent",
"backing_uuid": "xxx",
"controller_bus_number": 0,
"controller_type": "paravirtual"
}
},
"failed": false,
"changed": false
},
"_ansible_verbose_always": true,
"_ansible_no_log": false,
"changed": false
}
We now want to get capacity_in_kb for every disk. Some VMs only have one disk attached, some have two, some three and so on...
We tried
- name: with_dict
ansible.builtin.debug:
msg: "{{ item.value.label }} - {{ item.value.capacity_in_kb }}"
loop: "{{ disk_info.guest_disk_info|dict2items }}"
but that also doesn't work for us and only gives us the output of the first disk [0] in the list.
Does anyone have an idea? Thanks in advance :)
Edit: as #β.εηοιτ.βε pointet out it's
- loop: "{{ disk_info|dict2items }}"
+ loop: "{{ disk_info.guest_disk_info|dict2items }}"
but still it only iterates over the first disk and not the second, third, etc..
EDIT2: it worked, but I just didn't see it...dumb me
If you want to iterate the data, the task below
- debug:
msg: "{{ item.value.label }}: {{ item.value.capacity_in_kb }}"
loop: "{{ disk_info.guest_disk_info|dict2items }}"
loop_control:
label: "{{ item.key }}"
gives
ok: [localhost] => (item=0) =>
msg: 'Hard disk 1: 16777216'
ok: [localhost] => (item=1) =>
msg: 'Hard disk 2: 262144000'
The next option is to avoid the iteration and create a dictionary
label_capacity_q: '*.[label, capacity_in_kb]'
label_capacity: "{{ dict(disk_info.guest_disk_info|
json_query(label_capacity_q)) }}"
gives
label_capacity:
Hard disk 1: 16777216
Hard disk 2: 262144000
Example of a complete playbook for testing
- hosts: localhost
vars:
label_capacity_q: '*.[label, capacity_in_kb]'
label_capacity: "{{ dict(disk_info.guest_disk_info|
json_query(label_capacity_q)) }}"
tasks:
- debug:
var: label_capacity
- debug:
msg: "{{ item.key }}: {{ item.value }}"
loop: "{{ label_capacity|dict2items }}"
- debug:
msg: |-
{% for k,v in label_capacity.items() %}
{{ k }}: {{ v }}
{% endfor %}
gives (abridged)
TASK [debug] *********************************************************************************
ok: [localhost] =>
label_capacity:
Hard disk 1: 16777216
Hard disk 2: 262144000
TASK [debug] *********************************************************************************
ok: [localhost] => (item={'key': 'Hard disk 1', 'value': 16777216}) =>
msg: 'Hard disk 1: 16777216'
ok: [localhost] => (item={'key': 'Hard disk 2', 'value': 262144000}) =>
msg: 'Hard disk 2: 262144000'
TASK [debug] *********************************************************************************
ok: [localhost] =>
msg: |-
Hard disk 1: 16777216
Hard disk 2: 262144000
Given the data, for example in group_vars
shell> cat group_vars/all/disk_info.yml
---
disk_info:
changed: false
failed: false
guest_disk_info:
'0':
backing_datastore: xxx_log
backing_disk_mode: persistent
backing_diskmode: persistent
backing_eagerlyscrub: false
backing_filename: '[xxx] xxx.vmdk'
backing_thinprovisioned: false
backing_type: FlatVer2
backing_uuid: xxxxxxxxxxxxxxxx
backing_writethrough: false
capacity_in_bytes: 17179869184
capacity_in_kb: 16777216
controller_bus_number: 0
controller_key: 1000
controller_type: paravirtual
key: 2000
label: Hard disk 1
summary: 16,777,216 KB
unit_number: 0
'1':
backing_datastore: xxx_log
backing_disk_mode: persistent
backing_diskmode: persistent
backing_eagerlyscrub: false
backing_filename: '[xxx] xxx.vmdk'
backing_thinprovisioned: false
backing_type: FlatVer2
backing_uuid: xxx
backing_writethrough: false
capacity_in_bytes: 268435456000
capacity_in_kb: 262144000
controller_bus_number: 0
controller_key: 1000
controller_type: paravirtual
key: 2001
label: Hard disk 2
summary: 262,144,000 KB
unit_number: 1
I am trying to iterate over level3 variable in Ansible. I've prepared a playbook like this:
---
- hosts: localhost
gather_facts: no
connection: local
vars:
level1:
- abc
- def
level2:
- { name: "app1", level3: ["aaaa","bbbb"]}
- { name: "app2", level3: ["eeeee"]}
tasks:
- name: test
debug: msg="{{ item.0 }} {{ item.1.name }} {{ item.2 }}"
with_nested:
- "{{ level1 }}"
- "{{ level2 }}"
- "{{ level2.level3 }}"
But I get error:
PLAY [localhost] ***************************************************************************************************************************************************
TASK [test] ********************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "'list object' has no attribute 'level3'"}
PLAY RECAP *********************************************************************************************************************************************************
localhost : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
What is bad with my config? I want to have on output each element.
level2.level3 does not exist. level2 is a list so e.g. level2.0.level3 exists.
I'm not entirely sure from your question but I think what you are looking for is:
- debug:
msg: "{{ item.0 }} {{ item.1.0.name }} {{ item.1.1 }}"
loop: "{{ level1 | product(level2 | subelements('level3')) }}"
You can keep the older with_ syntax using the nested lookup if you wish. The produced result is a bit different so you have to adapt to the datastructure:
- debug:
msg: "{{ item.0 }} {{ item.1.name }} {{ item.2 }}"
with_nested:
- "{{ level1 }}"
- "{{ level2 | subelements('level3') }}"
which can eventually be written back to a loop: syntax as follow:
- debug:
msg: "{{ item.0 }} {{ item.1.name }} {{ item.2 }}"
loop: "{{ q('nested', level1, level2 | subelements('level3')) }}"
All examples give the same result (delta the spitted item for each iteration, example output with first proposition)
PLAY [localhost] *****************************************************************************************************************************************
TASK [debug] *********************************************************************************************************************************************
ok: [localhost] => (item=['abc', [{'name': 'app1', 'level3': ['aaaa', 'bbbb']}, 'aaaa']]) => {
"msg": "abc app1 aaaa"
}
ok: [localhost] => (item=['abc', [{'name': 'app1', 'level3': ['aaaa', 'bbbb']}, 'bbbb']]) => {
"msg": "abc app1 bbbb"
}
ok: [localhost] => (item=['abc', [{'name': 'app2', 'level3': ['eeeee']}, 'eeeee']]) => {
"msg": "abc app2 eeeee"
}
ok: [localhost] => (item=['def', [{'name': 'app1', 'level3': ['aaaa', 'bbbb']}, 'aaaa']]) => {
"msg": "def app1 aaaa"
}
ok: [localhost] => (item=['def', [{'name': 'app1', 'level3': ['aaaa', 'bbbb']}, 'bbbb']]) => {
"msg": "def app1 bbbb"
}
ok: [localhost] => (item=['def', [{'name': 'app2', 'level3': ['eeeee']}, 'eeeee']]) => {
"msg": "def app2 eeeee"
}
PLAY RECAP ***********************************************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
I'm using the Ansible uri module to trigger the pfSense API.
Now I want to create firewall rules in a task (code is truncated).
---
# tasks file for creating firewall rules
- name: "Create firewall rules"
uri:
url: "https://{{ pf_hostname }}/api/v1/firewall/rule"
method: "POST"
body: "{ \
\"client-id\": \"{{ pf_user }}\",
\"client-token\": \"{{ pf_password }}\",
\"type\": \"{{ pf_fw_type_01 }}\",
\"interface\": \"{{ pf_fw_interface_01 }}\",
}"
The vars file looks like this.
---
# vars file for creating firewall rules
# Authentication
pf_hostname: "pfsense.local"
pf_user: "admin"
pf_password: "pfsense"
# Rule 01
pf_fw_type_01: "pass"
pf_fw_interface_01: "wan"
How can I now repeat the task without unnecessary redundancy (e.g. with loop) for other rules?
I only come up with the following idea, but it doesn't seem ideal to me.
loop:
- "{{ item.client-id: {{ pf_user }}, item.type: {{ pf_fw_type_01 }} }}"
- "{{ item.client-id: {{ pf_user }}, item.type: {{ pf_fw_type_02 }} }}"
How about putting the rules as a dynamic parameter in the list?
For example, here's like.
vars.yml
---
# vars file for creating firewall rules
# Authentication
pf_hostname: "pfsense.local"
pf_user: "admin"
pf_password: "pfsense"
rules:
- num: 01
type: "pass"
pf_fw_interface: "wan"
- num: 02
type: "pass"
pf_fw_interface: "wan"
playbook
---
- hosts: localhost
gather_facts: false
vars_files:
- vars.yml
tasks:
- debug:
msg: |
{
"client-id": "{{ pf_user }}",
"client-token": "{{ pf_password }}",
"type": "{{ item.type }}",
"interface": "{{ item.pf_fw_interface }}"
}
loop: "{{ rules }}"
result
$ ansible-playbook main.yml
(snip)
PLAY [localhost] *********************************************************************************************************************************************************************
TASK [debug] *************************************************************************************************************************************************************************
ok: [localhost] => (item={'type': 'pass', 'pf_fw_interface': 'wan'}) => {
"msg": {
"client-id": "admin",
"client-token": "pfsense",
"interface": "wan",
"type": "pass"
}
}
ok: [localhost] => (item={'type': 'pass', 'pf_fw_interface': 'wan'}) => {
"msg": {
"client-id": "admin",
"client-token": "pfsense",
"interface": "wan",
"type": "pass"
}
}
(snip)
I have an inventory in which many host_vars are set. Each host will contain a different number of datasets.
e.g.
host: host1
ip-addr: 192.0.2.12/24
datasets:
set1:
var1: 'east'
var2: 'west'
set2:
var1: 'north'
var2: 'south'
I can create a loop to count the datasets, but I don't seem able to use it to reference [varX]:
- name: "test loop"
debug:
msg:
- "{{ item }}"
- "{{ 'datasets.set' + item + '.var1' }}"
- "{{ datasets.set1.var1 }}"
loop: "{{ query('sequence', 'start=1 end='+((datasets|length)|string)) }}"
This appears to assemble the variable name I'm attempting to reference, however does not return the value associated with it. Manually calling that variable does return the interesting value.
ok: [host1] => (item=1) => {
"msg": [
"1",
"datasets.set1.var1",
"east"
]
}
ok: [host1] => (item=2) => {
"msg": [
"2",
"datasets.set2.var1",
"east"
]
}
Is what I'm doing possible, or should I be approaching it from another angle?
thanks in advance.
The task
- debug:
msg: "set{{ item }}.var1 = {{ datasets['set' ~ item].var1 }}"
loop: "{{ range(1, datasets|length+1)|list }}"
gives
"msg": "set1.var1 = east"
"msg": "set2.var1 = north"