how do I use an ansible string variable that contains jinja delimiters? - ansible

this
- name: ugly
hosts: localhost
vars:
badstr: "asdf{%jkl"
mydir: "."
mydict:
filea:
Value: "blue!42!"
fileb:
Value: "a{%isbad"
tasks:
- copy:
dest: "{{ item.key }}"
content: "{{ item.value.Value }}"
loop: "{{ mydict|default({})|dict2items }}"
gives me this error:
fatal: [localhost]: FAILED! => {"msg": "An unhandled exception occurred while templating 'asdf{%jkl'. Error was a <class 'ansible.errors.AnsibleError'>, original message: template error while templating string: Encountered unknown tag 'jkl'.. String: asdf{%jkl"}
The 'mydict' structure is returned from a plugin and I do not get to define the members. One of the 'Value's contains a "{%". Any reference to it will cause an error, whether as a variable, file content or in a template.
I have tried all kinds of quoting and combinations of unsafe, {{, %raw, etc. It either gives me the error or puts the name of the variable in the file.
How can I write the value to a file? Or just use it as a variable?
Ansible 2.8.4 on MacOS 11.3, also ansible 2.9 on RHEL 7.

You can use !unsafe for the variables expected to have these chars. Check this documentation. when !unsafe is used, the string/variable will never get templated.
- name: ugly
hosts: localhost
vars:
badstr: !unsafe "asdf{%jkl"
mydir: "."
mydict:
filea:
Value: !unsafe "blue!42!"
fileb:
Value: !unsafe "a{%isbad"
tasks:
- copy:
dest: "{{ item.key }}"
content: "{{ item.value.Value }}"
loop: "{{ mydict|default({})|dict2items }}"
When handling values returned by lookup plugins, Ansible uses a data
type called unsafe to block templating. Marking data as unsafe
prevents malicious users from abusing Jinja2 templates to execute
arbitrary code on target machines. The Ansible implementation ensures
that unsafe values are never templated. It is more comprehensive than
escaping Jinja2 with {% raw %} ... {% endraw %} tags.
You can use the same unsafe data type in variables you define, to
prevent templating errors and information disclosure. You can mark
values supplied by vars_prompts as unsafe. You can also use unsafe in
playbooks. The most common use cases include passwords that allow
special characters like { or %, and JSON arguments that look like
templates but should not be templated. For example:
---
mypassword: !unsafe 234%234{435lkj{{lkjsdf

The problem here is not in the copy task where the values are
evaluated; the problem is how they are being set. For example, if I
create a simple ansible module named example.sh that looks like
this:
#!/bin/sh
cat <<EOF
{
"files": {
"filea": {
"Value": "blue!42!"
},
"fileb": {
"Value": "a{%isbad"
}
}
}
EOF
I can write a playbook like this:
- name: ugly
hosts: localhost
tasks:
- example:
register: mydict
- copy:
dest: "{{ item.key }}"
content: "{{ item.value.Value }}"
loop: "{{ mydict.files|dict2items }}"
And this runs as expected, creating without any errors a file fileb with the content:
a{%isbad
Similarly, if I read the data from a JSON file and pass it through from_json, it also works fine:
- name: ugly
hosts: localhost
tasks:
- set_fact:
mydict: "{{ lookup('file', 'data.json')|from_json }}"
- copy:
dest: "{{ item.key }}"
content: "{{ item.value.Value }}"
loop: "{{ mydict.files|dict2items }}"
The problem only happens if you define the variables in a context in
which Ansible is looking for Jinja templating -- so, as the values of
variables in a playbook, a vars file, the arguments to set_fact,
etc.
You can potentially work around the problem by changing how you are
consuming these values.

Related

Ansible conditionally loop through with_items?

Is it possible to loop through a list of items if a string is defined in a variable i will specify.
Essentially i want to have a list of variables defined and utilized the aws_s3 module to download the files only if they are defined when running the playbook
e.g
say i have the list "var1,var2"
and I have the following variables defined:
apps_location:
- { name: 'vars1', src: 'vars1.tgz', dest: '/tmp/vars1_file.tgz' }
- { name: 'vars2', src: 'vars2.tgz', dest: '/tmp/vars2_file.tgz' }
- { name: 'vars3', src: 'vars3.tgz', dest: '/tmp/vars3_file.tgz' }
Task:
- name: "Splunk Search Head | Download Splunk Apps from S3"
aws_s3:
bucket: "{{ resource_bucket_name }}"
object: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: get
with_items: "{{ apps_location }}"
I want to run the command:
ansible-playbook -i inventory -e "var1,var2"
and download only var1 and var2 on that specific run.
I tried utilizing "lookups" but couldnt get the syntax right. Im not entirely sure if this best way of doing this, but i want to have a predefined list of file locations and only download the ones that i'm passing during runtime.
Note the only reason "name" exists in apps_location is to see if i can do a lookup and only install that one but i couldnt get the syntax right.
Define a variable containing a list of defined apps. I'm trying:
- name: "Set Fact"
set_fact:
dict: "{{ apps_location[item].dest }}"
with_items: "{{ my_vars|default([]) }}"
However whenever I output dict I only get the last value.
Any help would be appreciated :)
The extra-vars must be an assignment of a variable and value. For example
shell> ansible-playbook -i inventory -e "my_vars=['vars1','vars2']"
A more convenient structure of the data would be a dictionary for this purpose. For example
apps_location:
vars1:
src: 'vars1.tgz'
dest: '/tmp/vars1_file.tgz'
vars2:
src: 'vars2.tgz'
dest: '/tmp/vars2_file.tgz'
vars3:
src: 'vars3.tgz'
dest: '/tmp/vars3_file.tgz'
Then the loop might look like
- aws_s3:
bucket: "{{ resource_bucket_name }}"
object: "{{ apps_location[item].src }}"
dest: "{{ apps_location[item].dest }}"
mode: get
loop: "{{ my_vars|default([]) }}"
Q: "Define a variable containing a list of defined apps."
A: Try this
- set_fact:
my_list: "{{ my_list(default([]) +
[apps_location[item].dest] }}"
loop: "{{ my_vars|default([]) }}"
(not tested)

Ansible - passing dynamic variables into Jinja2 template

I'm having a problem accessing dynamically named Ansible variables in a Jinja2 template. I have a list of tenants like this:
tenants:
- liamtest1
- liamtest2
In my playbook I create terraform configuration files for each of these tenants like this:
- name: Generate a .tf file for each tenant in list
template:
src: templates/tenant.tf.j2
dest: "{{ enviro }}/terraform/{{ item }}.tf"
with_items: "{{ hostvars[inventory_hostname][enviro]['tenants'] }}"
Later in the playbook I use the terraform module to apply my configuration and register the outputs to a variable:
- name: Run terraform
terraform:
project_path: "{{ enviro }}/terraform/"
state: present
register: tf_result
I've prefixed my terraform outputs with the tenant name so that I don't get duplicates. This bit is all working fine and I can I can display these outputs with a debug task, for example tenant_domain:
- debug:
var: tf_result.outputs.{{ item + '_domain' }}.value
with_items: "{{ hostvars[inventory_hostname][enviro]['tenants'] }}"
Produces this output:
ok: [localhost] => (item=liamtest1) => {
"ansible_loop_var": "item",
"item": "liamtest1",
"tf_result.outputs.liamtest1_domain.value": "liamtest1.mydomain.com"
}
ok: [localhost] => (item=liamtest2) => {
"ansible_loop_var": "item",
"item": "liamtest2",
"tf_result.outputs.liamtest2_domain.value": "liamtest2.mydomain.com"
}
The bit I can't seem to do is generate another set of files (this time javascript files for mongodb) from another Jinja2 template.
I've tried this:
- name: Generate a .js file for each tenant in list
vars:
domain: tf_result.outputs.{{ item + '_domain' }}.value
template:
src: templates/tenant.js.j2
dest: "{{ enviro }}/mongodb/{{ item }}.js"
with_items: "{{ hostvars[inventory_hostname][enviro]['tenants'] }}"
If I reference that in my Jinja2 template using {{ domain }} it ends up with just a string e.g. tf_result.outputs.liamtest1_domain.value in the first file and tf_result.outputs.liamtest2_domain.value in the second file.
I also tried using lookup in the Jinja2 template like this:
{{ lookup('vars', domain) }}
Which gives me:
"AnsibleUndefinedVariable: No variable found with this name: tf_result.outputs.liamtest1_domain.value"
I've also tried some other variations such as:
{{ lookup(hostvars[inventory_hostname], domain) }}
I've tried a few other things as well, I'm not sure they're all worth mentioning as none of them worked but for example I tried setting the variable inside the Jinja template instead of at the task level like this for example:
{% set domain = lookup('vars', 'tf_result.outputs.' + item + '_domain' %}
You simply have a syntax problem in your yaml.
# Wrong
vars:
domain: tf_result.outputs.{{ item + '_domain' }}.value
This is declaring a var which value is a concatenation of (literally) "tf_result.outputs." followed by the value of the current item and "_domain.value". What you want is the actual value contained in that full variable. This is the correct syntax:
# Correct
vars:
domain: "{{ tf_result.outputs[item + '_domain'].value }}"

How to register with_items and act on conditional check result for each item

I'd like to register the contents of bashrc for two users and edit as/if required. My play is as follows.
- name: Check bashrc
shell: cat {{ item }}/.bashrc
register: bashrc
with_items:
- "{{ nodepool_home }}"
- "{{ zuul_home }}"
- name: Configure bashrc
shell:
cmd: |
cat >> {{ item }}/.bashrc <<EOF
STUFF
EOF
with_items:
- "{{ nodepool_home }}"
- "{{ zuul_home }}"
when: '"STUFF" not in bashrc.stdout'
It fails as follows:
fatal: [ca-o3lscizuul]: FAILED! => {"failed": true, "msg": "The conditional check '\"STUFF\" not in bashrc.stdout' failed. The error was: error while evaluating conditional (\"STUFF\" not in bashrc.stdout): Unable to look up a name or access an attribute in template string ({% if \"STUFF\" not in bashrc.stdout %} True {% else %} False {% endif %}).\nMake sure your variable name does not contain invalid characters like '-': argument of type 'StrictUndefined' is not iterable\n\nThe error appears to have been in '/root/openstack-ci/infrastructure-setup/staging/zuul/create-user.yml': line 35, 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 - name: Configure bashrc\n ^ here\n"}
I think, if I understand your requirement correctly, you can use the 'lineinfile' or 'blockinfile' modules and save yourself the hassle of testing for the existence of the content:
- name: Noddy example data
set_fact:
single_line: "STUFF"
multi_line: |
STUFF
STUFF
profile_dirs:
- "{{ nodepool_home }}"
- "{{ zuul_home }}"
- name: Ensure STUFF exists in file
lineinfile:
path: "{{ item }}/.bashrc"
line: "{{ single_line }}"
loop: "{{ profile_dirs }}"
- name: Ensure block of STUFF exists in file
blockinfile:
path: "{{ item }}/.bashrc"
block: "{{ multi_line }}"
loop: "{{ profile_dirs }}"
Both modules give a lot more control and you can find their docs here: lineinfile | blockinfile

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: Access facts set by set_fact

I need to be able to set variables using tasks in Ansible. I use set_fact for this, but cannot seem to access the fact I set with this. What is wrong with the code below:
- name: kludge1
set_fact: fake_y = "{{ [] }}"
- name: Loop
debug:
msg: "{{ item }}"
with_items: "{{ fake_y }}"
You have spaces before and after =...
- name: kludge1
set_fact: fake_y="{{ [] }}"
Avoid var= shortcut syntax. Use original YAML syntax instead, it gives less errors:
- name: kludge1
set_fact:
fake_y: "{{ [] }}"

Resources