Templating(Jinja2)

正如变量部分中已经提到的,Ansible 使用 Jinja2 模板来使用动态表达式和访问变量。Ansible 大大扩展了可用的过滤器,并添加了新的插件类型:查找(lookups)。

请注意,在 Ansible 控制器上进行所有模板替换。这样做是为了最大程度地减少对目标主机的要求(仅在控制器上需要 jinja2),并且能够传递任务所需的最少信息,因此目标主机不需要控制器具有的所有数据的副本。

[TOC]

格式化数据的过滤器

以不同格式显示

{{ some_variable | to_json }}
{{ some_variable | to_yaml }}

人类可读:

{{ some_variable | to_nice_json }}
{{ some_variable | to_nice_yaml }}

更改缩进:

{{ some_variable | to_nice_json(indent=2) }}
{{ some_variable | to_nice_yaml(indent=8) }}

阅读已经格式化的数据

{{ some_variable | from_json }}
{{ some_variable | from_yaml }}

例如:

tasks:
  - shell: cat /some/path/to/file.json
    register: result

  - set_fact:
      myvar: "{{ result.stdout | from_json }}"

解析多文档 yaml 字符串,使用from_yaml_all过滤器:

tasks:
  - shell: cat /some/path/to/multidoc-file.yaml
    register: result
 - debug:
     msg: '{{ item }}'
  loop: '{{ result.stdout | from_yaml_all | list }}'

未定义变量默认值(default)

{{ some_variable | default(5) }}

如果未定义变量some_variable,则使用的值为 5,而不是抛出错误。

如果要在变量评估为 false 或为空字符串时使用默认值,则必须将第二个参数设置为 true:

{{ lookup('env', 'MY_USER') | default('admin', true) }}

省略参数(omit)

- name: touch files with an optional mode
  file: dest={{ item.path }} state=touch mode={{ item.mode | default(omit) }}
  loop:
    - path: /tmp/foo
    - path: /tmp/bar
    - path: /tmp/baz
      mode: "0444"

对于列表中的前两个文件,默认 mode 将由系统的 umask 决定。

列表过滤

最小数

{{ list1 | min }}

最大数

{{ [3, 4, 2] | max }}

展开列表

{{ [3, [4, 2] ] | flatten }}

只展开一级列表:

{{ [3, [4, [2]] ] | flatten(levels=1) }}

集合过滤

列表中唯一值

{{ list1 | unique }}

两个列表并集

{{ list1 | union(list2) }}

两个列表交集

{{ list1 | intersect(list2) }}

两个列表的差异

1 中存在,2 中没有:

{{ list1 | difference(list2) }}

两个列表的对称差异

每个列表的独特项:

{{ list1 | symmetric_difference(list2) }}

字典过滤

字典到项目元素

{{ dict | dict2items }}

从:

tags:
  Application: payment
  Environment: dev

到:

- key: Application
  value: payment
- key: Environment
  value: dev

项目元素到字典

{{ tags | items2dict }}

从:

tags:
  - key: Application
    value: payment
  - key: Environment
    value: dev

到:

Application: payment
Environment: dev

items2dict 支持参数:

{{ tags | items2dict(key_name='key', value_name='value') }}

打包过滤(zip)

打包

- name: give me list combo of two lists
  debug:
   msg: "{{ [1,2,3,4,5] | zip(['a','b','c','d','e','f']) | list }}"

- name: give me shortest combo of two lists
  debug:
    msg: "{{ [1,2,3] | zip(['a','b','c','d','e','f']) | list }}"

用尽所有元素

其他列表不够,使用 fillvalue填充:

- name: give me longest combo of three lists , fill with X
  debug:
    msg: "{{ [1,2,3] | zip_longest(['a','b','c','d','e','f'], [21, 22, 23], fillvalue='X') | list }}"

快速生成字典

items2dictdict

{{ dict(keys_list | zip(values_list)) }}

从:

list_one:
  - one
  - two
list_two:
  - apple
  - orange

到:

one: apple
two: orange

子元素过滤

生成一个对象的乘积和该对象的子元素值:

{{ users | subelements('groups', skip_missing=True) }}

从:

users:
  - name: alice
    authorized:
      - /tmp/alice/onekey.pub
      - /tmp/alice/twokey.pub
    groups:
      - wheel
      - docker
  - name: bob
    authorized:
      - /tmp/bob/id_rsa.pub
    groups:
      - docker

到:

-
  - name: alice
    groups:
      - wheel
      - docker
    authorized:
      - /tmp/alice/onekey.pub
  - wheel
-
  - name: alice
    groups:
      - wheel
      - docker
    authorized:
      - /tmp/alice/onekey.pub
  - docker
-
  - name: bob
    authorized:
      - /tmp/bob/id_rsa.pub
    groups:
      - docker
  - docker

使用 loop过滤:

- name: Set authorized ssh key, extracting just that data from 'users'
  authorized_key:
    user: "{{ item.0.name }}"
    key: "{{ lookup('file', item.1) }}"
  loop: "{{ users | subelements('authorized') }}"

随机 Mac 地址

"{{ '52:54:00' | random_mac }}"
# => '52:54:00:ef:1c:03'

随机数

"{{ ['a','b','c'] | random }}"
# => 'c'
{{ 101 | random(step=10) }}
# => 70
{{ 101 | random(1, 10) }}
# => 31
{{ 101 | random(start=1, step=10) }}
# => 51
"{{ 60 | random(seed=inventory_hostname) }} * * * * root /script/from/cron"

从种子中初始化随机数生成器。这样,您可以创建随机但幂等的数字。

随机过滤

{{ ['a','b','c'] | shuffle }}
# => ['c','a','b']
{{ ['a','b','c'] | shuffle }}
# => ['b','c','a']
{{ ['a','b','c'] | shuffle(seed=inventory_hostname) }}
# => ['b','a','c']

数字

对数(log)

默认为 e:

{{ myvar | log }}

以 10 为底的对数:

{{ myvar | log(10) }}

幂(pow)

{{ myvar | pow(2) }}
{{ myvar | pow(5) }}

根(root)

{{ myvar | root }} # 平方根
{{ myvar | root(3) }} # 立方根

JSON 过滤

domain_definition:
    domain:
        cluster:
            - name: "cluster1"
            - name: "cluster2"
        server:
            - name: "server11"
              cluster: "cluster1"
              port: "8080"
            - name: "server12"
              cluster: "cluster1"
              port: "8090"
            - name: "server21"
              cluster: "cluster2"
              port: "9080"
            - name: "server22"
              cluster: "cluster2"
              port: "9090"
        library:
            - name: "lib1"
              target: "cluster1"
            - name: "lib2"
              target: "cluster2"

集群名

- name: "Display all cluster names"
  debug:
    var: item
  loop: "{{ domain_definition | json_query('domain.cluster[*].name') }}"

服务名

- name: "Display all server names"
  debug:
    var: item
  loop: "{{ domain_definition | json_query('domain.server[*].name') }}"

展示端口

- name: "Display all ports from cluster1"
  debug:
    var: item
  loop: "{{ domain_definition | json_query(server_name_cluster1_query) }}"
  vars:
    server_name_cluster1_query: "domain.server[?cluster=='cluster1'].port"

使用 ,号分割:

- name: "Display all ports from cluster1 as a string"
  debug:
    msg: "{{ domain_definition | json_query('domain.server[?cluster==`cluster1`].port') | join(', ') }}"

注意反引号,或者:

- name: "Display all ports from cluster1"
  debug:
    var: item
  loop: "{{ domain_definition | json_query('domain.server[?cluster==''cluster1''].port') }}"

通过将单引号加倍,可以在 YAML 中将单引号转义。

hash 映射

- name: "Display all server ports and names from cluster1"
  debug:
    var: item
  loop: "{{ domain_definition | json_query(server_name_cluster1_query) }}"
  vars:
    server_name_cluster1_query: "domain.server[?cluster=='cluster2'].{name: name, port: port}"

IP 过滤

测试字符串是否为 IP:

{{ myvar | ipaddr }}

特定 IP 版本:

{{ myvar | ipv4 }}
{{ myvar | ipv6 }}

IP 地址过滤器也可以用来从 IP 地址中提取特定的信息。例如,要从 CIDR 获取 IP 地址本身:

{{ '192.0.2.1/24' | ipaddr('address') }}

网络 CLI 过滤

使用parse_cli过滤 JSON 输出:

{{ output | parse_cli('path/to/spec') }}

spec 文件应为有效格式的 YAML。它定义了如何解析 CLI 输出并返回 JSON 数据。

下面是一个有效的 spec 文件示例,该文件将解析show vlan命令的输出。

---
vars:
  vlan:
    vlan_id: "{{ item.vlan_id }}"
    name: "{{ item.name }}"
    enabled: "{{ item.state != 'act/lshut' }}"
    state: "{{ item.state }}"

keys:
  vlans:
    value: "{{ vlan }}"
    items: "^(?P<vlan_id>\\d+)\\s+(?P<name>\\w+)\\s+(?P<state>active|act/lshut|suspended)"
  state_static:
    value: present

上面的 spec 文件将返回 JSON 数据结构,该数据结构是带有已解析的 VLAN 信息的哈希列表。

通过使用 key 和 values 指令,可以将同一命令解析为哈希。

---
vars:
  vlan:
    key: "{{ item.vlan_id }}"
    values:
      vlan_id: "{{ item.vlan_id }}"
      name: "{{ item.name }}"
      enabled: "{{ item.state != 'act/lshut' }}"
      state: "{{ item.state }}"

keys:
  vlans:
    value: "{{ vlan }}"
    items: "^(?P<vlan_id>\\d+)\\s+(?P<name>\\w+)\\s+(?P<state>active|act/lshut|suspended)"
  state_static:
    value: present

解析 CLI 命令的另一个常见用例是将一个大命令分解为可以解析的块:

---
vars:
  interface:
    name: "{{ item[0].match[0] }}"
    state: "{{ item[1].state }}"
    mode: "{{ item[2].match[0] }}"

keys:
  interfaces:
    value: "{{ interface }}"
    start_block: "^Ethernet.*$"
    end_block: "^$"
    items:
      - "^(?P<name>Ethernet\\d\\/\\d*)"
      - "admin state is (?P<state>.+),"
      - "Port mode is (.+)"

上面的示例会将show interface的输出解析为哈希列表。

网络 XML 过滤

{{ output | parse_xml('path/to/spec') }}

show vlan | display xml

---
vars:
  vlan:
    vlan_id: "{{ item.vlan_id }}"
    name: "{{ item.name }}"
    desc: "{{ item.desc }}"
    enabled: "{{ item.state.get('inactive') != 'inactive' }}"
    state: "{% if item.state.get('inactive') == 'inactive'%} inactive {% else %} active {% endif %}"

keys:
  vlans:
    value: "{{ vlan }}"
    top: configuration/vlans/vlan
    items:
      vlan_id: vlan-id
      name: name
      desc: description
      state: ".[@inactive='inactive']"
---
vars:
  vlan:
    key: "{{ item.vlan_id }}"
    values:
        vlan_id: "{{ item.vlan_id }}"
        name: "{{ item.name }}"
        desc: "{{ item.desc }}"
        enabled: "{{ item.state.get('inactive') != 'inactive' }}"
        state: "{% if item.state.get('inactive') == 'inactive'%} inactive {% else %} active {% endif %}"

keys:
  vlans:
    value: "{{ vlan }}"
    top: configuration/vlans/vlan
    items:
      vlan_id: vlan-id
      name: name
      desc: description
      state: ".[@inactive='inactive']"

Hashing 过滤

获取 sha1

{{ 'test1' | hash('sha1') }}

获取 md5

{{ 'test1' | hash('md5') }}

获取 checksum

{{ 'test2' | checksum }}

密码 hash

{{ 'passwordsaresecret' | password_hash('sha512') }}

随机 hash

{{ 'secretpassword' | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}

Hashes 字典组合(Combine)

{{ {'a':1, 'b':2} | combine({'b':3}) }}

结果:

{'a':1, 'b':3}

递归覆盖:

{{ {'a':{'foo':1, 'bar':2}, 'b':2} | combine({'a':{'bar':3, 'baz':4}}, recursive=True) }}

结果:

{'a':{'foo':1, 'bar':3, 'baz':4}, 'b':2}

多参数合并:

{{ a | combine(b, c, d) }}

在这种情况下,d 中的键将覆盖 c 中的键,后者将覆盖 b 中的键,依此类推。

提取值

将索引列表映射到容器(哈希或数组)中的值列表

{{ [0,2] | map('extract', ['x','y','z']) | list }}
{{ ['x','y'] | map('extract', {'x': 42, 'y': 31}) | list }}

结果:

['x', 'z']
[42, 31]

过滤器可以带另一个参数:

{{ groups['x'] | map('extract', hostvars, 'ec2_ip_address') | list }}

这将获取“ x”组中的主机列表,在 hostvars 中查找它们,然后查找结果的 ec2_ip_address。最终结果是“ x”组中主机的 IP 地址列表。

过滤器的第三个参数也可以是列表,用于在容器内进行递归查找:

{{ ['a'] | map('extract', b, ['x','y']) | list }}

注释过滤

使用选定的注释样式装饰文本。

{{ "Plain style (default)" | comment }}

结果:

#
# Plain style (default)
#

C (//...), C block (/*...*/), Erlang (%...) and XML (``):

{{ "C style" | comment('c') }}
{{ "C block style" | comment('cblock') }}
{{ "Erlang style" | comment('erlang') }}
{{ "XML style" | comment('xml') }}

指定字符:

{{ "My Special Case" | comment(decoration="! ") }}

结果:

!
! My Special Case
!
{{ "Custom style" | comment('plain', prefix='#######\n#', postfix='#\n#######\n   ###\n    #') }}
#######
#
# Custom style
#
#######
   ###
    #

URL 分割过滤

urlsplit过滤器从 URL 中提取片段,主机名,netloc,密码,路径,端口,查询,方案和用户名。不带参数的情况下,返回所有字段的字典:

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('hostname') }}
# => 'www.acme.com'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('netloc') }}
# => 'user:[email protected]:9000'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('username') }}
# => 'user'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('password') }}
# => 'password'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('path') }}
# => '/dir/index.html'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('port') }}
# => '9000'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('scheme') }}
# => 'http'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('query') }}
# => 'query=term'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit('fragment') }}
# => 'fragment'

{{ "http://user:[email protected]:9000/dir/index.html?query=term#fragment" | urlsplit }}
# =>
#   {
#       "fragment": "fragment",
#       "hostname": "www.acme.com",
#       "netloc": "user:[email protected]:9000",
#       "password": "password",
#       "path": "/dir/index.html",
#       "port": 9000,
#       "query": "query=term",
#       "scheme": "http",
#       "username": "user"
#   }

正则过滤( regex_search )

查找

# search for "foo" in "foobar"
{{ 'foobar' | regex_search('(foo)') }}

# will return empty if it cannot find a match
{{ 'ansible' | regex_search('(foobar)') }}

# case insensitive search in multiline mode
{{ 'foo\nBAR' | regex_search("^bar", multiline=True, ignorecase=True) }}

匹配所有

# Return a list of all IPv4 addresses in the string
{{ 'Some DNS servers are 8.8.8.8 and 8.8.4.4' | regex_findall('\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b') }}

替换

# convert "ansible" to "able"
{{ 'ansible' | regex_replace('^a.*i(.*)$', 'a\\1') }}

# convert "foobar" to "bar"
{{ 'foobar' | regex_replace('^f.*o(.*)$', '\\1') }}

# convert "localhost:80" to "localhost, 80" using named groups
{{ 'localhost:80' | regex_replace('^(?P<host>.+):(?P<port>\\d+)$', '\\g<host>, \\g<port>') }}

# convert "localhost:80" to "localhost"
{{ 'localhost:80' | regex_replace(':80') }}

# add "https://" prefix to each item in a list
{{ hosts | map('regex_replace', '^(.*)$', 'https://\\1') | list }}

在正则表达式中转义特殊字符,请使用regex_escape过滤器:

# convert '^f.*o(.*)$' to '\^f\.\*o\(\.\*\)\$'
{{ '^f.*o(.*)$' | regex_escape() }}

其他有用的过滤

shell 添加引号

- shell: echo {{ string_value | quote }}

判断

如果相同,则返回Mr

{{ (name == "John") | ternary('Mr','Ms') }}

列表组成字符串

{{ list | join(" ") }}

basename

{{ path | basename }}

/etc/asdf/foo.txt输出foo.txt

dirname

{{ path | dirname }}

展开~

{{ path | expanduser }}

展开环境变量路径

{{ path | expandvars }}
{{ path | realpath }}

计算相对路径

{{ path | relpath('/etc') }}

分割文件和扩展名

# with path == 'nginx.conf' the return would be ('nginx', '.conf')
{{ path | splitext }}

Base64 编码

{{ encoded | b64decode }}
{{ decoded | b64encode(encoding='utf-16-le') }}

UUID

{{ hostname | to_uuid }}

转换类型

- debug:
    msg: test
  when: some_string_value | bool

使用复杂变量列表中每个项目的属性

# get a comma-separated list of the mount points (e.g. "/,/mnt/stuff") on a host
{{ ansible_mounts | map(attribute='mount') | join(',') }}

从字符串获取日期

# Get total amount of seconds between two dates. Default date format is %Y-%m-%d %H:%M:%S but you can pass your own format
{{ (("2016-08-14 20:00:12" | to_datetime) - ("2015-12-25" | to_datetime('%Y-%m-%d'))).total_seconds()  }}

# Get remaining seconds after delta has been calculated. NOTE: This does NOT convert years, days, hours, etc to seconds. For that, use total_seconds()
{{ (("2016-08-14 20:00:12" | to_datetime) - ("2016-08-14 18:00:00" | to_datetime)).seconds  }}
# This expression evaluates to "12" and not "132". Delta is 2 hours, 12 seconds

# get amount of days between two dates. This returns only number of days and discards remaining hours, minutes, and seconds
{{ (("2016-08-14 20:00:12" | to_datetime) - ("2015-12-25" | to_datetime('%Y-%m-%d'))).days  }}

使用字符串格式化日期

# Display year-month-day
{{ '%Y-%m-%d' | strftime }}

# Display hour:min:sec
{{ '%H:%M:%S' | strftime }}

# Use ansible_date_time.epoch fact
{{ '%Y-%m-%d %H:%M:%S' | strftime(ansible_date_time.epoch) }}

# Use arbitrary epoch value
{{ '%Y-%m-%d' | strftime(0) }}          # => 1970-01-01
{{ '%Y-%m-%d' | strftime(1441357287) }} # => 2015-09-04

组合过滤

返回组合列表的列表:

- name: give me largest permutations (order matters)
  debug:
    msg: "{{ [1,2,3,4,5] | permutations | list }}"

- name: give me permutations of sets of three
  debug:
    msg: "{{ [1,2,3,4,5] | permutations(3) | list }}"
- name: give me combinations for sets of two
  debug:
    msg: "{{ [1,2,3,4,5] | combinations(2) | list }}"

debugging 过滤

{{ myvar | type_debug }}

Tests

测试语法

测试语法和过滤语法(variable | filte)不同。在 Ansible 2.5 之后,使用 jinja2 测试将会显示警告。

语法:

variable is test_name

或:

result is failed

测试字符串

匹配字符串:

vars:
  url: "http://example.com/users/foo/resources/bar"

tasks:
    - debug:
        msg: "matched pattern 1"
      when: url is match("http://example.com/users/.*/resources/.*")

    - debug:
        msg: "matched pattern 2"
      when: url is search("/users/.*/resources/.*")

    - debug:
        msg: "matched pattern 3"
      when: url is search("/users/")

match要求完全匹配字符串,而search仅需要匹配字符串的子集。

版本比较

{{ ansible_facts['distribution_version'] is version('12.04', '>=') }}

操作符:

<, lt, <=, le, >, gt, >=, ge, ==, =, eq, !=, <>, ne

严格版本解析:

{{ sample_version_var is version('1.0', operator='lt', strict=True) }}

集合测试

子集和超集

vars:
    a: [1,2,3,4,5]
    b: [2,3]
tasks:
    - debug:
        msg: "A includes B"
      when: a is superset(b)

    - debug:
        msg: "B is included in A"
      when: b is subset(a)

全真或至少一个真

vars:
  mylist:
      - 1
      - "{{ 3 == 3 }}"
      - True
  myotherlist:
      - False
      - True
tasks:

  - debug:
      msg: "all are true!"
    when: mylist is all

  - debug:
      msg: "at least one is true"
    when: myotherlist is any

路径测试

- debug:
    msg: "path is a directory"
  when: mypath is directory

- debug:
    msg: "path is a file"
  when: mypath is file

- debug:
    msg: "path is a symlink"
  when: mypath is link

- debug:
    msg: "path already exists"
  when: mypath is exists

- debug:
    msg: "path is {{ (mypath is abs)|ternary('absolute','relative')}}"

- debug:
    msg: "path is the same file as path2"
  when: mypath is same_file(path2)

- debug:
    msg: "path is a mount"
  when: mypath is mount

任务结果测试

tasks:

  - shell: /usr/bin/foo
    register: result
    ignore_errors: True

  - debug:
      msg: "it failed"
    when: result is failed

  # in most cases you'll want a handler, but if you want to do something right now, this is nice
  - debug:
      msg: "it changed"
    when: result is changed

  - debug:
      msg: "it succeeded in Ansible >= 2.1"
    when: result is succeeded

  - debug:
      msg: "it succeeded"
    when: result is success

  - debug:
      msg: "it was skipped"
    when: result is skipped

Templating (Jinja2)

Last updated

Was this helpful?