009.Ansible模板管理 Jinja2

Posted zyxnhr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了009.Ansible模板管理 Jinja2相关的知识,希望对你有一定的参考价值。

一 Jinja2简介

Jinja2是基于python的模板引擎。

假设说现在我们需要一次性在10台主机上安装redis,这个通过playbook现在已经很容易实现。默认情况下,所有的redis安装完成之后,我们可以统一为其分发配置文件。这个时候就面临一个问题,这些redis需要监听的地址各不相同,我们也不可能为每一个redis单独写一个配置文件。因为这些配置文件中,绝大部分的配置其实都是相同的。这个时候最好的方式其实就是用一个通用的配置文件来解决所有的问题。将所有需要修改的地方使用变量替换

二 模板使用

playbook使用template模块来实现模板文件的分发,其用法与copy模块基本相同,唯一的区别是,copy模块会将原文件原封不动的复制到被控端,而template会将原文件复制到被控端,并且使用变量的值将文件中的变量替换以生成完整的配置文件。

2.1 redis模板配置

创建一个模板目录

[root@node1 ansible]# mkdir template

为了方便区分,模板文件最好使用.j2结尾,就知道是模板文件,在复制时需要使用template模块

[root@node1 ansible]# vim template/redis.conf.j2

daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis

maxmemory {{redismem }}

bind {{ ansible_ens33.ipv4.address }} 127.0.0.1

timeout 300
loglevel notice

databases 16
save 900 1
save 300 10
save 60 10000

rdbcompression yes

maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec

[root@node1 ansible]# vim redis_config.yml 

- hosts: all
  tasks:
    - name: set redis-server
      set_fact: redismem="{{ ansible_memtotal_mb/2|int }}"
    - name: install redis
      yum:
        name: redis
        state: present
    - name: ensure sest direectory exists
      file:
        path: "{{ item }}"
        state: directory
        mode: 0755
        recurse: yes
        owner: redis
        group: redis
      with_items:
        - "/var/log/redis"
        - "/data/redis"
    - name: cp redis.conf to /etc
      template:
        src: template/redis.conf.j2
        dest: /etc/redis.conf
        mode: 0755
      notify: restart redis
    - name: start redis
      systemd:
        name: redis
        state: restarted
  handlers:
    - name: restart redis
      systemd:
        name: redis
        state: restarted

关于template模块的更多参数说明:

  • backup:如果原目标文件存在,则先备份目标文件
  • dest:目标文件路径
  • force:是否强制覆盖,默认为yes
  • group:目标文件属组
  • mode:目标文件的权限
  • owner:目标文件属主
  • src:源模板文件路径
  • validate:在复制之前通过命令验证目标文件,如果验证通过则复制

执行

[root@node1 ansible]# ansible-playbook redis_config.yml

PLAY [all] ************************************************************************************************************************************

TASK [set redis-server] ***********************************************************************************************************************
ok: [demo4.example.com]
ok: [demo5.example.com]
ok: [demo1.example.com]
ok: [demo2.example.com]
ok: [demo3.example.com]

TASK [install redis] **************************************************************************************************************************
ok: [demo5.example.com]
ok: [demo2.example.com]
ok: [demo3.example.com]
ok: [demo1.example.com]
ok: [demo4.example.com]

TASK [ensure sest direectory exists] **********************************************************************************************************
changed: [demo1.example.com] => (item=/var/log/redis)
changed: [demo5.example.com] => (item=/var/log/redis)
changed: [demo2.example.com] => (item=/var/log/redis)
changed: [demo3.example.com] => (item=/var/log/redis)
changed: [demo4.example.com] => (item=/var/log/redis)
changed: [demo5.example.com] => (item=/data/redis)
changed: [demo2.example.com] => (item=/data/redis)
changed: [demo1.example.com] => (item=/data/redis)
changed: [demo3.example.com] => (item=/data/redis)
changed: [demo4.example.com] => (item=/data/redis)

TASK [cp redis.conf to /etc] ******************************************************************************************************************
ok: [demo1.example.com]
ok: [demo4.example.com]
ok: [demo5.example.com]
ok: [demo3.example.com]
ok: [demo2.example.com]

TASK [start redis] ****************************************************************************************************************************
changed: [demo5.example.com]
changed: [demo1.example.com]
changed: [demo4.example.com]
changed: [demo2.example.com]
changed: [demo3.example.com]

PLAY RECAP ************************************************************************************************************************************
demo1.example.com          : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
demo2.example.com          : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
demo3.example.com          : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
demo4.example.com          : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
demo5.example.com          : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[root@node1 ansible]# ansible all -m shell -a "cat /etc/redis.conf|grep bind"

demo2.example.com | CHANGED | rc=0 >>
bind 192.168.132.132 127.0.0.1
demo1.example.com | CHANGED | rc=0 >>
bind 192.168.132.131 127.0.0.1
demo3.example.com | CHANGED | rc=0 >>
bind 192.168.132.133 127.0.0.1
demo5.example.com | CHANGED | rc=0 >>
bind 192.168.132.135 127.0.0.1
demo4.example.com | CHANGED | rc=0 >>
bind 192.168.132.134 127.0.0.1

使用条件判断

2.2 条件语句

在上面的示例中,我们直接取了被控节点的ens33网卡的ip作为其监听地址。那么假如有些机器的网卡是bond0,这种做法就会报错。这个时候我们就需要在模板文件中定义条件语句如下:

[root@node1 ansible]# cat template/redis.conf.j2

daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis

maxmemory {{redismem }}
{% if ansible_bond0 is defined  %}
bind {{ ansible_bind0.ipv4.address }} 127.0.0.1
{% elif ansible_ens33 is defined %}
bind {{ ansible_ens33.ipv4.address }} 127.0.0.1
{% else %}
bind 0.0.0.0
{% endif %}
timeout 300
loglevel notice

databases 16
save 900 1
save 300 10
save 60 10000

rdbcompression yes

maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec
You have new mail in /var/spool/mail/root

让redis主从角色都可以使用该文件:

配置主从条件

[root@node1 ansible]# vim inventory 

[redis]
demo3.example.com
demo4.example.com  masterip=demo3.example.com

模板文件

[root@node1 ansible]# vim template/redis.conf.j2

daemonize yes
pidfile /var/run/redis.pid
port 6379
logfile "/var/log/redis/redis.log"
dbfilename dump.rdb
dir /data/redis

maxmemory {{redismem }}
{% if ansible_bond0 is defined  %}
bind {{ ansible_bind0.ipv4.address }} 127.0.0.1
{% elif ansible_ens33 is defined %}
bind {{ ansible_ens33.ipv4.address }} 127.0.0.1
{% else %}
bind 0.0.0.0
{% endif %}

{% if masterip is defined %}
slaveof {{ masterip }} {{ materport|default(6379) }}
{% endif %}
timeout 300
loglevel notice

databases 16
save 900 1
save 300 10
save 60 10000

rdbcompression yes

maxclients 10000
appendonly yes
appendfilename appendonly.aof
appendfsync everysec

[root@node1 ansible]# vim redis_config.yml

- hosts: redis
  tasks:
    - name: set redis-server
      set_fact: redismem="{{ ansible_memtotal_mb/2|int }}"
    - name: install redis
      yum:
        name: redis
        state: present
    - name: ensure sest direectory exists
      file:
        path: "{{ item }}"
        state: directory
        mode: 0755
        recurse: yes
        owner: redis
        group: redis
      with_items:
        - "/var/log/redis"
        - "/data/redis"
    - name: cp redis.conf to /etc
      template:
        src: template/redis.conf.j2
        dest: /etc/redis.conf
        mode: 0755
      notify: restart redis
    - name: start redis
      systemd:
        name: redis
        state: restarted
  handlers:
    - name: restart redis
      systemd:
        name: redis
        state: restarted

节点查看

[root@node4 ~]# redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:demo3.example.com
master_port:6379
master_link_status:up
master_last_io_seconds_ago:10
master_sync_in_progress:0
slave_repl_offset:57
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
[root@node3 ~]# redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.132.134,port=6379,state=online,offset=421,lag=0
master_repl_offset:421
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:420

2.3 jinj2的循环语句

现在把proxy主机组中的主机作为代理服务器,安装nginx做反向代理,将请求转发至后面的两台webserver,即webserver组的服务器。

[root@node1 ansible]# vim inventory 

[webserver]
demo1.example.com
demo2.example.com
demo3.example.com

[proxy]
demo5.example.com

[redis]
demo3.example.com
demo4.example.com  masterip=demo3.example.com

[root@node1 ansible]# vim systeminit.yml

- hosts: all
  tasks:
    - name: ipatbles flush filter
      iptables:
        chain: "{{ item }}"
        flush: yes
      with_items: [INPUT,FORWARD,OUTPUT]

[root@node1 ansible]# ansible-playbook systeminit.yml

部署httpd

[root@node1 ansible]# vim config_httpd.yml

- hosts: webserver
  tasks: 
    - name: install httpd
      yum:
        name: httpd
        state: present
    - name: start httpd
      systemd:
        name: httpd
        state: started
        enabled: yes
        daemon_reload: yes

[root@node1 ansible]# ansible-playbook config_httpd.yml

配置nginxproxy

[root@node1 ansible]# vim config_proxy.yml

- name: gather facts   #这里需要配置缓存,触发setup,把facts参数缓存到本地,否则在下面获取到的fact将是nginx proxy的fact值,就不会有结果
  gather_facts: False
  hosts: webserver
  tasks: 
    - name: gather facts
      setup: 
- name: Configue Nginx
  hosts: proxy
  tasks: 
    - name: install nginx
      yum:
        name: nginx
        state: present
    - name: copy nginx.conf to dest
      template:
        src: template/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: reload nginx
    - name: start nginx
      systemd:
        name: nginx
        enabled: yes
        daemon_reload: yes
  handlers:
    - name: reload nginx
      systemd:
        name: nginx
        state: reloaded

[root@node1 ansible]# vim  template/nginx.conf.j2 

user nginx;
worker_processes {{ ansible_processor_vcpus }};
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
    worker_connections 65535;
    use epoll;
}
http {
    map $http_x_forwarded_for $clientRealIP {
        "" $remote_addr;
        ~^(?P<firstAddr>[0-9.]+),?.*$ $firstAddr;
    }
    log_format  real_ip ‘{ "datetime": "$time_local", ‘
                        ‘"remote_addr": "$remote_addr", ‘
                        ‘"source_addr": "$clientRealIP", ‘
                        ‘"x_forwarded_for": "$http_x_forwarded_for", ‘
                        ‘"request": "$request_uri", ‘
                        ‘"status": "$status", ‘
                        ‘"request_method": "$request_method", ‘
                        ‘"request_length": "$request_length", ‘
                        ‘"body_bytes_sent": "$body_bytes_sent", ‘
                        ‘"request_time": "$request_time", ‘
                        ‘"http_referrer": "$http_referer", ‘
                        ‘"user_agent": "$http_user_agent", ‘
                        ‘"upstream_addr": "$upstream_addr", ‘
                        ‘"upstream_status": "$upstream_status", ‘
                        ‘"upstream_http_header": "$upstream_http_host",‘
                        ‘"upstream_response_time": "$upstream_response_time", ‘
                        ‘"x-req-id": "$http_x_request_id", ‘
                        ‘"servername": "$host"‘
                        ‘ }‘;
    access_log  /var/log/nginx/access.log  real_ip;
    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;
    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;
    include /etc/nginx/conf.d/*.conf;

    upstream web {
    {% for host in groups[‘webserver‘] %}
        {% if hostvars[host][‘ansible_bond0‘][‘ipv4‘][‘address‘] is defined %}
        server {{ hostvars[host][‘ansible_bond0‘][‘ipv4‘][‘address‘] }}:80;
        {% elif hostvars[host][‘ansible_ens33‘][‘ipv4‘][‘address‘] is defined %}
        server {{ hostvars[host][‘ansible_ens33‘][‘ipv4‘][‘address‘] }}:80;
        {% endif %}
    {% endfor %}
    }
    server {
        listen       80 default_server;
        server_name  _; 
        location / { 
            proxy_pass http://web;
        }   
    }   
}  

执行验证

[root@node1 ansible]# ansible-playbook config_proxy.yml

[root@node5 ~]# vim /etc/nginx/nginx.conf

user nginx;
worker_processes 4;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
    worker_connections 65535;
    use epoll;
}
http {
    map $http_x_forwarded_for $clientRealIP {
        "" $remote_addr;
        ~^(?P<firstAddr>[0-9.]+),?.*$ $firstAddr;
    }
    log_format  real_ip ‘{ "datetime": "$time_local", ‘
                        ‘"remote_addr": "$remote_addr", ‘
                        ‘"source_addr": "$clientRealIP", ‘
                        ‘"x_forwarded_for": "$http_x_forwarded_for", ‘
                        ‘"request": "$request_uri", ‘
                        ‘"status": "$status", ‘
                        ‘"request_method": "$request_method", ‘
                        ‘"request_length": "$request_length", ‘
                        ‘"body_bytes_sent": "$body_bytes_sent", ‘
                        ‘"request_time": "$request_time", ‘
                        ‘"http_referrer": "$http_referer", ‘
                        ‘"user_agent": "$http_user_agent", ‘
                        ‘"upstream_addr": "$upstream_addr", ‘
                        ‘"upstream_status": "$upstream_status", ‘
                        ‘"upstream_http_header": "$upstream_http_host",‘
                        ‘"upstream_response_time": "$upstream_response_time", ‘
                        ‘"x-req-id": "$http_x_request_id", ‘
                        ‘"servername": "$host"‘
                        ‘ }‘;
    access_log  /var/log/nginx/access.log  real_ip;
    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;
    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;
    include /etc/nginx/conf.d/*.conf;

    upstream web {
                    server 192.168.132.131:80;
                            server 192.168.132.132:80;
                            server 192.168.132.133:80;
                }
    server {
        listen       80 default_server;
        server_name  _;
        location / {
            proxy_pass http://web;
        }
    }
}

域名解析服务bind的配置文件 named.conf的jinja2模板示例:

[root@node1 ansible]# vim inventory 

[dnsmaster]
demo2.example.com
demo3.example.com

[dnsslave]
demo4.example.com
demo5.example.com

[root@node1 ansible]# vim config_dns.yml

- hosts: dnsmaster,dnsslave
  tasks:
    - template:
        src: template/named.conf.j2
        dest: /tmp/named.conf                  

[root@node1 ansible]# vim template/named.conf.j2

options {

listen-on port 53 {
127.0.0.1;
{% for ip in ansible_all_ipv4_addresses %}
{{ ip }};
{% endfor %}
};

listen-on-v6 port 53 { ::1; };
directory "/var/named";
dump-file "/var/named/data/cache_dump.db";
statistics-file "/var/named/data/named_stats.txt";
memstatistics-file "/var/named/data/named_mem_stats.txt";
};

zone "." IN {
type hint;
file "named.ca";
};

include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";

{% if dnsmaster in group_names %}    #设置变量,属于这个组设为master
{% set zone_type = master %}
{% set zone_dir = data %}
{% else %}
{% set zone_type = slave %}          #否则设为salve
{% set zone_dir = slaves %}
{% endif %}

zone "internal.example.com" IN {
type {{ zone_type }};
file "{{ zone_dir }}/internal.example.com";   #引用变量
{% if dnsmaster not in group_names %}
masters { 192.168.2.2; };
{% endif %}
};

执行anslibe查看主从

node2和node3

技术图片

node4和node5

技术图片

三 Jinja2过滤器

3.1  default过滤器

例如上一个redis案例

{% if masterip is defined %}
slaveof {{ masterip }} {{ materport|default(6379) }}
{% endif %}

 另一个示例

- hosts:
  gather_facts: false
  vars:
    - path: /tmp/test
      mode: 0400
    - path: /tmp/foo
    - path: /tmp/bar
  tasks:
    - file:
        dest: {{item}}
        state: touch
        mode: {{ item.mode|default(omit) }}   #如果存在设置,不存在忽略
      with_items: {{ paths }}

3.2 字符串相关过滤器

  • upper:将所有字符串转换为大写
  • |ower:将所有字宇符串转换为小写
  • capitalize:将字符串的首字母大写,其他字母小写
  • reverse:将宇符串倒序排列
  • first:返回字符串的第一个宇符
  • last:返回字符串的最后一个字符
  • trim:将宇符串开头和结尾的空格去掉
  • center(30):将宇符串放在中间,并且字符串两边用空格补齐30位
  • length:返回字符串的长度,与 count等价
  • |ist:将宇符串转换为列表
  • shuffle:list将宇符串转换为列表,但是顺序排列, shuffle同样将宇符串转换为列表,但是会随机打乱宇符串顺序

3.3 数字相关操作

  • int:将对应的值转换为整数
  • float:好对应的值转换为浮点数
  • abs:获取绝对值
  • round:小数点四舍五入
  • randon:从一个给定的范围中获取随机值
- hosts: demo2.example.com
  gather_facts: no
  vars:
    testnum: -1
  tasks:
    - debug:
        msg: "{{ 8+(‘8‘|int) }}"
    - debug:
        msg: "{{ ‘a‘|int(default=6) }}"
    - debug:
        msg: "{{ ‘8‘|float }}"
    - debug:
        msg: "{{ testnum|abs }}"
    - debug:
        msg: "{{ 12.5|round }}"
    - debug:
        msg: "{{ 3.1415926|round(5) }}"
    - debug:
        #从0到100随即返回一个数字
        msg: "{{ 100|random }}"
    - debug:
        #从5到10中随机返回一个数字
        msg: "{{ 10|random(start=5) }}"
    - debug:
        #从4到15随机返回一个数字,步长为3
        #返回的随机数这只可能是:4 7 10 13中的一个
        msg: "{{ 15|random(start=5,step=3) }}"
    - debug:
        #从0到15随机返回一个数字,步长为4
        msg: "{{ 15|random(step=4) }}"

执行结果

TASK [debug] **************************************************************************************************************************************
ok: [demo2.example.com] => {
    "msg": "16"
}
TASK [debug] **************************************************************************************************************************************
ok: [demo2.example.com] => {
    "msg": "6"
}
TASK [debug] **************************************************************************************************************************************
ok: [demo2.example.com] => {
    "msg": "8.0"
}
TASK [debug] **************************************************************************************************************************************
ok: [demo2.example.com] => {
    "msg": "1"
}
TASK [debug] **************************************************************************************************************************************
ok: [demo2.example.com] => {
    "msg": "13.0"
}
TASK [debug] **************************************************************************************************************************************
ok: [demo2.example.com] => {
    "msg": "3.14159"
}
TASK [debug] **************************************************************************************************************************************
ok: [demo2.example.com] => {
    "msg": "11"
}
TASK [debug] **************************************************************************************************************************************
ok: [demo2.example.com] => {
    "msg": "7"
}
TASK [debug] **************************************************************************************************************************************
ok: [demo2.example.com] => {
    "msg": "11"
}
TASK [debug] **************************************************************************************************************************************
ok: [demo2.example.com] => {
    "msg": "0"
}

3.4 列表过滤器

  • length:返回列表长度
  • first:返回列表的第一个值
  • last:返回列表的最后一个值
  • min:返回列表中最小的值
  • max:返回列表中最大的值
  • sort:重新排列列表,默认为升序排列, sort(reverse=true)为降序
  • sum:返回皱教宁非嵌套列表中所有数字的和I
  • flatten:如果列表中包含列表,则 flatten可拉平嵌套的列表 levels参数可用于指定被拉平的层级
  • join:将列表中的元素合并为一个字符串
  • random:从列表中随机返回一个元素
  • shuffle
  • upper
  • lower
  • union:将两个列表合并,如果元素有重复,则只留下一个
  • intersect:获取两个列表的交集
  • difference:获取存在于第一个列表中,但不存在于第二个列表中的元素
  • symmetric difference:取出两个列表中各自独立的元素,如果重复则只留一个

3.5 应用于文件路径的过滤器

  • basename:返回文件路径中的文件名部分
  • dirname:返回文件路径中的目录部分
  • expanduser:将文件路径中的~替换为用户目录
  • realpath:处理符号链接后的文件实际路径

示例:

- name: test basename
  hosts: test
  vars:
    homepage: /usr/share/nginx/html/index.html
  tasks:
    - name: copy homepage
      copy:
        src: files/index.html
        dest: {{ homepage }}

改写

- name: test basename
  hosts: test
  vars:
    homepage: /usr/share/nginx/html/index.html
  tasks:
    - name: copy homepage
      copy:
        src: files/{{ homepage | basename }}
        dest: {{ homepage }}

3.6 自定义过滤器

举个简单的例子,现在有一个playbook如下:

- name: test filter
  hosts: demo2.example.com
  vars:
    domains: ["www.example.com","example.com"]
  tasks:
    - template:
        src: template/test.conf.j2
        dest: /tmp/test.conf

template/test.conf.j2如下:

hosts = [{{ domains | join(,) }}]

执行playbook后,在目标机上的test.conf如下:

[root@node1 ansible]# ansible demo2.example.com  -m shell -a "cat /tmp/test.conf"

demo2.example.com | CHANGED | rc=0 >>
hosts = [www.example.com,example.com]

现在如果希望目标机上的test.conf文件返回结果如下:

hosts = ["www.example.com","example.com"]

没有现成的过滤器来帮我们做这件事情。我们可以自己简单写一个surround_by_quote.py内容如下:

我们需要开启ansible.cfg的配置项:

filter_plugins     = /etc/ansible/plugins/filter

[root@node1 ansible]# mkdir -p /etc/ansible/plugins/filter

[root@node1 ansible]# vim /etc/ansible/plugins/filter/surround_by_quote.py

#!/usr/bin/env python
def surround_by_quote(a_list):
#  return ["%s" % an_element for an_element in a_list]    #这个是下面的简写,python语法
  lst = []
  for index in a_list:
    lst.append("%s" %index)
  return lst
class FilterModule(object):
  def filters(self):
    return {surround_by_quote: surround_by_quote}

将刚刚编写的代码文件放入/etc/ansible/plugins/filter目录下,然后修改templates/test.conf.j2如下:

hosts = [{{ domains |surround_by_quote|join(,) }}]

执行查看

[root@node1 ansible]# ansible demo2.example.com  -m shell -a "cat /tmp/test.conf"

技术图片


博主声明:本文的内容来源主要来自誉天教育晏威老师,由本人实验完成操作验证,需要的博友请联系誉天教育(http://www.yutianedu.com/),获得官方同意或者晏老师(https://www.cnblogs.com/breezey/)本人同意即可转载,谢谢!

以上是关于009.Ansible模板管理 Jinja2的主要内容,如果未能解决你的问题,请参考以下文章

将文件部署到管理主机 | jinja2模板

12. 爬虫训练场项目,jinja2 模板继承,项目继续迭代

appium+python自动化50-生成定位对象模板templet(jinja2)

appium+python自动化50-生成定位对象模板templet(jinja2)

12. 爬虫训练场项目,jinja2 模板继承,项目继续迭代

Ansible Jinja2 模板概述 --01