第九章:Jinja2 模板引擎

掌握 Jinja2 模板语法,用于生成动态配置文件。

最后更新: 2024-01-23
页面目录

Jinja2 模板引擎

Jinja2 是 Python 的一个现代模板引擎,Ansible 使用它来生成动态配置文件。

模板基础

Ansible template 模块

tasks:
  - name: Generate config from template
    template:
      src: app.conf.j2
      dest: /etc/myapp/app.conf
      owner: root
      group: root
      mode: '0644'
      validate: "/usr/sbin/nginx -t -c %s"
    notify: Restart app

Jinja2 语法

变量输出

{# 单行注释 #}

{# 多行
   注释 #}

{# 变量 #}
{{ variable }}
{{ user.name }}
{{ user['name'] }}
{{ ansible_hostname }}

{# 过滤器 #}
{{ name | upper }}
{{ name | lower }}
{{ name | default('Anonymous') }}
{{ name | length }}

{# 表达式 #}
{{ 1 + 1 }}
{{ 'hello' ~ 'world' }}

控制结构

if 条件

{% if条件 %}
    内容
{% elif 条件 %}
    内容
{% else %}
    内容
{% endif %}

示例:

{% if ansible_facts['os_family'] == 'RedHat' %}
    yum install nginx
{% elif ansible_facts['os_family'] == 'Debian' %}
    apt install nginx
{% else %}
    echo "Unsupported OS"
{% endif %}

for 循环

{% for item in items %}
    {{ item }}
{% endfor %}

示例:

# 循环列表
{% for user in users %}
User: {{ user.name }} - {{ user.email }}
{% endfor %}

# 循环字典
{% for key, value in config.items() %}
{{ key }} = {{ value }}
{% endfor %}

# 带索引
{% for item in items %}
{{ loop.index }}. {{ item }}
{% endfor %}

循环变量

变量 说明
loop.index 当前循环索引(从1开始)
loop.index0 当前循环索引(从0开始)
loop.first 是否为第一次循环
loop.last 是否为最后一次循环
loop.length 循环总数
loop.depth 当前递归深度

Jinja2 过滤器

字符串过滤器

{{ name | upper }}              {# 大写 #}
{{ name | lower }}              {# 小写 #}
{{ name | capitalize }}         {# 首字母大写 #}
{{ name | title }}              {# 每个单词首字母大写 #}
{{ name | reverse }}            {# 反转字符串 #}
{{ name | length }}             {# 字符串长度 #}
{{ name | trim }}               {# 去除首尾空格 #}
{{ name | striptags }}          {# 去除 HTML 标签 #}
{{ name | wordcount }}          {# 单词数量 #}
{{ path | basename }}           {# 获取文件名 #}
{{ path | dirname }}            {# 获取目录名 #}
{{ path | expanduser }}         {# 展开 ~ #}
{{ path | realpath }}           {# 真实路径 #}

列表过滤器

{{ list | length }}            {# 列表长度 #}
{{ list | first }}              {# 第一个元素 #}
{{ list | last }}               {# 最后一个元素 #}
{{ list | min }}                {# 最小值 #}
{{ list | max }}                {# 最大值 #}
{{ list | sum }}                {# 求和 #}
{{ list | unique }}             {# 去重 #}
{{ list | sort }}               {# 排序 #}
{{ list | reverse }}            {# 反转 #}
{{ list | join(',') }}          {# 拼接 #}
{{ list | random }}              {# 随机元素 #}
{{ list | slice(3) }}           {# 分组 #}

字典过滤器

{{ dict | keys }}              {# 获取所有键 #}
{{ dict | values }}            {# 获取所有值 #}
{{ dict | items }}             {# 键值对 #}
{{ dict | length }}            {# 字典长度 #}
{{ dict | to_yaml }}           {# 转换为 YAML #}
{{ dict | to_nice_yaml }}      {# 美化 YAML #}
{{ dict | to_json }}           {# 转换为 JSON #}
{{ dict | to_nice_json }}      {# 美化 JSON #}

类型转换过滤器

{{ '123' | int }}              {# 转换为整数 #}
{{ 123 | string }}             {# 转换为字符串 #}
{{ value | bool }}             {# 转换为布尔值 #}
{{ value | float }}            {# 转换为浮点数 #}
{{ value | list }}             {# 转换为列表 #}
{{ value | dict }}             {# 转换为字典 #}

默认值过滤器

{{ value | default('N/A') }}
{{ value | default True }}
{{ value | default(False) }}
{{ value | d('default') }}

其他过滤器

{{ uuid | hash('sha256') }}    {# 哈希计算 #}
{{ path | md5 }}               {# MD5 #}
{{ path | sha1 }}              {# SHA1 #}
{{ name | match('pattern') }}  {# 正则匹配 #}
{{ text | regex_replace('old', 'new') }}  {# 正则替换 #}

测试

{# 检查变量是否定义 #}
{% if value is defined %}
    {{ value }}
{% endif %}

{# 检查变量未定义 #}
{% if value is not defined %}
    default
{% endif %}

{# 检查值 #}
{% if value is truthy %}
{% if value is falsey %}
{% if value is none %}
{% if value is string %}
{% if value is number %}
{% if value is iterable %}
{% if value is mapping %}

完整模板示例

Nginx 配置模板

# nginx.conf.j2
# Ansible managed file
# Generated at: {{ ansible_date_time.iso8601 }}

user {{ nginx_user | default('www-data') }};
worker_processes {{ nginx_worker_processes | default(ansible_facts.cpu_cores) }};
pid {{ nginx_pid_file | default('/run/nginx.pid') }};
error_log {{ nginx_error_log | default('/var/log/nginx/error.log') }};

events {
    worker_connections {{ nginx_worker_connections | default(1024) }};
    {% if nginx_multi_accept %}
    multi_accept on;
    {% endif %}
}

http {
    include {{ nginx_mime_types_file | default('/etc/nginx/mime.types') }};
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log {{ nginx_access_log | default('/var/log/nginx/access.log') }} main;

    sendfile {{ nginx_sendfile | default('on') }};
    tcp_nopush {{ nginx_tcp_nopush | default('on') }};
    tcp_nodelay {{ nginx_tcp_nodelay | default('on') }};
    keepalive_timeout {{ nginx_keepalive_timeout | default(65) }};
    types_hash_max_size {{ nginx_types_hash_max_size | default(2048) }};

    # Gzip compression
    {% if nginx_gzip | default(True) %}
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss;
    {% endif %}

    include {{ nginx_conf_dir | default('/etc/nginx/conf.d') }}/*.conf;
}

多站点配置模板

# sites-enabled.conf.j2
{% for site in nginx_sites %}
server {
    listen {{ site.port | default(80) }};
    {% if site.ssl | default(False) %}
    listen {{ site.port | default(443) }} ssl;
    {% endif %}
    
    server_name {{ site.server_name }};
    root {{ site.document_root }};
    
    index {{ site.index | default('index.html index.htm') }};
    
    access_log /var/log/nginx/{{ site.server_name }}_access.log;
    error_log /var/log/nginx/{{ site.server_name }}_error.log;
    
    location / {
        {% if site.try_files is defined %}
        try_files {{ site.try_files }};
        {% else %}
        try_files $uri $uri/ =404;
        {% endif %}
    }
    
    {% if site.php_enabled | default(False) %}
    location ~ \.php$ {
        include {{ nginx_php_config | default('/etc/nginx/fastcgi_params') }};
        fastcgi_pass {{ site.php_socket | default('unix:/var/run/php/php-fpm.sock') }};
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
    {% endif %}
    
    {% if site.ssl | default(False) %}
    ssl_certificate {{ site.ssl_cert }};
    ssl_certificate_key {{ site.ssl_key }};
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    {% endif %}
}
{% endfor %}

应用配置文件模板

# app.config.j2
# Application Configuration
# Generated by Ansible

[application]
name = {{ app_name }}
version = {{ app_version }}
environment = {{ app_environment | default('production') }}
debug = {{ app_debug | default(False) | lower }}

[server]
host = {{ app_host | default('0.0.0.0') }}
port = {{ app_port }}
workers = {{ app_workers | default(ansible_facts.cpu_cores) }}
timeout = {{ app_timeout | default(30) }}

[database]
host = {{ db_host }}
port = {{ db_port | default(3306) }}
name = {{ db_name }}
user = {{ db_user }}
{% if db_password is defined %}
password = {{ db_password }}
{% endif %}
pool_size = {{ db_pool_size | default(10) }}
pool_recycle = {{ db_pool_recycle | default(3600) }}

[redis]
{% if redis_host is defined %}
host = {{ redis_host }}
port = {{ redis_port | default(6379) }}
{% if redis_password is defined %}
password = {{ redis_password }}
{% endif %}
db = {{ redis_db | default(0) }}
{% endif %}

[logging]
level = {{ app_log_level | default('INFO') }}
format = {{ app_log_format | default('%(asctime)s - %(name)s - %(levelname)s - %(message)s') }}
file = {{ app_log_file | default('/var/log/myapp/app.log') }}

{% if app_features is defined %}
[features]
{% for feature, enabled in app_features.items() %}
{{ feature }} = {{ enabled | lower }}
{% endfor %}
{% endif %}

负载均衡器模板

# haproxy.cfg.j2
# HAProxy Configuration
# Generated: {{ ansible_date_time.iso8601 }}

global
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    stats socket /var/run/haproxy.sock mode 0600 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon
    {% if haproxy_maxconn is defined %}
    maxconn {{ haproxy_maxconn }}
    {% endif %}

defaults
    log global
    mode http
    option httplog
    option dontlognull
    option http-server-close
    option forwardfor except 127.0.0.0/8
    option redispatch
    retries 3
    timeout connect {{ haproxy_timeout_connect | default('5s') }}
    timeout client {{ haproxy_timeout_client | default('30s') }}
    timeout server {{ haproxy_timeout_server | default('30s') }}
    errorfile 400 /etc/haproxy/errors/400.http
    errorfile 403 /etc/haproxy/errors/403.http
    errorfile 503 /etc/haproxy/errors/503.http

# Stats page
listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 30s
    stats auth admin:{{ haproxy_stats_password | default('admin') }}

{% for backend in haproxy_backends %}
# Backend: {{ backend.name }}
listen {{ backend.name }}
    bind {{ backend.bind | default('*:80') }}
    {% if backend.ssl | default(False) %}
    bind {{ backend.bind_ssl | default('*:443') }} ssl crt {{ backend.ssl_cert }}
    {% endif %}
    mode {{ backend.mode | default('http') }}
    balance {{ backend.balance | default('roundrobin') }}
    
    {% for server in backend.servers %}
    server {{ server.name }} {{ server.ip }}:{{ server.port }}
        {% if server.check | default(True) %}
        check inter 2000 rise 2 fall 3
        {% endif %}
        {% if server.cookie is defined %}
        cookie {{ server.cookie }}
        {% endif %}
    {% endfor %}
    
    {% if backend.stats is defined %}
    stats enable
    stats uri {{ backend.stats.uri | default('/stats') }}
    {% endif %}
{% endfor %}

模板技巧

空白控制

{# 默认保留空白 #}
{% for item in items -%}
{{ item }}
{%- endfor %}

{# 去掉前后空白 #}
{{- value -}}

多行文本

{% set multiline = "line1
line2
line3" %}

{% raw %}
    # 这部分不会被 Jinja 处理
{% endraw %}

宏定义

{% macro input(name, value='', type='text', size=20) %}
<input type="{{ type }}" name="{{ name }}" value="{{ value|e }}" size="{{ size }}">
{% endmacro %}

{# 使用宏 #}
{{ input('username') }}
{{ input('password', type='password') }}

下一步

现在你已经掌握了 Jinja2 模板。接下来让我们学习 Roles 角色管理。

👉 Roles 角色管理