Include files in Jinja, applying template then filtering - templates

Summary
I have a Jinja2 template which I'm running with Ansible.
I would like my template to load another file, as a template (i.e. evaluating {{ var }}), then I'll filter that, and then paste the result in to the top level template.
I think I'm almost there, I just need to find a Jinja2 filter which takes in a string and parses it as a template.
MWE
In this example lets assume the filter I want to apply is just to make the file uppercase.
(Obviously this case is so simple I could do it in one template file. But my real use case is more complex.)
Top level template main.yaml.j2:
---
something:
blah:
x: {{ y }}
{%- set names = [ 'John', 'Amy' ] %}
z: >
{{ lookup('file', './other-file.j2') | upper | indent(4*2) }}
other-file.j2:
{%- for name in names %}
Hello {{ name }}
{%- endfor %}
Running it with this Ansible playbook:
---
- hosts: localhost
connection: local
tasks:
- name: generate template
template:
src: "main.yaml.j2"
dest: "output.yaml.j2"
trim_blocks: False
register: templating
vars:
y: 5
Desired output
---
something:
blah:
x: 5
z: >
HELLO JOHN
HELLO AMY
Actual Output
---
something:
blah:
x: 5
z: >
{%- FOR NAME IN NAMES %}
HELLO {{ NAME }}
{%- ENDFOR %}
Best Guess
I think I'm almost there.
I just need a filter which applies a Jinja2 template to text.
i.e. something like:
{{ lookup('file', './other-file.j2') | template | upper | indent(4*2) }}
(But template is not a real filter. Maybe there's another name?)
What else I've tried
{{ include './other-file.j2' | upper | indent(4*2) }}
doesn't work.
fatal: [127.0.0.1]: FAILED! => {"changed": false, "msg": "AnsibleError: template error while templating string: expected token 'end of print statement', got 'string'. String: ---\nsomething:\n blah:\n x: {{ y }}\n {%- set names = [ 'John', 'Amy' ] %}\n z: >\n {{ include './other-file.j2' | upper | indent(4*2) }}"}
{% include './other-file.j2' | upper | indent(4*2) %}
"TemplateNotFound: ./OTHER-FILE.J2"
doesn't work.
Use Case
For context, my use case is that I have a Jinja2 template generating AWS CloudFormation templates.
I'm trying to do it all in YAML, not JSON.
(Because YAML can have comments, and you don't have to worry about whether the last item in a list has a trailing comma, and it's generally easier to read and write and debug.)
Some CloudFormation resources need literal JSON pasted into the YAML file. (e.g. CloudWatch Dashboard bodies).
So I want to have another file in YAML, which Jinja2 converts to json, and pastes into my overall YAML template.
I want this dashboard to be generated with a for loop, and to pass in variables.
I would like to have a separate

Instead of file plugin
lookup('file', './other-file.j2')
use template plugin
lookup('template', './other-file.j2')
Note that the scope of the variable {% set names = ['John', 'Amy'] %} is the template main.yaml.j2. If this variable is used in the template other-file.j2 the command lookup('template', './other-file.j2') will crash with the error:
"AnsibleUndefinedVariable: 'names' is undefined"
Solution
Declare the variable in the scope of the playbook. For example
- template:
src: "main.j2"
dest: "output.txt"
vars:
names: ['John', 'Amy']
main.j2
{{ lookup('template', './other-file.j2') }}
other-file.j2
{% for name in names %}
Hello {{ name }}
{% endfor %}
give
shell> cat output.txt
Hello John
Hello Amy

Related

Unable to set true/false as an environment variable's value for Cloud Function

I am writing a Deployment Manager script which creates a Cloud Function and sets some environment variables.
Everything works well apart from the fact that one of my properties/variables is not recognized by the Deployment Manager correctly. I keep on getting an error.
I have a property is-local that I supply from CMD line.
Its value needs to be false/true or I can also live with yes/no.
In the schema file if I specify the property as boolean and supply the value as false/true then the deployment starts and only the Cloud Function component fails with an error. I have specified the error as Error#1 below.
if I specify the property as string and supply the value as false/true then the deployment starts but fails immediately with an error. I have specified the error as Error#2 below.
main.jinja
{% set PROJECT_NAME = env['project'] %}
{% set CODE_BUCKET = properties['code-bucket'] %}
{% set IS_LOCAL = properties['is-local'] %}
resources:
- name: create-cf
type: create_cloud_function.jinja
properties:
name: test-cf
project: {{ PROJECT_NAME }}
region: europe-west1
bucket: {{ CODE_BUCKET }}
runtime: nodejs10
entryPoint: test
topic: test
environmentVariables: { 'CODE_BUCKET': {{ CODE_BUCKET }}, 'IS_LOCAL': {{IS_LOCAL}} }
main.jinja.schema
imports:
- path: create_cloud_function.jinja
required:
- code-bucket
- is-local
properties:
code-bucket:
type: string
description: Name of the code bucket to host the code for Cloud Function.
is-local:
type: boolean
description: Will Cloud Function run locally or in cloud.
create_cloud_function.jinja
{% set codeFolder = properties['name'] %}
{% set environmentVariables = properties['environmentVariables'] %}
resources:
#- type: cloudfunctions.v1.function
- type: gcp-types/cloudfunctions-v1:projects.locations.functions
name: {{ properties['name'] }}
properties:
parent: projects/{{ properties['project'] }}/locations/{{ properties['region'] }}
location: {{ properties['region'] }}
function: {{ properties['name'] }}
sourceArchiveUrl: gs://$(ref.{{ properties['bucket'] }}.name)/{{ codeFolder }}.zip
entryPoint: {{ properties['entryPoint'] }}
runtime: {{properties['runtime']}}
eventTrigger:
resource: $(ref.{{ properties['topic'] }}.name)
eventType: providers/cloud.pubsub/eventTypes/topic.publish
environmentVariables:
{% for key, value in environmentVariables.items() %}
{{ key }} : {{ value }}
{% endfor %}
Deployment Manager CMD
gcloud deployment-manager deployments create setup --template main.jinja --properties code-bucket:something-random-test-code-bucket,is-local:false
Error#1: - when the property type is boolean in schema file
{"ResourceType":"gcp-types/cloudfunctions-v1:projects.locations.functions","ResourceErrorCode":"400","ResourceErrorMessage":{"code":400,"message":"Invalid value at 'function.environment_variables[1].value' (TYPE_STRING), false","status":"INVALID_ARGUMENT","details":[{"#type":"type.googleapis.com/google.rpc.BadRequest","fieldViolations":[{"field":"function.environment_variables[1].value","description":"Invalid value at 'function.environment_variables[1].value' (TYPE_STRING), false"}]}],"statusMessage":"Bad Request","requestPath":"https://cloudfunctions.googleapis.com/v1/projects/someproject/locations/europe-west1/functions","httpMethod":"POST"}}
Error#2: - when the property type is string in schema file
errors:
- code: MANIFEST_EXPANSION_USER_ERROR
location: /deployments/setup/manifests/manifest-1571821997285
message: |-
Manifest expansion encountered the following errors: Invalid properties for 'main.jinja':
True is not of type 'string' at ['is-local']
Resource: main-jinja Resource: config
Any idea whats the issue here...
I'm unfamiliar with jinja but from my understanding, environment variables cannot be anything else but strings.
Said this, reading Error#1 I'd conclude that, effectively, the var type has to be string.
Then, at the second error we can clearly see that you are trying to put a boolean into a string.
So yeah, you have to play with true / false as strings.
You can define set the value as a string within the jinja file itself. See this post for some details and this page that provides different methods you can use.
In your case, you can edit the create_cloud_function.jinja file and change:
environmentVariables:
{% for key, value in environmentVariables.items() %}
{{ key }} : {{ value }}
to:
environmentVariables:
{% for key, value in environmentVariables.items() %}
{{ key }} : {{ value|string }}
Once the manifest is fully expanded, the value should be considered a string for the purpose of the API call to the Cloud Functions API
Eventually what I had 2 do was pass IS_LOCAL: '''false'''from the command line and {{ key }} : {{ value }} in my jinja file.
According to this documentation about Using environment variables in Jinja, you should use the following syntax to add an environment var to your templates:
{{ env["deployment"] }} # Jinja
And they show the following example:
- type: compute.v1.instance
name: vm-{{ env["deployment"] }}
properties:
machineType: zones/us-central1-a/machineTypes/f1-micro
serviceAccounts:
- email: {{ env['project_number'] }}-compute#developer.gserviceaccount.com
scopes:
- ...
Given that you are providing the value of is-local from CMD line, and according to this documentation:
Boolean values are case insensitive, so TRUE, true, and True are treated the same.
AND
To specify multiple properties, provide comma-separated key:value pairs. It does not matter in what order you specify the pairs. For example:
`gcloud deployment-manager deployments create my-igm
--template vm_template.jinja
--properties zone:us-central1-a,machineType:n1-standard-1,image:debian-9`
You should use TRUE, true, or True for is-local param.

Jinja2 template does value exist in array? [duplicate]

I have an Ansible playbook that sets a lot of variables. One the playbooks has this task:
- name: create config file
template:
src: 'templates/main_config.j2'
dest: "{{ tmp_dir }}/main_config.json"
The template main_config.j2 writes strings that are defined as variables in the parent Ansible playbooks and tasks.
I want to include another Jinja2 template based on a value of an Ansible variable.
{% include "./templates/configurations.j2" %},
{% include "./templates/security.j2" %},
{% include './templates/' + {{ job }} + '_steps.j2' %}
job is a Ansible variable set in a parent playbook.
This is not working. What could be the problem?
You don't need to open a Jinja2 expression ({{ ... }}) to refer to a variable inside a statement ({% ... %}). You can use the variable name directly:
{% include './templates/' + job + '_steps.j2' %}

How to compared a nested pillar key value in an if statement in jinja2 for saltstack

I am working on a saltstack state with some salt wrapped in jinja2.
When I attempt to compare a value from a pillar using jinja2 it appears argument evaluates to nothing.
If I query the value using salt cli, it returns the expected value.
I expect I am referencing the value incorrectly in the if statement with jinja2.
Here is all the needed info to understand and look at this problem:
Salt Master id is salt-dev
Salt Minion id is on same instance and is salt-dev
Here is the pillar top file:
base:
'salt-dev':
- docker-daemon.docker-daemon
Here is the nested pillar file locate at /srv/pillar/docker-daemon/docker-daemon.sls
docker-daemon:
- action: start
- runlevel: enabled
Here is the output of the salt cli command returning the content of the pillar for the minion salt-dev:
# salt 'salt-dev' pillar.items
salt-dev:
----------
docker-daemon:
|_
----------
action:
start
|_
----------
runlevel:
enabled
Here is the output of the value I am using in the if statement where the value returns nothing with jinja2, but returns as expected here with cli:
# salt 'salt-dev' pillar.get docker-daemon:action
salt-dev:
start
The line of jinja2 that is incorrect is:
{% if salt['pillar.get']('docker-daemon:action') == 'start' %}
It appears: salt['pillar.get']('docker-daemon:action') returns nothing, but from cli as shown above it does return something.
Also if I add a default value, which is used in the event this arg returned nothing it also works.
An example of adding a default value is:
{% if salt['pillar.get']('docker-daemon:action', 'def_value') == 'start' %}
I have shown it in context below:
Here is the state file where the if statements are having the same issue:
{% if ( (grains['osfinger'] == 'Oracle Linux Server-6') and (grains['osarch'] == 'x86_64')) %}
sync_docker-init:
file.managed:
- name: /etc/init.d/docker
- source: salt://docker-daemon/templates/docker-init
- user: root
- group: root
- mode: 755
action_docker-init:
{% if salt['pillar.get']('docker-daemon:action') == 'start' %}
service.running:
{% endif %}
{% if salt['pillar.get']('docker-daemon:action') == 'stop' %}
service.dead:
{% endif %}
- name: docker
- require:
- pkg: install_docker-engine
- watch:
- file: sync_docker-init
{% if salt['pillar.get']('docker-daemon:runlevel') == 'enabled' %}
-- enable: True
{% endif %}
{% if salt['pillar.get']('docker-daemon:runlevel') == 'disabled' %}
-- enable: False
{% endif %}
{% else %}
event.send:
- tag: 'salt/custom/docker-init/failure'
- data: "Management of docker init failed, OS not permitted."
{% endif %}
I am quite new at the moment to salt and jinja2, so this is 101 stuff, but I would appreciate some help, I have found nothing for some hours yet.
I attempted to echo this out and it seemed I just get a blank line
I found the solution.
The pillar file /srv/pillar/docker-daemon/docker-daemon.sls was formed as a list instead of a map.
I changed it to this:
docker-daemon:
action: restart
runlevel: disabled

Access template passed variables in Django

I pass to the template:
testruns, which is "get_list_or_404(TestRun)"
and
dict, which is smth like this:
for testrun in testruns:
dict[testrun.id] = {
'passed' : bla bla,
'failed' : bla bla 2
}
Practically a map between testrun.id and some other info from a set from TestRun Model
In the template I want to do this:
{% for testrun in testruns %}
console.log("{{ dict.testrun.id }}");
{% endfor %}
But doesn't output anything
console.log("{{ testrun.id }}"); will output a specific id ("37" for example)
console.log("{{ dict.37 }}"); will output the corresponding value from the dict
So, why doesn't this output anything?
console.log("{{ dict.testrun.id }}");
How should I get the data from 'passed' and 'failed' from the dict:
Also, this:
console.log("{{ dict[testrun.id] }}");
Will output this error:
TemplateSyntaxError at /path/dashboard
Could not parse the remainder: '[testrun.id]' from 'dict[testrun.id]'
The dot will be considered as a trigger for attribute lookup by template engine, so dict.testrun.id will be parsed as "try to find id attribute from testrun attribute from dict". Instead, if you want to show the whole dict content, you might just iterate through dictionary:
{% for key, value in dict.items %}
Testcase: {{ key }}
Passed: {{ value.passed }}
Failed: {{ value.failed }}
{% endfor %}
Or, if you are looking for dict lookup by variable's value, you will have to make custom template tag, like it was described here - Django template how to look up a dictionary value with a variable

Saltstack load pillar in a for loop

I am developing a automatic proftd installation whit Salt, i wont to get the ftp users from a template but I cant get work the pillar, i initialized the pillar whit the users data and call it into a for loop, but you don't get the pillar user data in the loop.
When i make salt-call pillar.get ftpusers in the minion, the response is:
local:
This is my pillar ftpusers.sls:
ftp-server.ftpusers:
user:
- user: user
- passhash: j2k3hk134123l1234ljh!"ยท$ser
- uuid: 1001
- guid: 1001
- home: /srv/ftp/user
- shel: /bin/false
And this is the for loop:
{% for users in pillar.get('ftpusers', {}).items() %}
/srv/herma-ftp/.ftpusers:
file.managed:
- user: root
- group: root
- mode: 444
- contents:'{{ user }}:{{ args['passhash'] }}:{{args['uuid'] }}:{{ args['guid'] }}::{{ args['home'] }}:{{ args['shel'] }}'
- require:
- file: /srv/herma-ftp
/srv/herma-ftp/{{user}}:
file.directory:
- user: nobody
- group: nobody
- dir_mode: 775
- makedirs: True
- require:
- file: /srv/herma-ftp
- watch:
- file: /srv/herma-ftp
module.run:
- name: file.set_selinux_context
- path: {{ args['home']}}
- type: public_content_t
- unless:
- stat -c %C {{ args['home'] }} |grep -q public_content_t
{% endfor %}
When I make in the minion
salt-call -l debug state.sls herma-ftp-server saltenv=My-enviroment test=True
Don't expect this for because don't can get the pillar data.
Your loop should also look like:
{% for user, args in pillar.get('ftpusers', {}).items() %}
Also, contents argument for a file.managed doesn't support templating. What you need to do is move /srv/herma-ftp/.ftpusers state outside of the loop, and make the loop inside the file template. The final layout of your state should look like:
/srv/herma-ftp/.ftpusers
file.managed:
source: salt://ftpserver/dot.ftpusers
template: jinja
...
...
{% for user, args in pillar.get('ftpusers', {}).items() %}
/srv/herma-ftp/{{user}}:
file.managed:
...
{% endfor %}
And your ftpserver/dot.ftpusers would look like:
{% for user, args in pillar.get('ftpusers', {}).items() %}
{{ user }}:{{ args['passhash'] }}:{{args['uuid'] }}:{{ args['guid'] }}::{{ args['home'] }}:{{ args['shel'] }}
{% endfor %}