Ansible looping over nested variables - loops

I have a set of variables which define FQDNs.
domains:
- erp: erp.mycompany.com
- crm: crm.mycompany.com
- git: git.mycompany.com
Indeed, I both need to loop over them and access them namely (in a template file). So accessing them like domains.erpworks like a charm. But I can't get ansible to loop over these.
Obviously, if I do:
- name: Print domains
debug:
msg: test {{ item }}
with_items:
- "{{ domains }}"
It prints both the key and the value… And if I do:
- name: Print domains
debug:
msg: test {{ domains[{{ item }}] }}
with_items:
- "{{ domain }}"
But that doesn't work. I also tried the hashes form as mentionned in the docs, but didn't get any luck either…

Finally, I had to use a dict.
It didn't work the first time because unlike with_items, which has the items going each on their own line, with_dict is a one liner without - before the element to loop through.
domains:
erp:
address: erp.mycompany.com
crm:
address: crm.mycompany.com
git:
address: git.mycompany.com
# used by letsencrypt
webserverType: apache2
withCerts: true
tasks:
- name: Print phone records
debug:
msg: "{{ item.value.address }}"
with_dict: "{{ domains }}"
# I can still access a given domain by its name when needed like so:
{{ domains.erp.address }}

Looks like you figured out your issue. Your original attempt uses a list of dictionaries that do not contain the same keys, making it difficult to access the values uniformly across each list item.
Your second solution creates a dictionary where the keys refer to other dictionaries.
Another solution than what you posted if you still wanted to use a list:
- hosts: localhost
vars:
domains:
- name: erp
address: erp.mycompany.com
- name: crm
address: crm.mycompany.com
- name: git
address: git.mycompany.com
tasks:
- name: Print phone records
debug:
msg: "{{ item.address }}"
with_items: "{{ domains }}"
To me this approach is simpler but your second approach works as well.

Related

Ansible loop with array

Could someone let me know how we can create a code as below?
- name: TEST1
set_fact:
list_a: "{{ list_a + [item.json.SearchResult.resources] }}"
with_items:
- "{{ source_list.results[0] }}"
- "{{ source_list.results[1] }}"
- "{{ source_list.results[x] }}"
... (unknown how many items in result from API)
vars:
list_a: []
source_list.results[x] comes from an API result. The reason why I need to create an array is that the number of API result is maximum 100. But there are over 500 items.
Note: since we have no idea what you initial data structure looks like exactly, the below might not be 100% fitting your use case. For your next questions, please read How to ask and pay attention to the Minimal, complete and reproducible example section. Thanks
You are taking this the wrong way. Simply extract the attribute you need from each result using the map(attribute=x) Jinja2 filter.
For the below I inferred (see above note) that:
you called your API with ansible.builtin.uri in a loop to get batches of 100 results which are returned as a list in the SearchResult.ressources field
you want in the end a flattened list where all resources are at top level
- name: Show my list of single attributes
ansible.builtin.debug:
var: "source_list.results
| map(attribute='json.SearchResult.resources') | flatten"
You actually don't need to set_fact:
For a single use, just use the above expression directly in the relevant parameter (e.g. loop or a module param....) or eventually declare this in a var at task level.
If you want to reuse this in different parts of your playbook, just declare a var at play level and expand it anywhere once you have called your API and populated the source_list var. In that case, just add a default value to prevent an error if API was not yet called.
Example for the second case above in this pseudo playbook
---
- hosts: localhost
gather_facts: false
vars:
list_a: "{{ source_list.results | d([])
| map(attribute='json.SearchResult.resources') | flatten }}"
tasks:
- name: "This will return an empty list (i.e. [])
as we did not populate source_list yet"
ansible.builtin.debug:
var: list_a
- name: Call our API and register source_list
ansible.builtin.uri:
uri: https://my.api.com/api/v1/some/endpoint
# [... more parameters here ... ]
loop: "{{ my_list_of_ressources }}"
register: source_list
- name: "This will now return a populated list
after calling the API and registering source_list"
ansible.builtin.debug:
var: list_a
Now, to still give a direct answer to your initial question: you can construct that list iteratively inside a set_fact task. This is definitely not efficient as it involves a task running inside a loop (both unneeded as demonstrated above) and possibly on multiple hosts in your play. But for learning purpose, here it is:
- name: very inefficient way to get the same result as above
set_fact:
list_a: "{{ list_a | d([]) + item.SearchResult.resources }}"
loop: "{{ source_list.results }}"
After reading #Zeitounator's answer, I realized I was taking this the wrong way. I used the idea in the answer (json query and flatten) and I changed my task to the below which works as expected.
- name: Create ID list from API result data
set_fact:
list_a: "{{ source_list | json_query('results[*].json.SearchResult.resources[*].id') | flatten }}"
- name: API get again with id
uri:
url: "{{ request_url }}/{{ item }}"
...
register: result_data
with_items:
- "{{ list_a }}"

Ansible: loop with using collection and role

I´m doing the first steps in Ansible this week and I break on include_tasks for looping ofer a role.
The needed task is to create Letsencrypt certificates for a bunch of domains, thanks to T-Systems-MMS, there is already a collection to do this via APIs of letsencrypt and AutoDNS (see https://github.com/T-Systems-MMS/ansible-collection-acme/blob/master/docs/dns-challenge/autodns.md).
Filling this playbook with my settings, it is working fine for one domain. My try to loop over is (hopefully there was no mistake while anonymising the code):
playbook_getsslcert_main.yml:
---
- hosts: localhost
connection: local
vars:
ansible_python_interpreter: auto
tasks:
- name: Get SSL certificate
include_tasks: playbook_getsslcert_task.yml
loop:
- sub1.domain1.com
- sub2.domain1.com
playbook_getsslcert_task.yml:
---
- name: Doing letsencrypt ACME with AutoDNS
collections:
- t_systems_mms.acme
roles:
- acme
vars:
nbb_emailadress: my.email#example.com
nbb_autodnsuser: login.user#other.com
acme_domain:
certificate_name: "{{ item }}"
zone: "domain1.com"
email_address: "{{ nbb_emailadress }}"
subject_alt_name:
- "{{ item }}"
acme_challenge_provider: autodns
acme_use_live_directory: true
acme_conf_dir: /etc/letsencrypt
acme_account_email: "{{ nbb_emailadress }}"
acme_dns_user: "{{ nbb_autodnsuser }}"
acme_dns_password: "supersecret"
The error I get is
fatal: [localhost]: FAILED! => {"reason": "conflicting action statements: hosts, roles\n\nThe error appears to be in 'playbook_getsslcert_task.yml': line 2, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n---\n- name: Doing letsencrypt ACME with AutoDNS\n ^ here\n"}
My collegues and me are experienced Linux guys, we tested a lot; also we checked the YAML with formatcheckers and so on, did different styles for looping, tried an example tasks.ym just for writing a message, checked file formats (for linefeeds, correct HEX values,...) and so on.
But Ansible doesnt like the playbook.
Thanks for all your suggestions.
Edit:
Ubuntu 18.04 LTS, Python 3.6.9, Ansible 2.9.27
Thanks to #Zeitounator (sorry for overlooing your first link), a suitable and working solution have been found:
---
- hosts: all
connection: local
vars:
ansible_python_interpreter: auto
tasks:
- name: "Doing letsencrypt ACME with AutoDNS for {{ nbb_domain }}"
collections:
- t_systems_mms.acme
include_role:
name: acme
vars:
nbb_emailadress: my.email#example.com
nbb_autodnsuser: login.user#other.com
acme_domain:
certificate_name: "{{ nbb_domain }}"
zone: "domain1.com"
email_address: "{{ nbb_emailadress }}"
subject_alt_name:
- "{{ nbb_domain }}"
acme_challenge_provider: autodns
acme_use_live_directory: true
acme_conf_dir: /etc/letsencrypt
acme_account_email: "{{ nbb_emailadress }}"
acme_dns_user: "{{ nbb_autodnsuser }}"
acme_dns_password: "supersecret"
loop:
- sub1.domain1.com
- sub2.domain1.com
loop_control:
loop_var: nbb_domain

Ansible find matched patterns directory, remove all but keep the last 3 versions

I have surf the stackoverflow site around but couldn't find anything similar to what I want to achieve and hope that someone can point me out would be much appreciated.
I have a directory which stored all the artifacts Release Candidate and Dev versions when ever there is a bamboo build kick in. So far, I can figure out how to find the directory patterns and verify the results to remove. But could not filter the results to exclude the last 3 latest versions which I want to keep.
Here is the structures and the code
Structures
---
- name: Ansible find match directory, keep the last 3 version and remove all other
hosts: localhost
connection: local
vars:
base_dir: "/opt/repo/"
artifacts:
- "subject-mapper"
- "artemis-margin-api"
tasks:
- name: Find Release Candidate Directory Packages
become: yes
find:
paths: "{{ base_dir }}/{{ item }}"
patterns:
- "{{ item }}-[0-9]*.[0-9]*.[0-9]*$"
use_regex: yes
recurse: no
file_type: directory
loop: "{{ artifacts }}"
register: output
- debug:
msg: "{{ output }}"
- name: Filter out the Release Candidate results and keep the last 3 versions
set_fact:
files_to_delete: "{{ (files_to_delete|default([])) + (item['files'] | sort(attribute='mtime'))[:-3] }}"
loop: "{{ output['results'] }}"
- debug:
msg: "{{ files_to_delete }}"
- name: Delete the filtered results but keep the last 3 version
file:
path: "{{ item.path }}"
state: absent
loop: "{{ files_to_delete }}"
when: confirm|default(false)|bool
register: output_delete
- debug:
msg: "{{ output_delete }}"
here
Simplest way would be to pop first three matches from each result:
- set_fact:
files_to_delete: "{{ (files_to_delete|default([])) + (item['files'] | sort(attribute='mtime'))[3:] }}"
loop: "{{ output['results'] }}"

Ansible nested variables with with_sequence loop

i'm trying to create multiple VMs by setting the number of machines as a variable then iterate over that number using with_sequence. The deal is that I want to manually assign static IP addresses to my VMs so I have to iterate with nested variables.
My main file:
- name: Create Azure VM
hosts: localhost
connection: local
vars_files:
- vault.yml
pre_tasks:
- set_fact:
cluster: "testvm"
- set_fact:
subnetName: "default"
- set_fact:
instancesCount: 2
- set_fact:
IP1: "172.16.32.83"
- set_fact:
IP2: "172.16.32.84"
- set_fact:
vmSize: "Standard_DS1_v2"
- set_fact:
osDiskType: "Standard_LRS"
- set_fact:
dataDiskType: "Premium_LRS"
- set_fact:
diskSize: "4"
roles:
- azure
The snippet for the azure role used above where we have issues:
- name: Create virtual network interface cards
azure_rm_networkinterface:
resource_group: "{{ envir }}-emp-{{ cluster }}"
name: "{{ envir }}-emp-{{ cluster }}-nic-{{ item }}"
virtual_network: "/subscriptions/{{ subscriptionId }}/resourceGroups/{{ vnetResourceGroup }}/providers/Microsoft.Network/virtualNetworks/{{ virtualNetworkName }}"
subnet: "{{ subnetName }}"
public_ip: no
create_with_security_group: False
ip_configurations:
- name: ipconfig1
primary: yes
private_ip_address: "{{vars[IP].instancesCount}}" --->> here lies the question
private_ip_allocation_method: Static
primary: True
with_sequence: "count={{ instancesCount }}"
How would we call the values of the IP1 and IP2 inside the loop under the with_sequence block?
You may want to try with something like this:
"{{ lookup('vars', 'IP{}'.format(instancesCount)) }}"

Ansible loop using ansible_hostnames

I'm trying to update a configuration file for a NiFi deployment, the inital deployment configuration needs to include the nodes to allow HTTPS connections to be established between them.
I have an ansible tasks that makes the required structural changes to the configuration files, but I can't seem to get the right details inserted.
- name: Add each host to the authorizers.xml
lineinfile:
path: /opt/nifi/conf/authorizers.xml
line: "<property name=\"Node Identity {{ item }}\">CN={{ item }}, OU=NiFi</property>"
insertafter: <!--accessPolicyProvider Node Identities-->
loop: "{{ query('inventory_hostnames', 'nifi') }}"
This puts the ip addresses for the hosts, and I need to get the ansible_hostname for each node instead.
I've played around with ansible_play_batch and loop: "{{ groups['nifi'] }}" but I'm getting the result, outputting the ip addresses instead of the short hostnames each time.
The short hostnames are not stored in my ansible configuration anywhere, they are (if I understand correctly) determined at run time via the gathering facts process. I'd really like to not have to put the node names into a list variable.
Q: "Get the ansible_hostname for each node"
A: Given the inventory
shell> cat hosts
[nifi]
10.1.0.51
10.1.0.52
The playbook below
- hosts: nifi
tasks:
- debug:
var: ansible_hostname
gives (abridged)
ok: [10.1.0.51] =>
ansible_hostname: test_01
ok: [10.1.0.52] =>
ansible_hostname: test_02
It's possible to iterate the hosts in the group and get ansible_hostname from the hostvars. For example, delegate_to localhost and run_once
- debug:
msg: "{{ hostvars[item].ansible_hostname }}"
loop: "{{ groups.nifi }}"
delegate_to: localhost
run_once: true
gives
ok: [10.1.0.51 -> localhost] => (item=10.1.0.51) =>
msg: test_01
ok: [10.1.0.51 -> localhost] => (item=10.1.0.52) =>
msg: test_02

Resources