#4 Playbook杂谈
4.1 再谈 Ansible 变量
变量作用域:
- Ansible 配置文件中定义的变量。
- 环境变量。
- ansible(1)/ansible-playbook(1)在命令行中传进来的变量
- Play 中 vars 关键字下定义的变量。
- 通过模块include_vars 定义的变量。
- role 在文件 default/main.yml 和 vars/main.yml 中定义的变量。
- 定义在主机清单中的变量。
- 主机的系统变量(Facts)。
- 注册变量(register)。
变量的优先级:
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 类型的参数格式:
在INI类型的lookup中,每个参数都有默认值,每一个参数都是可选的,没有传人的参数会使用默认值:
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中某一个单元的值,其他参数都是可选的:
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') }}"
过滤器对文件路径的操作:
dirname:获取文件的目录。
expanduser:扩展~为实际的目录。
realpath: 获得链接文件所指文件的真实路径。
relpath: 获得相对某一根目录的相对路径。
splitext:把文件名用点号(.)分割成多个部分。
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」):
max – 取最大值。
join – 将 List 中的所有元素连接成一个新的字符串。
shuffle – 将 List 做顺序打乱成一个新的 List。
map – 实现对 List 的映射操作。
对 Set 的操作还有(「Set Theory Filters」):
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 的配置文件中,每种类型的插件都有自己的配置变量,因此放置的目录并不相同。
插件类型:
# 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 插件 – 为前面提到的过滤器提供更多的功能。
lookup 插件 – 为前面提到的 lookup 提供更多的功能。
strategy 插件 – 为执行 Playbook 提供更多的执行策略,在后面会介绍 Ansible Strategy 的功能和用法
shell 插件 – 通过 shell 插件可以提供远程主机上更多类型的 shell ( csh 、ksh、tcsh)等的支持。
test 插件 – 为前面提到的 Ansbile Jinja2 test 提供更多的功能。
vars 插件 – Ansible 可以将 inventory/playbook/命令行中的变量注入 Ansible 中 。通过 vars 插件, 可以实现更多方式的变量注入。
callback 插件 – Ansible 执行 Playbook 后 ,提供额外的行为。例如,将执行结果发送到 E-mail 中 , 或者将执行结果写入 log 中, 等等。
如何使用已经写好的callback插件:
(2)这里以timer和log_plays 插件为例,分别实现 Playbook的执行计时,并把每一个主机的执行结果记录在/var/log/ansible/hosts/中对应主机名文件下。
(3)配置 ansible.cfg文件(与Playbook同级):
在所有的 callback 插件中,你想使用哪些 callback 插件:callback_whitelist = timer, log plays