Looping through a list when building group_vars in ansible - loops

I'm rather new to ansible and would like to deploy prometheus-grok-exporter (via ansible-grok-exporter role) with a specific configuration for all my nodes that run the cacti application.
My inventory is like this:
cacti_first ansible_host=192.168.50.50
cacti_second ansible_host=192.168.50.51
[group__cacti]
cacti_first
cacti_second
Inside group_vars/group__cacti I want to add something like this:
---
prometheus_grok_services_template:
- name: cacti_metrics
config_version: 3
input:
type: file
paths:
{% for cacti_dir in cacti_path %}
- "{{cacti_dir}}/log/cacti.log"
{% endfor %}
readall: false
extaConfigContinuesFromHere: true
And I have host config like this:
host_vars/cacti_first:
---
cacti_path:
- /usr/share/cacti
prometheus_grok_services:
- prometheus_grok_services_template
host_vars/cacti_second:
---
cacti_path:
- /usr/share/cacti
- /usr/share/cacti2
prometheus_grok_services:
- prometheus_grok_services_template
Inside the playbook I do a loop for prometheus_grok_services and use the yaml data to provision the service.
Now - this works as long as I don't try to use a loop inside group_vars/group__cacti. ansible-inventory reports that:
$ ansible-inventory -i hosts --list cacti_second
ERROR! Syntax Error while loading YAML.
found character that cannot start any token
The error appears to be in '/home/bastion/ansible-playbooks/group_vars/group__cacti': line 8, column 10, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
paths:
{% for cacti_dir in cacti_path %}
^ here
So, I'd like to ask - is it allowed to do jinja loops to build yaml for group vars? Is it a syntax error on my end? How am I supposed to template it?
I'd like to avoid moving the block to host vars (which I know works), mostly because it's a large piece of code (about 2KB of yaml config) and it's not as elegant as using group vars.
Thanks!

Fix the group_vars. For example
shell> cat group_vars/group__cacti
---
prometheus_grok_services_template:
- name: cacti_metrics
config_version: 3
input:
type: file
paths: "{{ paths_str|from_yaml }}"
readall: false
extaConfigContinuesFromHere: true
paths_str: |
{% for cacti_dir in cacti_path %}
- {{ cacti_dir }}/log/cacti.log
{% endfor %}
Then, the playbook
- hosts: group__cacti
gather_facts: false
tasks:
- debug:
msg: "{{ lookup('vars', item) }}"
loop: "{{ prometheus_grok_services }}"
gives
ok: [cacti_first] => (item=prometheus_grok_services_template) =>
msg:
- config_version: 3
input:
extaConfigContinuesFromHere: true
paths:
- /usr/share/cacti/log/cacti.log
readall: false
type: file
name: cacti_metrics
ok: [cacti_second] => (item=prometheus_grok_services_template) =>
msg:
- config_version: 3
input:
extaConfigContinuesFromHere: true
paths:
- /usr/share/cacti/log/cacti.log
- /usr/share/cacti2/log/cacti.log
readall: false
type: file
name: cacti_metrics

You can't use this kind of for loop in a variables file or a playbook - it only works in template files. To acheive what you're after, you can use product filters, as described https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#products
In your example, you would have:
---
prometheus_grok_services_template:
- name: cacti_metrics
config_version: 3
input:
type: file
paths: "{{ cacti_path | product(['/log/cacti.log']) | map('join') | list }}"
readall: false
extaConfigContinuesFromHere: true

Related

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

Can I create dynamic lists within Ansible vars_files?

I have a variables file that includes important info about our databases; the server they are on, the db version, the DB_HOME directory, etc. In the variables file, I would like to dynamically create lists that capture the unique values of those properties, so they can be easily iterated through in a task.
I have equivalent functionality by creating the list on the fly in a task's loop option, but that means repeating that loop syntax (violates DRY principle) and I would like less sophisticated Ansible colleagues to be able to use a pre-defined list.
example of the variables file databases.yml:
databases:
- name: test_db1
server: ora_901
listener: LISTENER_XYZ
version: '11.2.0.4'
oracle_home: '/app/oracle/product/11.2.0.4/db_home'
- name: test_db2
server: ora_902
listener: LISTENER_ABC
version: '11.2.0.4'
oracle_home: '/app/oracle/product/11.2.0.4/db_home'
## This didn't work... was hoping I could build this list dynamically
listeners:
- name: "{{ item }}"
loop: "{{ databases | map(attribute = 'listener') | list | unique }}"
servers:
- name: "{{ item }}"
loop: "{{ databases | map(attribute = 'server') | list | unique }}"
I would then use this loop through either the 'listeners' or 'servers' lists directly with some tasks.
When I tried a task that referenced the listeners variable, it failed. Referencing databases works and all items are returned, so I know it's getting some data from the vars_file...
- vars_files:
- vars/databases.yml
tasks:
- debug:
msg: "{{ databases }}"
- debug:
msg: "{{ listeners }}"
TASK [debug] **************************************************************************
ok: [FQDN] => {
"msg": [
{
"listener": "LISTENER_XYZ",
"name": "test_db1",
"oracle_home": "/app/oracle/product/11.2.0.4/db_home",
"server": "ora_901",
"version": "11.2.0.4"
},
{
"listener": "LISTENER_ABC",
"name": "test_db2",
"oracle_home": "/app/oracle/product/11.2.0.4/dbhome_1",
"server": "ora_902",
"version": "11.2.0.4"
},
fatal: [FQDN]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'item' is undefined\n\nThe error appears to have been in '/home/xxx/test_vars.yml': line 21, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - debug:\n ^ here\n"}
I would really like to keep these dynamic definitions in the same place as the server definitions, and don't see why it wouldn't be possible, I'm just sure I'm using the wrong mechanism.
Check this out from Ansible doc// Just to give you an idea, about "loop_control".
You can nest two looping tasks using include_tasks. However, by default Ansible sets the loop variable item for each loop. This means the inner, nested loop will overwrite the value of item from the outer loop. You can specify the name of the variable for each loop using loop_var with loop_control:
# main.yml
- include_tasks: inner.yml
loop:
- 1
- 2
- 3
loop_control:
loop_var: outer_item
# inner.yml
- debug:
msg: "outer item={{ outer_item }} inner item={{ item }}"
loop:
- a
- b
- c

Arrays in Ansible

I have a json like below
{
"nodes":[
{
"node_values":[
"[test1]",
"10.33.11.189",
"10.33.11.185"
]
},
{
"node_values":[
"[test2]",
"10.33.11.189",
"10.33.11.185"
]
}
]
}
I am trying to read only the node values and put it in the text files. I am using below ansible code
hosts: localhost
vars:
tmpdata1: "{{ lookup('file','test.json')|from_json }}"
tasks:
- name: Add mappings to /etc/hosts
blockinfile:
path: /home/s57232/Ansible-Install/Install_Inventory.txt
content: item
marker: "# {mark} ANSIBLE MANAGED BLOCK {{ item.node_values[0] }}"
loop: "{{ tmpdata1 |json_query('nodes[*].node_values[*]') }}"
i am getting below error
**TASK [Add mappings to /etc/hosts] **********************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'list object' has no attribute 'node_values'\n\nThe error appears to have been in '/home/s57232/Ansible-Install/prepare_inventory.yml': line 14, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - name: Add mappings to /etc/hosts\n ^ here\n"}**
When i am trying to read with with items and file format with out the blockinfile, if the same IP is there in multiple places, it is not writing, because it is looking for unique values. I am not able to proceed further. Can anyone please help me?
when i am using
- name: Add mappings to /etc/hosts
blockinfile:
path: /home/s57232/Ansible-Install/Install_Inventory.txt
content: "{{ item.node_values }}"
marker: "# {mark} ANSIBLE MANAGED BLOCK {{ item.node_values[0] }}"
loop: "{{ tmpdata1 |json_query('nodes[*]') }}"
I am getting
# BEGIN ANSIBLE MANAGED BLOCK [test1]
['[test1]', '10.33.11.189', '10.33.11.185']
# END ANSIBLE MANAGED BLOCK [test1]
# BEGIN ANSIBLE MANAGED BLOCK [test2]
['[test2]', '10.33.11.189', '10.33.11.185']
# END ANSIBLE MANAGED BLOCK [test2]
my expectation is
# BEGIN ANSIBLE MANAGED BLOCK [test1]
[test1]
10.33.11.189
10.33.11.185
# END ANSIBLE MANAGED BLOCK [test1]
# BEGIN ANSIBLE MANAGED BLOCK [test2]
[test2]
10.33.11.189
10.33.11.185
# END ANSIBLE MANAGED BLOCK [test2]
Here you are:
- name: Add mappings to /etc/hosts
blockinfile:
path: /home/s57232/Ansible-Install/Install_Inventory.txt
content: "{{ item.node_values | join('\n') }}"
marker: "# {mark} ANSIBLE MANAGED BLOCK {{ item.node_values.0 }}"
loop: "{{ tmpdata1.nodes }}"
You don't need to use JMESPath unless you wanted to filter some values. Other than that, you have two lists: one to loop over, the other to join the elements with a newline character.

Ansible looping over nested variables

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.

ansible loop over list and dictionary at the same time

I am writing a playbook that ensure nodes appear in /etc/fstab.
I am using loops to prevent code duplication.
The logic is first to check if the line appears using grep (with perl regex because it is a multi line) and store the results in a register.
Then I want to add only the lines that are not in fstab file. To achieve that I need to loop over list (the register with the grep return codes) and a dictionary (that contains the fstab entries).
I am having errors with the parallel loop. I tried to follow these steps.
One or more undefined variables: 'str object' has no attribute 'item'
tasks/fstab.yaml:
---
- name: Make dirs
sudo: yes
file: path={{ item.value }} state=directory
with_dict:
"{{ fstab.paths }}"
- name: Check whether declared in fstab
sudo: no
command: grep -Pzq '{{ item.value }}' /etc/fstab
register: is_declared
with_dict:
"{{ fstab.regexs }}"
- name: Add the missing entries
sudo: yes
lineinfile: dest=/etc/fstab line="{{ item.1.item.value }}"
when: item.0.rc == 1
with_together:
- "{{ is_declared.results }}"
- "{{ fstab.entries }}"
vars/main.yml:
---
fstab:
paths:
a: "/mnt/a"
b: "/mnt/b"
regexs:
a: '\n# \(a\)\nfoo1'
b: '\n# \(b\)\nfoo2'
entries:
a: "\n# (a)\nfoo1"
b: "\n# (b)\nfoo2"
I am not using template on purpose (I want to add entries to existing files and not to over write them).
UPDATE: I see ansible has module "mount" which deals with fstab. However I am still looking for a solution to this issue because I might be needed it again later on.
I have a couple ideas as to why your original approach was failing, but let's scratch that for a moment. It looks like you're overcomplicating things- why not use a complex list var to tie it all together, and use the regexp arg to the lineinfile module instead of a separate regex task? (though your sample data should work fine even without the regexp param) Something like:
---
- name: Make dirs
sudo: yes
file: path={{ item.path }} state=directory
with_items: fstab
- name: Add the missing entries
sudo: yes
lineinfile: dest=/etc/fstab line={{ item.entry }} regexp={{ item.regex }}
with_items: fstab
fstab:
- path: /mnt/a
regex: '\n# \(a\)\nfoo1'
entry: "\n# (a)\nfoo1"
- path: /mnt/b
regex: '\n# \(b\)\nfoo2'
entry: '\n# (b)\nfoo2'

Resources