Create ansible list variable with jinja2 - ansible

The following code is in the defaults/main.yml file for a role:
file_env: "{% if cf_env is equalto 'cf10_dev' %}\
dev\
{% elif cf_env is equalto 'cf10_stg' %}\
stg\
{% elif (cf_env is equalto 'cf10_prd') or (cf_env is equalto 'cf10_prd_ext') %}\
prd\
{% elif cf_env is equalto 'cf11' %}\
[dev, prd]
{% endif %}"
The first 3 conditional statements work fine, where the file_env var is set to a single value, but when trying to set the file_env var to a list (the last elif statement), it doesn't work:
failed: [server-new.xxx.com] (item=[dev, prd] ) => {"ansible_loop_var": "item", "changed": false, "item": "[dev, prd] ", "msg": "Destination /opt/coldfusion11/[dev, prd] 01/bin/jvm.config does not exist !", "rc": 257}
Here is the task that generates the above error:
- name: Update jvm.config for coldfusion11 server
lineinfile:
path: /opt/coldfusion11/{{ item }}01/bin/jvm.config
regexp: '^java.home'
line: 'java.home=/usr/lib/jvm/jre-openjdk'
loop:
- "{{ file_env }}"
notify:
- handler_create_script_list
- handler_restart_coldfusion10
when:
- cf_env == "cf11"
How can I set the file_env var to a list?

I believe your issue is that it's interpreting the variable as a string, literally '[dev, prd] ' (with a space at the end, judging by the error message).
I think you want to try a different format as suggested in https://serverfault.com/a/913936/512181

If your are going to loop over your var, you should always return a list (with single element when needed) to avoid possible tricky errors. You simply need to modify your loop expression to the following:
loop: "{{ file_env }}"
Your template expressions are not using white space control which will end up with new lines in the output when they should not appear.
Your variable definition is hardly readable (and makes me think you have a deeper project architecture issue but that's an other story). Taking into account point 1 above, I suggest you refactor to the following more readable, sustainable and working form using a key/value mapping in a dict and getting the corresponding value in your final var
files_by_env
cf10_dev:
- dev
cf10_stg:
- stg
cf10_prd:
- prd
cf10_prd_ext:
- prd
cf11:
- dev
- prd
file_env: "{{ files_by_env[cf_env] }}"
Alternatively, you can reverse the definition in the dict (i.e. list environments for every file) which is slightly less verbose but leads to a more complicated expression to get the result
env_by_file:
dev:
- cf10_dev
- cf11
stg:
- cf10_stg
prd:
- cf10_prd
- cf10_prd_ext
- cf11
file_env: "{{ env_by_file | dict2items | selectattr('value', 'contains', cf_env) | map(attribute='key') | list }}"

Related

Ansible command loop amend string value and push original value into new array

Hi I'm using ansible to create some elastic search indexes using laravel's artisan commands. The issue I have is that when I have to create an index I have to use the php class name which the base name is snake cased from camel case e.g. UserConfigurator = user_configurator. In my vars I have the following;
elastic_search:
version: 6.3.2
indexes:
- "App\\ElasticSearch\\Configurators\\UserConfigurator"
- "App\\ElasticSearch\\Configurators\\ClientConfigurator"
models:
- "App\\Models\\User"
- "App\\Models\\Client"
and in my playbook I have the following;
- name: check if indexes exist
uri: url='http://localhost:9200/{{ index_name }}'
method=HEAD
status_code=200,404
- name: create indexes
command: php artisan elastic:create-index "{{ item }}"
args:
chdir: "{{site_root}}"
with_items: "{{elastic_search.indexes}}"
The playbook isn't sufficient enough to do what I want to do due to lack of experience. Any ideas how I may loop over each elastic_search.indexes and convert the class basename to a snake case and check to see if the index exists or not and push them into two separate arrays so then I can use the one of the new variables to create the index and the other variable to update the index?
Any ideas how I may loop over each elastic_search.indexes and convert the class basename to a snake case
The algorithm I use for snake-casing something is to break the word at an upper-to-lower boundary, lowercase the entire thing, then reassemble the parts, being mindful that the first upper-to-lower transition is special and should not be separated by _.
We can leverage the behavior that when set_fact sees a block of text that looks like JSON, it will coerce that block of text into a dict (you'll see that behavior on the command line with ansible -e '{"hello": "world"}' too), along with the infinitely handy regex_replace jinja2 filter provided by ansible:
- hosts: all
gather_facts: no
vars:
class_names:
- 'App\ElasticSearch\Configurators\UserConfigurator'
- 'App\ElasticSearch\Configurators\ClientConfigurator'
tasks:
- set_fact:
index_names: >-
[
{% for cn in class_names -%}
{{ '' if loop.first else ',' }}
{{ cn |
regex_replace(".*\\([^\\]+$)", "\1") |
regex_replace("([A-Z])([a-z])", "\x00\1\2") |
lower | replace("\x00", "_") | regex_replace("^_", "") |
to_json }}
{% endfor %}
]
with_items: '{{ class_names }}'
- debug: var=index_names verbosity=0
- debug: var=item verbosity=0
with_items: '{{ index_names }}'
which produces the correct output:
TASK [debug] *******************************************************************
ok: [localhost] => (item=user_configurator) => {
"item": "user_configurator"
}
ok: [localhost] => (item=client_configurator) => {
"item": "client_configurator"
}
and now you can use those indices in a command of your choosing.

Jinja templates in ansible loop

I need to run an ansible loop based on input from a CSV file. I am using the following question / answer as reference. However, I cannot seem to figure out where to actually include the jinja part for the loop.
So far this is what I have, but it throws an error:
---
- hosts: localhost
connection: local
gather_facts: no
vars:
csv_var: "{{ lookup ('file', 'file.csv') }}"
tasks:
- debug:
msg: "{{ item }}"
with_items:
- {% set list = csv_var.split(",") %}
file.csv has the following content: 345,1234,1234
Ideally the message should print out the numbers above.
The syntax error I was getting is:
The offending line appears to be:
with_items:
- {% set list = csv_var.split(",") %}
^ here
exception type: <class 'yaml.scanner.ScannerError'>
exception: while scanning for the next token
found character that cannot start any token
in "<unicode string>", line 19, column 10
You should use Jinja2 expression not a statement.
You should also quote any string that starts with { in Ansible:
- debug:
msg: "{{ item }}"
with_items: "{{ csv_var.split(',') }}"
And there is no need to wrap the resulting list in another list (dash before element), although Ansible handles this automatically.

Ansible conditional template variable substitution

I am trying to create configuration files from a template with include variables based on the fourth character of {{ ansible_hostname }}.
What works:
playbook:
---
- hosts: spock
roles:
- templaterole
role:
---
- name: testing autofs template on spock
template:
src=autofs
dest=/tmp/autofs
with_items:
- "{{ var_a }}"
when: ('{{ ansible_hostname }}' == "spock")
vars/main.yml:
var_a:
-
var_1: 'this is var_a1'
var_2: 'this is var_a2'
var_b:
-
var_1: 'this is var_b1'
var_2: 'this is var_b2'
template:
{{ item.var_1 }}
#
{{ item.var_2 }}
#
This works as expected and the output produces a /tmp/autofs file on the spock host that looks like:
this is var_a1
#
this is var_a2
#
Now, if I try to write the file based on trying to pull out the 4th character of the {{ ansible_hostname }}, the play does not get a match on the conditional and does not write the file. I'm trying this conditional in my role:
---
- name: testing autofs template on spock
template:
src=autofs
dest=/tmp/autofs
with_items:
- "{{ var_a }}"
when: ('{{ ansible_hostname }} | cut -c4' == "c") or
('{{ ansible_hostname }} | cut -c4' == "k")
the play skips this task due to not matching on the conditional. Ultimately i want to be able to pull any 4th character of our hostnames as this will always be predictable (can only be one of 4 known characters which defines my environment and lets me define the correct template variables based on these diff production environments.)
Can anyone help me to redefine my when statement such that i can do or conditionals and pull characters out of defined ansible variables like ansible_hostname?
Don't use curly brackets inside when statement, it's already a Jinja2 statement.
And in Jinja2 statements you use | to apply filter, but there is no cut filter available.
Your statement should be as simple as:
when: ansible_hostname[3] in 'ck'

Ansible - iterate over a list of dictionaries

I built the following list but I don't succeed to iterate over it.
Should I use with_items? with_elements? or something else?
My goal is to iterate over all the hosts in the inventory, get their name and their IP, and finally print it.
- set_fact:
list_of_hosts: |
{% set myList = [] %}
{% for host in groups['all'] %}
{% set ignored = myList.extend([{'server_name': host, 'server_ip': hostvars[host].ansible_eth0.ipv4.address }]) %}
{% endfor %}
{{ myList }}
- debug: msg="{{ item.server_name }}"
with_items: "{{ list_of_hosts }}"
Here is my list when I debug it:
TASK [common : debug] ************************************************************************************************
ok: [my1stServer] => {
"msg": " [{'server_ip': u'192.168.0.1', 'server_name': u'my1stServer'}, {'server_ip': u'192.168.0.2', 'server_name': u'my2ndServer'}]\n"
}
And here is the error but it is not really relevant :
fatal: [my1stServer]: FAILED! => {"failed": true, "msg": "the field 'args' has an invalid value, which appears to include a variable that is undefined. The error was: 'ansible.vars.unsafe_proxy.AnsibleUnsafeText object' has no attribute 'server_name'\n\nThe error appears to have been in 'hosts.yml': line 19, 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- debug: msg=\"{{ item.server_name }}\"\n ^ here\nWe could be wrong, but this one looks like it might be an issue with\nmissing quotes. Always quote template expression brackets when they\nstart a value. For instance:\n\n with_items:\n - {{ foo }}\n\nShould be written as:\n\n with_items:\n - \"{{ foo }}\"\n"}
Please forgive me for bluntness, but the proposed implementation makes it an effort to understand what the idea actually is. which is simple: to print some variables anyway present in hostvars[host] for a list of hosts picked by various criteria.
If we keep implementation close to that idea, implementation is simpler.
So what I'd do to create a list of hosts picked by group membership, or possibly 'hand picked' is to actually do what I just wrote :).
Consider this task list:
# this task creates an empty list
- name: create my_cool_list
set_fact:
my_cool_list: []
# this task adds to the list all hosts in groups we're iterating over
- name: update my cool list with whole groups
set_fact: '{{my_cool_list + groups[curr_grp]}}'
with_items:
- grp1
- grp2
loop_control:
loop_var: curr_grp
# this task adds to the list all hosts we're iterating over
- name: update my cool list with specific hosts
set_fact: '{{my_cool_list + [curr_node]}}'
with_items:
- node001
- node101
loop_control:
loop_var: curr_node
# now we can iterate over the list, accessing specific fields on each host
- name: go over my cool list and print ansible_init_mgr
debug:
msg: 'hostvars["{{curr_host}}"].ansible_init_mgr: {{hostvars[curr_host].ansible_init_mgr}}'
with_items: '{{my_cool_list|default([], true)|list}}'
Furthermore, you can add safety when: by validating the key you're accessing is defined..
And, to print a selection of variables about each host, you should use jinja filter map('extract',...):
- name: print some vars from each host
debug:
msg: {'server_name': '{{hostvars[curr_node]|selectattr("ansible_hostname")}}', 'ip_address': '{{hostvars[curr_node]|selectattr("ansible_eth0.ipv4.address")}}'}
with_items: '{{my_cool_list|default([], true)|list}}'
loop_control:
loop_var: curr_node
IF you want to increase readability though, you better write a filter plugin, which would do the above stuff, and hide iteration ugliness in readable way, so you can have:
Either (For generic approach, i.e. without renaming attributes)
- name: print some vars from each host
debug:
msg: '{{my_cool_list|multi_select_in_dict(["ansible_hostname", "ansible_eth0.ipv4.address")}}'
Or specific approach (so that you are using specific hard coded remapping of attributes...)
- name: print some vars from each host
debug:
msg: '{{my_cool_list|my_cool_filter}}'

Ansible with_items if item is defined

Ansible 1.9.4.
The script should execute some task only on hosts where some variable is defined. It works fine normally, but it doesn't work with the with_items statement.
- debug: var=symlinks
when: symlinks is defined
- name: Create other symlinks
file: src={{ item.src }} dest={{ item.dest }} state=link
with_items: "{{ symlinks }}"
when: symlinks is defined
But I get:
TASK: [app/symlinks | debug var=symlinks] *********************
skipping: [another-host-yet]
TASK: [app/symlinks | Create other symlinks] ******************
fatal: [another-host-yet] => with_items expects a list or a set
Maybe I am doing something wrong?
with_items: "{{ symlinks | default([]) }}"
The reason for this behavior is conditions work differently inside loops. If a loop was defined the condition is evaluated for every item while iterating over the items. But the loop itself requires a valid list.
This is also mentioned in the docs:
Note that when combining when with with_items (see Loops), be aware that the when statement is processed separately for each item. This is by design:
tasks:
- command: echo {{ item }}
with_items: [ 0, 2, 4, 6, 8, 10 ]
when: item > 5
I think this is a bad design choice and for this functionality they better should have introduced something like with_when.
As you have already figured out yourself, you can default to an empty list.
with_items: "{{ symlinks | default([]) }}"
Finally if the list is dynamically loaded from a var, say x, use:
with_items: "{{ symlinks[x|default('')] | default([])}}"
This will default to an empty list when 'x' is undefined
Accordingly, fall back to an empty dict with default({}):
# service_facts skips, then dict2items fails?
with_dict: "{{ ansible_facts.services|default({})|dict2items|selectattr('key', 'match', '[^#]+#.+\\.service')|list|items2dict }}"

Resources