ansible how to use a variable in a for loop - yaml

I have an ansible task like this:
- name: coreos network configuration
{% for interface in argument['interfaces'] %}
{% if argument[interface]['role'] == 'ingest' %}
script: netconfiginput.sh -i {{interface}} #incorrect, how to get the value of the interface variable of the for loop?
{% endif %}
{% endfor %}
While running this ansible task, I pass a JSON string argument:
ansible-playbook --extra-vars 'argument={"interfaces":["eno1","eno2","ep8s0"],"eno2":{"role":"ingest"}}' network-config.yml
What I want to do is, loop through the JSON array called interfaces, which are a list of network interfaces, when the role of the interface is called ingest, I run a script and pass the network interface as an argument to the script, my implementation is incorrect, how can I do this?

You need to use with_items and replace variable name with item.
A rough example:
name: task name
script: netconfiginput.sh -i {{ item }}
with_items: interfaces_array
when: some_role == 'ingest'
To understand what kind of data you're sending, use the following:
name: debugging
debug:
var: argument
That should show you, amongst other things, whether or not Ansible is considering parts of your variable's structure valid arrays or not.

Jinja2 can be used in ansible templates, not in playbooks.
Ansible supports looping over hashes. You can try this:
---
- hosts: <test_servers> # replace with your hosts
vars:
interfaces:
eno1:
role: null
eno2:
role: ingest
ep8s0:
role: null
tasks:
- name: coreos network configuration
script: netconfiginput.sh -i {{ item.key }}
with_dict: "{{interfaces}}"
when: item.value.role == "ingest"

Related

ansible variables usage - ansible_hostname or {{ ansible_hostname }}

I am new to ansible. What is the correct to call ansible variables? Here are the 3 playbooks, playbook 1 uses "{{ ansible_hostname }}", however, playbook 2 and 3 uses "ansible_hostname" directly. What are the differences? Thanks!
Playbook 1:
tasks:
- name: Jinja2 template
template:
src: template.j2
dest: "/tmp/{{ ansible_hostname }}_template.out"
trim_blocks: true
mode: 0644
Playbook 2:
tasks:
- name: Ansible Jinja2 if
debug:
msg: >
--== Ansible Jinja2 if statement ==--
{# If the hostname is ubuntu-c, include a message -#}
{% if ansible_hostname == "ubuntu-c" -%}
This is ubuntu-c
{% endif %}
Playbook 3:
tasks:
- name: Exploring register
command: hostname -s
when:
- ansible_distribution == "CentOS"
- ansible_distribution_major_version | int >= 8
register: command_register
playbook 1 uses "{{ ansible_hostname }}", however, playbook 2 uses "ansible_hostname"
That's not entirely correct. Both playbooks use the variable name ansible_hostname inside a Jinja templating context.
In the first playbook, it's simple variable substitution, so we use the {{ ... }} markers.
In the second playbook, it's being used in a control expression, so we use the {% ... %} markers.
In the third playbook, you're looking at the clauses of a when expression. From the documentation:
The when clause is a raw Jinja2 expression without double curly braces...
You can read more about Jinja syntax here.

How can I loop over a list of dicts and their content lists

I have the following var
---
- hosts: all
vars:
new_service:
name: test
Unit:
- option: Description
value: "Testname"
Service:
- option: ExecStart
value: "/usr/bin/python3 -m http.server 8080"
- option: WorkingDirectory
value: /home/test/testservice/html
I want to be able to use the ini_file module to create a service template so that the above var is converted into the following ini file
[Unit]
Description=Testname
[Service]
ExecStart=/usr/bin/python3 -m http.server 8080
WorkingDirectory=/home/test/testservice/html
I cannot figure out how to loop over this. I was thinking to use the product() so as to loop over nested lists, maybe something like this?
- name: "Create new unit section of service file"
ini_file:
path: "~{{ USERNAME }}/.config/systemd/user/{{ new_service[name] }}"
section: "{{ item.0 }}"
option: "{{ item.1.option }}"
value: "{{ item.1.value }}"
loop: "{{ ['unit', 'service'] | product({{ new_service[item.0] }})"
But I do not believe item is defined in the loop definition itself
(The reason I'm going with ini_file rather than template is because I want the service file creation to be able to handle any number of fields on demand)
You can still use a template to have a variable number of sections and options. Using loop with ini_file here is not efficient IMO. The only real use case would be if you need to keep the original contents of the file only adding new ones. But performance will be dramatically lower than a single template, especially if your have a lot of elements.
The only difficulty I see is that you have a name attribute in your dict which is not a section title. But it can be easily ruled out.
template.j2
{% for section in new_service %}
{% if section != 'name' %}
[{{ section }}]
{% for option in new_service[section] %}
{{ option.option }}={{ option.value }}
{% endfor %}
{% endif %}
{% endfor %}
Back to original question
If you really want to go through the loop route, it is still possible but will require quite a bit of effort with your actual data structure (loop/set_fact/... to finally get a single loopable structure).
If possible, I would change it to the following:
new_service:
name: test
sections:
- section: Unit
options:
- option: Description
value: "Testname"
- section: Service
options:
- option: ExecStart
value: "/usr/bin/python3 -m http.server 8080"
- option: WorkingDirectory
value: /home/test/testservice/html
And you can then directly loop through this structure using the subelements lookup. Note that "name" (on top level) is not a var but a string identifier for your service name value and should be used as such (fixed in my below example):
- name: "Create new unit section of service file"
ini_file:
path: "~{{ USERNAME }}/.config/systemd/user/{{ new_service.name }}"
section: "{{ item.0.section }}"
option: "{{ item.1.option }}"
value: "{{ item.1.value }}"
loop: "{{ lookup('subelements', new_service.sections, 'options') }}"
You can easily adapt my first example template to this new data structure as well if needed.

Ansible: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}

Can someone help me in understanding and resolving it
- name: Copying file to sever2
fetch:
src: /tmp/
dest: /u0/test/
when:
"{{ inventory_hostname == 'testdb' }}"
In your case, you should use when condition without Jinja delimiters.
Example:
when: inventory_hostname == 'testdb'
Detailed explanation:
Jinja template delimiters are used when variable interpolation is required in context of text and templates. This tells Ansible to use the value of the variable instead of the variable name.
Consider everything as text unless told otherwise (with delimiters)
Example:
vars:
fav_tool: Ansible
tasks:
- debug:
msg: "I like fav_tool"
This will output:
"msg": "I like fav_tool"
This is not what I wanted, I wanted to display "I like Ansible". So then I have to "tell" ansible to use the value of fav_tool.
- debug:
msg: "I like {{ fav_tool }}"
Similarly we use Jinja template delimiters in templates. Where we want to separate the variable, and expressions from text.
Example template such as below:
if fav_tool == 'Ansible'
I like Ansible
endif
... will result in exactly the same text without evaluating:
if fav_tool == 'Ansible'
I like Ansible
endif
However, when we use Jinja delimiters:
{% if fav_tool == 'Ansible' %}
I like Ansible
{% endif %}
It will result in:
I like Ansible
When we use conditions such as when:, we don't need delimiters as the conditionals will automatically interpolate the variable to value.
Consider everything as variables and expressions unless told otherwise (with '')
Example:
The case is reversed here and whatever is not enclosed in single-quotes is automatically evaluated. Only 'Ansible' is considered as text here (not evaluated).
vars:
fav_tool: Ansible
tasks:
- debug:
msg: "Ansible rocks!"
when: fav_tool == 'Ansible'
It worked for me with
when:
(inventory_hostname in groups['testdb'])

Get Number from hostname in Ansible

In Puppet I can extract the number of the Hostname with this example:
$host_number = regsubst($hostname, '^\w+(\d\d)', '\1')
Is there something similar in Ansible?
e.g.:
fqdn: test01.whatever
hostname: test01
output -> newvariable: 01
I want to extract only the number out of the Hostname, so I can use it in my Playbook as a variable.
This will fetch the inventory_hostname and replace any character text with nothing, leaving the numeric text. Obviously, you can use your imagination to apply whatever regex needed.
Ansible playbook:
vars:
host_names:
- {{ inventory_hostname | regex_replace('[^0-9]','') }}
Jinja2 Template:
{% for host in groups['some_group_in_inventory'] %}
Some description of the host where I needed the Number {{ host | regex_replace('[^0-9]','') }}
{% endfor %}
Theres multiple ways of doing the same thing...
You can use ansible_facts, ansible_hostname, shell commands, etc...
---
- hosts: localhost
gather_facts: yes
tasks:
- debug: var=ansible_facts.hostname
- debug: var=ansible_hostname
As raVan96 said, you need to explain better what you need and what you expect...
If you need to extract only "test01" from hostname, then you can use ansible build in filter "regex_replace". Here is my example:
{{ ansible_hostname | regex_replace('^([a-zA-Z_]+)(\d+)$','\\2') | int }}
Then you get: 1
If you need "01", then delete part to pass to integer - "int"

How to assign an array to a variable in an Ansible-Playbook

In a playbook I got the following code:
---
- hosts: db
vars:
postgresql_ext_install_contrib: yes
postgresql_pg_hba_passwd_hosts: ['10.129.181.241/32']
...
I would like to replace the value of postgresql_pg_hba_passwd_hosts with all of my webservers private ips. I understand I can get the values like this in a template:
{% for host in groups['web'] %}
{{ hostvars[host]['ansible_eth1']['ipv4']['address'] }}
{% endfor %}
What is the simplest/easiest way to assign the result of this loop to a variable in a playbook? Or is there a better way to collect this information in the first place? Should I put this loop in a template?
Additional challenge: I'd have to add /32 to every entry.
You can assign a list to variable by set_fact and ansible filter plugin.
Put custom filter plugin to filter_plugins directory like this:
(ansible top directory)
site.yml
hosts
filter_plugins/
to_group_vars.py
to_group_vars.py convert hostvars into list that selected by group.
from ansible import errors, runner
import json
def to_group_vars(host_vars, groups, target = 'all'):
if type(host_vars) != runner.HostVars:
raise errors.AnsibleFilterError("|failed expects a HostVars")
if type(groups) != dict:
raise errors.AnsibleFilterError("|failed expects a Dictionary")
data = []
for host in groups[target]:
data.append(host_vars[host])
return data
class FilterModule (object):
def filters(self):
return {"to_group_vars": to_group_vars}
Use like this:
---
- hosts: all
tasks:
- set_fact:
web_ips: "{{hostvars|to_group_vars(groups, 'web')|map(attribute='ansible_eth0.ipv4.address')|list }}"
- debug:
msg: "web ip is {{item}}/32"
with_items: web_ips
in playbook:
vars:
- arrayname:
- name: itemname
value1: itemvalue1
value2: itemvalue2
- name: otheritem
value1: itemvalue3
value2: itemvalue4
in template: (example is of type ini file, with sections, keys and values):
{% for item in arrayname %}
[{{ item.name }}]
key1 = {{ item.value1 }}
key2 = {{ item.value2 }}
{% endfor %}
This should render the template as:
[itemname]
key1 = itemvalue1
key2 = itemvalue2
[otheritem]
key1 = itemvalue3
key2 = itemvalue4
Variables can be represented as standard YAML structures so you can assign a list value to a key like this:
---
- hosts: db
vars:
postgresql_ext_install_contrib: yes
postgresql_pg_hba_passwd_hosts:
- '10.129.181.241/32'
- '1.2.3.0/8'
You can use jinja2 filters:
{{ groups['nodes']|map('extract', hostvars, ['ansible_eth1','ipv4', 'address']) |list }}
will return a list of ip addresses. i.e.
---
- hosts: db
vars:
postgresql_ext_install_contrib: yes
postgresql_pg_hba_passwd_hosts: {{ groups['nodes']|map('extract', hostvars, ['ansible_eth1','ipv4', 'address']) |list }}
...
Does not include the challange (appending /32). But it should also be possible somehow with jinja2 filters.
Reqiures ansible version >= 2.1
To add '/32' to the address, you can use the Ansible ipaddr filter (converting to CIDR notation).
{{ ip_addresses|ipaddr('host') }}

Resources