Append a dict to a list of dicts in Ansible based on user configuration - ansible

I want to deploy an application via ansible that has a configurable number of application nodes running via a systemd service. I have a role that manages these services for me but expects the name of the services along with e.g. the priority.
In my playbook the user defines something like this
application_nodes:
- name: "node1"
api_port: 3900
- name: "node2"
api_port: 3910
Therefore I need to append the list that this the systemd role manages. I tried the following
service_list: "{% for node in application_nodes %}{'name': 'application_{{ node.name }}.service', 'priority': 1000}{% endfor %}"
devture_systemd_service_manager_services_list_auto: |
{{
([{{ service_list }}])
}}
This results in the following error
FAILED! => {"msg": "An unhandled exception occurred while templating '{{\n ([{{ service_list }}])\n}}\n'. Error was a <class 'ansible.errors.AnsibleError'>, original message: template error while templating string: expected token ':', got '}'. String: {{\n ([{{ service_list }}])\n}}\n. expected token ':', got '}'"}
I cannot figure out what I did wrong here.
I the end it should work like this (except automatically for every node)
devture_systemd_service_manager_services_list_auto: |
{{
([{'name': 'application_node1.service', 'priority': 1000}])
+
([{'name': 'application_node2.service', 'priority': 1000}])
}}
I also considered something like this
- name: Append services list by nodes
set_fact:
devture_systemd_service_manager_services_list_auto: "{{ devture_systemd_service_manager_services_list_auto + [{'name': 'application_{{ item.name }}.service', 'priority': 1000, 'groups': ['applicationname']}] }}"
loop: "{{ application_nodes }}"
- name: "Print services"
debug:
msg: "services {{ devture_systemd_service_manager_services_list_auto }}"
Where the error is similar to this issue but the solution does not work for me as I want to access a specific key of the dictionary
TASK [custom/base : Print services B] ********************************************************************************************************************************************************************************************************************
fatal: [mydomain]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: [{'name': 'application_node2.service', 'priority': 1000, 'groups': ['applicationname']}, {'name': 'application_{{ item.name }}.service', 'priority': 1000, 'groups': ['applicationname']}]: 'item' is undefined. 'item' is undefined. [{'name': 'application_node2.service', 'priority': 1000, 'groups': ['applicationname']}, {'name': 'application_{{ item.name }}.service', 'priority': 1000, 'groups': ['applicationname']}]: 'item' is undefined. 'item' is undefined\n\nThe error appears to be in '/.../roles/custom/base/tasks/add_services.yml': line 11, 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: \"Print services B\"\n ^ here\n"}

To start with, the var definition in the role your are using is overly complicated for something as simple as a list of dicts. This is a source of confusion. So to be clear the following expression:
devture_systemd_service_manager_services_list_auto: |
{{
([{'name': 'application_node1.service', 'priority': 1000}])
+
([{'name': 'application_node2.service', 'priority': 1000}])
}}
is actually the exact equivalent of:
devture_systemd_service_manager_services_list_auto:
- name: application_node1.service
priority: 1000
- name: application_node2.service
priority: 1000
The second source of confusion IMO is that you used the term append (which is in Ansible the fact to add more elements to a list) rather than combine (i.e. adding/updating keys in one dict from an other dict).
Those precisions being made, we can fulfill your requirement by:
Extracting the name attribute from each element in the original list
Transform that name so that it matches your expected service name
Append a dict containing that name and the expected priority in relevant keys to a result list
Note that having to transform the name makes it difficult to avoid looping inside a template for the var definition (which we generally tend to avoid in Ansible). But this can still be done in one single expression as demonstrated in the following test playbook:
---
- hosts: localhost
gather_facts: false
vars:
application_nodes:
- name: "node1"
api_port: 3900
- name: "node2"
api_port: 3910
devture_systemd_service_manager_services_list_auto: >-
{%- set result = [] -%}
{%- for node in application_nodes -%}
{%- set service_name = node.name | regex_replace('^(.*)$', 'application_\\1.service') -%}
{{ result.append({'name': service_name, 'priority': 1000}) }}
{%- endfor -%}
{{ result }}
tasks:
- name: Show our list
ansible.builtin.debug:
var: devture_systemd_service_manager_services_list_auto
which gives (relevant task output only)
TASK [Show our list] *********************************************************
ok: [localhost] => {
"devture_systemd_service_manager_services_list_auto": [
{
"name": "application_node1.service",
"priority": 1000
},
{
"name": "application_node2.service",
"priority": 1000
}
]
}

You almost had it in your first try. If you add a few things to your service_list it ends up being a list of dicts -- which is ultimately what you want to append to devture_systemd_service_manager_services_list_auto.
- hosts: all
vars:
application_nodes:
- name: "node1"
api_port: 3900
- name: "node2"
api_port: 3910
tasks:
- set_fact:
# The square brackets before and after the loop make this an actual
# list.
# To make sure that the list items are separated from each other, we
# have to add a comma after each item. Luckily Ansible / Python isn't
# too picky about a comma without a final item. ;-)
service_list: >
[
{% for node in application_nodes %}
{'name': 'application_{{ node.name }}.service', 'priority': 1000},
{% endfor %}"
]
- set_fact:
devture_systemd_service_manager_services_list_auto: >
{{ devture_systemd_service_manager_services_list_auto|default([]) +
service_list }}
- debug:
var: devture_systemd_service_manager_services_list_auto

Related

How to escape quotes in Jinja2 template in Ansible?

I try to filter the key value pairs, however the values may be quoted and these may vary between ' and ".
My data source looks like:
CONFIG='/etc/app/config'
LOG_FILE=/var/log/app.log
UPDATE="auto"
The only way I have found is to define the quotes in a separate variable and then embed them in the string via string concatenation.
- name: find all key-values, correctly with separate variable
debug:
msg: "{{ env_data | regex_findall('^(\\w+)=' ~ qc ~ '(.*?)' ~ qc ~ '$', multiline=True) }}"
vars:
qc: '[''"]?'
Correct result:
TASK [find all key-values, correctly with separate variable] *******************
ok: [localhost] => {
"msg": [
[ "CONFIG", "/etc/app/config" ],
[ "LOG_FILE", "/var/log/app.log" ],
[ "UPDATE", "auto" ]
]
}
Is there a way to embed both quotes directly into the string (with correct escaping) without having to define separate variable and act via string concatenation?
In general, the problem can probably be broken down to the output of both quotes from a Jinja2 string, embedded into Yaml (Ansible).
Attempt for all my values
# playbook.yml
---
- hosts: localhost
gather_facts: no
vars:
env_data: |
CONFIG='/etc/app/config'
LOG_FILE=/var/log/app.log
UPDATE="auto"
tasks:
# see below
I had hoped for success with this variant, unfortunately in vain:
- name: find all key-values, fails - single quotes not recognized
debug:
msg: "{{ env_data | regex_findall('^(\\w+)=[''\"]?(.*?)[''\"]?$', multiline=True) }}"
Result:
TASK [find all key-values, fails - single quotes not recognized] ***************
ok: [localhost] => {
"msg": [
[ "CONFIG", "'/etc/app/config'" ],
[ "LOG_FILE", "/var/log/app.log" ],
[ "UPDATE", "auto" ]
]
}
Failed attempts for output of both quotes
quotes: "{{ '\"''' }}"
Result: ", single quote is missing
quotes: '{{ ''"'''' }}'
Error: An unhandled exception occurred while templating
quotes: '{{ ''"\'''' }}'
Error: An unhandled exception occurred while templating
quotes: >-
{{ '"''' }}
Error: An unhandled exception occurred while templating
quotes: >-
{{ "\"'" }}
Error: An unhandled exception occurred while templating
Having gotten clarification that the OP is dealing with the stdout from a command module call. So...
Replace the command with shell, and change the format to YAML:
- name: Get results of process
shell: "my_command | sed 's/=/: /' > vars.yml"
Now read the file:
- name: Read file
vars_file: vars.yml
Done.
Parse the data first. For example,
regex_quotes: '[''"]'
env_data_lists: "{{ env_data.splitlines()|
map('regex_replace', regex_quotes, '')|
map('split', '=')|
list }}"
gives
env_data_lists:
- - CONFIG
- /etc/app/config
- - LOG_FILE
- /var/log/app.log
- - UPDATE
- auto
Create a dictionary if you want to easily reference the items
env_data_dict: "{{ dict(env_data_lists) }}"
gives
env_data_dict:
CONFIG: /etc/app/config
LOG_FILE: /var/log/app.log
UPDATE: auto
Then, you can format the lists as you like. The simplest option is using the filters to format data: YAML and JSON. For example, the filter to_nice_json
- debug:
var: env_data_lists|to_nice_json
gives
env_data_lists|to_nice_json: |-
[
[
"CONFIG",
"/etc/app/config"
],
[
"LOG_FILE",
"/var/log/app.log"
],
[
"UPDATE",
"auto"
]
]
You can format the data on your own if you want to. For example,
- copy:
dest: /tmp/test.yaml
content: |
---
[
{% for i in env_data_lists %}
[ "{{ i.0 }}", "{{ i.1 }}" ]{% if not loop.last %},{% endif %}
{% endfor %}
]
gives
shell> cat /tmp/test.yaml
---
[
[ "CONFIG", "/etc/app/config" ],
[ "LOG_FILE", "/var/log/app.log" ],
[ "UPDATE", "auto" ]
]
Example of a complete playbook for testing
- hosts: localhost
vars:
env_data: |
CONFIG='/etc/app/config'
LOG_FILE=/var/log/app.log
UPDATE="auto"
regex_quotes: '[''"]'
env_data_lists: "{{ env_data.splitlines()|
map('regex_replace', regex_quotes, '')|
map('split', '=')|
list }}"
env_data_dict: "{{ dict(env_data_lists) }}"
tasks:
- debug:
var: env_data_lists
- debug:
var: env_data_dict
- debug:
var: env_data_lists|to_nice_json
- copy:
dest: /tmp/test.yaml
content: |
---
[
{% for i in env_data_lists %}
[ "{{ i.0 }}", "{{ i.1 }}" ]{% if not loop.last %},{% endif %}
{% endfor %}
]

Ansible merge dictionaries using with_items and vars stores only last item

Trying to create a dictionary per item and merge them
---
- name: TestingLab
hosts: localhost
gather_facts: False
tasks:
- name: Hello Vars
set_fact:
two_nums:
- 1
- 2
- name: create empty dict
set_fact:
ids: {}
- name: Merge all
vars:
single_entry: "{ '{{ item }}': {{ item }} }"
set_fact:
ids: "{{ ids | combine(single_entry) }}"
with_items: "{{ two_nums }}"
- name: Print Result
debug:
msg: "{{ ids }}"
I thought I followed the right guidelines but I seem to be getting only the last item afterwards:
ok: [localhost] => {
"msg": {
"2": 2
} }
I tried replacing the single_entry with the expression in vars but it does not run.
Is there a different syntax to get this done?
EDIT: version info
ansible-playbook 2.5.1
python version = 2.7.17 [GCC 7.5.0]
Try the filters dict and zip. The zip is available since 2.3, e.g.
- set_fact:
d2: "{{ dict(two_nums|zip(two_nums)) }}"
- debug:
var: d2
- debug:
var: d2|type_debug
gives
d2:
1: 1
2: 2
d2|type_debug: dict
If this does not work try Jinja and the filter from_yaml, e.g.
- hosts: localhost
vars:
two_nums:
- 1
- 2
l1: |-
{% for i in two_nums %}
{{ i }}: {{ i }}
{% endfor %}
tasks:
- set_fact:
d1: "{{ l1|from_yaml }}"
- debug:
var: d1
- debug:
var: d1|type_debug
gives the same result
d1:
1: 1
2: 2
d1|type_debug: dict
If you need the keys to be strings quote it, e.g.
l1: |-
{% for i in two_nums %}
"{{ i }}": {{ i }}
{% endfor %}
gives
d1:
'1': 1
'2': 2
In the first case, map the list's items to string, e.g.
- set_fact:
d2: "{{ dict(two_nums|map('string')|zip(two_nums)) }}"
gives the same result
d2:
'1': 1
'2': 2
I can't reproduce the behavior you're describing. Running your
playbook verbatim, I get as output:
TASK [Print Result] **************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"1": 1,
"2": 2
}
}
I'm using Ansible core 2.11.2, but I've also tested your playbook with Ansible 2.9.20 and I get the same output.
I would probably drop the set_fact task, and also change how you're
setting single_entry:
- name: TestingLab
hosts: localhost
gather_facts: False
tasks:
- name: Hello Vars
set_fact:
two_nums:
- 1
- 2
- name: Merge all
vars:
ids: {}
single_entry: "{{ {item: item} }}"
set_fact:
ids: "{{ ids | combine(single_entry) }}"
with_items: "{{ two_nums }}"
- name: Print Result
debug:
msg: "{{ ids }}"
In this version, the template expression is returning a dictionary,
and only requires a single set of Jinja template markers. I'm curious
if this version behaves any differently for you.

Create ansible list variable with jinja2

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 }}"

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.

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}}'

Resources