Unable to create conf file using Jinja - Expected Variables for Nginx - nginx

I am learning to use Jinja today to template a config file that I manually created and tested for Nginx. So far the template works as expected and it pulls the dynamic variables as expected. However, I have one line in my conf that needs to be on two separate lines and for some reason, it puts the two host on the same line.
This seems really simple, but I can't seem to spot what is causing it. I've gotten this far so!
My nginx.yml file
---
test_url: abc.{{ nginx_domain }}
nginx_ssl_port: 443
nginx_proxy_conf_dir: /etc/some/place
nginx_ssl_key_dir: /etc/somekey/place
nginx_ssl_cert_dir: /etc/somecert/place
nginx_proxy_log_dir: /etc/some/proxy/place
##Env depends on ansible inventory
test_nginx_proxy_sites:
- name: test
params:
- 'listen {{ nginx_ssl_port }} ssl'
- 'server_name {{test_url}}'
- 'include {{ nginx_proxy_conf_dir }}/conf.d/ssl.conf'
- 'ssl_certificate {{ nginx_ssl_cert_dir }}/{{ nginx_domain }}.crt'
- 'ssl_certificate_key {{ nginx_ssl_key_dir }}/{{ nginx_domain }}.key'
- 'access_log {{ nginx_proxy_log_dir }}/management_access.log'
- 'error_log {{ nginx_proxy_log_dir }}/management_error.log'
locations:
- path: /
location_params:
- 'proxy_pass http://stream_{{ Env }}/'
- 'include {{ nginx_proxy_conf_dir }}/conf.d/proxy.conf'
upstreams:
- name: stream_{{ Env }}
params:
- '{% for host in groups.tag_Class_host %}
server {{ hostvars[host].ipv4_local }}:{{ management_port }};
{% endfor %}
'
My sites.conf.j2
{{ remotely_managed }}
server {
{% if item.blocks is defined and item.blocks|length > 0 %}
{% for block in item.blocks %}
{{ block }}
{% endfor %}
{% endif %}
{% for param in item.params %}
{{ param }};
{% endfor %}
{% if item.locations is defined and item.locations|length > 0 %}
{% for location in item.locations %}
location {% if location.match is defined %}{{ location.match }} {% endif %}{{ location.path }} {
{% if location.root is defined %}
root {{ location.root }};
{% endif %}
{% if location.location_params is defined and location.location_params|length > 0 %}
{% for param in location.location_params %}
{{ param }};
{% endfor %}
{% endif %}
}
{% endfor %}
{% endif %}
{% if item.errors is defined and item.errors|length > 0 %}
{% for error in item.errors %}
{{ error.directive }};
location {{ error.location }} {
{% for param in error.error_params %}
{{ param }};
{% endfor %}
}
{% endfor %}
{% endif %}
}
{% if item.upstreams is defined %}
{% for u in item.upstreams %}
upstream {{ u.name }} {
{% if u.params is defined %}
{% for param in u.params %}
{{ param }}
{% endfor %}
{% endif %}
}
{% endfor %}
{% endif %}
My output
server {
server_name abc.mytest.com;
include /etc/nginx/conf.d/ssl.conf;
ssl_certificate /etc/somecert/place/certs/abc.mytest.com.crt;
ssl_certificate_key /etc/somekey/place/private/abc.mytest.com.key;
access_log /var/log/nginx/management_access.log;
error_log /var/log/nginx/management_error.log;
location / {
proxy_pass http://stream_qa/;
include /etc/nginx/conf.d/proxy.conf;
}
}
upstream stream_qa {
server 1.1.1.09:11111; server 1.1.1.10:11111;
}
The upstream should print out like below:
upstream stream_qa {
server 1.1.1.09:11111;
server 1.1.1.10:11111;
}

Okay - so.
To understand this issue, it's important to know how jinja templating works.
For the solution: Simply put a new line in, like this
{% if item.upstreams is defined %}
{% for u in item.upstreams %}
upstream {{ u.name }} {
{% if u.params is defined %}
{% for param in u.params %}
{{ param }}
{% endfor %}
{% endif %}
The reason for this is due to how jinja templating works.
When rendering jinja, it does not know to put things on a new line, it just knows to put a copy of whatever is in your loop. So if your loop is missing provision for a new line, it will not put things on a new line.
so when you have a loop like yours, or more simply an array of [a, b, c, d, e, f]
{% for i in items %}
{{ i }}
{% endfor %}
it will print as abcdef because the {{i}} literrally means render i here.
By placing a new line in your loop.
{% for i in items %}
{{ i }}
{% endfor %}
it will render i at the end of the last item in the loop, ie on a new line.
In really simple terms, what you want to do is have i also include provision for a new line in your loop so that when jinja renders whats in the loop, its also rendering a new line.
If you look at it like this, the first loop I mentioned renders like this
abcde but the second loop renders like this: a\nb\nc\nd\ne, As you can see, the value for each item in this loop has provision for a new line.
ps: This was really difficult to explain :(

I was able to resolve this issue by editing my nginx.yml file:
upstreams:
- name: stream_{{ Env }}
params:
-
{% for host in groups.tag_Class_host %}
server {{ hostvars[host].ipv4_local }}:{{ management_port }};
{% endfor %}
Rather than looking at Jinja, I should have reviewed YAML. A new pipe in YAML will make my string a multi-string line.
The output matches the output above.

Related

Concatenate strings in Jinja?

I'm trying to concatenate strings in a state, and I'm not having much luck. I've seen the posts that suggest using (|join), but all my strings are not in a single dictionary. Here's my code:
sshd_content:
file.line:
{% set admin_groups = '' %}
{% for app in grains['application_groups'] %}
{% for group in pillar['admin_users'][app]['members'] %}
{% set admin_groups = admin_groups ~ ' ' ~ group ~ '#mydomain.com' %}
{% endfor %}
{% endfor %}
- name: /etc/ssh/sshd_config
- match: AllowGroups wheel fred
- mode: replace
- content: AllowGroups wheel fred bob {{ admin_groups }}
I've tried using + instead of ~ without luck, too.
What am I doing wrong?
This state works fine:
sudoers_asmgroups_content:
file.append:
- name: /etc/sudoers.d/mygroups
- text:
{% for app in grains['application_groups'] %}
{% for group in pillar['admin_users'][app]['members'] %}
- '%{{ group }}#mydomain.com ALL=(ALL) ALL'
{% endfor %}
{% endfor %}
I found a viable solution by modifying the solution here.
It appears to be a scoping issue with the admin_groups variable. Not sure why append works, but I'm not going to argue.
For the example in the OP above, here is the code:
sshd_content:
file.line:
{% set admin_groups = [] %}
{% for app in grains['application_groups'] %}
{% for group in pillar['admin_users'][app]['members'] %}
{% do admin_groups.append(group) %}
{% endfor %}
{% endfor %}
- name: /etc/ssh/sshd_config
- match: AllowGroups wheel myadmin
- mode: replace
- content: AllowGroups wheel fred bob {{ admin_groups|join('#mydomain.com ') }}#mydomain.com
{% endif %}
Need to add the second #domain.com since the items are AD group names, and join only adds the separator when there is another value.

Can't get global variables in twig to work

So I have Twig running on top of Symfony 2.7. In the output html I'd like to have a few modules of text and in the last module I want some summary from all the previous ones (some data from all the previous modules' Entities) and I figured I'd append these summaries to a global variable while generating modules themselves to avoid a second loop. The code I'm using:
{% extends 'base.html.twig' %}
{% set list = '' %} {# HERE I SET A GLOBAL VAR #}
{% block body %}
<section>
<h2>{% trans %}MODULES{% endtrans %}</h2>
{% for m in plan.Modules %}
{{- block('module') -}}
{{ list }} {# HERE JUST FOR TESTING - IT'S EMPTY #}
{% endfor %}
</section>
{{ list }} {# HERE I WANT IT DISPLAYED, YET IT'S EMPTY :( #}
{% endblock %}
{% block module %}
<h3>{{ m.Module.title }}</h3>
{# HERE SOME MODULE TEXT I GET FROM COMPLICATED RELATIONS #}
{% if m.Module.list %}
{% set temp %}
{{ m.Module.shortTitle }}<br/>
{% endset %}
{% set list = list~temp %}
{% for l in m.Module.list %}
{% set temp %}
{{ l }}
{% endset %}
{% set list = list~temp %}
{% endif %}
{% endfor %}
{% endif %}
{{ list }} {# HERE IT'S WORKING #}
{% endblock %}
Any ideas?

Twig - passing a parameter to a function with a dynamic variable name

I have Twig variables:
{{ totalAmountMonth0 }} ... {{ totalAmountMonth10 }}
I have a loop and I want to call this variable for example:
{{ totalAmountMonth5 }}
I want to give this variable to a function like this:
totalAmount.percentFromTotalAmount((totalAmountMonth5))
But this doesn't work:
{% for i in 0..10 %}
{{ totalAmount.percentFromTotalAmount(totalAmountMonth~i) }}
{% endfor %}
This doesn't work either:
{% for i in 0..10 %}
{{ totalAmount.percentFromTotalAmount('totalAmountMonth'~i) }}
{% endfor %}
Untested, but try this:
{% for i in 0..10 %}
{{ totalAmount.percentFromTotalAmount(attribute(_context, 'totalAmountMonth'~i)) }}
{% endfor %}
Just provide to your twig an array, which I suggest, or build it (see below example)
// use line below only if array isn't provided to twig
{% set totalAmounts = { totalAmount1, totalAmount2, ..., totalAmount10 } %} // pseudo-code; you need to declare all variables here
{% for ta in totalAmounts %}
{{ totalAmount.percentFromTotalAmount(ta) }}
{% endfor %}
Try to set that variable before and use it.
{% for i in 0..10 %}
{% set temp = 'totalAmountMonth'~i %}
{{ totalAmount.percentFromTotalAmount(temp) }}
{% endfor %}

Custom form field template with twig

I'd like to create a custom template in twig to render a form field.
Example:
{{ form_row(form.field) }}
This can be overriden by form theming
{% block form_row %}
... custom code
{% endblock form_row %}
What I would like to do is this:
{% block custom_row %}
... custom code
{% endblock custom_row %}
and use it like this:
{{ custom_row(form.field }}
however, this throws an exception that method custom_row is not found.
My understanding is that this can be done with Twig extension, but I don't know how to register a block to be a function.
Update
what I actually want:
I use twitter bootstrap and a bundle which overrides all the form themes. And it renders a div around a radio, so it can't be inlined. So I wanted to do something like this:
copy their template and get rid of the div:
{% block inline_radio_row %}
{% spaceless %}
{% set col_size = col_size|default(bootstrap_get_col_size()) %}
{% if attr.label_col is defined and attr.label_col is not empty %}
{% set label_col = attr.label_col %}
{% endif %}
{% if attr.widget_col is defined and attr.widget_col is not empty %}
{% set widget_col = attr.widget_col %}
{% endif %}
{% if attr.col_size is defined and attr.col_size is not empty %}
{% set col_size = attr.col_size %}
{% endif %}
{% if label is not sameas(false) %}
{% if not compound %}
{% set label_attr = label_attr|merge({'for': id}) %}
{% endif %}
{% if required %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %}
{% endif %}
{% if label is empty %}
{% set label = name|humanize %}
{% endif %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' radio-inline')|trim}) %}
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>
{{ block('radio_widget') }}
{{ label|trans({}, translation_domain) }}
</label>
{% else %}
{{ block('radio_widget') }}
{% endif %}
{{ form_errors(form) }}
{% endspaceless %}
{% endblock inline_radio_row %}
and then
{{ inline_radio_row(form.field) }}
I ended up just overriding the whole theme, and added ifs around the div in question, a the class (radio-inline). But I'm still wondering if there's a way to make this work. Seems like it makes you work so hard for something so simple.
Update 2
I found the functionality:
class FormExtension extends \Twig_Extension
{
public function getFunctions()
{
return array(
'inline_radio_row' => new \Twig_Function_Node(
'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode',
array('is_safe' => array('html'))
),
);
}
}
This does exactly what I want, but it says it's deprecated. Anyone knows an updated version of how to use this?
Update 3
Similar functionality can be also achieved with http://twig.sensiolabs.org/doc/tags/include.html
You can use the twig functions for each part of a form row:
form_label(form.field)
form_widget(form.field)
form_errors(form.field)
For example:
<div class="form_row">
{{ form_label(form.field) }} {# the name of the field #}
{{ form_errors(form.field) }} {# the field #}
{{ form_widget(form.field) }} {# the errors associated to the field #}
</div>
You can use form theming.
Step by step:
1. Form Type Class
Check the name in your class
public function getName() {
return 'hrQuestionResponse';
}
2. Include a custom theme in your template
{% form_theme form 'InterlatedCamsBundle:Form:fields.html.twig' %}
3. Find the block
Can be quite difficult. For the bootstrap bundle, as you seem to have found it is in ./vendor/braincrafted/bootstrap-bundle/Braincrafted/Bundle/BootstrapBundle/Resources/views/Form/bootstrap.html.twig and you have found the block radio_row. I have been finding the block by putting output in the source template and overriding more blocks than I need. In 2.7 there is a theme 'rendering call graph'.
4. Override the block
Copy the block from the master template and call it replace the standard term with the name of in the FormType found in step 1.
Don't forget to also change the endblock name.
e.g.
{% block hrQuestionResponse_widget %}
hrQuestionResponse_row
{% spaceless %}
{% set class = '' %}
...
{% endspaceless %}
{% endblock hrQuestionResponse_widget %}
In your case because you can only call form_widget() you will need to override _widget. You could extract only the content that you need, or you can override the block chain to radio_row.

Defining custom twig form block for errors rendering

I'm trying to define a specific new block for form field errors rendering, keeping form_errors unchanged for common errors rendering.
# Twig Configuration
twig:
debug: %kernel.debug%
strict_variables: %kernel.debug%
form:
resources:
- 'ApplicationMyBundle:Main:form/customFormTheme.html.twig'
In customFormTheme.html.twig I overwrite a few blocks copied from form_div_layout.html.twig plus I added the folloowing new one.
{% block field_errors %}{% spaceless %}
{% if errors|length > 0 %}
<ul class="errors">
{% for error in errors %}
{% if error.messageTemplate|length %}
<li class="error">{{ error.messageTemplate|trans(error.messageParameters, 'validators') }}</li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
{% endspaceless %}{% endblock %}
Then I expect to be able to use this block in my views like this :
<div>
{{ form_label(form.message, 'message.label'|trans({},'contact')|raw ) }}
{{ form_widget(form.message, {attr: {maxlength:1000, size:1000, rows:8}}) }}
{{ field_errors(form.message) }}
</div>
but I receive the following error :
The function "field_errors" does not exist. Did you mean "form_errors"
I also tried by naming my block text_errors or textarea_errors mentioned here but I haven't been luckier.
Any idea ?
Actually it works by defining the block text_errors or textarea_errors only and still use {{ form_errors(field.name) }} in your template. If a block named after the type of your field exists (according to form field types) it will be used instead of form_errors.
!! But you can't use directly {{ text_errors(field.name) }} in your twig template !!
The same way you can have a custom row for a specific type like this
{% block textarea_row %}{% spaceless %}
<div class="textarea l-field {{ (form_errors(form)?'error':'') }}">
{{ form_label(form) }}
{{ form_widget(form) }}
{{ form_errors(form) }}
</div>
{% endspaceless %}{% endblock textarea_row %}
and use it in your template as follow :
{# message has textarea field type #}
{{ form_row(form.message, {
label: 'message.label'|trans({},'contact')|raw ,
attr: {maxlength:1000, size:1000, rows:8}})
}}
You can also pass many custom parameters by using the object attr{}
{% block form_row %}
{% spaceless %}
<div class="form-field {{ (form_errors(form)?'error':'') }}">
{{ form_label(form) }}
{{ form_widget(form) }}
{{ dump(attr) }}
{% if attr.help is defined and not attr.help == '' %}<p class="form-help">{{ attr.help }}</p>{% endif %}
{{ form_errors(form) }}
</div>
{% endspaceless %}
{% endblock form_row %}
and use it like this
{{ form_row(form.message, {
label: 'message.label'|trans({},'contact')|raw ,
attr: {
maxlength:1000, size:1000, rows:8,
help: 'password.help'|trans({})|raw
}
})
}}

Resources