「Ansible」- Playbook

#4 Playbook杂谈

4.1 再谈 Ansible 变量

变量作用域:

Global ,作用域为全局
  • Ansible 配置文件中定义的变量。
  • 环境变量。
  • ansible(1)/ansible-playbook(1)在命令行中传进来的变量

Play,作用域为Play(一个Playbook由多个Play构成)
  • Play 中 vars 关键字下定义的变量。
  • 通过模块include_vars 定义的变量。
  • role 在文件 default/main.yml 和 vars/main.yml 中定义的变量。

Host,作用域为某个主机
  • 定义在主机清单中的变量。
  • 主机的系统变量(Facts)。
  • 注册变量(register)。

变量的优先级:

role defaults – 放在文件 roles/x/defaults/main.yml 中的变量。

dynamic inventory variables – 在主机清单(/etc/ansible/hosts)中定义的变量。

inventory variables

inventory group_vars – 主机清单中定义的组变量,或者在group_vars/下与主机组名同名文件中定义的。

inventory host_vars – 主机清单中定义的主机变量,或者在host_vars/下与主机名同名文件中定义的。

playbook group_vars – 与Playbook同级的group_vars/下。

playbook host_vars – 与Playbook同级的host_vars/下。

host facts – 从远程主机收集的变量。使用ansible hostl -m setup查看。

registered variables – 使用register指令将执行结果注册到的变量。

set_facts – 模块,在任务中通过 set_facts 加入一些 Facts 变量。

play variables – 在Playbook中,使用vars定义的变量。

play vars_prompt – 在执行Playbook时,需要用户输入的变量(使用prompt指定)。

play vars_files – 使用var_files指定引入的变量文件。

role variables and include variables – 在roles/x/vars/main.yml中定义的变量。或在role/x/tasks/main.yml中,通过关键字include_vars加载进来的变量。

block variables – 在blocks中使用vars定义的变量。

task variables – 在tasks中使用vars定义的变量。

extra variables – 通过命令行传入的变量。

大体的规律如下:

  • 除了 role defaults 变量外,其他变量的作用域越小 ,变量的优先级越高。
  • 优先级最高的是 extra 变量,又叫命令行变量,只在某一次执行时生效,且变量优先级最高。
  • 主机清单中的全局变量可以被 Play 中的变量覆盖,Playbook 中的变量可以被 host 变量覆盖, host 变量又可以被 task 变量覆盖

变量的优先级只有在变量重名的时候才需要区分 , 所以建议除了 role defaults 变量外,尽量不要有相同名字的变量。

4.2 使用 lookup 访问外部文件或数据库中的数据

如果变量过多、过复杂,那么传统vars的局限性比较多。使用lookup可以解决此类问题,既可以从文件中读取,也可以从数据库中读取。

lookup工作于管理节点上。

将 Ansible 管理节点上的文件 data/plain.txt 的内容读取出来,并赋值给变量”contents“。

---
- hosts : all
  remote user: root
  gather facts : false
    vars:
      contents: "{{ 1ookup('file', 'data/plain.txt') }}"

“file”告诉 lookup 读取对象的类型是 File ,直接读取文件内容,无须做特别的处理。

生成随机密码。第一次执行时,如果密码文件/tmp/password/kitty不存在, lookup 会生成长度为 5 的随机密码,存储在文件中。如果密码文件存在,那么直接读取该文件中的内容作为密码:

---
- hosts: all
  remote_user: root
  vars:
    password: "{{ 1ookup('password ','/tmp/password/kitty length=5') }}"
  tasks:
    - debug: var=password

读取环境变量。env 类型的 lookup 可以读取 Linux 上的环境变量,如 下面的示例:

---
- hosts: all
  remote_user: root
  tasks:
    - debug: msg="{{ lookup('env', 'HOME') }} is an environment variable"

读取 Linux 命令的执行结果。pipe 类型的 lookup 可以将 Linux 上命令的执行结果读取到 Ansible 中 :

---
- hosts: all
  remote_user: root
  tasks:
    - debug: msg="{{ lookup ('pipe', 'date') }} is the raw result of running this command"

读取 template 变量替换后的文件。template 类型的 lookup 可以将一个 template 文件经过变量替换后的内容读取到 Ansible 中。如果在 template 文件中有未定义的变量 ,则会报错:

---
- hosts: all
  remote_user: root
  tasks:
    - debug: msg="{{ lookup ('template', 'data/some_template.j2') }} is the raw result of running this command"

读取INI配置文件。如下示例,读取integration部分下user的值:

---
- hosts: all
  remote_user: root
  tasks:
    - debug: msg="{{ lookup ('ini', 'user section=integration file=data/users.ini ') }} is the raw result of running this command"

读取properties配置文件。与INI类似,只是多加一个TYPE参数:

---
- hosts: all
  remote_user: root
  tasks:
    - debug: msg="{{ lookup ('ini', 'user.name type=properties file=data/user.propertie') }} is the raw result of running this command"

ini 类型的参数格式:

lookup(‘ini’,’ key [type=<properties|ini>] [section=section] [file=file. ini] [re=true] [default=<defaultvalue>])

在INI类型的lookup中,每个参数都有默认值,每一个参数都是可选的,没有传人的参数会使用默认值:

type – ini – 文件的类型

file – ansible.ini – 加载文件的名字

section – global – 默认的在哪个 section 里面查找 key

re – False – key是否为的正则表达式

default – empty string – 当key不存在时的返回值

读取 csv 文件的指定单元:

---
- hosts: all
  remote_user: root
  tasks:
    - debug: msg="The atomic mass of Lithium is {{ lookup ('csvfile', 'Li file=elements.csv delimiter=, col=2') }} ”

语法为:lookup(‘csvfile’, ‘key argl=vall arg2=val2…’)。key 是 column 0中某一个单元的值,其他参数都是可选的:

file ansible.csv 加载文件的名字

col 1 输出的列的索引,索引从0开始计数

delimiter TAB csv 文件的分隔符。tab 可以用 Tab或者 t 来表示

default 空字符串 如何元素不存在时的返回值

encoding utf-8 csv 文件的编码

读取 DNS 解析的值。使用 dig 类型的 lookup 查看 DNS ,不仅使用了正向 DNS 解析查询,还使用了反向 DNS 解析查询 , 此外还可以指定查询的 DNS 服务器:

---
- hosts: all
  remote_user: root
  tasks :
    # 正向查询某一个域名的 DNS 记录
    - debug: msg="The IPv4 address for baidu.com. is {{ lookup('dig', 'baidu.com.') }}"
    - debug: msg="The TXT record for baidu.com. is {{ lookup ('dig', 'baidu.com.', 'qtype=TXT' }} "
    - debug: msg="The TXT record for baidu.com. is {{ lookup ('dig', 'baidu.com./TXT') }}"
    - debug: msg="One of the MX records for 163.com. is {{ item }}"
      with_items: "{{ lookup ('dig', '163.com./MX', wantlist=True) }}"

    # 反向查询某一个 IP 对应的 DNS 记录
    - debug: msg="Reverse DNS for 23.214.122.249 is {{ lookup ('dig', '23.214.122.249/PTR') }}"
    - debug: msg="Reverse DNS for 23.214.122.249 is {{ lookup ('dig', '249.122.214.23.in-addr.arpa./PTR') }}"
    - debug: msg="Reverse DNS for 23.214.122.249 is {{ lookup ('dig', '249.122.214.23.in-addr.arpa.', 'qtype=PTR') }} "

    # 指定查询的 DNS 服务器为 8.8.8.8
    - debug: msg="Querying 8.8.8.8 for IPv4 address for baidu.com. produces {{ lookup('dig', 'baidu.com', '@8.8.8.8') }} ”

关于lookup更多功能,可以参考手册「Lookups

4.3 过滤器

过滤器( filter )由Jinja2提供,用于操作数据,在管理节点上执行。可以使用Jinja2自带的过滤器,还可以使用Ansible提供的过滤器,还可以自定义过滤器。

Jinja2中的过滤器参考:「Jinja2 filters

下面是Ansible自带的过滤器:

quote 过滤器的功能是给字符串加引号:

---
- hosts: localhost
  remote user: root
  gather_facts : false
  vars :
    my_test_string: "This is the test string"
  tasks:
    - name: "quote {{ my_test_string }}"
      debug: msg="echo {{ my_test_strinq | quote }}"

default 为没有定义的变量提供默认值。因为变量 some_undefined_varible 表示没有定义,所以下面的任务会输出 Default

- debug: msg="{{ some undefined variable | default ('Default') }}"

omit 忽略变量的占位符。如下示例,文件/tmp/foo没有定义参数 mode ,所以 default(omit)会在没有定义 mode 时忽略 mode 变量:

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

mandatory强制变量必须定义,否则抛错。在Ansible的默认的配置中,如果变量没有定义 ,那么直接使用未定义的变量会抛错。如果定义了error_on_undefined_vars = False则不会抛错。在此时如果想约束某一个变量必须定义 , 就可以使用 mandatory关键字:

---
- hosts: localhost
  remote_user: root
  gather_facts: false
  vars:
    some_variable: "DefinedValue"
  tasks:
    # Please notice , the default setting of ansible is enforcing the variable to to defined
    # The following undefined variable is passed, just because in the ansible.cfg in this folder is overwrite the setting by:
    # error on undefined vars = False
  - debug : msg="{{ some_variable }}"
  - debug : msg="{{ some_undefined_variable }}"
  - debug : msg="{{ some_undefined_variable | mandatory }}"

bool 判断变量是否为布尔类型:

---
- hosts: localhost
  remote_user: root
  gather_facts: false
  vars:
    some_string_value: "Test"
    some_book_vaule: True
  tasks:
    - debug: msg=test
      when: some_string_value | bool

ternary,Playbook 的条件表达式。类似于编程语言中的三元运算( A ? B : C):

vars:
  name: "John"
tasks:
  - name: "true or false take different value"
    debug: msg="{{ (name == 'John') | ternary( 'Mr', 'Ms') }}"

过滤器对文件路径的操作:

Linux 文件路径的操作的过滤器如下:
basename: 获取路径中的文件名。

dirname:获取文件的目录。

expanduser:扩展~为实际的目录。

realpath: 获得链接文件所指文件的真实路径。

relpath: 获得相对某一根目录的相对路径。

splitext:把文件名用点号(.)分割成多个部分。

Windows 文件路径的操作的过滤器有:
win_basename: 获得 Windows 路径的文件名。

win_dimame:获得 Windows 路径的文件目录。

win_splitdrive: 把 Windows 路径分割成多个部分。

过滤器对字符串变量的操作:

quote,给字符串加引号:

debug: msg="echo {{ my_test_string | quote }}"

base64,得到字符串的 Base64 编码:

- debug: msg="{{ my_comment | b64encode }}"
- debug: msg="{{ my comment | to_uuid }}"

hash,获取字符串的哈希值。计算啥希值的算法有很多,如 shal 、 md5 、 checksum 等:

vars:
  my_password: "mytestpassword"
tasks :
  - name: "Get shal hash of string {{ my_password }}"
    debug: msg="{{ my_password | hash('shal') }}"
  - name: "Get md5 hash of string {{ my_password }}"
    debug: msg="{{ my_password | hash('md5') }}"
  - name: "Get checksum of string {{ my_password }}"
    debug: msg="{{ my password Ichecksum }}"
  - name: "Get blowfish hash of string {{ my_password }}"
    debug: msg="{{ my_password lhash ('blowfish') }}"
  - name: "Get sha512 password hash (random salt ) of string {{ my_password }}"
    debug: msg="{{ my_password | password_hash('sha512') }}"
  - name: "Get get a sha256 password hash with a speci fic salt of string ({ my_password }}"
    debug: msg="{{ my_password | password_hash('sha256', 'mysecretsalt') }}"

comment,把字符串变成代码注释的一部分。comment 有很多风格和格式:

vars:
  my_comment: "This is the test comment "
tasks:
  - name: "simple comment"
    debug: msg="{{ my_comment | comment }}"
  - name: "C style comment  "
    debug: msg="{{ my_comment | comment('c') }}"
  - name: "cblock style comment "
    debug: msg="{{ my_comment | comment('cblock') }}"
  - name: "erlang style comment  "
    debug: msg="{{ my_comment | comment('erlang') }}"
  - name : "xml style comment "
    debug: msg="{{ my_comment | comment ('xml') }}"
  - name: "customize style comment  "
    debug: msg="{{ my_comment | comment('plain', prefix='#######\n#', postfix= '#\n#######\n###\n#') }} "

regex,利用正则表达式对字符串进行替换:

- debug: msg="{{ 'ansible' | regex_replace('<a . *i ( . *)$', 'a\\l') }}"
- debug: msg="{{ '^f.*o(.*)' | regex_escape() }}"

ip,判断字符串是否是合法的IP地址:

vars:
  my_valid_ip: "192.168.0.166"
  my_invalid_ip: "192.168.0.266"
  my_valid_ipv6: "fe80::dcfd:le54:728d:a99e"
tasks:
  # Need to install python package netaddr to support ipaddr filter
  # pip install netaddr
  - name: "Check {{ my_valid | ip }} is an valid ip"
    debug: msg="{{ my_valid_ip | ipaddr }}"
  - name: "Check {{ my_invalid_ip }} is an valid ip"
    debug: msg=" {{ my_invalid_ip | ipaddr }}"
  - name: "Check {{ my_valid_ip }} is an valid ipv4 addr"
    debug: msg="{{ my_valid_ip I ipv4 }}"
  - name: "Check {{my_valid_ipv6 }} is an valid ipv6 addr"
    debug : msg="{{ my_valid_ipv6 | ipv6 }}"
  - name: "Check {{ '192.0.2.1/24' | ipaddr('address') }} is an valid ip"
    debug : msg="{{ '192.0.2.1/24' | ipaddr('address') }}"

datetime,将字符串类型的时间转换成时间戳:

tasks:
  - name: "datetime filter"
    debug: msg="{{ '2016-08-04 20:00:12' | to_datetime }}"

转化为JSON字符串:

tasks:
  - debug: msg="{{ some_dic_variable | to_json }}"
  - debug: msg="{{ some_dic_variable | to_yaml }}"
  - debug: msg="{{ some_dic_variable | to_nice_json }}"
  - debug: msg="{{ some_dic_variable | to_nice_yaml }}"

json_query,在一个 JSON 对象里,搜索符合条件的属性,返回符合条件的属性数组:

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

返回cluster下由所有key为name的value组成的数组。

combine,合并两个 JSON 对象的值:

- debug: msg="{{ {'a': 1, 'b': 2} | combine({'b': 3}) }}"
# 进行深度递归
- debug: msg="{{ {'a': {'foo': 1, 'bar': 2}, 'b': 2} | combine({'a': {'bar': 3, 'baz': 4}}, recursive=True) }}"

random,取随机数。即支持List数据,也支持随机数:

- debug: msg="{{ ['a', 'b', 'c'] | random }}"
- debug: msg="{{ 59 | random )} * * * * root /script/from/cron"
- debug : msg=" {{ 100 | random(step=10) }} "
# 指定起始位置和步长
- debug : msg="{{ 100 | random(2, 10) }}"
- debug : msg="{{ 100 | random(start=2, step=10) }}"

对 List 的操作还有(「List Filters」):

min – 取最小值。

max – 取最大值。

join – 将 List 中的所有元素连接成一个新的字符串。

shuffle – 将 List 做顺序打乱成一个新的 List。

map – 实现对 List 的映射操作。

对 Set 的操作还有(「Set Theory Filters」):

unique – 去除重复的元素。

union – 取交集。

differentce – 取差集。

symmetric_difference – 取对称差。

过滤器还可以像管道一样使用:

- debug: msg="{{ (0,2) | map('extract', ['x', 'Y', 'z']) | join('|') }}”

4.4 测试变量或表达式是否符合条件

检查变量或表达式是否符合某一个条件。测试的返回结果是 true 和 false 。

在Ansible中,除了支持 Jinja2 自带的所有测试外,还提供了几个在 Ansible 中常用的测试功能。

Jinja2中的测试参考:「List of Builtin Tests

match,search – 是用于测试字符串是否符合某一个正则表达式的测试。其中 match 是完全匹配 ,search 只需部分匹配:

- debug: "msg='matched pattern 1'"
  when: url | match("http://example.com/users/.*/resources/.*")
- debug: "msg='matched pattern 2'"
  when: url | search ("/users/.*/resources/.*")

version_compare – 比较版本号

- debug: msg="{{ ansible_distribution_version | version_compare('12.04', '>=') }}"
- debug: msg="{{ ansible_distribution_version | version_compare('12.04', operator='lt', strict=True) }}"

测试List的包含关系:

vars:
 a: [1 , 2 , 3 , 4 , 5]
 b: [2 , 3]
tasks :
  - debug : msg="A includes B"
    when : a | issuperset(b)
  - debug : msg="B is included in A"
    when : b | issubset(a)

文件路径测试:

vars:
  mypath : /tmp
tasks :
  - debug: msg="path is a directory"
    when: mypath | is_dir
  - debug: msg="path is a file"
    when: mypath | is_file
  - debug: msg="path is a s symlink"
    when: mypath | is_link
  - debug: msg="path already exists"
    when: mypath | exists
  - debug: msg="path is {{ (mypath | is_abs ) | ternary ('absolute', 'relative') }}"

测试任务执行的结果:

tasks:
  - shell: /usr/bin/foo
    register: result
    ignore_errors: True
  - debug: msg="it failed"
    when: result | failed
  # in most cases you’11 want a handler , but if you want to do something right now , this is nice
  - debug: msg="it changed"
    when: result | chanqed
  - debug: msg="it succeeded in Ansible >= 2.1"
    when: result | succeeded
  - debug: msg="it succeeded"
    when: result | success
  - debug: msg="it was skipped"
    when: result | skipped

4.5 认识插件

它只是对 Ansible 功能的补充。插件会因类型不同而使用不同的方法。

如果想对上面提到过的 lookup 写更多的插件,使其功能更加丰富,那么应使用lookup插件。使用新插件语法结构如下:

{{ 1ookup('new_lookup_plugin',"paramters" }}

而过滤器类型的插件,使用时的语法结构如下:

{{ variable | new_filter_plugin}}

在 Ansible 的配置文件中,每种类型的插件都有自己的配置变量,因此放置的目录并不相同。

插件类型:

# action_plugins = /usr/share/ansible/plugins/action

# cache_plugins = = /usr/share/ansible/plugins/cache

# callback_plugins = /usr/share/ansible/plugins/callback

# connection_plugins = /usr/share/ansible/plugins/connection

# lookup_plugins = /usr/share/ansible/plugins/lookup

# inventory_plugins = /usr/share/ansible/plugins/inventory

# vars_plugins = /usr/share/ansible/plugins/vars

# filter_plugins = /usr/share/ansible/plugins/filter

# test_plugins = /usr/share/ansible/plugins/test

# strategy_plugins = /usr/share/ansible/plugins/strategy

Action 插件 – 和模块的使用方法类似,只不过执行 目标不是远程主机 , 而是在 Ansible 的管理节点上。

Cache 插件 – 为 Facts (主机变量 )提供缓存, 以避免多次执行 Playbook 时在搜集 Facts 上有过度的时间开销。

Connection 插件 – 为管 理节点和远程 主 机之间提供了更多的连接方法。默认的连接协议是基于paramiko 的 SSH 协议。 paramiko 对于大多数读者的基本需求已经够用 , 如果有高级的需求 , 则可以通过自定义的插件来提供 SNMP 、 Message bug 等传输协议。

filters 插件 – 为前面提到的过滤器提供更多的功能。

语法结构如下:{{ variable | new_filter_plugin }}

lookup 插件 – 为前面提到的 lookup 提供更多的功能。

除了前面提到的{{lookup()}}语法外 ,像“with_fileglob”和”with_items“这样的使用方法 ,也是通过 lookup 插件来实现的。

strategy 插件 – 为执行 Playbook 提供更多的执行策略,在后面会介绍 Ansible Strategy 的功能和用法

控制 Play 在执行时的策略。默认的策略是 linear , 即所有的远程主机都执行完某一个任务之后,再执行下一个任务。 Ansible 官方的 strategy 插件提供了另外一种策略-free,即允许每一个远程主机尽快地执行到 Play 的结尾。

shell 插件 – 通过 shell 插件可以提供远程主机上更多类型的 shell ( csh 、ksh、tcsh)等的支持。

test 插件 – 为前面提到的 Ansbile Jinja2 test 提供更多的功能。

vars 插件 – Ansible 可以将 inventory/playbook/命令行中的变量注入 Ansible 中 。通过 vars 插件, 可以实现更多方式的变量注入。

callback 插件 – Ansible 执行 Playbook 后 ,提供额外的行为。例如,将执行结果发送到 E-mail 中 , 或者将执行结果写入 log 中, 等等。

为 Playbook 执行后添加额外的行为,如果任务没有执行成功,则发送一封 E-mail ,或是在执行后将执行结果放在 log 文件中。「Callback Plugins」。如果配置了 callball 插件 , 则 Ansible 会根据执行后的状态调用对应的 callback 插件。

如何使用已经写好的callback插件:

(1)下载 callback plugins 文件。「Plugin List」。将下载的文件保存在Playbook同级的plugins/callback目录下。

(2)这里以timer和log_plays 插件为例,分别实现 Playbook的执行计时,并把每一个主机的执行结果记录在/var/log/ansible/hosts/中对应主机名文件下。

(3)配置 ansible.cfg文件(与Playbook同级):
配置放置 callback 插件的 Python 文件的位置:callback_plugins = plugins/callback

在所有的 callback 插件中,你想使用哪些 callback 插件:callback_whitelist = timer, log plays