jinja做为一个模板语言,不仅使得saltstack中的sls文件能根据pillar和grains的值进行动态变化,同时因为引入了逻辑判断,使得原本十分呆板的普通配置文件也可以变得灵活起来。这一节我们通过实际操作来一起看看jinja如何玩耍。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
这里还是坚持前几节一直用的官方vagrant演示环境,我们在这个基础上来搭建一个简单的jinja测试环境,用来实际感受下jinja的语法。
首先准备一个pillar文件用来存放jinja中会使用的变量,创建/srv/pillar/jinja_test.sls
内容如下
name: james
age: 28
然后创建一个空文件/srv/salt/files/jinja_test.txt
用来进行jinja模板的编辑
根据默认情况下的file_roots配置,/srv/salt/在file.managed中可以简单表示为salt://
最后创建一个state文件/srv/salt/jinja_test.sls
来将上面的jinja模板同步到minion
/home/vagrant/jinja_test.txt:
file.managed:
- source: salt://files/jinja_test.txt
- template: jinja
这样一个简单的测试环境就搭建好了,来将上面定义的pillar变量用jinja模板表示看看。新开一个文本文件jinja_test.txt
,编辑如下
name: {{ pillar['name'] | capitalize() }}
age: {{ pillar['age'] }}
简单测试,语法不懂也没关系
然后在salt master上把pillar下发下去
root@saltmaster:/home/vagrant# salt * saltutil.refresh_pillar
minion1:
True
minion2:
True
之后对minion2去套用刚才的state文件
root@saltmaster:/srv/salt/files# salt 'minion2' state.apply jinja_test
成功了的话就可以在minion2上看到新建的/home/vagrant/jinja_test.txt
文件,内容如下
vagrant@minion2:~$ cat jinja_test.txt
name: James
age: 28
如果成功显示了变量的内容。并且James
开头是大写的J
,那就表示测试环境配置成功了,可以开始进行下面具体的学习和实际操作了。后面我们用jinja语法去编辑jinja_test.txt
文件然后下发到minion就可以看到实际效果了。
在jinja中,两个大括号中间用来放会被打印为具体结果的表达式(Expressions)。常规的表达式包含以下几个部分:
例如整型数,浮点型数,字符串,列表,元组,字典等等
在测试环境的/srv/pillar/jinja_test.sls
中增加下面两个变量
lucky number: [2,7,16]
pets: {'cat':'chouchou','dog':'haha'}
同时修改jinja模板jinja_test.txt
添加下列内容
loved cat: {{ pillar['pets']['cat'] }}
3rd lucky number: {{ pillar['lucky number'][2] }}
然后对minion2更新文件
root@saltmaster:/home/vagrant# salt 'minion2' state.apply jinja_test
之后会发现目标文件多了两行
loved cat: chouchou
3rd lucky number: 16
例如常规的加减乘除 + - * /,以及余数%和求幂**
在上述的模板文件中添加
{{ 1+4 }}
{{ 2**4 }}
{{ 6%5 }}
目标文件会多出三行
5
16
1
python中可以用加号 + 去链接多个字符串,在jinja里面虽然也可以,但是推荐用专门的波浪线 ~ 去链接字符串
就和python中一样,==/!=/>/>=/</<=/and/or/not。这里就不举例子了。需要注意的是这里只是纯粹的比较符号和逻辑符号,如果是和if语句一起使用的话就不能用两个大括号了,而要用到后面提到的百分号。
in符号 - 和python中一样,判断元素是否在集合中。例如下面的表达式会返回True
{{ 'apple' in ['apple','banana','cherry'] }}
~符号 - 连接多个字符串。例如
{{ 'life'~' is '~'wonderful' }}
会返回life is wonderful
|符号 - 管道符号和shell里面一样,用来对前面的输出做进一步处理,在jinja中叫filter。可以查看官方的内建filter函数。举几个例子,例如
{{ 'debug the world' | capitalize() }}
{{ 'debug the world' | center(40) }}
{{ what | default('this') }}
{{ pillar['pets'] | dictsort(reverse=True) }}
{{ [1,2,3] | join('-') }}
会返回
Debug the world
debug the world
this
[('dog', 'haha'), ('cat', 'chouchou')]
1-2-3
可以直接在jinja里面执行salt的命令行命令,中括号接命令,小括号接命令的参数,例如
{{ salt['pillar.get']('people','xiaofu') }}
相当于执行salt.states.pillar.get
,查找key为people的pillar值,如果没查到就返回默认值xiaofu
因为可以有默认返回值,可以比{{ pillar['people'] }}
能处理更复杂的情况,例如key不存在的时候,建议用命令的方式去获取pillar或者grains的值。
一个大括号和百分号一起,中间放条件选择和循环等等流程控制表达式。
在前面的/srv/pillar/jinja_test.sls
中添加一个list如下
names: ['kobe','lebron','t-mac','wade']
然后修改jinja_test.txt
添加下列内容,格式和python中的for很像,不过一定要有一个结尾来标志for循环的结束。for循环的中间用前面表示变量的方式去输出循环体的内容。
{%- for name in pillar['names'] %}
player: {{ name }}
{%- endfor %}
对minion2套用state文件,就可以看到目标文件多了下列内容
player: kobe
player: lebron
player: t-mac
player: wade
这里的百分号后面多了一个小短线,用来清除空格,可以查看后面的“格式优化”部分
针对上面pillar中定义的字典也是可以和python中一样进行循环
{%- for type, name in pillar['pets'].items() %}
type: {{ type }}, name: {{ name }}
{%- endfor %}
目标文件结果为
type: dog, name: haha
type: cat, name: chouchou
需要注意的是,字典是没有顺序的,可以用dictsort()
的filter去预先进行排序处理一下。
同时,在for循环中,还有一些内建的变量和函数可以用来实现一些额外功能,见下面的这个官网提供的表格
Variable | Description |
---|---|
loop.index | The current iteration of the loop. (1 indexed) |
loop.index0 | The current iteration of the loop. (0 indexed) |
loop.revindex | The number of iterations from the end of the loop (1 indexed) |
loop.revindex0 | The number of iterations from the end of the loop (0 indexed) |
loop.first | True if first iteration. |
loop.last | True if last iteration. |
loop.length | The number of items in the sequence. |
loop.cycle | A helper function to cycle between a list of sequences. See the explanation below. |
loop.depth | Indicates how deep in a recursive loop the rendering currently is. Starts at level 1 |
loop.depth0 | Indicates how deep in a recursive loop the rendering currently is. Starts at level 0 |
loop.previtem | The item from the previous iteration of the loop. Undefined during the first iteration. |
loop.nextitem | The item from the following iteration of the loop. Undefined during the last iteration. |
loop.changed(*val) | True if previously called with a different value (or not called at all). |
同样是前面第一个for循环的例子,如果改成下面这样
{%- for name in pillar['names'] %}
{%- if loop.index == 1 %}
player: {{ name | upper() }}
{%- else %}
player: {{ name }}
{%- endif %}
{%- endfor %}
这里的if语句下面马上会讲到
结果就变成了第一次循环的名字会大写
player: KOBE
player: lebron
player: t-mac
player: wade
正如上面所展示的那样,if选择结构如下
{% if xxx %}
xxx
{% else %}
xxx
{% endif %}
如果else后面还要继续嵌套if的话,可以用下面的结构
{% if xxx %}
xxx
{% elif xxx %}
xxx
{% else %}
xxx
{% endif %}
这里就不额外举例子了
类似于编程语言中的函数,jinja使用Macro去将一些会被经常使用的片段进行复用。
在jinja_test.txt
中添加下列macro的定义,其中macro的名字为info,带入4个参数,其中第4个参数是一个list。而且要注意这里是用短线去除了for循环元素前后的空格,然后利用波浪线~在内容的前面加了空格
{%- macro info(name, age, team, pets) -%}
We have a new player coming, his name is {{ name }}, and only {{ age }} years old. He belongs to team {{ team }}, and he loves
{%- for pet in pets -%}
{{ ' '~pet }}
{%- endfor %}
{%- endmacro -%}
然后在下面跟使用函数一样去使用macro,要注意这里用双大括号
{{ info('xiaofu', 18, 'suns', ['chouchou','haha']) }}
这样在目标文件中会输出下列内容
We have a new player coming, his name is xiaofu, and only 18 years old. He belongs to team suns, and he loves chouchou haha
需要注意的是这里是在同一个文件中进行定义和使用,如果macro的定义是在另一个模板文件中,还需要用后面讲到的import
先进行导入才可以使用。
还需要注意,在两个大括号中再嵌套两个大括号的话,会报错。例如在macro中使用变量。
macro就跟函数一样,有过python编程经验的朋友应该知道python中在.py文件中定义的函数可以通过在别的文件中import来进行复用。jinja中也同样支持这个import方法。
首先在/srv/salt/files/form.txt
中定义一个macro如下
{%- macro textarea(name, value='', rows=10, cols=40) -%}
<textarea name="{{ name }}" rows="{{ rows }}" cols="{{ cols }}">{{ value | e }}</textarea>
{%- endmacro %}
然后在/srv/salt/files/jinja_test.txt
中修改如下内容
{% import "files/form.txt" as form %}
<p>
{{ form.textarea('comment') }}
</p>
最后下发到minion以后的效果如下
<p>
<textarea name="comment" rows="10" cols="40"></textarea>
</p>
除了可以import整个文件,也可以像python一样只是import单个函数
{% from "files/form.txt" import textarea as area %}
<p>
{{ area('comment') }}
</p>
最后的效果也是一样的。
可以利用set标签加上等号去给单个变量赋值,例如
{% set exam = "Excellent" %}
{% set student = "xiaofu" %}
Your name: {{ student }}
Your exam result: {{ exam }}
和编程语言一样,需要注意的是变量的范围,区分全局变量和局部变量。
如果想把多行内容赋值给变量,可以用下面的方式
{%- set mylist -%}
<li><a href="/">Index</a></li>
<li><a href="/downloads">Downloads</a></li>
{%- endset %}
使用方式还是一样,例如
<ul>
{{ mylist }}
</ul>
就会输出下列结果
<ul>
<li><a href="/">Index</a></li>
<li><a href="/downloads">Downloads</a></li>
</ul>
用于模板的继承和复用,见下面的模板的继承和复用部分
除了可以对模板进行继承来达到复用的效果,还可以比较简单粗暴地直接将现有的模板拿过来直接插入使用。使用的关键字就是{% include xxx %}
,然后被插入的模板内容就会直接出现在关键字所在的地方。
例如在/srv/salt/files/
目录下有两个文件jinja_test.txt
和child.html
。编辑jinja_test.txt
添加下列内容
{% include "files/child.html" %}
就可以把child.html
的内容原封不动地复制到这里来。需要注意的是,这里的文件路径还是相对于salt://。
有一个比较难以理解的名词叫做context。默认情况下,include的模板会传递context,而import的模板不会传递context。
这个context跟变量的引用有一些关系,所以如果出现变量引用类的报错可以考虑修改下context的配置。
手动修改include以及import的context配置可以用下面的方法,这里用include来举例
{% from 'forms.html' import input with context %}
{% include 'header.html' without context %}
默认情况下,for循环中会在输出的内容后自动加上一个空行,例如上面for循环的例子
{% for name in pillar['names'] %}
player: {{ name }}
{% endfor %}
这里没有加上去除空格的短横线,目标文件结果为
player: kobe
player: lebron
player: t-mac
player: wade
如果加上短横线,目标文件结果为
player: kobe
player: lebron
player: t-mac
player: wade
根据官方文档,在前后加短线的效果分别是去除前面和后面的空格
You can also strip whitespace in templates by hand. If you add a minus sign (-) to the start or end of a block (e.g. a For tag), a comment, or a variable expression, the whitespaces before or after that block will be removed
前面讲到了macro关键字可以把一个片段类似于函数一样去复用,我们还可以更进一步,将一整个模板文件进行继承(Inheritance)。
继承是jinja非常重要的一个特性,尤其是在网页生成上,相信在github pages
上自己用jekyll
搭过网页的人应该都知道。可以首先搭建一个基础的骨骼出来,然后预留出一些区域供别的子模板去复写。在jinja里面,这些预留的区域用block
关键字来表示。
用实际的例子看会比较容易理解。
首先创建/srv/salt/files/base.html
文件内容如下
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
{% block head %}
<meta charset="utf-8">
<link rel="stylesheet" href="/css/master.css">
<title>{% block title %}{% endblock %} - My Webpage</title>
{% endblock %}
</head>
<body>
<div id="content">
{% block content %}
{% endblock %}
</div>
<div id="footer">
{% block footer %}
© Copyright 2019
{% endblock %}
</div>
</body>
</html>
可以看到上面用{% block xx %}{% endblock %}
的方式去定义了四个区域,其中head
区域还嵌套了title
区域。这些区域都可以在别的模板中用对应的名字去覆盖,这个我们待会再来看。首先我们直接把这个文件用jinja的格式传递给minion看看是什么结果,编辑/srv/salt/jinja_test.sls
/home/vagrant/base.html:
file.managed:
- source: salt://files/base.html
- template: jinja
然后下发到minion2
root@saltmaster:/home/vagrant# salt 'minion2' state.apply jinja_test
结果如下
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/css/master.css">
<title> - My Webpage</title>
</head>
<body>
<div id="content">
</div>
<div id="footer">
© Copyright 2019
</div>
</body>
</html>
然后我们新建一个/srv/salt/files/child.html
去继承这个模板并且添加进自己的内容
{% extends "files/base.html" %}
{% block content %}
<h1>Index</h1>
<p class="important">Welcome to my awesome homepage</p>
{% endblock %}
{% block title %}
Index
{% endblock %}
{% block head %}
{{ super() }}
<style type="text/css">
.important { color: #336699; }
</style>
{% endblock %}
这里的extends
关键字用来声明继承关系,需要注意的是被继承的文件名是相对于salt://的路径,所以即使继承文件和被继承文件在同一个目录下也不能直接用文件名,会报错找不到文件。然后用和base.html
中一样的block名字去进行覆盖。子文件中的block顺序不需要和父模板中一致,例如这里就是先覆盖的{% block content %}
然后是{% block title %}
最后是{% block head %}
。
然后试着把这个子文件下发到minion2
/home/vagrant/child.html:
file.managed:
- source: salt://files/child.html
- template: jinja
最后效果如下
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/css/master.css">
<title>
Index
- My Webpage</title>
<style type="text/css">
.important { color: #336699; }
</style>
</head>
<body>
<div id="content">
<h1>Index</h1>
<p class="important">Welcome to my awesome homepage</p>
</div>
<div id="footer">
© Copyright 2019
</div>
</body>
</html>
虽然格式有点问题,但是可以看到子文件中的内容已经对应地填充到父模板的相应block了。值得一提的是{% block head %}
,如果想复用父模板中已经存在的内容,可以用{{ super() }}关键字去指明。
具体到saltstack里面,和普通的jinja区别就在于saltstack会传递给jinja很多内置的变量,例如pillar和grains,所以可以用这些变量的内容去进行逻辑判断以及模板服用。尤其是在state文件的生成上。
还是举个具体的例子来看。
有两个minion,分别是minion1
和minion2
。现在要跑一个state文件,将两个不同的文件minion1.txt
和minion2.txt
自动分配到两个minion的对应位置。两个位置用pillar下发下去。
首先在/srv/pillar/locations.sls
中定义两个路径
location1: '/home/vagrant/'
location2: '/'
然后在/srv/pillar/top.sls
中下发这个pillar到全部的minion
base:
'*':
- locations
下发
root@saltmaster:/srv/salt/files# salt '*' saltutil.refresh_pillar
minion1:
True
minion2:
True
然后创建/srv/salt/files/minion1.txt
this is for minion1
以及/srv/salt/files/minion2.txt
this is for minion2
之后就是重点了,创建一个state文件,自动根据minion的id去找到对应的文件和地址。创建/srv/salt/example.sls
如下
{% if grains['id'] == 'minion1' %}
{% set location = pillar['location1']~'minion1.txt' %}
{% set source = 'minion1.txt' %}
{% else %}
{% set location = pillar['location2']~'minion2.txt' %}
{% set source = 'minion2.txt' %}
{% endif %}
{{ location }}:
file.managed:
- source: salt://files/{{ source }}
首先根据grains的内容进行一个逻辑判断,并对location
和source
这两个变量分别进行赋值。其中location
的赋值还用到了pillar的内容。然后就是一个普通的文件复制state函数的写法了,只不过用到了上面定义的两个变量。
然后将这个state文件进行应用
root@saltmaster:/srv/salt/files# salt '*' state.apply example
然后就可以在两个minion的指定目录中分别看到指定的文件了
vagrant@minion1:~$ pwd
/home/vagrant
vagrant@minion1:~$ cat minion1.txt
this is for minion1
root@minion2:/# pwd
/
root@minion2:/# cat minion2.txt
this is for minion2
jinja这种模板语言不仅在saltstack中使用,在很多网页搭建平台中也会用到。而且因为本身就是为python量身定做的模板引擎,以后利用Django后端对saltstack进行API二次开发的时候就会更加容易。如果对saltstack二次开发感兴趣的同学欢迎关注我,我们一起学习进步。