ansible 文档——playbook (1)

459 阅读11分钟

ansible playbook

playbook 介绍

ansible playbook 提供了一种可复用、简单的配置管理和多机部署系统,非常适合部署复杂的应用程序。
ansible-example repository 中提供了一些通用的playbook 以供参考。

playbook 语法

playbook 使用yaml 语法格式进行编写,相关的编辑器可参阅other tools and programs
playbook 由列表中的一个或多个play 组成,每个play 都会执行playbook 总体目标的一部分,运行一个或多个task,而每个task 都会调用一个ansible 模块。

playbook 执行

playbook 按从上至下的顺序运行。在play 中,task 也是按从上至下的顺序运行。
每个play 最少需要包括两部分:

  • 目标受控节点
  • 至少执行一个task

示例:第一个play 目标节点为webserver,第二个play 目标节点为database server。

---
- name: Update web servers
  hosts: webservers
  remote_user: root

  tasks:
  - name: Ensure apache is at the latest version
    ansible.builtin.yum:
      name: httpd
      state: latest

  - name: Write the apache config file
    ansible.builtin.template:
      src: /srv/httpd.j2
      dest: /etc/httpd.conf

- name: Update db servers
  hosts: databases
  remote_user: root

  tasks:
  - name: Ensure postgresql is at the latest version
    ansible.builtin.yum:
      name: postgresql
      state: latest

  - name: Ensure that postgresql is started
    ansible.builtin.service:
      name: postgresql
      state: started

playbook 不仅能包含hosts 和tasks,示例中还包含了remote_user,指的是ssh 连接使用的用户。也可以在playbook 中添加其他关键字,以控制连接插件、是否提权、如何处理错误等。ansible 还支持在配置或inventory 中设置这些关键字,但是它们存在生效的优先级规则,可以参阅文档了解。

task 执行

默认情况下,ansible 会在所有匹配到的主机上按顺序执行每项task,一次一项。每个task 都执行特定参数的模块。当某个task 在所有目标主机上执行完毕后,才会继续执行下一个task。可以通过配置策略来更改此默认行为。
ansible 会将同一个task 应用到所有匹配到的主机,如果某个主机上执行task 失败,则会将该主机从剩下的task 中剔除。

状态和幂等性

大多数ansible 模块都会先检查是否已达到所需状态,如果已达到,则会退出不执行任何操作,因此多次执行不会改变最终状态。这被称为幂等性,无论运行几次playbook,结果都是相同的。但并非所有playbook 模块都具有该特性。

运行playbook

运行playbook,需要使用ansible-playbook 命令。

ansible-playbook playbook.yml -f 10

运行playbook 时可以使用--verbose 参数查看模块详细输出。

在检查模式下运行playbook

ansible 的检查模式可以无需对系统进行任何更改的情况下,执行playbook。在生产环境实施前,可以使用检查模式来测试playbook,在命令后加参数-c--check 即可:

ansible-playbook --check playbook.yaml

检查模式执行后,会提供所有更改的报告,包含文件修改、命令执行和模块调用等详细信息。

ansible-pull

ansible-pull 命令用于ansible 的pull 模式,与默认的push 模式工作方式相反。
push 模式通过控制节点来管理受控节点,会在控制节点生成需要执行的python 脚本,然后传输到受控节点,并在受控节点执行。
pull 模式是通过从git 仓库中获取配置和脚本,并在本地执行,或是检查git 仓库发生改变时,拉取新的文件并在本地执行。
该模式适合在高并发场景下应用,结合对受控节点的crontab 配置,定期执行ansible-pull 命令,当git 仓库更新后,就执行对应内容。可以参考官方ansible-pull 示例对需要执行ansible-pull 命令的受控节点进行配置。

验证playbook

ansible-playbook 提供了多个验证选项,包括--check--diff--list-hosts--list-tasks--syntax-check。官方还给出了其他验证playbook 工具,以供参考。

ansible-lint

在执行playbook 之前,可以使用ansible-lint 获取详细反馈。例如,执行示例verify-apache.yml 的结果如下:

$ ansible-lint verify-apache.yml
[403] Package installs should not use latest
verify-apache.yml:8
Task/Handler: ensure apache is at the latest version

ansible-lint 默认规则描述了每个错误,对于[403],建议将playbook 中的state: latest 改为state: present

playbook 使用讲解

playbook 的基础功能可以用于管理远程主机的配置和部署,高级功能可以对多层次滚动更新部署进行编排,并将拆分的操作分发到不同主机。

模板化(jinja2)

ansible 使用jinja2 模板来实现动态表达式以及对变量facts 的引用。利用模板和template 模块功能,可以为配置文件创建模块,然后将该配置文件部署到多个环境并为每个环境提供正确的数据。还可以通过模板化任务名称等,编写动态的playbook。
ansible 同时还包含了jinja2 中用于数据转换的过滤器,用于评估模板表达式的测试,以及用于从外部源检索数据的查询插件
在任务发送到目标主机上之前,模板已经在管理主机上完成了解析,这样能减少目标主机上所需的依赖。ansible 仅会将解析后所必须的信息传输到目标主机,一定程度上减少了传输数据量。

注:template 模块使用的文件和数据必须采用utf-8 编码。

jinja2 示例

示例功能,将主机名写入文件/tmp/hostname ,模板目录结构如下:

├── hostname.yml
├── templates
    └── test.j2

hostname.yml 内容如下:

---
- name: Write hostname
  hosts: all
  tasks:
  - name: write hostname using jinja2
    ansible.builtin.template:
       src: templates/test.j2
       dest: /tmp/hostname

test.j2 内容如下:

My name is {{ ansible_facts['hostname'] }}

数据过滤器

过滤器可以将json 数据转换为yaml 数据、拆分url 以提取主机名、获取字符串哈希、乘以或加上整数等。还可以通过python 方法来转换数据,或是创建自定义ansible 过滤器插件
由于模板解析发生在ansible 管理主机上,因此过滤器也是在管理主机上执行并转换数据。

处理未定义变量

过滤器可以通过提供默认值或使变量可选来避免未定义变量的报错。

提供默认值

defalut 过滤器可以提供变量的默认值,还可以简写为d()

{{ some_variable | default(5) }}

如上示例中,变量some_variable 未定义的情况,默认值就是5,不会引发报错。
从2.8 版本开始,访问未定义变量将会返回另一个未定义值,而不是立即报错。
如果想在变量计算结果为false 或空字符串时,使用默认值,则必须将default 过滤器的第二个参数设置为true

{{ lookup('env', 'MY_USER') | default('admin', true) }}
使变量可选

默认情况下,ansible 需要模板表达式中所有变量的值,但是,可以将特定的模块变量设置为可选。设置可选需要将默认值设置为特殊变量omit。例如,希望对某些项目使用系统默认值,某些项目使用变量值控制:

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

此示例中,文件/tmp/foo/tmp/bar 因为没有设置mode 变量,则会使用系统默认的umask,而文件/tmp/baz 则会使用变量mode=0444。

注:如果在过滤器default(omit) 之后需要加其他过滤器,则应该使用这种方式:"{{ foo | default(None) | some_filter or omit }}"。这种方式如果变量未定义则会使用默认值None,将导致后续过滤器失败,从而执行or omit 部分。

定义必须值

如果ansible 通过将DEFAULT_UNDEFINED_VAR_BEHAVIOR 设置为false来忽略未定义的变量,但是可能需要确保某些变量不能是未定义的,这就可以将某些变量定义为必须值:

{{ variable | mandatory }}

被定义为必须值的变量,基本配置为忽略未定义变量,也会在执行时报错。
如果在未定义变量时想自定义报错提示,可以使用undef 方法:

galaxy_url: "https://galaxy.ansible.com"
galaxy_api_key: "{{ undef(hint='You must specify your Galaxy API key') }}"

为true/false/null 定义不同的值

可以通过判断语句后接ternary 过滤器,当判断为true 时,返回第一个参数,判断为false 时,返回第二个参数:

{{ (status == 'needs_restart') | ternary('restart', 'continue') }}

此外,变量值除了true/false 外,当为null 时还可返回第三个参数:

{{ enabled | ternary('no shutdown', 'shutdown', omit) }}

管理数据类型

在很多情况下,需要变换数据类型。例如,当下一个任务需要列表时,而注册变量可能包含字典。可以使用ansible.builtin.type_debugansible.builtin.dict2itemsansible.builtin.items2dict 等过滤器来调整数据类型。

显示数据类型

如果不确定变量的数据类型,可以使用ansible.builtin.type_debug 过滤器来显示,这在调试时非常有用:

{{ myvar | type_debug }}

还可通过类型测试来判断变量类型。

将字典转换为列表

可以使用ansible.builtin.dict2items 过滤器将字典转换为适合循环的列表:

{{ dict | dict2items }}

原始字典数据:

tags:
  Application: payment
  Environment: dev

转换后的列表数据:

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

如果想要将转换后的列表中,keyvalue 关键字替换,可以使用参数key_namevalue_name 进行过滤器配置:

{{ files | dict2items(key_name='file', value_name='path') }}

原始字典数据:

files:
  users: /etc/passwd
  groups: /etc/group

转换后的列表数据:

- file: users
  path: /etc/passwd
- file: groups
  path: /etc/group
将列表转换为字典

使用ansible.builtin.items2dict 过滤器可以将列表转换为字典,内容可以映射为key: value 对。

{{ tags | items2dict }}

原始列表数据:

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

转换后的字典数据:

Application: payment
Environment: dev

如果列表中并没有使用key/value 关键字,则需要给过滤器传key_name/value_name 两个参数来转换:

{{ fruits | items2dict(key_name='fruit', value_name='color') }}
强制类型转换

可以将某些值转换为某些类型,例如,从vars_prompt 中输入“True”,并希望将其识别为布尔值而不是字符串:

- ansible.builtin.debug:
     msg: test
  when: some_string_value | bool

如果想对变量进行大小比较,并希望将其识别为整数而不是字符串:

- shell: echo "only on Red Hat 6, derivatives, and later"
  when: ansible_facts['os_family'] == "RedHat" and ansible_facts['lsb']['major_release'] | int >= 6

格式化数据:yaml 和json

ansible.builtin.to_jsonansible.builtin.to_yaml 过滤器可以将模板中的数据格式从json 或yaml 转换为yaml 或json,并提供格式化、缩进和加载数据的选项:

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

ansible.builtin.to_nice_jsonansible.builtin.to_nice_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) }}

ansible.builtin.to_yaml 和ansible.builtin.to_nice_yaml 过滤器使用pyYAML 库,该库默认字符串长度限制为80个字符。如果第80个字符后有空格,可能会导致意外换行。可以使用width 参数来避免:

{{ some_variable | to_yaml(indent=8, width=1337) }}
{{ some_variable | to_nice_yaml(indent=8, width=1337) }}

还可从已经格式化好的变量中读取数据并解析:

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

例如,从json 文件中读取json 数据并将其保存为json 格式的变量:

tasks:
  - name: Register JSON output as a variable
    ansible.builtin.shell: cat /some/path/to/file.json
    register: result

  - name: Set a variable
    ansible.builtin.set_fact:
      myvar: "{{ result.stdout | from_json }}"

如果需要解析yaml 多文档文件,可以使用ansible.builtin.from_yaml_all过滤器,解析后将会返回一个可迭代的生成器: 例如,yaml 文件中包含多个文档部分:

---
part_one: one
...

---
part_two: two
...

解析方式:

tasks:
  - name: Register a file content as a variable
    ansible.builtin.shell: cat /some/path/to/multidoc-file.yaml
    register: result

  - name: Print the transformed variable
    ansible.builtin.debug:
      msg: '{{ item }}'
    loop: '{{ result.stdout | from_yaml_all | list }}'
to_json 过滤器字符编码

默认情况下,ansible.builtin.to_json 和ansible.builtin.to_nice_json 会将收到的数据转换为ASCII,例如:

{{ 'München'| to_json }}

将转换为:

'M\u00fcnchen'

如果要保留unicode 字符,需要设置过滤器参数ensure_ascii=False

{{ 'München'| to_json(ensure_ascii=False) }}

'München'

组合和选择数据

ansible 可以组合来自多个源和类型的数据,或从大型数据结构中选择数据,从而精细控制复杂的数据。

组合多个列表中的值

通过ansible.builtin.zip 过滤器可以组合多个列表中的元素进行组合:

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

# => [[1, "a"], [2, "b"], [3, "c"], [4, "d"], [5, "e"], [6, "f"]]

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

# => [[1, "a"], [2, "b"], [3, "c"]]

如果组合的列表长度不一致,可以使用ansible.builtin.zip_longest,并设置fillvalue 参数,当列表不够长时,使用其他数据来填充:

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

# => [[1, "a", 21], [2, "b", 22], [3, "c", 23], ["X", "d", "X"], ["X", "e", "X"], ["X", "f", "X"]]

通过zip 还可以构造出字典:

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

原始列表数据:

keys_list:
  - one
  - two
values_list:
  - apple
  - orange

构造出的字典:

one: apple
two: orange
组合对象和子元素

ansible.builtin.subelements 过滤器可以生成对象和该对象的子元素值的乘积,例如:

{{ 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
    - /tmp/alice/twokey.pub
  - wheel
-
  - name: alice
    groups:
    - wheel
    - docker
    authorized:
    - /tmp/alice/onekey.pub
    - /tmp/alice/twokey.pub
  - docker
-
  - name: bob
    authorized:
    - /tmp/bob/id_rsa.pub
    groups:
    - docker
  - docker

可以通过这种方法,来迭代多个对象的同一子元素:

- name: Set authorized ssh key, extracting just that data from 'users'
  ansible.posix.authorized_key:
    user: "{{ item.0.name }}"
    key: "{{ lookup('file', item.1) }}"
  loop: "{{ users | subelements('authorized') }}"
组合哈希/字典

ansible.builtin.combine 过滤器可以合并哈希值,例如,覆盖哈希中的一个键:

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

生成结果:

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

过滤器还可以采用多个变量进行合并:

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

这种情况,b 中的键会覆盖ac 中的键会覆盖bd 中的键会覆盖c
该过滤器还可以设置两个可选参数:recursivelist_merge
recursive 是一个布尔值,默认为False。该参数是用来设置是否进行递归合并嵌套的哈希值。该参数与配置文件中的hash_behaviour 无关。
list_merge 参数默认值为replace,还可设置为keepappendprependappend_rpprepend_rp。当要合并的哈希包含数组/列表时,该参数会修改合并的行为。 例如,原始哈希如下:

default:
  a:
    x: default
    y: default
  b: default
  c: default
patch:
  a:
    y: patch
    z: patch
  b: patch

使用recursive 默认值进行合并,则不会合并嵌套哈希:

{{ default | combine(patch) }}

合并结果:

a:
  y: patch
  z: patch
b: patch
c: default

如果设置recursive=True,则会递归嵌套哈希并进行合并:

{{ default | combine(patch, recursive=True) }}

合并结果:

a:
  x: default
  y: patch
  z: patch
b: patch
c: default

例如,原始哈希如下:

default:
  a:
    - default
patch:
  a:
    - patch

使用list_merge 默认值进行合并,则右边的哈希会替换左侧的哈希:

{{ default | combine(patch) }}

合并结果:

a:
  - patch

如果设置list_merge='keep',则将保留左侧哈希中的值:

{{ default | combine(patch, list_merge='keep') }}

合并结果:

a:
  - default

如果设置list_merge='append',则右侧哈希将附加到左侧哈希之后:

{{ default | combine(patch, list_merge='append') }}

合并结果:

a:
  - default
  - patch

如果设置list_merge='prepend',则右侧哈希将添加左侧哈希之前:

{{ default | combine(patch, list_merge='prepend') }}

合并结果:

a:
  - patch
  - default

例如,原始哈希如下:

default:
  a:
    - 1
    - 1
    - 2
    - 3
patch:
  a:
    - 3
    - 4
    - 5
    - 5

如果设置list_merge='append_rp',则右侧哈希将替换左侧的哈希,并保留两个哈希中不重复的元素:

{{ default | combine(patch, list_merge='append_rp') }}

合并结果:

a:
  - 1
  - 1
  - 2
  - 3
  - 4
  - 5
  - 5

如果设置list_merge='prepend_rp',其行为与append_rp 类似,但右侧哈希会被添加到左侧之前:

{{ default | combine(patch, list_merge='prepend_rp') }}

合并结果:

a:
  - 3
  - 4
  - 5
  - 5
  - 1
  - 1
  - 2

例如,原始哈希如下:

default:
  a:
    a':
      x: default_value
      y: default_value
      list:
        - default_value
  b:
    - 1
    - 1
    - 2
    - 3
patch:
  a:
    a':
      y: patch_value
      z: patch_value
      list:
        - patch_value
  b:
    - 3
    - 4
    - 4
    - key: value

recursivelist_merge 一起使用时:

{{ default | combine(patch, recursive=True, list_merge='append_rp') }}

合并结果:

a:
  a':
    x: default_value
    y: patch_value
    z: patch_value
    list:
      - default_value
      - patch_value
b:
  - 1
  - 1
  - 2
  - 3
  - 4
  - 4
  - key: value
从数组或哈希中选择值

过滤器map('extract') 根据指定位置或指定key,提取出列表或字典内的数据:

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

上述结果为:

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

map('extract') 还可设置其他参数实现复杂的提取工作,例如,需要提取主机组x的ip 地址,则可通过 hostvars 变量中获取:

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

map('extract') 的第三个参数也可以是一个列表,用于递归查找:

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

上述将返回b['a']['x']['y'] 的值。

组合列表

ansible.builtin.permutations 过滤器 可以获取列表的排列(相当于数学运算中的排列A):

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

- name: Give me permutations of sets of three
  ansible.builtin.debug:
    msg: "{{ [1,2,3,4,5] | ansible.builtin.permutations(3) | list }}"

ansible.builtion.combinations 过滤器 可以获取列表的组合(相当于数学运算中的C):

- name: Give me combinations for sets of two
  ansible.builtin.debug:
    msg: "{{ [1,2,3,4,5] | ansible.builtin.combinations(2) | list }}"

product 过滤器 可以返回可迭代对象的笛卡尔积,相当于嵌套for 循环的生成表达式:

- name: Generate multiple hostnames
  ansible.builtin.debug:
    msg: "{{ ['foo', 'bar'] | product(['com']) | map('join', '.') | join(',') }}"

输出结果为:

{ "msg": "foo.com,bar.com" }
选择json 数据

要从json 格式的复杂结构中选择单个元素或数据子集,可以使用community.general.json_query 过滤器

注:此过滤器已经迁移到community.general 中,使用前需要进行安装操作。
同时,此过滤器依赖jmespath,因此还需提前手动安装jmespath。 例如,原始json 数据如下:

{
    "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
  ansible.builtin.debug:
    var: item
  loop: "{{ domain_definition | community.general.json_query('domain.cluster[*].name') }}"

要从此结构中提取所有服务器名:

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

要从cluster1 中提取端口:

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

注:使用变量查询更具可读性

要以逗号分隔的字符串打印cluster1 中的端口:

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

注:使用反引号引用字符串,可以避免转义引号并保持可读性

也可以使用yaml 的单引号转义

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

注:yaml 中单引号转义可以通过单引号加倍来实现

要获取包含集群的所有端口和名称的哈希映射:

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

要从名称以server1 开头的所有集群中提取端口:

- name: Display ports from all clusters with the name starting with 'server1'
  ansible.builtin.debug:
    msg: "{{ domain_definition | to_json | from_json | community.general.json_query(server_name_query) }}"
  vars:
    server_name_query: "domain.server[?starts_with(name,'server1')].port"

要从名称含server1 的所有集群中提取端口:

- name: Display ports from all clusters with the name containing 'server1'
  ansible.builtin.debug:
    msg: "{{ domain_definition | to_json | from_json | community.general.json_query(server_name_query) }}"
  vars:
    server_name_query: "domain.server[?contains(name,'server1')].port"

随机化数据

当需要生成随机数据时,可以使用如下的过滤器。

随机mac 地址

random_mac 过滤器可以从字符串前缀生成随机mac 地址。

注:此过滤器已经迁移到community.general 中,使用前需要进行安装操作。

例如,要从以"52:54:00" 开头的字符串前缀中获取随机mac 地址:

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

从ansible 2.9 版本开始,还可以从seed 初始化随机数生成器,以创建随机但幂等的mac 地址:

"{{ '52:54:00' | community.general.random_mac(seed=inventory_hostname) }}"
随机数或随机列表元素

ansible.builtin.random 过滤器是jinja2 默认随机过滤器的扩展,可以从列表中返回随机元素,或根据范围生成随机数。
要从列表中返回随机元素:

"{{ ['a','b','c'] | random }}"
# => 'c'

要获取0(含)到指定整数(不含)之间的随机数:

"{{ 60 | random }} * * * * root /script/from/cron"
# => '21 * * * * root /script/from/cron'

要获取0 到100 之间步长为10 的随机数:

{{ 101 | random(step=10) }}
# => 70

要获取1 到100 之间步长为10的随机数:

{{ 101 | random(1, 10) }}
# => 31
{{ 101 | random(start=1, step=10) }}
# => 51

可以通过seed 初始化随机数生成器以创建针对主机幂等的随机数:

"{{ 60 | random(seed=inventory_hostname) }} * * * * root /script/from/cron"
打乱列表

ansible.builtin.shuffle 过滤器可以随机化现有列表的顺序。

要从现有列表中获取随机列表:

{{ ['a','b','c'] | shuffle }}
# => ['c','a','b']
{{ ['a','b','c'] | shuffle }}
# => ['b','c','a']

可以通过seed 初始化shuffle 生成器以生成针对主机幂等的随机列表:

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

管理列表变量

ansible 提供了可以搜索列表中最小值、最大值,或者将嵌套列表展平为一维列表等功能。 要从数字列表中获取最小值:

{{ list1 | min }}

要获取对象列表的最小值:

{{ [{'val': 1}, {'val': 2}] | min(attribute='val') }}

要从数字列表中获取最大值:

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

要获取对象列表的最大值:

{{ [{'val': 1}, {'val': 2}] | max(attribute='val') }}

展平嵌套列表:

{{ [3, [4, 2] ] | flatten }}
# => [3, 4, 2]

展平嵌套列表的第一级:

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

在展平列表时默认会删除空值,若需保留:

{{ [3, None, [4, [2]] ] | flatten(levels=1, skip_nulls=False) }}
# => [3, None, 4, [2]]

集合与列表的选择操作

ansible 提供了从集合或列表中选择或组合元素的功能。 要从列表中获取唯一的集合:

# list1: [1, 2, 5, 1, 3, 4, 10]
{{ list1 | unique }}
# => [1, 2, 5, 3, 4, 10]

要获取两个列表的并集:

# list1: [1, 2, 5, 1, 3, 4, 10]
# list2: [1, 2, 3, 4, 5, 11, 99]
{{ list1 | union(list2) }}
# => [1, 2, 5, 1, 3, 4, 10, 11, 99]

要获取两个列表的交集:

# list1: [1, 2, 5, 3, 4, 10]
# list2: [1, 2, 3, 4, 5, 11, 99]
{{ list1 | intersect(list2) }}
# => [1, 2, 5, 3, 4]

要获取两个列表的差异(列表1 中元素不存在于列表2):

# list1: [1, 2, 5, 1, 3, 4, 10]
# list2: [1, 2, 3, 4, 5, 11, 99]
{{ list1 | difference(list2) }}
# => [10]

要获取两个列表的对称差(每个列表中独有的元素):

# list1: [1, 2, 5, 1, 3, 4, 10]
# list2: [1, 2, 3, 4, 5, 11, 99]
{{ list1 | symmetric_difference(list2) }}
# => [10, 11, 99]

计算数字

可以通过ansible 过滤器计算数字的对数、幂和根等。jinja2 还提供了其他数学函数,如abs()round() 等。 获取对数(默认为e):

{{ 8 | log }}
# => 2.0794415416798357

获取以10 为底的对数:

{{ 8 | log(10) }}
# => 0.9030899869919435

获取数字的5次幂:

{{ 8 | pow(5) }}
# => 32768.0

获取数字的平方根或5次根:

{{ 8 | root }}
# => 2.8284271247461903

{{ 8 | root(5) }}
# => 1.5157165665103982

管理网络交互

本节介绍的过滤器可以实现常见的网络任务。

本节介绍的过滤器已经迁移到ansible.netcommon 集合,使用前需要先安装。

ip 地址过滤器

ansible.netcommon.ipaddr 过滤器可以实现ip 地址的相关操作。
要测试字符串是否是有效的ip 地址:

{{ myvar | ansible.netcommon.ipaddr }}

可以从设备字典中提取特定版本的ip 地址:

{{ myvar | ansible.netcommon.ipv4 }}
{{ myvar | ansible.netcommon.ipv6 }}

ip 地址过滤器还可以从ip 地址中提取特定信息,例如,从CIDR 获取ip 地址:

{{ '192.0.2.1/24' | ansible.netcommon.ipaddr('address') }}
# => 192.0.2.1
网络cli 过滤器

要将网络设备cli 命令(如交换机上的命令)的输出转换为结构化json 输出,需要使用ansible.netcommon.parse_cli 过滤器

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

parse_cli 过滤器将加载规范文件并通过它传递命令输出,返回json 输出。yaml 规范文件可以定义如何解析cli 输出。
如下,是一个有效的规范文件示例,将用来解析交换机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

上面的规范文件将返回一个json 数据结构。
可以使用key 和values 指令将相同的命令解析到一个哈希。下面是使用相同命令show vlan 解析输出的示例:

---
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 命令的另一个常见用例是将大型命令分解为可解析的块。可以使用start_blockend_block 指令来分解:

---
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 命令解析为哈希列表。
网络过滤器还支持使用TextFSM 库解析cli 命令的输出,要使用TextFSM 解析cli 输出,需要使用以下过滤器:

{{ output.stdout[0] | ansible.netcommon.parse_cli_textfsm('path/to/fsm') }}

使用TextFSM 过滤器需要安装TextFSM 库。

网络xml 过滤器

要将网络设备命令的xml 输出转换为结构化json 输出,需要使用ansible.netcommon.parse_xml 过滤器

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

parse_xml 过滤器将加载规范文件并以json 格式传递命令输出。
规范文件为yaml 格式,并定义了如何解析xml 输出并返回json 数据。
如下,是一个有效的规范文件示例,将解析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']"

上面的规范文件将返回一个json 数据结构,是带有解析的vlan 信息的哈希列表。 可以使用key 和values 指令将相同的命令解析为哈希。下面是使用相同命令show vlan | display xml 将输出解析为哈希的示例:

---
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']"

top 的值是相对于xml 根节点的xpath。在下面给出的xml 输出示例中,top 的值为configuration/vlans/vlan,它是相对于根节点(<rpc-reply>)的xpath 表达式。configuraiontop 值为最外层容器节点,vlan 为最内层容器节点。
items 是键值对的字典,它将用户定义的名称映射到选择元素的xpath 表达式。xpath 表达式是相对于top 中包含的xpath 值的值。例如,vlan_id 规范文件中的top 是用户定义的名称,其值vlan-id 是相对于top 中xpath 的值。
可以使用xpath 表达式提取xml 标签的属性,规范中state 的值是一个xpath 表达式,用于获取输出xml 中vlan 标记的属性:

<rpc-reply>
  <configuration>
    <vlans>
      <vlan inactive="inactive">
       <name>vlan-1</name>
       <vlan-id>200</vlan-id>
       <description>This is vlan-1</description>
      </vlan>
    </vlans>
  </configuration>
</rpc-reply>

注:有关支持的xpath 表达式的更新信息,请参阅XPath 支持

网络vlan 过滤器

ansible.netcommon.vlan_parser 过滤器可以根据类似ios 的vlan 列表规则将未排序的vlan 整数列表转换为排序的整数字符串列表。该列表具有以下属性:

  • vlan 按升序列出
  • 三个或更多连续的vlan 用破折号列出
  • 列表的第一行可以是first_line_len 个字符长
  • 后续列表行可以是other_line_len 个字符

对vlan 列表进行排序:

{{ [3003, 3004, 3005, 100, 1688, 3002, 3999] | ansible.netcommon.vlan_parser }}

此示例结果为:

['100,1688,3002-3005,3999']

另一个jinja2 模板示例:

{% set parsed_vlans = vlans | ansible.netcommon.vlan_parser %}
switchport trunk allowed vlan {{ parsed_vlans[0] }}
{% for i in range (1, parsed_vlans | count) %}
switchport trunk allowed vlan add {{ parsed_vlans[i] }}
{% endfor %}

这允许在cisco ios 标记接口上动态生成vlan 列表。可以存储接口所需的确切vlan 的详尽原始列表,然后将其与实际为配置生成的解析ios 输出进行比较。

散列、加密字符串和密码

获取字符串的sha1 哈希值:

{{ 'test1' | hash('sha1') }}
# => "b444ac06613fc8d63795be9ad0beaf55011936ac"

获取字符串的md5 哈希值:

{{ 'test1' | hash('md5') }}
# => "5a105e8b9d40e1329780d62ea2265d8a"

获取字符串校验和:

{{ 'test2' | checksum }}
# => "109f4b3c50d7b0df729d299bc6f8e9ef9066971f"

其他哈希值:

{{ 'test2' | hash('blowfish') }}

获取sha512 密码哈希(随机盐):

{{ 'passwordsaresecret' | password_hash('sha512') }}
# => "$6$UIv3676O/ilZzWEE$ktEfFF19NQPF2zyxqxGkAceTnbEgpEKuGBtk6MlU4v2ZorWaVQUMyurgmHCh2Fr4wpmQ/Y.AlXMJkRnIS4RfH/"

获取具有特定盐的sha256 密码哈希:

{{ 'secretpassword' | password_hash('sha256', 'mysecretsalt') }}
# => "$5$mysecretsalt$ReKNyDYjkKNqRVwouShhsEqZ3VOE8eoVO4exihOfvG4"

使用一致的盐可以为每个主机生成幂等的唯一哈希值:

{{ 'secretpassword' | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}
# => "$6$43927$lQxPKz2M2X.NWO.gK.t7phLwOKQMcSq72XxDZQ0XzYV6DlL1OD72h417aj16OnHTGxNzhftXJQBcjbunLEepM0"

不同的哈希过滤器需要依赖不同的库,ansible.builtin.hash 依赖hashlibansible.builtin.password_hash 依赖passlib。如果未安装passlib,则会使用crypt
某些哈希类型允许提供rounds 参数:

{{ 'secretpassword' | password_hash('sha256', 'mysecretsalt', rounds=10000) }}
# => "$5$rounds=10000$mysecretsalt$Tkm80llAxD4YHll6AgNIztKn0vzAACsuuEfYeGP7tm7"

不同的rounds 参数会产生不同的效果,crypt 的默认值为5000,passlib 的默认值分别为535000(sha256),656000(sha512):

{{ 'secretpassword' | password_hash('sha256', 'mysecretsalt', rounds=5001) }}
# => "$5$rounds=5001$mysecretsalt$wXcTWWXbfcR8er5IVf7NuquLvnUA6s8/qdtOhAZ.xN."

哈希类型blowfish(BCrypt)提供了指定算法版本的参数:

{{ 'secretpassword' | password_hash('blowfish', '1234567890123456789012', ident='2b') }}
# => "$2b$12$123456789012345678901uuJ4qFdej6xnWjOQT.FStqfdoY8dYUPC"

注:该参数仅适用于blowfish(BCrypt),此参数的有效值为:['2', '2a', '2y', '2b']

还可以使用ansible.builtin.vault 过滤器来加密数据:

# simply encrypt my key in a vault
vars:
  myvaultedkey: "{{ keyrawdata|vault(passphrase) }}"

- name: save templated vaulted data
  template: src=dump_template_data.j2 dest=/some/key/vault.txt
  vars:
    mysalt: '{{ 2**256|random(seed=inventory_hostname) }}'
    template_data: '{{ secretdata|vault(vaultsecret, salt=mysalt) }}'

然后使用unvault 过滤器解密:

# simply decrypt my key from a vault
vars:
  mykey: "{{ myvaultedkey|unvault(passphrase) }}"

- name: save templated unvaulted data
  template: src=dump_template_data.j2 dest=/some/key/clear.txt
  vars:
    template_data: '{{ secretdata|unvault(vaultsecret) }}'

操作文本

ansible 提供了多个可以处理文本的过滤器,包括操作url、文件名和路径名等。

向文件添加注释

ansible.builtin.comment 过滤器可以根据模板中的内容在文件中创建多种注释样式。默认情况下,会使用# 进行注释,并在注释文本的上方和下方添加空白注释行。例如:

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

输出注释:

#
# Plain style (default)
#

ansible 也提供了C(//...)、C block(/*...*/)、Erlang(%...) 和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
#
#######
   ###
    #

该过滤器还可以应用于任意ansible 变量。例如,为了使变量输出更具可读性,可以在ansible.cfg 文件中定义ansible_managed 变量为:

[defaults]

ansible_managed = This file is managed by Ansible.%n
  template: {file}
  date: %Y-%m-%d %H:%M:%S
  user: {uid}
  host: {host}

然后使用变量进行注释:

{{ ansible_managed | comment }}

注释将输出为:

#
# This file is managed by Ansible.
#
# template: /home/ansible/env/dev/ansible_managed/roles/role1/templates/test.j2
# date: 2015-09-10 11:02:58
# user: ansible
# host: myhost
#
url 编码变量

urlencode 过滤器可以将UTF-8 编码的字符串转换为url 编码格式:

{{ 'Trollhättan' | urlencode }}
# => 'Trollh%C3%A4ttan'
分割url

ansible.builtin.urlsplit 过滤器可以从url 中提取片段、主机名、netloc、密码、路径、端口、查询、scheme 和用户名等。如果不设置参数的话,默认返回所有字段的字典:

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

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('netloc') }}
# => 'user:password@www.acme.com:9000'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('username') }}
# => 'user'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('password') }}
# => 'password'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('path') }}
# => '/dir/index.html'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('port') }}
# => '9000'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('scheme') }}
# => 'http'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('query') }}
# => 'query=term'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit('fragment') }}
# => 'fragment'

{{ "http://user:password@www.acme.com:9000/dir/index.html?query=term#fragment" | urlsplit }}
# =>
#   {
#       "fragment": "fragment",
#       "hostname": "www.acme.com",
#       "netloc": "user:password@www.acme.com:9000",
#       "password": "password",
#       "path": "/dir/index.html",
#       "port": 9000,
#       "query": "query=term",
#       "scheme": "http",
#       "username": "user"
#   }
使用正则表达式搜索字符串

ansible.builtin.regex_search 过滤器可以通过正则表达式搜索字符串或提取字符串的一部分:

# Extracts the database name from a string
{{ 'server1/database42' | regex_search('database[0-9]+') }}
# => 'database42'

# Example for a case insensitive search in multiline mode
{{ 'foo\nBAR' | regex_search('^bar', multiline=True, ignorecase=True) }}
# => 'BAR'

# Example for a case insensitive search in multiline mode using inline regex flags
{{ 'foo\nBAR' | regex_search('(?im)^bar') }}
# => 'BAR'

# Extracts server and database id from a string
{{ 'server1/database42' | regex_search('server([0-9]+)/database([0-9]+)', '\\1', '\\2') }}
# => ['1', '42']

# Extracts dividend and divisor from a division
{{ '21/42' | regex_search('(?P<dividend>[0-9]+)/(?P<divisor>[0-9]+)', '\\g<dividend>', '\\g<divisor>') }}
# => ['21', '42']

如果过滤器找不到匹配项则会返回空字符串:

{{ 'ansible' | regex_search('foobar') }}
# => ''

注:ansible.builtin.regex_search 过滤器在jinja2 表达式(例如与运算符、其他过滤器等结合)中使用时会返回None。例如:

{{ 'ansible' | regex_search('foobar') == '' }}
# => False
{{ 'ansible' | regex_search('foobar') is none }}
# => True
# 说明regex_search 在找不到字符串时并没有按预期返回空字符串,而是返回了None

这是由于ansible 中的历史行为和jinja2 内部的自定义实现造成的。如果希望regex_search 过滤器始终返回None,需要启用设置jinja2_native。请参阅为什么 regex_search 过滤器返回 None 而不是空字符串?

要提取字符串中所有出现的正则表达式匹配项,可以使用ansible.builtin.regex_findall 过滤器

# Returns 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') }}
# => ['8.8.8.8', '8.8.4.4']

# Returns all lines that end with "ar"
{{ 'CAR\ntar\nfoo\nbar\n' | regex_findall('^.ar$', multiline=True, ignorecase=True) }}
# => ['CAR', 'tar', 'bar']

# Returns all lines that end with "ar" using inline regex flags for multiline and ignorecase
{{ 'CAR\ntar\nfoo\nbar\n' | regex_findall('(?im)^.ar$') }}
# => ['CAR', 'tar', 'bar']

要使用正则表达式替换字符串中的文本,可以使用ansible.builtin.regex_replace 过滤器

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

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

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

# Convert "localhost:80" to "localhost"
{{ 'localhost:80' | regex_replace(':80') }}
# => 'localhost'

# Comment all lines that end with "ar"
{{ 'CAR\ntar\nfoo\nbar\n' | regex_replace('^(.ar)$', '#\\1', multiline=True, ignorecase=True) }}
# => '#CAR\n#tar\nfoo\n#bar\n'

# Comment all lines that end with "ar" using inline regex flags for multiline and ignorecase
{{ 'CAR\ntar\nfoo\nbar\n' | regex_replace('(?im)^(.ar)$', '#\\1') }}
# => '#CAR\n#tar\nfoo\n#bar\n'

注:如果想通过* 匹配整个字符串,需要确保正则表达式中使用开始/结束锚点。例如,^(.*)$ 将始终匹配一个结果,而(.*) 在某些python 版本上将匹配整个字符串和末尾的空字符串,这意味着将进行两次替换。

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

BAD:
{{ hosts | map('regex_replace', '(.*)', 'https://\\1') | list }}

# append ':80' to each item in a list
GOOD:
{{ hosts | map('regex_replace', '^(.*)$', '\\1:80') | list }}
{{ hosts | map('regex_replace', '(.+)', '\\1:80') | list }}
{{ hosts | map('regex_replace', '$', ':80') | list }}

BAD:
{{ hosts | map('regex_replace', '(.*)', '\\1:80') | list }}

要转义标准python 正则表达式中的特殊字符,可以使用ansible.builtin.regex_escape 过滤器

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

要转义posix 基本正则表达式中的特殊字符,需要将regex_escape 过滤器参数设置为re_type='posix_basic'

# convert '^f.*o(.*)$' to '\^f\.\*o(\.\*)\$'
{{ '^f.*o(.*)$' | regex_escape('posix_basic') }}
管理文件名和路径名

要获取路径中的文件名,例如/etc/asdf/foo.txt 中的foo.txt

{{ path | basename }}

要获取windows 路径格式的文件名:

{{ path | win_basename }}

要将windows 的盘符与文件路径的其余部分分开:

{{ path | win_splitdrive }}

要仅获取windows 的盘符:

{{ path | win_splitdrive | first }}

要获取不带盘符的其余部分:

{{ path | win_splitdrive | last }}

要从路径获取目录:

{{ path | dirname }}

要从windows 路径获取目录:

{{ path | win_dirname }}

要将包含~ 符号的路径替换为完整路径:

{{ path | expanduser }}

要将包含环境变量的路径补充完整:

{{ path | expandvars }}

要获取链接的真实路径:

{{ path | realpath }}

要获取链接的相对路径,从起点开始:

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

要获取路径或文件名的根目录和扩展名:

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

ansible.builtin.splitex 过滤器 始终返回一对字符串,可以使用firshlast 返回不同部分:

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

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

要连接一个或多个路径组件:

{{ ('/etc', path, 'subdir', file) | path_join }}

操作字符串

要为shell 用法添加引号,可以使用ansible.builtin.quote 过滤器

- name: Run a shell command
  ansible.builtin.shell: echo {{ string_value | quote }}

要将列表连接成字符串:

{{ list | join(" ") }}

要将字符串拆分为列表:

{{ csv_string | split(",") }}

要使用base64 编码的字符串,可以使用ansible.builtin.b64encode 过滤器

{{ encoded | b64decode }}
{{ decoded | string | b64encode }}

要使用不同的编码类型,可以添加encoding 参数,默认值为utf-8

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

注:b64decode 的返回值是一个字符串。如果使用b64decode 解密二进制blob,然后将其写入文件,可能会发现二进制文件已损坏。如果需要获取base64 编码的二进制文件,最好使用系统命令base64 与shell 模块结合,并使用stdin 参数通过管道传入编码数据。例如:shell: cmd="base64 --decode > myfile.bin" stdin="{{ encoded }}"

管理uuid

创建uuidv5 命名空间:

{{ string | to_uuid(namespace='11111111-2222-3333-4444-555555555555') }}

要使用默认ansible 命名空间“361E6D51-FAEC-444A-9079-341386DA8E2E” 创建uuidv5 命名空间:

{{ string | to_uuid }}

要使用复杂变量列表中每一项的一个属性,可以使用jinja2 map 过滤器:

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

处理日期和时间

要从字符串中获取日期对象,可以使用to_datetime 过滤器:

# Get the 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, and so on 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 the 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  }}

注:有关使用python 日期格式字符串的完整格式代码列表,可以参阅python 日期时间文档

要将日期输出为字符串,可以使用strftime 过滤器:

# Display year-month-day
{{ '%Y-%m-%d' | strftime }}
# => "2021-03-19"

# Display hour:min:sec
{{ '%H:%M:%S' | strftime }}
# => "21:51:04"

# Use ansible_date_time.epoch fact
{{ '%Y-%m-%d %H:%M:%S' | strftime(ansible_date_time.epoch) }}
# => "2021-03-19 21:54:09"

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

注:strftime 过滤器默认获取的是本地时间,如果想要获取远程主机时间,可以通过获取远程主机时间戳ansible_date_time.epoch,然后转换为需要的时间格式。

strftime 可以设置utc 参数,默认为False,表示采用本地时区:

{{ '%H:%M:%S' | strftime }}           # time now in local timezone
{{ '%H:%M:%S' | strftime(utc=True) }} # time now in UTC

要获取其他的时间转换格式,可参阅docs.python.org/3/library/t…

获取kubernetes 资源信息

注:本节介绍的过滤器已经迁移到了kubernetes.core 集合,使用前请先安装集合。

可以使用k8s_config_resource_name 过滤器来获取kubernetes configmap 或secret 的名称,包括其哈希值:

{{ configmap_resource_definition | kubernetes.core.k8s_config_resource_name }}

然后可以使用它来引用pod 规范中的哈希值:

my_secret:
  kind: Secret
  metadata:
    name: my_secret_name

deployment_resource:
  kind: Deployment
  spec:
    template:
      spec:
        containers:
        - envFrom:
            - secretRef:
                name: {{ my_secret | kubernetes.core.k8s_config_resource_name }}

测试test

jinja2 中的测试是评估模板表达式并返回True 或False 的一种方法,jinja2 内置了很多测试方法,可以参考jinja.palletsprojects.com/en/latest/t…

测试语法

测试语法与过滤器语法不同。使用jinja2 的测试语法如下:

variable is test_name

例如:

result is failed

测试字符串

要将字符串与子字符串或正则表达式匹配,需要使用matchsearchregex 测试:

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

tasks:
    - debug:
        msg: "matched pattern 1"
      when: url is match("https://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")

    - debug:
        msg: "matched pattern 4"
      when: url is regex("example.com/\w+/foo")

match 如果从字符串开头匹配到则成功,search 如果在字符串内的任何位置匹配到则成功。默认情况下,regex 其匹配方式与search 类似,但也可以通过传递match_type 参数来配置执行其他测试。完整的参数设置方法可参考:docs.python.org/3/library/r…>
所有字符串测试方法均可设置ignorecase(不区分大小写) 和multiline(多行模式) 参数。

vault

可以通过vault_encrypted 来测试变量是否经过加密:

vars:
  variable: !vault |
    $ANSIBLE_VAULT;1.2;AES256;dev
    61323931353866666336306139373937316366366138656131323863373866376666353364373761
    3539633234313836346435323766306164626134376564330a373530313635343535343133316133
    36643666306434616266376434363239346433643238336464643566386135356334303736353136
    6565633133366366360a326566323363363936613664616364623437336130623133343530333739
    3039

tasks:
  - debug:
      msg: '{{ (variable is vault_encrypted) | ternary("Vault encrypted", "Not vault encrypted") }}'

测试真实性

ansible 可以通过truthyfalsy 来执行真假判断:

- debug:
    msg: "Truthy"
  when: value is truthy
  vars:
    value: "some string"

- debug:
    msg: "Falsy"
  when: value is falsy
  vars:
    value: ""

此外,truthyfalsy 可以测试时接受名为convert_bool 的参数,该参数会尝试将布尔指标转换为实际布尔值:

- debug:
    msg: "Truthy"
  when: value is truthy(convert_bool=True)
  vars:
    value: "yes"

- debug:
    msg: "Falsy"
  when: value is falsy(convert_bool=True)
  vars:
    value: "off"

版本比较

要比较版本号,可以使用version 测试方法,例如比较ansible_facts['distribution_version'] 版本是否大于或等于"12.04",如果大于等于"12.04",则返回True,否则返回False:

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

version 测试支持以下运算符:

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

此方法还支持第三个参数strict,该参数定义是否使用ansible.module_utils.compat.version.StrictVersion 定义的严格版本解析。默认为False(使用ansible.module_utils.compat.version.LooseVersion),True 启用严格版本解析:

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

从ansible 2.11 开始,version 测试支持与strict 参数互斥的参数version_type,该参数支持以下值:

loose, strict, semver, semantic, pep440
  • loose
    该类型对应python distutils.version.LooseVersion 类。所有版本格式都对此类型有效,比较规则简单可预测,但并不能总是得到预期结果。
  • strict
    该类型对应python distutils.version.StrictVersion 类。支持版本号由两个或三个点分割的数字部分组成,末尾带有可选的“预发布”标签。预发布标签由点个字母“a”或“b”后跟一个数字组成。如果两个版本号的数字部分相等,则带有预发布标签的版本将始终被视为比不带预发布标签的版本小(早)。
  • semver/semantic
    该类型实现了用于版本比较的语义版本方案。
  • pep440
    该类型实现了用于版本比较的python PEP-440 版本控制规则。

使用version_type 比较语义版本将实现如下所示:

{{ sample_semver_var is version('2.0.0-rc.1+build.123', 'lt', version_type='semver') }}

在ansible 2.14 中,version_type 添加了pep440 选项,该类型的规则在PEP-440 中定义。以下示例展示了这种类型如何将预发布版本区分为低于常规版本:

{{ '2.14.0rc1' is version('2.14.0', 'lt', version_type='pep440') }}

当在when 中使用version 时,请勿使用{{ }},参阅常见问题解答

vars:
    my_version: 1.2.3

tasks:
    - debug:
        msg: "my_version is higher than 1.0.0"
      when: my_version is version('1.0.0', '>')

集合测试

要查看一个列表是否包含另一个列表或被另一个列表包含,可以使用subset(子集)和superset(超集):

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)

测试列表是否包含值

ansible 支持contains 操作,该操作类似jinja2 中的in 测试。该contains 测试主要用来和selectrejectselectattrrejectattr 过滤器一起使用:

vars:
  lacp_groups:
    - master: lacp0
      network: 10.65.100.0/24
      gateway: 10.65.100.1
      dns4:
        - 10.65.100.10
        - 10.65.100.11
      interfaces:
        - em1
        - em2

    - master: lacp1
      network: 10.65.120.0/24
      gateway: 10.65.120.1
      dns4:
        - 10.65.100.10
        - 10.65.100.11
      interfaces:
          - em3
          - em4

tasks:
  - debug:
      msg: "{{ (lacp_groups|selectattr('interfaces', 'contains', 'em1')|first).master }}"

测试列表是否为True

可以使用anyall 来检查列表中的任意或所有元素是否为真:

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

- debug:
    msg: "path is a directory"
  when: mypath is directory
  vars:
     mypath: /my/path

- debug:
    msg: "path is a file"
  when: "'/my/path' is file"

测试单位格式

human_readablehuman_to_bytes 方法可以确保任务中使用正确的大小格式,并为机器提供字节格式,或为用户提供可读格式。

可读格式

assert 可以判断给定的字符串是否是人类可读的:

- name: "Human Readable"
  assert:
    that:
      - '"1.00 Bytes" == 1|human_readable'
      - '"1.00 bits" == 1|human_readable(isbits=True)'
      - '"10.00 KB" == 10240|human_readable'
      - '"97.66 MB" == 102400000|human_readable'
      - '"0.10 GB" == 102400000|human_readable(unit="G")'
      - '"0.10 Gb" == 102400000|human_readable(isbits=True, unit="G")'

结果是:

{ "changed": false, "msg": "All assertions passed" }
字节格式

以字节格式返回给定字符串:

- name: "Human to Bytes"
  assert:
    that:
      - "{{'0'|human_to_bytes}}        == 0"
      - "{{'0.1'|human_to_bytes}}      == 0"
      - "{{'0.9'|human_to_bytes}}      == 1"
      - "{{'1'|human_to_bytes}}        == 1"
      - "{{'10.00 KB'|human_to_bytes}} == 10240"
      - "{{   '11 MB'|human_to_bytes}} == 11534336"
      - "{{  '1.1 GB'|human_to_bytes}} == 1181116006"
      - "{{'10.00 Kb'|human_to_bytes(isbits=True)}} == 10240"

结果是:

{ "changed": false, "msg": "All assertions passed" }

测试任务结果

以下示例展示了检查任务状态的测试:

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

注:任务状态还可通过success、failure、change 和skip 来匹配

类型测试

当需要判断变量类型时,虽然可以使用type_debug 过滤器与需要判断的类型字符串进行比较,但是规范的做法应该使用类型测试进行比较:

tasks:
  - name: "String interpretation"
    vars:
      a_string: "A string"
      a_dictionary: {"a": "dictionary"}
      a_list: ["a", "list"]
    assert:
      that:
      # Note that a string is classed as also being "iterable" and "sequence", but not "mapping"
      - a_string is string and a_string is iterable and a_string is sequence and a_string is not mapping

      # Note that a dictionary is classed as not being a "string", but is "iterable", "sequence" and "mapping"
      - a_dictionary is not string and a_dictionary is iterable and a_dictionary is mapping

      # Note that a list is classed as not being a "string" or "mapping" but is "iterable" and "sequence"
      - a_list is not string and a_list is not mapping and a_list is iterable

  - name: "Number interpretation"
    vars:
      a_float: 1.01
      a_float_as_string: "1.01"
      an_integer: 1
      an_integer_as_string: "1"
    assert:
      that:
      # Both a_float and an_integer are "number", but each has their own type as well
      - a_float is number and a_float is float
      - an_integer is number and an_integer is integer

      # Both a_float_as_string and an_integer_as_string are not numbers
      - a_float_as_string is not number and a_float_as_string is string
      - an_integer_as_string is not number and a_float_as_string is string

      # a_float or a_float_as_string when cast to a float and then to a string should match the same value cast only to a string
      - a_float | float | string == a_float | string
      - a_float_as_string | float | string == a_float_as_string | string

      # Likewise an_integer and an_integer_as_string when cast to an integer and then to a string should match the same value cast only to an integer
      - an_integer | int | string == an_integer | string
      - an_integer_as_string | int | string == an_integer_as_string | string

      # However, a_float or a_float_as_string cast as an integer and then a string does not match the same value cast to a string
      - a_float | int | string != a_float | string
      - a_float_as_string | int | string != a_float_as_string | string

      # Again, Likewise an_integer and an_integer_as_string cast as a float and then a string does not match the same value cast to a string
      - an_integer | float | string != an_integer | string
      - an_integer_as_string | float | string != an_integer_as_string | string

  - name: "Native Boolean interpretation"
    loop:
    - yes
    - true
    - True
    - TRUE
    - no
    - No
    - NO
    - false
    - False
    - FALSE
    assert:
      that:
      # Note that while other values may be cast to boolean values, these are the only ones that are natively considered boolean
      # Note also that `yes` is the only case-sensitive variant of these values.
      - item is boolean

查找lookup

lookup 插件可以从外部源(例如文件、数据库、键/值存储、api 和其他服务)检索数据。和其他模板一样,lookup 在ansible 控制机上执行。ansible 2.5 之前,lookup 主要是在with_<lookup> 循环构造中间接使用。从ansible 2.5 开始,lookup 主要用作jinja2 表达式中。

在变量中使用lookup

可以使用lookup 来给变量赋值,ansible 每次执行时都会计算该值:

vars:
  motd_value: "{{ lookup('file', '/etc/motd') }}"
tasks:
  - debug:
      msg: "motd value is {{ motd_value }}"

有关ansible-core 中lookup 插件更多详细的信息,可以参阅使用插件。还可以在集合中找到lookup 插件。如果需要获取lookup 插件的列表可以使用命令ansible-doc -l -t lookup 查看。

模板中使用python

ansible 可以通过jinja2 中的模板和变量来使用python 的数据类型和标准函数,但在使用时,必须注意python 版本之间的差异。

字典视图

在python2 中,dict.keys()dict.values()dict.items() 方法会返回一个列表。
在python3 中,这些方法会返回一个字典视图对象。ansible 为了将jinja2 返回的字典视图解析为列表,可以使用list 过滤器:

vars:
  hosts:
    testhost1: 127.0.0.2
    testhost2: 127.0.0.3
tasks:
  - debug:
      msg: '{{ item }}'
    # Only works with Python 2
    #loop: "{{ hosts.keys() }}"
    # Works with both Python 2 and Python 3
    loop: "{{ hosts.keys() | list }}"

dict.iteritems()

python2 中字典有这些方法:iterkeys()itervalues()iteritems() 方法。
而在python3 中没有这些方法。可以使用dict.keys()dict.values()dict.items() 方法来实现兼容:

vars:
  hosts:
    testhost1: 127.0.0.2
    testhost2: 127.0.0.3
tasks:
  - debug:
      msg: '{{ item }}'
    # Only works with Python 2
    #loop: "{{ hosts.iteritems() }}"
    # Works with both Python 2 and Python 3
    loop: "{{ hosts.items() | list }}"

now 方法获取当前时间

jinja2 中now() 方法可以生成当前时间的python 日期时间对象或字符串表示形式。
该方法支持2个参数:

  • utc
    指定True 获取当前utc 时间,默认为False
  • fmt
    接受strftime 字符串,该字符串返回格式化的日期时间字符串。

循环

ansible 提供loopwith_<lookup>until 关键字来多次执行任务。常用的循环包括使用file 模块更改多个文件或目录的所有权,使用user 模块创建多个用户等。

注:loop 是ansible 2.5 版本新增的功能,目前并未完全取代with_<lookup>
loop 语法改进,可以参阅更新日志

比较loopwith_*

  • with_<lookup> 关键字依赖于Lookup 插件
  • loop 关键字相当于with_list,并且是简单循环的最佳选择。
  • loop 关键字不接受字符串作为输入,请参阅Ensuring list input for loop: using query rather than lookup
  • 通常情况下,从with_* 迁移到loop,可以直接替换关键字即可。
  • with_items 更改到loop,需要注意嵌套列表的情况。with_items 默认是单级展平的,而loop 需要使用flatten(1) 才能获得相同的结果。
  • with_items 转换为loop 需要搭配lookup 插件时,不建议修改。

例如:

with_items:
  - 1
  - [2,3]
  - 4

等价于:

loop: "{{ [1, [2, 3], 4] | flatten(1) }}"

例如,如下格式更为简洁:

with_fileglob: '*.txt'

等价但不建议修改为:

loop: "{{ lookup('fileglob', '*.txt', wantlist=True) }}"

标准循环

迭代一个简单的列表

重复任务可以编写为简单字符串列表上的标准循环,并可以直接在任务中定义列表:

- name: Add several users
  ansible.builtin.user:
    name: "{{ item }}"
    state: present
    groups: "wheel"
  loop:
     - testuser1
     - testuser2

可以在变量文件或play 的vars 部分中定义列表,然后再任务中引用列表的名称:

loop: "{{ somelist }}"

可以将列表作为参数直接传递给某些插件或模块,大多数模块(例如yumapt)都具有此功能。列表传参比循环任务更好,例如:

- name: Optimal yum
  ansible.builtin.yum:
    name: "{{ list_of_packages }}"
    state: present

- name: Non-optimal yum, slower and may cause issues with interdependencies
  ansible.builtin.yum:
    name: "{{ item }}"
    state: present
  loop: "{{ list_of_packages }}"

可以参阅模块文档查看具体模块是否支持列表传参。

迭代哈希列表

可以在循环中引用哈希列表的子项,例如:

- name: Add several users
  ansible.builtin.user:
    name: "{{ item.name }}"
    state: present
    groups: "{{ item.groups }}"
  loop:
    - { name: 'testuser1', groups: 'wheel' }
    - { name: 'testuser2', groups: 'root' }

将条件语句与循环结合时,when: 将针对每次迭代单独处理该语句,相关示例可参阅Basic conditionals with when

迭代字典

要循环字典,可以使用dict2items

- name: Using dict2items
  ansible.builtin.debug:
    msg: "{{ item.key }} - {{ item.value }}"
  loop: "{{ tag_data | dict2items }}"
  vars:
    tag_data:
      Environment: dev
      Application: payment

使用循环注册变量

可以将循环的输出注册为变量,例如:

- name: Register loop output as a variable
  ansible.builtin.shell: "echo {{ item }}"
  loop:
    - "one"
    - "two"
  register: echo

当循环时使用register,存储在变量中的数据结构将包含一个result 属性,该属性值是来自所有响应的一个列表。这与不使用循环时返回的数据结构不同:

{
    "changed": true,
    "msg": "All items completed",
    "results": [
        {
            "changed": true,
            "cmd": "echo "one" ",
            "delta": "0:00:00.003110",
            "end": "2013-12-19 12:00:05.187153",
            "invocation": {
                "module_args": "echo "one"",
                "module_name": "shell"
            },
            "item": "one",
            "rc": 0,
            "start": "2013-12-19 12:00:05.184043",
            "stderr": "",
            "stdout": "one"
        },
        {
            "changed": true,
            "cmd": "echo "two" ",
            "delta": "0:00:00.002920",
            "end": "2013-12-19 12:00:05.245502",
            "invocation": {
                "module_args": "echo "two"",
                "module_name": "shell"
            },
            "item": "two",
            "rc": 0,
            "start": "2013-12-19 12:00:05.242582",
            "stderr": "",
            "stdout": "two"
        }
    ]
}

若需检查注册变量结果,可以对其进行迭代操作,如下所示:

- name: Fail if return code is not 0
  ansible.builtin.fail:
    msg: "The command ({{ item.cmd }}) did not have a 0 return code"
  when: item.rc != 0
  loop: "{{ echo.results }}"

在迭代过程中,当前项的结果将被放置在变量中:

- name: Place the result of the current item in the variable
  ansible.builtin.shell: echo "{{ item }}"
  loop:
    - one
    - two
  register: echo
  changed_when: echo.stdout != "one"

复杂循环

迭代嵌套列表

可以使用jinja2 表达式来迭代复杂的列表,例如,循环可以组合嵌套列表:

- name: Give users access to multiple databases
  community.mysql.mysql_user:
    name: "{{ item[0] }}"
    priv: "{{ item[1] }}.*:ALL"
    append_privs: true
    password: "foo"
  loop: "{{ ['alice', 'bob'] | product(['clientdb', 'employeedb', 'providerdb']) | list }}"
重试任务直到满足条件

可以使用until 关键字重试任务,直到满足条件为止,例如:

- name: Retry a task until a certain condition is met
  ansible.builtin.shell: /usr/bin/foo
  register: result
  until: result.stdout.find("all systems go") != -1
  retries: 5
  delay: 10

此任务最多运行5次,每次尝试之间有10 秒的延迟。如果任意尝试的结果标准输出中包含"all systems go",则任务成功。重试的默认值为3,延迟的默认值为5。
要查看单独重试的结果,可以使用-vv参数运行。
当运行包含until 并将结果注册为变量的任务时,注册的变量将包含一个名为"attempts" 的键,该键记录了重试的次数。

循环主机列表inventory

要循环inventory 或者其子集,可以使用带有ansible_play_batchgroups 变量正则的循环:

- name: Show all the hosts in the inventory
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ groups['all'] }}"

- name: Show all the hosts in the current play
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ ansible_play_batch }}"

还可以使用inventory_hostnames 查找插件,来迭代inventory:

- name: Show all the hosts in the inventory
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ query('inventory_hostnames', 'all') }}"

- name: Show all the hosts matching the pattern, ie all but the group www
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ query('inventory_hostnames', 'all:!www') }}"

有关主机列表的匹配可以参考Patterns: targeting hosts and groups

确保loop 输入为列表

loop 关键字需要列表作为输入,但lookup 默认情况下返回的是以逗号分割的值。在ansible 2.5 中引入了一个新jinja2 函数query,该函数始终返回一个列表,在配合loop 使用时更为简单。
当然,也可使用wantlist=True 参数将lookup 的返回转换为列表。
如下操作是等价的:

loop: "{{ query('inventory_hostnames', 'all') }}"

loop: "{{ lookup('inventory_hostnames', 'all', wantlist=True) }}"

向循环添加控件

配置loop_control 关键字可以管理循环。

限制循环输出

当循环复杂的数据结构时,任务的控制台输出可能会很大。要限制显示的输出,可以使用配置label 属性的loop_control

- name: Create servers
  digital_ocean:
    name: "{{ item.name }}"
    state: present
  loop:
    - name: server1
      disks: 3gb
      ram: 15Gb
      network:
        nic01: 100Gb
        nic02: 10Gb
        ...
  loop_control:
    label: "{{ item.name }}"

此任务输出将仅显示每次循环中的name 字段,而不是{{ item }} 变量的全部内容。

注:限制循环输出是为了使控制台更具可读性,而不是为了保护敏感数据。如果在循环中有敏感数据,需要在该任务中设置no_log: true

在循环内暂停

要控制任务中每次循环执行的时间(以秒为单位),需要使用loop_control 并设置pause

# main.yml
- name: Create servers, pause 3s before creating next
  community.digitalocean.digital_ocean:
    name: "{{ item }}"
    state: present
  loop:
    - server1
    - server2
  loop_control:
    pause: 3
通过循环跟踪进度

要跟踪循环中的位置,需要使用带index_var 参数的loop_control 指令。该指令通过指定一个变量名来存储当前循环的索引:

- name: Count our fruit
  ansible.builtin.debug:
    msg: "{{ item }} with index {{ my_idx }}"
  loop:
    - apple
    - banana
    - pear
  loop_control:
    index_var: my_idx

注:index_var 索引从0 开始。

定义循环内外变量

可以通过include_tasks 来实现任务的嵌套循环。但默认情况下,ansible 会为每次循环设置item 变量,这意味着内部循环会覆盖外部循环的item 值。为了避免这种情况,可以使用带loop_var 参数的loop_control 指令,为每个循环指定变量名:

# main.yml
- include_tasks: inner.yml
  loop:
    - 1
    - 2
    - 3
  loop_control:
    loop_var: outer_item

# inner.yml
- name: Print outer and inner items
  ansible.builtin.debug:
    msg: "outer item={{ outer_item }} inner item={{ item }}"
  loop:
    - a
    - b
    - c

注:如果ansible 检测到当前循环正在使用已定义的变量,则会引发错误使任务失败。

扩展循环变量

从ansible 2.8 开始,可以使用extended 循环控制选项获取扩展循环信息。此选项可以获取如下信息:

变量描述
ansible_loop.allitems循环中所有itme 的列表
ansible_loop.index循环当前迭代的索引(从1 开始)
ansible_loop.index0循环当前迭代的索引(从0 开始)
ansible_loop.revindex最终循环的迭代次数(从1 开始)
ansible_loop.revindex0最终循环的迭代次数(从0 开始)
ansible_loop.first是否第一次迭代
ansible_loop.last是否最后一次迭代
ansible_loop.length循环的item 数
ansible_loop.previtem循环的上一次迭代中的item,首次迭代不存在
ansible_loop.nextitem循环的下一次迭代中的itme,最后迭代不存在
loop_control:
  extended: true

注:当使用loop_control.extended 时,会消耗控制节点更多的内存。这是ansible_loop.allitmes 包含每个循环完整数据引用的结果。当为了展示主进程回调插件的序列化结果,这些引用会被取消,从而导致内存使用量增加。

要禁用ansible_loop.allitems,以减少内存消耗,可以设置:

loop_control:
  extended: true
  extended_allitems: false
获取loop_var

从ansible 2.8 开始,可以通过ansible_loop_var 变量获取loop_control.loop_var 设置的名称。
可以通过以下方式获取循环中变量的值:

"{{ lookup('vars', ansible_loop_var) }}"

从with_X 迁移到loop

大多数情况下,循环最好使用loop 而不是with_X
loop 通常最好使用过滤器来完成任务,而不是使用更复杂的querylookup
如下示例展示了如何将常见with_ 结构转换为loop 与过滤器的结合。

with_list

with_list 可以直接替换为loop

- name: with_list
  ansible.builtin.debug:
    msg: "{{ item }}"
  with_list:
    - one
    - two

- name: with_list -> loop
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop:
    - one
    - two
with_items

with_items 可以通过loopflatten 过滤器替换:

- name: with_items
  ansible.builtin.debug:
    msg: "{{ item }}"
  with_items: "{{ items }}"

- name: with_items -> loop
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ items|flatten(levels=1) }}"
with_indexed_items

with_indexed_items 可以通过loopflatten 过滤器和loop_control.index_var 替换:

- name: with_indexed_items
  ansible.builtin.debug:
    msg: "{{ item.0 }} - {{ item.1 }}"
  with_indexed_items: "{{ items }}"

- name: with_indexed_items -> loop
  ansible.builtin.debug:
    msg: "{{ index }} - {{ item }}"
  loop: "{{ items|flatten(levels=1) }}"
  loop_control:
    index_var: index
with_flattened

with_flattened 可以通过loopflatten 过滤器替换:

- name: with_flattened
  ansible.builtin.debug:
    msg: "{{ item }}"
  with_flattened: "{{ items }}"

- name: with_flattened -> loop
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ items|flatten }}"
with_together

with_together 可以通过loopzip 过滤器替换:

- name: with_together
  ansible.builtin.debug:
    msg: "{{ item.0 }} - {{ item.1 }}"
  with_together:
    - "{{ list_one }}"
    - "{{ list_two }}"

- name: with_together -> loop
  ansible.builtin.debug:
    msg: "{{ item.0 }} - {{ item.1 }}"
  loop: "{{ list_one|zip(list_two)|list }}"

另一个复杂数据的例子:

- name: with_together -> loop
  ansible.builtin.debug:
    msg: "{{ item.0 }} - {{ item.1 }} - {{ item.2 }}"
  loop: "{{ data[0]|zip(*data[1:])|list }}"
  vars:
    data:
      - ['a', 'b', 'c']
      - ['d', 'e', 'f']
      - ['g', 'h', 'i']
with_dict

with_dict 可以通过loopdict2itemsdictsort 过滤器替换:

- name: with_dict
  ansible.builtin.debug:
    msg: "{{ item.key }} - {{ item.value }}"
  with_dict: "{{ dictionary }}"

- name: with_dict -> loop (option 1)
  ansible.builtin.debug:
    msg: "{{ item.key }} - {{ item.value }}"
  loop: "{{ dictionary|dict2items }}"

- name: with_dict -> loop (option 2)
  ansible.builtin.debug:
    msg: "{{ item.0 }} - {{ item.1 }}"
  loop: "{{ dictionary|dictsort }}"
with_sequence

with_sequence 可以通过looprange 函数和format 过滤器替换:

- name: with_sequence
  ansible.builtin.debug:
    msg: "{{ item }}"
  with_sequence: start=0 end=4 stride=2 format=testuser%02x

- name: with_sequence -> loop
  ansible.builtin.debug:
    msg: "{{ 'testuser%02x' | format(item) }}"
  loop: "{{ range(0, 4 + 1, 2)|list }}"
with_subelements

with_subelements 可以通过loopsubelements 过滤器替换:

- name: with_subelements
  ansible.builtin.debug:
    msg: "{{ item.0.name }} - {{ item.1 }}"
  with_subelements:
    - "{{ users }}"
    - mysql.hosts

- name: with_subelements -> loop
  ansible.builtin.debug:
    msg: "{{ item.0.name }} - {{ item.1 }}"
  loop: "{{ users|subelements('mysql.hosts') }}"
with_nested/with_cartesian

with_nestedwith_cartesian 可以通过loopproduct 过滤器替换:

- name: with_nested
  ansible.builtin.debug:
    msg: "{{ item.0 }} - {{ item.1 }}"
  with_nested:
    - "{{ list_one }}"
    - "{{ list_two }}"

- name: with_nested -> loop
  ansible.builtin.debug:
    msg: "{{ item.0 }} - {{ item.1 }}"
  loop: "{{ list_one|product(list_two)|list }}"
with_random_choice

with_random_choice 只需通过random 过滤器替换,无需使用loop

- name: with_random_choice
  ansible.builtin.debug:
    msg: "{{ item }}"
  with_random_choice: "{{ my_list }}"

- name: with_random_choice -> loop (No loop is needed here)
  ansible.builtin.debug:
    msg: "{{ my_list|random }}"
  tags: random

控制任务运行的位置:委派和本地操作

默认情况下,ansible 会收集facts 并在与playbook hosts 相符的主机上执行所有任务。
此节将介绍如何将任务委派给不同的主机或组、将facts 委派给特定的主机或组、或在本地运行整个playbook。

不能委派的任务

有些任务只能在控制节点上执行,这些任务(例如includeadd_hostdebug)不能被委派。
可以通过任务文档中connection 属性来确定是否能被委派。如果该属性显示supportFalseNone,则该操作不使用连接并且无法委派。

委派任务

如果要在一台主机上执行任务并引用其他主机,需要在任务上使用delegate_to 关键字。这非常适合管理负载平衡池中的节点或控制中断窗口。
可以使用带有serial 关键字的委派任务来控制同时执行的主机数量:

---
- hosts: webservers
  serial: 5

  tasks:
    - name: Take out of load balancer pool
      ansible.builtin.command: /usr/bin/take_out_of_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

    - name: Actual steps would go here
      ansible.builtin.yum:
        name: acme-web-stack
        state: latest

    - name: Add back to load balancer pool
      ansible.builtin.command: /usr/bin/add_back_to_pool {{ inventory_hostname }}
      delegate_to: 127.0.0.1

此playbook 中第一个和第三个任务在127.0.0.1 上执行,这是运行ansible 的控制机。
如果要在本地执行任务,还可简写语法为:local_action

---
# ...

  tasks:
    - name: Take out of load balancer pool
      local_action: ansible.builtin.command /usr/bin/take_out_of_pool {{ inventory_hostname }}

# ...

    - name: Add back to load balancer pool
      local_action: ansible.builtin.command /usr/bin/add_back_to_pool {{ inventory_hostname }}

可以使用本地操作调用rsync 以递归方式将文件复制到远程主机上:

---
# ...

  tasks:
    - name: Recursively copy files from management server to target
      local_action: ansible.builtin.command rsync -a /path/to/files {{ inventory_hostname }}:/path/to/target/

需要指定更多参数,可以使用以下语法:

---
# ...

  tasks:
    - name: Send summary mail
      local_action:
        module: community.general.mail
        subject: "Summary Mail"
        to: "{{ mail_recipient }}"
        body: "{{ mail_body }}"
      run_once: True

注:ansible_host 变量和其他连接变量可以反映任务委派到的主机信息,而不是inventory_hostname

注:尽管delegate_to 可以使用inventory 中不存在的主机,但这样做不会将主机添加到inventory,并且可能会导致问题。不存在于inventory 的主机不会从all 组中继承变量,连接用户和密钥等变量会丢失。如果必须这样做,可以使用add host module

委派上下文中的模板化

在委派下,执行解释器(通常是python)、connectionbecomeshell 插件选项将使用委派给主机的值进行模板化。
inventory_hostname 变量的所有变量都将从该主机而不是原始任务主机获取。如果需要来自原始任务主机的变量,则必须使用hostvars[inventory_hostname]['varname'],或是inventory_hostanme_short

委派和并行执行

默认情况下,ansible 任务是并行执行的。委派任务不会改变这一点,也不会处理并发问题(多个分支写入同一个文件)。用户在更新委派的单个主机上的单个文件时会受到此影响(例如,copytemplatelineinfile 模块),并行运行会相互覆盖。
可以通过如下方法处理:

- name: "handle concurrency with a loop on the hosts with `run_once: true`"
  lineinfile: "<options here>"
  run_once: true
  loop: '{{ ansible_play_hosts_all }}'

还可以通过在任务级别中使用带有serial: 1throttle: 1 的play 来控制并行,更多playbook 执行策略可参阅Controlling playbook execution: strategies and more

委派facts

默认情况下,委派任务收集的任何facts 都会分配给当前主机inventory_hostname。要将收集的facts 分配给委派主机,需要设置delegate_factstrue

---
- hosts: app_servers

  tasks:
    - name: Gather facts from db servers
      ansible.builtin.setup:
      delegate_to: "{{ item }}"
      delegate_facts: true
      loop: "{{ groups['dbservers'] }}"

此任务收集dbservers 组中主机的facts 并将其分配给这些主机,即使操作的目标是app_servers 组。

本地playbook

在远程主机上本地化使用playbook 可能比通过ssh 连接更有用。通过将playbook 放入crontab 中,可以用来确保系统配置。也可以用在操作系统安装程序中运行playbook,例如anaconda kickstart。
要在本地运行整个playbook,只需将hosts: 设置为hosts: 127.0.0.1,然后运行:

ansible-playbook playbook.yml --connection=local

或者,即使playbook 中的其他play 使用默认的远程连接类型,也可以在单个play 中使用本地连接:

---
- hosts: 127.0.0.1
  connection: local

注:如果将连接设置为本地并且没有设置ansible_python_interpreter,则模块将在/usr/bin/python 下运行。可以在host_vars/localhost.yml 中设置ansible_python_interpreter: "{{ ansible_playbook_python }}",也可以使用local_actiondelegate_to: localhost 来避免此问题。

条件语句

在playbook 中,根据fact、变量或先前任务结果的不同,可能需要执行不同的任务或有不同的目标。
ansible 在条件语句中可以使用jinja2 测试过滤器。ansible 支持所有标准测试和过滤器,同时还添加了一些特殊的测试和过滤器。

注:ansible 中有很多控制执行流程的选项。可以在jinja.palletsprojects.com/en/latest/t… 找到支持的条件示例。

基本条件语句

最简单的条件语句适用于单个任务。创建任务,然后添加when 语句,该语句内容是一个没有双花括号的原始jinjia 2 表达式(参阅group_by_module)。当运行任务时,ansible 会评估所有主机的测试,并在测试通过(放回True)的主机上执行该任务。例如,只希望在启用了selinux 的主机上运行mysql:

tasks:
  - name: Configure SELinux to start mysql on any port
    ansible.posix.seboolean:
      name: mysql_connect_any
      state: true
      persistent: true
    when: ansible_selinux.status == "enabled"
    # all variables can be used directly in conditionals without double curly braces
基于ansible_facts 的条件

通常可能希望根据fact 执行或跳过任务,fact 是各个主机的属性,包括ip 地址、操作系统、文件系统的状态等等。有关条件语句中常用的fact,可参阅Commonly-used facts。但并非所有主机都有所有fact,要查看系统上可用的fact,可添加调试任务:

- name: Show facts available on the system
  ansible.builtin.debug:
    var: ansible_facts

基于fact 的条件示例:

tasks:
  - name: Shut down Debian flavored systems
    ansible.builtin.command: /sbin/shutdown -t now
    when: ansible_facts['os_family'] == "Debian"

如果有多个条件,可以用括号将它们分组:

tasks:
  - name: Shut down CentOS 6 and Debian 7 systems
    ansible.builtin.command: /sbin/shutdown -t now
    when: (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "6") or
          (ansible_facts['distribution'] == "Debian" and ansible_facts['distribution_major_version'] == "7")

还可以使用逻辑运算符来组合条件。当有多个条件都需要为真(逻辑and)时,可以将它们指定为列表:

tasks:
  - name: Shut down CentOS 6 systems
    ansible.builtin.command: /sbin/shutdown -t now
    when:
      - ansible_facts['distribution'] == "CentOS"
      - ansible_facts['distribution_major_version'] == "6"

如果fact 或变量是字符串,并且需要对其进行数学比较,需要使用过滤器确保值读取为整数:

tasks:
  - ansible.builtin.shell: echo "only on Red Hat 6, derivatives, and later"
    when: ansible_facts['os_family'] == "RedHat" and ansible_facts['lsb']['major_release'] | int >= 6

可以将fact 存储为变量用于条件逻辑:

tasks:
    - name: Get the CPU temperature
      set_fact:
        temperature: "{{ ansible_facts['cpu_temperature'] }}"

    - name: Restart the system if the temperature is too high
      when: temperature | float > 90
      shell: "reboot"
基于注册变量的条件

有时会希望playbook 根据先前任务的结果执行或跳过任务。可以使用关键字register 创建注册变量的名称。注册变量会包含创建时的任务状态以及任务生成的任何输出。
可以在模板和操作行以及条件语句when 中使用注册变量,可以使用variable.stdout 访问已注册变量的字符串内容:

- name: Test play
  hosts: all

  tasks:

      - name: Register a variable
        ansible.builtin.shell: cat /etc/motd
        register: motd_contents

      - name: Use the variable in conditional statement
        ansible.builtin.shell: echo "motd contains the word hi"
        when: motd_contents.stdout.find('hi') != -1

如果变量是列表,则可以在任务循环中使用注册的结果。如果变量不是列表,则可以使用stdout_linesvariable.stdout.split() 将其转换为列表:

- name: Registered variable usage as a loop list
  hosts: all
  tasks:

    - name: Retrieve the list of home directories
      ansible.builtin.command: ls /home
      register: home_dirs

    - name: Add home dirs to the backup spooler
      ansible.builtin.file:
        path: /mnt/bkspool/{{ item }}
        src: /home/{{ item }}
        state: link
      loop: "{{ home_dirs.stdout_lines }}"
      # same as loop: "{{ home_dirs.stdout.split() }}"

注册变量的字符串内容可以为空,可以通过判断注册变量的字符串是否为空,来选择是否执行任务:

- name: check registered variable for emptiness
  hosts: all

  tasks:

      - name: List contents of directory
        ansible.builtin.command: ls mydir
        register: contents

      - name: Check contents for emptiness
        ansible.builtin.debug:
          msg: "Directory is empty"
        when: contents.stdout == ""

ansible 会为每个主机的注册变量注册内容,即便主机相应的任务失败或不满足条件而跳过任务。可以通过判断注册变量是否是is skipped,来确认任务是否被跳过。更多的信息可参阅Registering variables
如果希望在发生错误时继续在主机上执行任务,可以忽略错误(ignore_errors: true)。以下是基于任务成功或失败的条件示例:

tasks:
  - name: Register a variable, ignore errors and continue
    ansible.builtin.command: /bin/false
    register: result
    ignore_errors: true

  - name: Run only if the task that registered the "result" variable fails
    ansible.builtin.command: /bin/something
    when: result is failed

  - name: Run only if the task that registered the "result" variable succeeds
    ansible.builtin.command: /bin/something_else
    when: result is succeeded

  - name: Run only if the task that registered the "result" variable is skipped
    ansible.builtin.command: /bin/still/something_else
    when: result is skipped

  - name: Run only if the task that registered the "result" variable changed something.
    ansible.builtin.command: /bin/still/something_else
    when: result is changed

注:旧版的ansible 使用successfail,目前这些都是正确的。

基于变量的条件

可以基于playbook 或inventory 中定义的变量创建条件。由于条件需要布尔输入,因此必须对非布尔变量应用过滤器| bool,例如'yes'、'on'、'1' 和'true' 的字符串变量转换为真:

vars:
  epic: true
  monumental: "yes"

在条件中使用上述变量:

tasks:
    - name: Run the command if "epic" or "monumental" is true
      ansible.builtin.shell: echo "This certainly is epic!"
      when: epic or monumental | bool

    - name: Run the command if "epic" is false
      ansible.builtin.shell: echo "This certainly isn't epic!"
      when: not epic

如果所需的变量尚未定义,可以使用jinja2 defined 测试来跳过任务:

tasks:
    - name: Run the command if "foo" is defined
      ansible.builtin.shell: echo "I've got '{{ foo }}' and am not afraid to use it!"
      when: foo is defined

    - name: Fail if "bar" is undefined
      ansible.builtin.fail: msg="Bailing out. This play requires 'bar'"
      when: bar is undefined

这种测试非常适合判断导入文件中的变量。

在循环中使用条件

如果将when 语句与循环结合起来,ansible 会单独处理每次迭代的条件。因此可以在循环中的某些迭代执行任务,并跳过某些迭代:

tasks:
    - name: Run with items greater than 5
      ansible.builtin.command: echo {{ item }}
      loop: [ 0, 2, 4, 6, 8, 10 ]
      when: item > 5

如果在循环变量未定义时需要跳过整个任务,可以使用| default 过滤器提供一个空迭代器:

- name: Skip the whole task when a loop variable is undefined
  ansible.builtin.command: echo {{ item }}
  loop: "{{ mylist|default([]) }}"
  when: item > 5

在循环字典时也可以这样操作:

- name: The same as above using a dict
  ansible.builtin.command: echo {{ item.key }}
  loop: "{{ query('dict', mydict|default({})) }}"
  when: item.value > 5
加载自定义fact

Should you develop a module? 中所述,可以设置自定义的fact。要加载它们,只需调用任务列表顶部的自定义fact 收集模块:

tasks:
    - name: Gather site specific fact data
      action: site_facts

    - name: Use a custom fact
      ansible.builtin.command: /usr/bin/thingy
      when: my_custom_fact_just_retrieved_from_the_remote_system == '1234'
复用条件

可以将条件语句与可复用的任务文件、playbook 和角色一起使用。ansible 会对动态复用(includes)和静态复用(imports)条件语句有不同的执行方式,更多复用信息可参阅Re-using Ansible artifacts

import 时使用条件

当向导入语句添加条件时,ansible 会将该条件应用于导入文件中的所有任务。这种行为相当于Tag inheritance: adding tags to multiple tasks。例如,如果想要定义并显示之前未定义的变量:

# all tasks within an imported file inherit the condition from the import statement
# main.yml
- hosts: all
  tasks:
  - import_tasks: other_tasks.yml # note "import"
    when: x is not defined

# other_tasks.yml
- name: Set a variable
  ansible.builtin.set_fact:
    x: foo

- name: Print a variable
  ansible.builtin.debug:
    var: x

ansible 在执行时会将其等价扩展为:

- name: Set a variable if not defined
  ansible.builtin.set_fact:
    x: foo
  when: x is not defined
  # this task sets a value for x

- name: Do the task if "x" is not defined
  ansible.builtin.debug:
    var: x
  when: x is not defined
  # Ansible skips this task, because x is now defined

如果x 最初定义了,则这两个任务都会被跳过。如果最初未定义,则将跳过debug 任务。
如果不想跳过debug,可以使用include_* 语句仅将条件应用于该语句本身:

# using a conditional on include_* only applies to the include task itself
# main.yml
- hosts: all
  tasks:
  - include_tasks: other_tasks.yml # note "include"
    when: x is not defined

现在,如果x 最初未定义,则不会跳过debug 任务,因为条件不会对文件中的每个任务进行评估。
当使用import_* 与条件语句结合时,每个主机不符合条件的每个任务都会输出"skipped" 信息,可能会导致输出很多。可以使用group_by 模块来进行精简,具体使用方法可参阅Handling OS and distro differences

include 时使用条件

当在include_* 语句上使用条件时,条件仅应用于include_* 任务本身,不应用于include 文件中的其他任务:

# Includes let you reuse a file to define a variable when it is not already defined

# main.yml
- include_tasks: other_tasks.yml
  when: x is not defined

# other_tasks.yml
- name: Set a variable
  ansible.builtin.set_fact:
    x: foo

- name: Print a variable
  ansible.builtin.debug:
    var: x

ansible 在执行时会将其等价扩展为:

# main.yml
- include_tasks: other_tasks.yml
  when: x is not defined
  # if condition is met, Ansible includes other_tasks.yml

# other_tasks.yml
- name: Set a variable
  ansible.builtin.set_fact:
    x: foo
  # no condition applied to this task, Ansible sets the value of x to foo

- name: Print a variable
  ansible.builtin.debug:
    var: x
  # no condition applied to this task, Ansible prints the debug statement

通过使用include_tasks 替代import_tasksother_tasks.yml 中的两个任务都将按预期执行。includeimport 之间的更多差异,可参阅Re-using Ansible artifacts

角色中使用条件

可以通过三种方式将条件应用于角色:

  • 通过将when 语句放在roles 关键字下,向角色中的所有任务添加相同的条件。
  • 通过将when 语句放在playbook 中import_role 静态内容下,向角色中的所有任务添加相同的条件。
  • 将条件添加到角色内的各个任务或块,这是选择或跳过角色内某些任务的唯一方法。可以通过在playbook 中使用动态include_role,并将条件添加到include 中,ansible 会将条件应用于include 以及角色中也具有该when 语句的任何任务。

静态示例:

- hosts: webservers
  roles:
     - role: debian_stock_config
       when: ansible_facts['os_family'] == 'Debian'
根据fact 选择变量、文件或模板

有时会根据主机的fact 来选择不同的变量、文件或模板。例如,centos 和debian 上软件包名称不同;常见服务的配置文件在不同操作系统风格和版本也有所不同。
ansible 可以将变量与任务分开,防止playbook 带有嵌套条件。这样会使得条件规则更加简单。

根据fact 选择变量文件

可以通过将变量值放入文件中并有条件地导入它们,这样就能以精简的语法创建在不同平台和操作系统上运行的playbook。例如,要在centos 或debian 服务器上安装apache,创建变量文件:

---
# for vars/RedHat.yml
apache: httpd
somethingelse: 42

然后根据playbook 中主机上收集的fact 导入变量文件:

---
- hosts: webservers
  remote_user: root
  vars_files:
    - "vars/common.yml"
    - [ "vars/{{ ansible_facts['os_family'] }}.yml", "vars/os_defaults.yml" ]
  tasks:
  - name: Make sure apache is started
    ansible.builtin.service:
      name: '{{ apache }}'
      state: started

ansible 收集web 服务器组中主机的fact,然后将变量ansible_facts['os_family'] 插入文件名列表中。如果是centos,ansible 会查找"vars/RedHat.yml";如果是debian,会查找"vars/Debian.yml";如果文件不存在,ansible 会尝试加载"vars/os_defaults.yml"。

根据fact 选择文件和模板

当不同的操作系统风格或版本需要不同的配置文件或模板时,可以根据分配给每个主机的变量选择合适的文件或模板。
例如,可以模板化一个在centos 和debian 之间不同的配置文件:

- name: Template a file
  ansible.builtin.template:
    src: "{{ item }}"
    dest: /etc/myapp/foo.conf
  loop: "{{ query('first_found', { 'files': myfiles, 'paths': mypaths}) }}"
  vars:
    myfiles:
      - "{{ ansible_facts['distribution'] }}.conf"
      -  default.conf
    mypaths: ['search_location_one/somedir/', '/opt/other_location/somedir/']

调试条件

如果条件when 语句的行为不符合预期,可以添加debug 语句来确定条件的计算结果。
条件语句中出现意外行为的常见原因是将整数测试为字符串或将字符串测试为整数。
要调试条件语句,可以将整个语句添加为debug 中的var: 值。ansible 会显示测试以及语句的评估方式:

- name: check value of return code
  ansible.builtin.debug:
    var: bar_status.rc

- name: check test for rc value as string
  ansible.builtin.debug:
    var: bar_status.rc == "127"

- name: check test for rc value as integer
  ansible.builtin.debug:
    var: bar_status.rc == 127

示例输出为:

TASK [check value of return code] *********************************************************************************
ok: [foo-1] => {
    "bar_status.rc": "127"
}

TASK [check test for rc value as string] **************************************************************************
ok: [foo-1] => {
    "bar_status.rc == "127"": false
}

TASK [check test for rc value as integer] *************************************************************************
ok: [foo-1] => {
    "bar_status.rc == 127": true
}

常用fact

以下ansible fact 经常在条件语句中使用。

ansible_facts['distribution']

可能的值(示例,非完整列表):

Alpine
Altlinux
Amazon
Archlinux
ClearLinux
Coreos
CentOS
Debian
Fedora
Gentoo
Mandriva
NA
OpenWrt
OracleLinux
RedHat
Slackware
SLES
SMGL
SUSE
Ubuntu
VMwareESX
ansible_facts['distribution_major_version']

操作系统的主要版本。例如,对于Ubuntu16.04,该值为16。

ansible_facts['os_family']

可能的值(示例,非完整列表):

AIX
Alpine
Altlinux
Archlinux
Darwin
Debian
FreeBSD
Gentoo
HP-UX
Mandrake
RedHat
SGML
Slackware
Solaris
Suse
Windows

块block

块可以创建任务的逻辑组,还提供了处理任务错误的方法,类似于许多编程语言的异常处理。

用块对任务进行分组

块中的任务都继承了块级别应用的指令。可以应用于任务中的大多数语句(循环除外)同样也可应用于块中。因此,块可以轻松地设置任务通用的数据或指令。该指令不会影响块本身,仅由块所包含的任务继承。
例如,when 语句应用于块内的任务,而不是块本身:

 tasks:
   - name: Install, configure, and start Apache
     block:
       - name: Install httpd and memcached
         ansible.builtin.yum:
           name:
           - httpd
           - memcached
           state: present

       - name: Apply the foo config template
         ansible.builtin.template:
           src: templates/src.j2
           dest: /etc/foo.conf

       - name: Start service bar and enable it
         ansible.builtin.service:
           name: bar
           state: started
           enabled: True
     when: ansible_facts['distribution'] == 'CentOS'
     become: true
     become_user: root
     ignore_errors: true

在上述示例中,when 语句将在ansible 运行块中的三个任务之前进行评估。三个任务还将继承提权指令,以root 用户运行。ignore_errors: true 确保了即使某些任务失败,也会继续执行playbook。

注:块中的所有任务(包括通过include_role 包含的任务)继承了块级别应用的指令。

官方建议在块内或其他位置,所有的任务都应配置名称,以便在运行playbook 时更好地了解执行情况。

用块处理错误

可以使用带有rescuealways 的块来控制ansible 如何响应任务报错。
rescue 指定当块中较早的任务失败时要执行的内容。ansible 仅在任务返回"failed" 状态后运行rescue 块。任务本身定义的错误和无法访问主机不会触发rescue 块:

 tasks:
   - name: Handle the error
     block:
       - name: Print a message
         ansible.builtin.debug:
           msg: 'I execute normally'

       - name: Force a failure
         ansible.builtin.command: /bin/false

       - name: Never print this
         ansible.builtin.debug:
           msg: 'I never execute, due to the above task failing, :-('
     rescue:
       - name: Print when errors
         ansible.builtin.debug:
           msg: 'I caught an error, can do stuff here to fix it, :-)'

还可以将always 添加到块中,无论块中前一个任务的状态是什么,该部分中的内容都会执行:

 tasks:
   - name: Always do X
     block:
       - name: Print a message
         ansible.builtin.debug:
           msg: 'I execute normally'

       - name: Force a failure
         ansible.builtin.command: /bin/false

       - name: Never print this
         ansible.builtin.debug:
           msg: 'I never execute :-('
     always:
       - name: Always do this
         ansible.builtin.debug:
           msg: "This always executes, :-)"

这些方法结合起来可以进行更为复杂的错误处理:

 tasks:
   - name: Attempt and graceful roll back demo
     block:
       - name: Print a message
         ansible.builtin.debug:
           msg: 'I execute normally'

       - name: Force a failure
         ansible.builtin.command: /bin/false

       - name: Never print this
         ansible.builtin.debug:
           msg: 'I never execute, due to the above task failing, :-('
     rescue:
       - name: Print when errors
         ansible.builtin.debug:
           msg: 'I caught an error'

       - name: Force a failure in middle of recovery! >:-)
         ansible.builtin.command: /bin/false

       - name: Never print this
         ansible.builtin.debug:
           msg: 'I also never execute :-('
     always:
       - name: Always do this
         ansible.builtin.debug:
           msg: "This always executes"

block 中的任务正常执行。如果块中的任务返回failed,则rescue 部分执行任务以从错误中恢复。无论blockrescue 部分结果如何,always 部分都会运行。
如果块中发生错误并且rescue 任务成功,ansible 会恢复运行原始任务的失败状态,并继续运行play,且不会触发max_fail_percentageany_errors_fatal 配置。如果被救援任务失败,ansible 仍然会报告一个playbook 统计到的错误。
可以在rescue 任务中使用flush_handlers 来确保即使发生错误,所有handler 程序也能运行:

 tasks:
   - name: Attempt and graceful roll back demo
     block:
       - name: Print a message
         ansible.builtin.debug:
           msg: 'I execute normally'
         changed_when: true
         notify: Run me even after an error

       - name: Force a failure
         ansible.builtin.command: /bin/false
     rescue:
       - name: Make sure all handlers run
         meta: flush_handlers
 handlers:
    - name: Run me even after an error
      ansible.builtin.debug:
        msg: 'This handler runs even on error'

ansible 为rescue 块部分提供了几个变量:

  • ansible_failed_task
    返回"failed" 并触发了rescue 的任务。例如,要获取这类任务的名称,可使用ansible_failed_task.name
  • ansible_failed_result
    触发rescue 的失败任务其被捕获的返回结果。相当于在register 关键字中使用了此变量。

这些变量的使用示例如下:

 tasks:
   - name: Attempt and graceful roll back demo
     block:
       - name: Do something
         ansible.builtin.shell: grep $(whoami) /etc/hosts

       - name: Force a failure, if previous one succeeds
         ansible.builtin.command: /bin/false
     rescue:
       - name: All is good if the first task failed
         when: ansible_failed_task.name == 'Do Something'
         ansible.builtin.debug:
           msg: All is good, ignore error as grep could not find 'me' in hosts

       - name: All is good if the first task failed
         when: "'/bin/false' in ansible_failed_result.cmd | d([])"
         ansible.builtin.fail:
           msg: It is still false!!!

注:在ansible-core 2.14 及更高版本中,当嵌套块时,上述两个变量都会从内部块返回到外部rescue 块部分。