Ansible 不完全手册

Ansible

认识 andsible

最早是 厄休拉*勒古恩 在 1966 年的小说 《罗卡农的星球》中创造了 Ansible 这个词,用于表示一种能在浩瀚宇宙中即时通信的装置。

Ansible 的创始人 Michael DeHaan 想用这个词来比喻能控制远端大量主机的服务器。

特点

  1. 安装部署简单
  2. 学习曲线很平坦
  3. 支持多台主机并行管理
  4. 无代理,无需在客户机上安装额外软件,使用的是 SSH 协议通信
  5. 非 root 账户也可运行
  6. 不仅仅支持 python ,也可运行使用任何动态语言开发的模块

ansible 是以款自动化管理工具,ansible 公司,同时在 ansible 的基础之上,开发了基于 Web 界面友好的 Ansible Tower IT 自动化管理工具。

管理方式和架构

管理方式

封封丷

Ansible 管理系统由控制主机和一组被管理的节点组成。

控制主机通过 SSH 控制被控节点,被管节点的 IP 等信息,在控制主机的 Ansible 的资源清单(inventory)里进行分组管理

Ansible 通常使用 Ansible 的脚本文件来进行具体的管理配置。这个脚本文件称为 playbook(剧本), playbook 里存放着被作用的主机和这些主机需要执行的任务列表,任务列表是顺序执行的。

安装

Ansible 官方文档安装指南

  • 生产环境推荐:
yum   install ansible

  • 测试环境推荐
    python2.6 及以上版本

控制主机应该有这些模块

paramiko / PyYAML / Jinja2 / httplib2

$ git clone https://github.com/ansible/ansible.git
$ cd ./ansible
$ make rpm
$ sudo rpm -Uvh ./rpm-build/ansible-*.noarch.rpm

可以能需要安装一些软件依赖包

报错信息:

AsciiDoc 'a2x' command is not installed but is required to build

解决办法:

yum install asciidoc

File not found by glob:

这种报错和可能的原因是目前系统使用的默认版本和 rpm 使用的 python 版本不一致,建议放弃,或者使其一致。 

任务执行模式

两种: ad-hoc 和 playbook

  • ad-hoc 模式,是使用单个模块,支持批量执行单条命令

  • playbook 模式,是 Ansible 的主要管理方式,也是 Ansible 功能强大的关键
    playbook 是通过多个 task 集合完成的一类功能,如 Web 服务部署,数据库批量备份等。其实可以把它看做是组合了多个模块和多条 ad-hoc 操作的配置文件。

    基于推送(push)的方式执行任务

Ansible 内置模块都是等幂的。

同一个任务执行都此,得到的效果会是一样的。比如说,创建一个用户或这目录,被控主机假如没有则创建,假如已存在,ansible 则什么也不做。

==工作中最后自己编排适用于自己环境的 playbook,而不是尝试重用通用的 playbook。学习别人的 playbook,主要是看别人是如何实现的==

配置 Ansible 环境

Ansible 执行命令时,会按照预定配置的顺序查找以下配置文件

  1. ANSIBLE_CONFIG 先检查环境变量,以及这个环境变量指向的配置文件
  2. ./ansible.cfg 接着会检查执行命令的当前目录下的 ansible.cfg 配置文件
  3. ~/.ansible.cfg 之后会检查当前用户家目录下的 .ansible.cfg 这个隐藏文件
  4. /etc/ansible/ansible.cfg 最后才会检查用软件包管理工具安装 ansible 时自动产生的配置文件

假如你是通过 GitHub 安装的,ansible.cfg 配置文件会在 example 目录下,把它拷贝到 /etc/ansbile 目录下即可。

ansible.cfg 配置文件常用参数

# 配置资源清单文件的路径
inventory = /etc/ansible/hosts

# 存放 Ansible 模块的目录
library = /usr/share/ansible/

# 默认进程数
forks = 5

# 默认执行命令的用户,这个参数也可以在 playbook 中重新设置
sudo_user = root

# 被管主机的端口
remote_port = 22

# SSH 连接超时时间,单位是 秒
timeout = 60

# Ansible 自己的日志文件完整路径,记录 Ansible 输出的内容。Ansible 默认不记录日志
log_path = /var/log/ansible.log

==指定注意的是: 执行 ansible 的用户要有在被控主机上写入日志的权限,模块会调用被管主机的 syslog 来记录日志。==

使用公钥认证

假如第一次连接被控节点,控制主机默认会检查被控节点的公钥。想禁用的话设置如下配置项,两种方式任选一种:

  1. 在 ansible.cfg 配置文件中配置
# 在 defaults 域下配置
[defaults]
host_key_checking = False

  1. 直接在控制主机上配置 环境变量
export  ANSIBLE_HOST_KEY_CHECKING=False

配置 Linux 主机 SSH 无密码访问

# ssh-keygen -f ~/.ssh/id_rsa -N ""
# ssh-copy-id -i ~/.ssh/id_rsa.pub  [email protected]

172.16.153.129 是被控节点

蜻蜓点水

测试连通性和 ansible 可用性

  1. 先在 /etc/ansible/hosts 文件中配置 被控主机
# 单个主机
172.16.153.129

# 主机组
[openstack]
192.168.2.10
192.168.2.20
192.168.2.30
192.168.2.40

  • 配置 SSH 证书信任的情况下
    由于是第一次和这些主机建立连接,我在 ansible.cfg 配置文件中设置了不检测它们的 密钥
➜  ~ grep host_key_checking /etc/ansible/ansible.cfg
host_key_checking = False

  • 开始使用 ping 模块测试,测试了 openstack 组的主机连通性
➜  ~ ansible openstack -m ping
192.168.2.10 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
192.168.2.40 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
192.168.2.30 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
192.168.2.20 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

测完后,我把检查密钥的配置项进行了注释

➜  ~ grep host_key_checking /etc/ansible/ansible.cfg
#host_key_checking = False

这样就检查第一次连接的主机,但是我们再次连接刚才我们测试的主机就不会再次检查了。
因为刚才在建立连接的过程中,被控主机的公钥已经存放到我们控制主机的一个文件里了。

~/.ssh/known_hosts   # 这里存放了受信任的公钥

  • 也可以使用普通用户进行连接,并通过 --sudo 参数实现 root 权限

    前提条件是: 这个普通用户是 sudo 用户

    -u 指定普通用户

    ansible openstack -m ping  -u  ansible   --sudo
    
    

获取帮助

  • Ansible 的每个工具,都可以在其后面加上 -h 或者 --help 直接获取帮助信息
[ansible@ansible ~]$ ansible-doc -h
Usage: ansible-doc [options] [module...]

Options:
  -a, --all             Show documentation for all modules
  -h, --help            show this help message and exit
  -l, --list            List available modules
  -M MODULE_PATH, --module-path=MODULE_PATH
                        specify path(s) to module library (default=None)
  -s, --snippet         Show playbook snippet for specified module(s)
  -v, --verbose         verbose mode (-vvv for more, -vvvv to enable
                        connection debugging)
  --version             show program's version number and exit

  • 其中 -l 可以列出支持的模块
[ansible@ansible ~]$ ansible-doc --version
ansible-doc 2.3.2.0
[ansible@ansible ~]$ ansible-doc -l |wc -l
1039             # 太过分了!!! 2.3.2 已经支持一千多个模块了。

  • ansible-doc 模块名,可以列出这个模块的描述和使用示例
 ansible-doc yum

  • 用 -s 参数列出模块所支持的动作或者说参数
 ansible-doc yum -s

命令行的调试

使用 -v 或者 -vvv 会输出更详细的信息

ansible  openstack  -m ping -vvv -u  ansible --sudo


Ansible 组件介绍

Inventory (资源清单)

是存放被控节点主机的信息的一个文件

文件格式: INI

默认是 /etc/ansible/hosts

在命令行里执行命令 ansbile 和命令 ansible-playbook 时,可以使用 -i 参数临时指定:

[ansible@ansible ~]$ ansible openstack -m ping -i ./inventory.file

当然也可以定义环境变量: ANSIBLE_HOSTS 声明

定义被控主机和主机组

# 单个主机ip 和这个主机的 ssh 密码
172.16.153.129  ansible_ssh_pass='ansible'

# 单个主机,指定 SSH 端口
badwolf.example.com:5309

# 也可以使用变量来指定,其中 jumper 是这个主机的别名,可以使用这个别名对其操作
jumper ansible_port=5555 ansible_host=192.0.2.50

# 主机名或者FQDN,主机名和 FQDN 必须要可以被控制主机解析
ansible

# 定义了一个 openstack 组,成员 ip 是 192.168.2.10,20,30 和 40
[openstack]
192.168.2.[10:40]

# 定义一个组,成员192.168.2.20,21,22
[computes]
192.168.2.[20:22]

# 定义一个组的变量
[openstack:vars]
ansible_ssh_pass='ansible'

# openstack组下面有个子组,子组名叫 computes,注意这个组必须在此文件中已被定义
[openstack:children]
computes

ansible_ssh_pass 是 Inventory 的内置参数

==虽然可以在资源清单中定义主机组或者主机的变量,但是不建议在这里定义,最佳实战是主机或者主机组要与它们的变量分开文件存放,这些内容安排在后面的变量章节中==

==Ansible 内部有两个默认组:all(包含所有主机) and ungrouped(不属于任何组的主机)==

测试

使用小写字母 o 参数可以输出更漂亮格式的信息

[ansible@ansible ~]$ ansible 192.168.2.21 -m ping -o
192.168.2.21 | SUCCESS => {"changed": false, "ping": "pong"}
[ansible@ansible ~]$ ansible openstack -m ping -o
192.168.2.21 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.22 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.20 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.30 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.10 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.40 | SUCCESS => {"changed": false, "ping": "pong"}

使用多个 Inventory 文件

  1. 定义一个文件夹,里面存放不同的 inventory 文件

注意此目录下的文件的扩展名仅仅支持:空、.yml、.ymal、.json

[ansible@ansible ~]$ sudo mkdir /etc/ansible/Inventory
[ansible@ansible ~]$ cat /etc/ansible/Inventory/openstack
[openstack]
192.168.2.10
192.168.2.30
192.168.2.40

[computes]
192.168.2.20
192.168.2.21
192.168.2.22

[openstack:children]
computes

[ansible@ansible ~]$ cat /etc/ansible/Inventory/hosts.yml
172.16.153.129
ansible

  • 可以看/出定义多个 inventory 文件的内容和定义一个 inventory 文件的格式一样
  1. 再在 /etc/ansible/ansible.cfg 文件中的 [defaults] 配置块中 配置 inventory 的值指向 这个 文件夹
image
  1. 验证配置
    --list-hosts 列出主机或者主机组信息
[ansible@ansible ~]$ ansible computes --list-hosts
  hosts (3):
    192.168.2.20
    192.168.2.21
    192.168.2.22

==动态 Inventory==

在目前互联网环境下的实际生产环境中,inventory 更多的是动态获取的,比如从 CMDB 系统或者 Zabbix 监控系统中拉取所有的主机信息,之后使用 Ansible 进行管理。

动态 inventory 配置

  1. 在 ansible.cfg 文件中的 inventory 值定义为一个可执行脚本,这个脚本可以是任何编程言编写的。
[ansible@ansible ~]$ grep ^inventory /etc/ansible/ansible.cfg
inventory      = /etc/ansible/hosts.py

但是这个脚本必须支持以下参数:

  • --list 或者 -l (小写英文字母) 参数功能:显示所有的主机以及主机组信息(JSON格式)
  • --host 或者 -H 这个参数后面需要指定一个 host,运行结果会返回这台主机的所有信息(包括认证信息、主机变量等),也是 JSON 格式。
  1. 编辑这个脚本
  • 这里只是一个简单的脚本模板
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import json
import argparse

def lists():
    """
    indent 定义输出时的格式缩进的空格数
    """
    dic = {}
    host_list = [ '192.168.2.{}'.format(str(i) ) for i in range(20,23) ]
    hosts_dict = {'hosts': host_list}
    dic['openstack'] = hosts_dict

    return json.dumps(dic,indent=4)

def hosts(name):
    dic = {'ansibl_ssh_pass': '12345'}

    return json.dumps(dic)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-l', '--list', help='host list', action='store_true')
    parser.add_argument('-H', '--host', help='hosts vars')
    args = vars(parser.parse_args())

    if args['list']:
        print( lists() )
    elif args['host']:
        print( hosts(args['host']) )
    else:
        parser.print_help()

  • 改变文件权限为可执行
[ansible@ansible ~]$ sudo chmod 655 /etc/ansible/hosts.py

  • 测试一下
    先使用 -i 参数指定测试试试
[ansible@ansible ~]$ ansible -i /etc/ansible/hosts.py openstack -m ping -o
192.168.2.30 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.20 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.10 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.40 | SUCCESS => {"changed": false, "ping": "pong"}

由于前面我们设定好了 ansible.cfg 文件中的 inventory 的值为一个资源池(可动态获取主机信息的可执行文件),所以可以直接执行。

[ansible@ansible ~]$ ansible openstack -m ping -o
192.168.2.30 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.20 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.10 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.40 | SUCCESS => {"changed": false, "ping": "pong"}

使用混合模式的 inventory

如果在 Ansible 中 -i 给出的位置是一个目录,或者像上一节 使用多个 Inventory 文件 中的情况,在 ansible.cfg 中这样配置了 inventory 的值是一个资源池。

  • 在清单目录中,可执行文件将被视为动态清单来源,而大多数其他文件则被视为静态来源。

这样使用的就是混合云了

  • 在清单目录中,以下结尾的文件将被忽略
~, .orig, .bak, .ini, .cfg, .retry, .pyc, .pyo

这个忽略的列表可以在 ansible.cfg 文件中配置,或者使用此项 ANSIBLE_INVENTORY_IGNORE 配置环境变量。

inventory_ignore_extensions

  1. 配置 /etc/ansible/ansible.cfg
[ansible@ansible ~]$ grep ^inventory /etc/ansible/ansible.cfg
inventory      = /etc/ansible/Inventory/

  1. 静态 inventory 文件
[ansible@ansible ~]$ cat /etc/ansible/Inventory/openstack
[openstack]
192.168.2.10
192.168.2.30
192.168.2.40

[openstack:children]
computes           
# 注意这里定义的组名,并没有在这个文件中定义此组的主机,我将会在动态 inventory 文件中定义。
# 关注验证结果,会是正常的,这样证明了使用了混合 inventory 是成功的。

  1. 动态 inventory 脚本文件
[ansible@ansible ~]$ ls -l /etc/ansible/Inventory/hosts.py
-rw-r-xr-x 1 root root 840 Nov 18 14:19 /etc/ansible/Inventory/hosts.py

[ansible@ansible ~]$ cat /etc/ansible/Inventory/hosts.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import json
import argparse

def lists():
    """
    indent 定义输出时的格式缩进的空格数
    """
    dic = {}
    host_list = [ '192.168.2.{}'.format(str(i) ) for i in range(20,23) ]
    hosts_dict = {'hosts': host_list}
    dic['computes'] = hosts_dict  # 静态文件中的组,在这里定义了主机信息

    return json.dumps(dic,indent=4)

def hosts(name):
    dic = {'ansibl_ssh_pass': '12345'}

    return json.dumps(dic)

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-l', '--list', help='host list', action='store_true')
    parser.add_argument('-H', '--host', help='hosts vars')
    args = vars(parser.parse_args())

    if args['list']:
        print( lists() )
    elif args['host']:
        print( hosts(args['host']) )
    else:
        parser.print_help()

  • 验证配置
[ansible@ansible ~]$ ansible  openstack -m ping -o
192.168.2.20 | SUCCESS => {"changed": false, "ping": "pong"} # 动态 inventory 脚本文件获取的主机
192.168.2.22 | SUCCESS => {"changed": false, "ping": "pong"} # 动态 inventory 脚本文件获取的主机
192.168.2.30 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.10 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.21 | SUCCESS => {"changed": false, "ping": "pong"} # 动态 inventory 脚本文件获取的主机
192.168.2.40 | SUCCESS => {"changed": false, "ping": "pong"}
[ansible@ansible ~]$ ansible  computes -m ping -o
192.168.2.21 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.22 | SUCCESS => {"changed": false, "ping": "pong"}
192.168.2.20 | SUCCESS => {"changed": false, "ping": "pong"}

动态组和静态组

  • When defining groups of groups in the static inventory file, the child groups must also be defined in the static inventory file, or ansible will return an error.
    在静态库存文件中定义一些子组时,这个些子组是有一些限制的:
  1. 假如声明的子组是静态组,还必须在静态库存文件中定义这些子组,就是需要包含具体的 host,否则将返回一个错误。下面是一个错误的例子,我在静态 inventory 文件为组 opentack 定义了一个子组 networks ,但是我没有在这个静态 inventory 文件中定义这个子组 networks 及其成员。也没有在动态脚本中定义它

[ansible@ansible Inventory]$ cat openstack

[openstack]

192.168.2.10
192.168.2.30
192.168.2.40

[openstack:children]
networks
computes


* 所以 ansible 报错了。

[ansible@ansible Inventory]$ ansible openstack -m ping -o
ERROR! Attempted to read "/etc/ansible/Inventory/openstack" as YAML: 'AnsibleUnicode' object has no attribute 'keys'
Attempted to read "/etc/ansible/Inventory/openstack" as ini file: /etc/ansible/Inventory/openstack:7: Section [openstack:children] includes undefined group: networks


   ==经过验证,在静态 inventory 文件中定义这个子组为空,也不会报错==

[图片上传失败...(image-29f9c3-1543815968457)]

  • If you want to define a static group of dynamic child groups, define the dynamic groups as empty in the static inventory file.
    如果要定义的静态组是动态组的子组,请在静态库存文件中将动态组定义为空。

For example:

# 这里是静态 inventory 文件
[tag_Name_staging_foo] 

[tag_Name_staging_bar]

[staging:children]  
tag_Name_staging_foo
tag_Name_staging_bar

Ad-Hoc 命令

所谓 ad-hoc 命令是什么呢?

这其实是一个概念性的名字,是相对于写 Ansible playbook 来说的。两者之间的关系类似于在命令行敲入shell命令和 写shell scripts两者之间的关系

Ansible 命令都是并发执行的,默认的并发数是 ansible.cfg 中的 forks 值来控制的。

运行 ansible 命令行时可以用 -f 指定并发数。

基本格式:

ansible       -m        -a    

  • patterm_goes_here 是主机名、IP、或者已定义的主机组名
  • module_name 模块名
  • arguments 参数

示例:

ansible  computes  -m   shell   -a  "ls  /tmp" -f 10

ansible有许多模块,默认是 command,也就是命令模块,我们可以通过 -m 选项来指定不同的模块.

在 Ad-Hoc 中使用默认的 command 模块执行命令时,-m 参数 可省略,但是不支持管道。想支持的话就是用 shell 模块。

使用异步模式

  • 使用异步功能,请使用 -P 参数
    -P 0 会直接返回 job_id,然后可以根据主机的 job_id 查询执行结果

==2.3.2 版本没有返回 job_id==

File Transfer

  • 把控制主机本地文件拷贝到远端被控主机 copy
ansible test  -m  copy -a  "src=/path/to/sourcefile    dest=/path/to/destinationfile"

==在 playbook 里也可以用 template==

  • 可以添加一些可选选项
[ansible@ansible ~]$ ansible test -m copy -a "src=./add_ip.sh dest=/home/elk/add_ip_3.sh owner=elk group=elk mode=444" -o --sudo

[ansible@ansible ~]$ sudo ls -l /home/elk/
total 4
-r--r--r-- 1 elk elk 354 Nov 20 08:29 add_ip_3.sh

  • 直接更改远程被控主机的文件权限等信息 file
[ansible@ansible ~]$ ansible test -m file -a  "dest=/home/elk/add_ip_3.sh  owner=ansible mode=644 group=ansible" -o  --sudo
172.16.153.129 | SUCCESS => {"changed": true, "gid": 501, "group": "ansible", "mode": "0644", "owner": "ansible", "path": "/home/elk/add_ip_3.sh", "size": 354, "state": "file", "uid": 501}
[ansible@ansible ~]$ sudo ls -l /home/elk/
total 4
-rw-r--r-- 1 ansible ansible 354 Nov 20 08:29 add_ip_3.sh

  • file 模块也可以创建和删除目录,支持递归
[ansible@ansible ~]$ ansible test -m file -a "dest=/home/elk/a/b/c/ state=directory" -o --sudo

state=file         代表拷贝后是文件;
state=link         代表最终是个软链接;
state=directory    代表文件夹;
state=hard         代表硬链接;
state=touch        代表生成一个空文件;
state=absent       代表删除

Managing Packages

  • 安装软件包
    假如软件包安装了,则不做任何操作;没安装,则安装:
$ ansible webservers -m yum  -a  "name=httpd  state=present" --sudo -o

  • 指定版本安装:
$ ansible webservers -m yum -a "name=httpd-2.2.15 state=present"  --sudo -o

  • 卸载软件包:
$ ansible webservers -m yum -a "name=acme state=absent" --sudo -o

Users and Groups

使用 user 模块可以方便的创建账户,删除账户,或是管理现有的账户:

# 创建用户
## 首先应该创建一个 MD5 加密的密文,ansible 的 user 模块的 password 不接收明文密码
$ [ansible@ansible ~]$ echo test |openssl passwd -1 -stdin
$1$QzUtKCgG$q8jXR8iaImWNjk5DKtnYf1       # 把输出的密文复制给下面的命令
$ ansible test -m user -a  'name=test password="$1$QzUtKCgG$q8jXR8iaImWNjk5DKtnYf1" ' --sudo -o
# 删除用户
$ ansible all -m user -a "name=foo state=absent"  --sudo  -o

Managing Services

# 启动服务
$ ansible webservers -m service -a "name=httpd state=started"  --sudo  -o

# 重启服务
$ ansible webservers -m service -a "name=httpd state=restarted"  --sudo  -o

# 停止服务
$ ansible webservers -m service -a "name=httpd state=stopped"   --sudo  -o

获取主机的信息

  • facts
# 获取全部信息
$ ansible test -m setup

# 获取指定信息,下面是获取 绑定网卡的地址
$ ansible test -m setup |grep bond

  • facter 获取主机的静态信息(Puppet)
[ansible@ansible ~]$ ansible test -m yum -a "name=ruby-json,facter  state=installed" --sudo  -o
[ansible@ansible ~]$ ansible test -m facter

  • python 实现
import json
import subprocess
res = subprocess.getoutput("ansible 172.16.153.130 -m setup")
if 'SUCCESS' in res:
    res_dic_str = s.split('SUCCESS =>')[1]
    res_dic_obj = json.loads(res_dic_str)

for k,v in res_dic_obj['ansible_facts'].items():
    print(k,'==',v)    


Playbook 详解

Ansible 的 playbook 文件格式使用的是 YAML 语法。

YAML 语法参考 -->> 官网

简单来说,playbooks 是一种简单的配置管理系统与多机器部署系统的基础.与现有的其他系统有不同之处,且非常适合于复杂应用的部署.

Playbooks 可用于声明配置,更强大的地方在于,在 playbooks 中可以编排有序的执行过程,甚至于做到在多组机器间,来回有序的执行特别指定的步骤.并且可以同步或异步的发起任务

基本的 YAML 语法

对于 Ansible, 每一个 YAML 文件都是从一个列表开始. 列表中的每一项都是一个键值对, 通常它们被称为一个 “哈希” 或 “字典”. 所以, 我们需要知道如何在 YAML 中编写列表和字典.

所有的 YAML 文件(无论和 Ansible 有没有关系)首行都应该是 ---. 这是 YAML 格式的一部分, 表明一个 YAML 文件的开始.

  • 列表 中的所有成员都开始于相同的缩进级别, 并且使用一个 "- " 作为开头(一个横杠和一个空格):
---
# 一个美味水果的列表
- Apple
- Orange
- Strawberry
- Mango

  • 一个字典是由一个简单的 键: 值 的形式组成(这个冒号后面必须是一个空格):
---
# 一位职工的记录
name: Example Developer
job: Developer
skill: Elite

  • 指定一个布尔值(true/fase)
---
create_key: yes
needs_agent: no
knows_oop: True
likes_emacs: TRUE
uses_cvs: false

  • 组合在一起的示例
---
# 一位职工记录
name: Example Developer
job: Developer
skill: Elite
employed: True
foods:
    - Apple
    - Orange
languages:
    ruby: Elite
    python: Elite
# 相当于 {'name': "Example Developer",'job': 'Developer', 'skill': 'Elite', 'employed': True, 'foods': ['Apple', 'Orange'], 'languages': {'ruby': 'Elite', 'python': 'Elite'}}    

需要额外注意的地方

  • 用引号把含有冒号的值包含起来
foo: ""somebody said I should put a colon here: so I did""

  • 用 "{{ var }}" 来对变量进行引用
foo: "{{ variable }}"

编写 Ansible playbooks 掌握以上 YAML 基本的语法就可以了,下面就来讨论一下 playbook 的语法

Playbook 基本语法

Playbook 其实并不是那么神秘,要理解 playbook 很简单。

想理解 playbook ,首先需要懂得 Ansible 的命令行模式,即 Ad-Hoc 命令。

对于 ansible 来说记住 3 句话,对应了 ansible 实现功能的基本组成部分

  1. 针对的目标是谁?
  2. 让目标做什么,其中包含了具体怎么做?
  3. 假如在做的过程中,有变化了,再做什么?

下面看一个 Ad-Hoc 命令的示例:

ansible webservers -m copy  -a  "src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf"

# webservers 就是目标

# -m  copy  就是做什么

# -a  "src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf"  就是具体怎么做

对于第 3 项,在命令行里,是用再此执行另外一条命令来实现的

ansible webservers -m service -a "name=httpd state=restarted"

想象一下这种情况,当你想对目标主机安装一个服务,并且配置一下这个服务的配置文件,之后启动这个服务时。你会发现,你需要在命令行里执行一系列的 Ad-Hoc 命令。

于是你会想要有一种方法把这些整合在其一,一起执行,并且在执行的过程中加入一些逻辑判断,就像是写 shell 脚本一样。这种方法就是 playbook。

在 ansible 中,playbook 的内容,一般有三部分组成:

* hosts
* tasks
* handlers

hosts 就是目标

tasks(任务列表)  就是让目标做什么以及怎么做;一个 task 也称为一个 play,本质是对 ansible 一个模块的调用。

handlers   就是当执行过程中,目标的某些方面发生了改变,就会被触发的动作,这些动作也是一个一个的 task

下面通过一个简单的 playbook 来具体说明一下基本的语法

---     # 表示这个文件是 YMAL 文件
- hosts: webservers   # 指定了 webservers 这个主机组为 这个 playbook 的目标主机,也可以用 all 表示所有主机,其实支持 Ad-Hoc 模式的所有参数
  vars:               # 表示下面是目标主机的变量
    http_port: 80
    max_clients: 200
  remote_user: root   # 远程被控主机的用户名,用这个用户去执行下面所用的 tasks
  tasks:              # 表示下面是这个 playbook 的一个或者多个任务的集合
  - name: ensure apache is at the latest version       # 给每个具体任务起的名字
    yum: pkg=httpd state=latest                        # 第一个具体的任务,是安装最新版的 httpd 软件包,调用了 Ansible 的 yum 模块
  - name: write the apache config file
    template: src=/srv/httpd.j2 dest=/etc/httpd.conf   # 第二个具体的任务拷贝配置文件
    notify:           # 触发的意思,假如 httpd.conf 配置文件的 MD5 值有变化,就会触发下面定义的 handlers
    - restart apache  # 这个是触发的具体事件名字
  - name: ensure apache is running
    service: name=httpd state=started   # 第三个具体的任务,启动 httpd 服务
  handlers:
    - name: restart apache      # 这个 name 的值需要和上面 notify 的值一致
      service: name=httpd state=restarted   # 定义具体 handlers 的状态,重启 httpd 服务

Playbook 基本结构分解

一个基本的 playbook 的机构包括:

  • hosts(主机和用户) 用于指明 playbook 作用的目标
  • tasks(任务列表 ) 用于定义 playbook 所要执行的操作,比如安装软件,拷贝文件等
  • handlers (在发生改变时会被触发的操作)

Hosts

  • hosts 一行的值可以是一个或者多个主机,也可以一个或者多个主机组,之间用逗号分隔;
---
- hosts: 172.16.153.129,ansible   # 这些主机是必须在资源配置文件(Inventory)中定义好了

  • 用 remote_user 设置对远程被控主机执行操作的用户
---
- hosts: 172.16.153.129,ansible
  remote_user: root

  • 也可以这对每个 task 设置用户
---
- hosts: 172.16.153.129,ansible
  remote_user: root
  tasks:
  - name: ping cmd
    ping:
    remote_user: ansible   # ansible 是远程被控主机的用户名

  • 支持 sudo
---
- hosts: webservers
  remote_user: yourname
  sudo: yes
  tasks:
    - service: name=nginx state=started
      remote_user: yourname
      sudo: yes

  • sudo 为指定的用户
---
- hosts: webservers
  remote_user: yourname
  sudo: yes
  sudo_user: postgres

Tasks 列表

每个 playbook 都会有一个 tasks 列表,列表中的每个 task 会作用于所有的 hosts 值中声明的主机。

每个 task 的目标是执行一个 ansible 的模块。

tasks 列表中的 task 执行是按照从上向下顺序执行的。

假如在 hosts 中的某一个 host 执行某一个 task 失败,此 host 将会从整个 playbook 的 rotation(循环) 中移除. 如果发生执行失败的情况,请修正 playbook 中的错误,然后重新执行即可.

每一个 task 必须有一个名称 name,这样在运行 playbook 时,从其输出的任务执行信息中可以很好的辨别出是属于哪一个 task 的。

声明一个 task 使用格式:”module: options”

基本的 task 的定义,service moudle 使用 key=value 格式的来表示 moudle 的参数,这也是大多数 moudle 使用的参数格式:

tasks:
  - name: make sure apache is running
    service: name=httpd state=running

  • 特殊的 moudle

command 和 shell

tasks:
  - name: disable selinux
    command: /sbin/setenforce 0
  - name: test shell moudle
    shell: /bin/ls /  

在执行命令时,可以忽略执行命令中的错误

tasks:
  - name: run this command and ignore the result
    shell: /usr/bin/somecommand
    ignore_errors: True

  • 可以使用 space(空格) 或者 indent(缩进) 隔开连续的一行
tasks:
  - name: Copy ansible inventory file to client
    copy: src=/etc/ansible/hosts dest=/etc/ansible/hosts
            owner=root group=root mode=0644

  • 使用变量
tasks:
  - name: create a virtual host file for {{ vhost }}
    template: src=somefile.j2 dest=/etc/httpd/conf.d/{{ vhost }}

变量需要提前在 vars 里定义,后面会讲到如何定义变量

  • notify 设定触发的事件

notify 在每一个任务执行结束时会被触发,并且即使多个 task 指定了同一个 notify action ,触发条件达到时, notify action 只会被执行一次。

下面示例中是当文件有改变时,重启 2 个服务

- name: template configuration file
  template: src=template.j2 dest=/etc/foo.conf
  notify:
     - restart memcached
     - restart apache

==ntify 列表中每个值,需要和 Handlers 中的 name 值相同。==

Handlers: 在发生改变时执行的操作

Handlers 也是一些 task 的列表,通过名字来引用,如果没有被 notify,handlers 不会执行.不管有多少个通知者进行了 notify,等到 play 中的所有 task 执行完成之后,handlers 也只会被执行一次.

handlers:
    - name: restart memcached
      service:  name=memcached state=restarted
    - name: restart apache
      service: name=apache state=restarted

==Handlers 最佳的应用场景是用来重启服务,或者触发系统重启操作.除此以外很少用到.==
==handlers 会按照声明的顺序执行==

  • 立刻执行正在排队的 handler 命令

默认 handlers 会在 ‘pre_tasks’, ‘roles’, ‘tasks’, 和 ‘post_tasks’ 之间自动执行.

tasks:
   - shell: some tasks go here
   - meta: flush_handlers
   - shell: some other tasks

执行 playbook

  • 并发 10 个进程执行
ansible-playbook playbook.yml -f 10

  • 指定一个 Inventory 执行 playbook.yml
ansible-playbook  -i  /etc/ansible/Inventory/hosts.yml  playbook.yml -f 10

  • 查看一个 playbook 作用的目标主机信息: --list-host
ansible-playbook -i /etc/ansible/Inventory/hosts.yml test.yaml --list-host

  • 使用调试模式

如果你想看到执行成功的 modules 的输出信息,使用 --verbose 或者 -v(否则只有执行失败的才会有输出信息)

ansible-playbook -i /etc/ansible/Inventory/hosts.yml test.yaml -v

==可以使用 -vvv 或者 -vvvv 查看更多的输出信息==

Playbook 角色(Roles) 和 Include 语句

当我们刚开始学习运用 playbook 时,可能会把 playbook 写成一个很大的文件,到后来可能你会希望这些文件是可以方便去重用的,就像你在写脚本时,会把 变量写在一个文件中,静态文件放在一个目录中,函数写在一个文件中,所以想管理好 playbook 就需要重新去组织这些文件。

使用 include 语句引用 tasks 是将 tasks 从其他文件拉取过来。

因为 handlers 也是 tasks,所以你也可以使用 include 语句去引用 handlers 文件。

Playbook 同样可以使用 include 引用其他 playbook 文件中的 play。这时被引用的 play 会被插入到当前的 playbook 中,当前的 playbook 中就有了一个更长的的 play 列表。

Roles 的概念来自于这样的想法:通过 include 包含文件并将它们组合在一起,组织成一个简洁、可重用的抽象对象。

可以认为主要是用于管理多 playbook,本质是对日常使用的 playbook 的目录结构进行一些规范。

每次你写 playbook 的时候都应该使用 Roles。

下面先介绍 Include 语句

Inclued

  • 一个被引用的 task 文件一般是这样子的
---
# possibly saved as tasks/foo.yml

- name: placeholder foo
  command: /bin/foo

- name: placeholder bar
  command: /bin/bar

  • 在一个 playbook 中引用一个 task 文件
tasks:
  - include: tasks/foo.yml

  • 可以给 include 传递变量。称之为 '参数化的 include'
tasks:
  - include: wordpress.yml wp_user=timmy
  - include: wordpress.yml wp_user=alice
  - include: wordpress.yml wp_user=bob

  • 在一个 playbook 中的 task 里引用一个 变量文件

假如有这样的设定:

---
# file: group_vars/all
asdf: 10

---
# file: group_vars/os_CentOS
asdf: 42

可以这样引用

- hosts: all
  tasks:
    - include_vars: "os_{{ ansible_distribution }}.yml"
    - debug: var=asdf

  • 更精简的 include 语法

适用于 1.4 及之后的版本

tasks:
 - { include: wordpress.yml, wp_user: timmy, ssh_keys: [ 'keys/one.txt', 'keys/two.txt' ] }

  • handlers 的 include

假如你定义了一个重启 httpd 服务的 handlers,并且想要重用他,可以这么做

  1. 先创建这个 handlers 文件,名字叫 handers.yml
---
# this might be in a file like handlers/handlers.yml
- name: restart apache
  service: name=apache state=restarted

  1. 引用它
    然后在你的主 playbook 文件中,在一个 play 的最后使用 include 包含 handlers.yml
handlers:
  - include: handlers/handlers.yml

==Include 语句可以和其他非 include 的 tasks 和 handlers 混合使用。但是这种情况下,include 的优先级最高==

  • 在一个playbook中引用其他的 playbook 的例子
- name: this is a play at the top level of a file
  hosts: all
  remote_user: root

  tasks:

  - name: say hi
    tags: foo
    shell: echo "hi..."

- include: load_balancers.yml
- include: webservers.yml
- include: dbservers.yml

接下来,我们就来谈一谈如何很好利用 include 语句,并且更好的来组织 playbook

Role

  • 一个 Roles 项目的目录结构如下:
.
├── fooserver.yml
├── roles
│   ├── common           # 一个 role
│   │   ├── defaults     # 放置一个 role 的默认变量文件
│   │   ├── files        # 一般放置静态文件
│   │   ├── handlers     # 发生改变时做的 handlers 文件
│   │   ├── meta         # 放置 role 的依赖文件
│   │   ├── tasks        # 放置 task 文件
│   │   ├── templates    # 放模板文件
│   │   └── vars         # 放置变量文件
│   └── webservers       # 另一个 role
│       ├── defaults
│       ├── files
│       ├── handlers
│       ├── meta
│       ├── tasks
│       ├── templates
│       └── vars
├── site.yml              # Roles 的入口文件,也是一个 playbook
└── webserver.yml

  • 一般在 site.yml 这个 playbook 中来使用这个 roles
---
- hosts: webservers
  roles:
     - common
     - webservers

假如这个 playbook 为 'http' 这个角色定义的,那么有如下意义:

  • 如果 roles/http/tasks/main.yml 存在, 其中列出的 tasks 将被添加到 play 中
  • 如果 roles/http/handlers/main.yml 存在, 其中列出的 handlers 将被添加到 play 中
  • 如果 roles/http/vars/main.yml 存在, 其中列出的 variables 将被添加到 play 中
  • 如果 roles/http/meta/main.yml 存在, 其中列出的 “角色依赖” 将被添加到 roles 列表中 (1.3 and later)
  • 所有 copy tasks 可以引用 roles/http/files/ 中的文件,不需要指明文件的路径。
  • 所有 script tasks 可以引用 roles/http/files/ 中的脚本,不需要指明文件的路径。
  • 所有 template tasks 可以引用 roles/http/templates/ 中的文件,不需要指明文件的路径。
  • 所有 include tasks 可以引用 roles/http/tasks/ 中的文件,不需要指明文件的路径。

如果 roles 目录下有文件不存在,这些文件将被忽略。比如 roles 目录下面缺少了 ‘vars/’ 目录,这也没关系。

  • 当一些事情不需要频繁去做时,你也可以为 roles 设置触发条件,像下面这样
---
# This is  site.yml file 
- hosts: webservers
  roles:
    - { role: some_role, when: "ansible_os_family == 'RedHat'" }

它的工作方式是:将条件子句应用到 role 中的每一个 task 上。

  • 定义一些 tasks,让它们在 roles 之前以及之后执行
---

- hosts: webservers

  pre_tasks:
    - shell: echo 'hello'

  roles:
    - { role: some_role }

  tasks:
    - shell: echo 'still busy'

  post_tasks:
    - shell: echo 'goodbye'

  • 关于 role 的默认变量

要创建默认变量,只需在 roles 目录下添加 defaults/main.yml 文件。这些变量在所有可用变量中拥有最低优先级,可能被其他地方定义的变量(包括 inventory 中的变量)所覆盖。

角色依赖(Role Dependencies)

New in version 1.3.

“角色依赖” 使你可以自动地将其他 roles 拉取到现在使用的 role 中。”角色依赖” 保存在 roles 目录下的 meta/main.yml 文件中。这个文件应包含一列 roles 和 为之指定的参数,下面是在 roles/myapp/meta/main.yml 文件中的示例:

---
dependencies:
  - { role: common, some_parameter: 3 }
  - { role: apache, port: 80 }
  - { role: postgres, dbname: blarg, other_parameter: 12 }

更多参考 https://ansible-tran.readthedocs.io/en/latest/docs/playbooks_roles.html

Galzxy

Ansible 的 Galaxy 是 Ansible 官方一个分析 role 的功能平台。

网址

https://galaxy.ansible.com/list#/roles

安装(下载)一个 role

[图片上传失败...(image-45cf85-1543815968457)]

默认安装在以下目录下

/etc/ansible/roles/

使用方式和自己写的 role 一样

变量与引用

==In Ansible 1.2 or later the group_vars/ and host_vars/ directories can exist in the playbook directory OR the inventory directory. If both paths exist, variables in the playbook directory will override variables set in the inventory directory.==
假如变量同时存在于 playbook 目录和 inventory 目录,playbook 目录的变量优先

合法的变量名

在使用变量之前最好先知道什么是合法的变量名. 变量名可以为字母,数字以及下划线.变量始终应该以字母开头. “foo_port”是个合法的变量名.”foo5”也是. “foo-port”, “foo port”, “foo.port” 和 “12”则不是合法的变量名.

内置变量

首先需要了解的是默认变量,Ansible 定义了一些在 playbook 中永远可以访问的变量。

hostvars            --->  是一个字典, key 是 ansible 主机的名字,value 是这台主机的所有变量名和相应的变量值
inventory_hostname  --->  当前主机被 Ansible 识别的名字
group_names         --->  列表, 列表中存放了当前主机所属的所有主机组名
groups              --->  字典, key 是 ansible 的主机组名,value 是这个主机组所包含的所有主机,主机组包含了 all 和 ungrouped 分组。{"all":[...],"webservers":[...],"ungrouped":[...]}
play_hosts          --->  列表, 元素是当前 play 涉及到的目标主机的 inventory 主机名
ansible_version     --->  字典, Ansible 的版本信息

Ansible 变量优先级

==不要把事情搞复杂==

但是还是有必要告诉你变量的优先级,以满足你的好奇心。

基本原则有高到低是:

  • extra vars (在命令行中使用 -e)优先级最高
  • 然后是在inventory中定义的连接变量(比如ansible_ssh_user)
  • 接着是大多数的其它变量(命令行转换,play中的变量,included的变量,role中的变量等)
  • 然后是在inventory定义的其它变量
  • 然后是由系统发现的facts
  • 然后是 "role默认变量", 这个是最默认的值,很容易丧失优先权

在哪里定义变量

Ansbile 中定义变量非常灵活

  • (局部变量,作用于每个主机或主机组)在 Inventory 文件中,针对 host 和 groups 定义变量
172.16.153.129  key=hosts_value  username=ansible

[openstack]
192.168.2.[10:30]

[openstack:vars]
ansbible_python_interpreter=/usr/bin/python3.6
key=openstack

可以编辑一个 playbook 来验证一下

---
- hosts: all
  gather_facts: False
  tasks:
  - name: display Host Variable from hostfile
    debug: msg="The {{ inventory_hostname }} Vaule is [ {{ key }} ], username is [{{ username }} ]"

  • (全局变量,作用于所有的 playbook 目标主机)在 playbook 文件中使用 vars 定义变量
---
- hosts: all
  gather_facts: False
  vars:
      key: "openstack_value"
      username: "ansible"
  tasks:
  - name: display Host Variable from hostfile
    debug: msg="The {{ inventory_hostname }} Vaule is [ {{ key }} ], username is [{{ username }} ]"

  • 用引号避开在 playbook 中定义变量的陷阱
- hosts: app_servers
  vars:
       app_path: "{{ base_path }}/22"

  • (全局变量)在 playbook 中使用 vars_files 引用一个变量文件

可在一个文件中存放变量,之后在 playbook 中使用 vars_files 来引用它。
这个文件可以是 YAML 格式或者是 JSON 格式

➜  ~ head var.{yml,json}
==> var.yml <==
key: "Ansible value"
username: ansible

==> var.json <==
{"key": "josn value","username": "ansible"}

引用

➜  ~ head variale.yml
---
- hosts: all
  gather_facts: False
  vars_files:
  - var.json
  #- var.yml
  tasks:
  - name: display Host Variable from hostfile
    debug: msg="The {{ inventory_hostname }} Vaule is [ {{ key }} ], username is [{{ username }} ]"

  • (局部变量)==推荐的方式是通过专门的文件定义主机和主机组的变量==

通过创建 host_vars 目录和 group_vars 目录来分别对应主机和主机组进行变量的定义。
host_vars 目录下,存放的是以 Asnsible 的每个 inventory 主机名为文件名的 YAML 格式的文件。
group_vars 目录下,存放的是以 Asnsible 的每个主机组名为文件名的 YAML 格式的文件。

这两个目录存放的位置:

1\. Asnsible 的 ansible.cfg 配置文件中 inventory 项定义的目录下
2\. 或者可以建立在 playbook 文件的同级目录下

默认的情况下,/etc/ansible/ 目录下的结构

➜  ~ tree /etc/ansible
/etc/ansible
├── ansible.cfg
├── group_vars
│   └── http
├── hosts
├── host_vars
│   └── 172.16.153.129   # 在这个文件中定义针对 172.16.153.129  这台主机的变量
├── Inventory
    ├── hosts.py
    ├── hosts.yml
    └── openstack

  • (全局变量)在命令行里传递变量,优先级最高
➜  ~ ansible-playbook variale.yml -e "key=KEY username=Ansible"

在命令行里也可以引用文件,文件格式同样支持 YAML 和 JSON

➜  ~ ansible-playbook variale.yml -e "@/root/var.yml"
➜  ~ ansible-playbook variale.yml -e "@/root/var.json"

  • 使用 register 注册变量
    使用 register 注册一个变量,可以让这个变量在 task 之前互相传递
➜  ~ cat register.yml
---
- hosts: all
 gather_facts: no
 tasks:
 - name: register a variable
   shell: hostname
   register: info

 - name: display variable
   debug: msg="The variable is {{ info }}"

输出的部分信息

TASK [display variable] ********************************************************
ok: [172.16.153.129] => {
    "msg": "The variable is {'stderr_lines': [], u'changed': True, u'end': u'2017-12-03 11:36:07.667813', u'stdout': u'ansible', u'cmd': u'hostname', u'rc': 0, u'start': u'2017-12-03 11:36:07.598862', u'stderr': u'', u'delta': u'0:00:00.068951', 'stdout_lines': [u'ansible']}"
}
ok: [192.168.2.20] => {
    "msg": "The variable is {'stderr_lines': [], u'changed': True, u'end': u'2017-12-03 11:36:07.595477', u'stdout': u'ansible', u'cmd': u'hostname', u'rc': 0, u'start': u'2017-12-03 11:36:07.483411', u'stderr': u'', u'delta': u'0:00:00.112066', 'stdout_lines': [u'ansible']}"
}

可以看出输出的 info 变量的值是一个字典,我们想要的信息在 stdout 这个键的值,可以使用 python 标准的方式访问 info["stdout"],也可以使用点儿的方式 info.stdout

➜  ~ cat register.yml
***略***
  - name: display variable
    debug: msg="The variable is {{ info.stdout }}"
    #debug: msg="The variable is {{ info['stdout'] }}"

  • 使用 vars_prompt

在 playbook 中使用 vars_prompt,可以实现让用户输入变量的值,并且可以定义某个变量为私有的,当定义一个变量为私有时,用户输入此变量的值的时候,不会显示在屏幕上

定义

➜  ~ cat vars_prompt.yml
---
- hosts: all
  gather_facts: no
  vars_prompt:
  - name: "username"                 # 变量的 key
    prompt: "Please input username"  # 提示信息
    private: no                      # 公有属性
  - name: "passwd"
    prompt: "Please input password"
    default: 'good'                  # 变量的默认值
    private: yes                     # 私有属性
  tasks:
  - name: display one value
    debug: msg="one value is {{ username  }}"
  - name: display two value
    debug: msg="two value is {{ passwd  }}"

测试使用

➜  ~ ansible-playbook  var_prompt.yml -l 172.16.153.129
Please input username: ansible
Please input password [good]:

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

TASK [display one value] *******************************************************
ok: [172.16.153.129] => {
    "msg": "one value is ansible"
}

TASK [display two value] *******************************************************
ok: [172.16.153.129] => {
    "msg": "two value is upsa"
}

PLAY RECAP *********************************************************************
172.16.153.129             : ok=2    changed=0    unreachable=0    failed=0

条件

when 语句

有的 task 的执行,是需要某一个变量的值。
比如,假如系统是 RedHat 就使用 yum 模块安装软件包;假如系统是 Ubuntu 就使用 apt 模块安装软件

➜  ~ cat when.yml
---
- hosts: 172.16.153.129
  tasks:
  - name: "Install a packge"
    yum: name=httpd state=present
    when: ansible_os_family == "RedHat"
  - name: display os family
    debug: var=ansible_os_family

一系列的Jinja2 “过滤器” 也可以在when语句中使用, 但有些是Ansible中独有的. 比如我们想忽略某一错误,通过执行成功与否来做决定,我们可以像这样:

==待进一步核实验证==

# 注意这个示例中并没有使用 name,而是直接使用模块 
➜  ~ cat when_filter.yml
---
- hosts: 172.16.153.129
  gather_facts: no           # 不获取 facts 信息,默认是获取的
  tasks:
  - command: /bin/ls /a
    register: result
    ignore_errors: True      # 假如此 task 失败,继续执行下面的 task
  - shell: /bin/ls /root
    when: result|failed      # 假如 result 是失败的,执行此 task
  - file: dest=/a/b/ state=directory
    when: result|success         # 假如 result 是成功的,执行此 task
  - file: dest=/root/yan.txt state=file
    when: result|skipped        # 假如 result 是被忽略的,执行此 task
  - debug: msg="Then result is {{ result }}"

  • 更完整的示例
---
- hosts: openstack
  tasks:
  - name: Host 172.16.153.129 run this task
    debug: msg="{{ ansible_default_ipv4.address }}"
    when: ansible_default_ipv4.address == "192.168.2.10"

  - name: memtotal < 128 M and processor_cores == 2 run this task
    debug: msg="{{ ansible_fqdn }}"
    when: ansible_memtotal_mb < 128 and asible_processor_cores == 2

  - name: all host run this task
    shell: hostname
    register: info

  - name: Hostname is  ansible Machie run this task
    debug: msg="{{ ansible_fqdn }}"
    when: info['stdout'] == "ansible"

  - name: Hostname is startswith M run this task
    debug: msg="{{ ansible_fqdn }}"
    when: info.stdout.startswith('M')

执行结果中的 skipping 代表此台主机没有执行此 task

  • 可以使用变量的布尔值来作为判断条件
# 定义变量
vars:
    epic: true   

# 条件判断
tasks:
    - shell: echo "This certainly is epic!"
      when: epic
      # 或者下面这样
      when: not epic

  • 如果一个变量不存在,你可以使用Jinja2的defined命令跳过或略过
tasks:
    - when: foo is defined
    - when: bar is not defined

  • 还可以使用 运算符号
tasks:
    - command: echo {{ item }}
      with_items: [ 0, 2, 4, 6, 8, 10 ]
      when: item > 5

  • 条件导入
---
- hosts: all
  remote_user: root
  vars_files:
    - "vars/common.yml"
    - [ "vars/{{ ansible_os_family }}.yml", "vars/os_defaults.yml" ]
  tasks:
  - name: make sure apache is running
    service: name={{ apache }} state=running

这个具体事怎么工作的呢?
如果操作系统是’CentOS’, Ansible导入的第一个文件将是’vars/CentOS.yml’,紧接着 是’/var/os_defaults.yml’,如果这个文件不存在.而且在列表中没有找到,就会报错.
如果操作系统是 Debian,最先查看的将是’vars/Debian.yml’而不是’vars/CentOS.yml’, 如果没找到,则寻找默认文件’vars/os_defaults.yml’ 很简单.

如果使用这个条件性导入特性,你需要在运行playbook之前安装facter 或者 ohai.当然如果你喜欢, 你也可以把这个事情推给Ansible来做:

# for facter
ansible -m yum -a "pkg=facter state=present"
ansible -m yum -a "pkg=ruby-json state=present"

# for ohai
ansible -m yum -a "pkg=ohai state=present"

  • 基于变量选择文件和模版

下面的例子展示怎样根据不同的系统,例如CentOS,Debian制作一个配置文件的模版:

- name: template a file
   template: src={{ item }} dest=/etc/myapp/foo.conf
   with_first_found:
     - files:
        - {{ ansible_distribution }}.conf
        - default.conf
       paths:
        - search_location_one/somedir/
        - /opt/other_location/somedir/

循环

标准 loops

  • 一次安装多个软件包
---
- hosts: all
  gather_facts: no
  tasks:
  - name: Install  package
    yum: pkg={item} state=latest
    with_items:
    - httpd
    - vim

  • 一次创建多个用户
---
- hosts: all
  gather_facts: no
  remote_users: root
  tasks:
  - name: add several users
    user: name={{ item }} state=present groups=wheel
    with_items:
    - testuser1
    - testuser2

with_items 是固定的变量名,是一个列表的名字,Ansible 默认会对这个列表循环,循环的每个元素变量名是 item,可以对 item 的使用来引用列表里的每个元素。

  • 如果你在变量文件中或者 ‘vars’ 区域定义了一组YAML列表,你也可以这样做:
with_items: "{{somelist}}"

  • 也支持哈希的列表(字典)
- name: add several users
  user: name={{ item.name }} state=present groups={{ item.groups }}
  with_items:
    - { name: 'testuser1', groups: 'wheel' }
    - { name: 'testuser2', groups: 'root' }

==如果同时使用 when 和 with_items (或其它循环声明),when 会作用于每个循环的元素==

嵌套循环

主要实现的是 一个对多或者多对多的合并

在这里我想用一个更简单方式演示, debug 模块

---
- hosts: test
  gather_facts: no
  tasks:
  - name: debug log
    debug: msg="name ---> {{ item[0] }} vaule ---> {{ item[1] }} vv --> {{item[2]}}"
    with_nested:
        - ['A','B']
        - ['a1','a2','a3']
        - ['b1','b2','b3']

上面的嵌套循环等同于下面的普通 for 嵌套循环

for item0 in ['A','B']:
    for item1 in ['a1','a2','a3']:
        for item2 in ['b1','b2','b3']:
            print("name ---> {} vaule ---> {} vv --> {}".format(item0,item1,item2))

同样可以使用预定义好的变量。看下面这个使用的示例:

- name: here, 'users' contains the above list of employees
  mysql_user: name={{ item[0] }} priv={{ item[1] }}.*:ALL append_privs=yes password=foo
  with_nested:
    - "{{users}}"
    - [ 'clientdb', 'employeedb', 'providerdb' ]

使用 with_dict 进行哈希 loops

with_dict 可以对 python 字典格式(需要 yml.load 之后)的变量进行循环

哈希循环也叫散列循环,标准的循环要求,最外层必须是 python 的 list 数据类型

哈希循环支持更丰富的数据结构

比如有这样的变量

---
users:
  alice:
    name: Alice Appleworth
    shell: bash
    telephone: 123-456-7890
  bob:
    name: Bob Bananarama
    shell: zsh
    telephone: 987-654-3210

可以这样循环使用它

tasks:
  - name: Print phone records
    debug: msg="User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }}), shell is --> {{ item.value.shell }}"
    with_dict: "{{users}}"

使用 with_fileglob 对文件列表使用循环

with_fileglob 可以以非递归的方式对某个目录下的特定文件进行循环,所有它是支持模糊匹配的.如下面所示:

---
- hosts: all

  tasks:

    # first ensure our target directory exists
    - file: dest=/etc/fooapp state=directory

    # copy each file over that matches the given pattern
    - copy: src={{ item }} dest=/etc/fooapp/ owner=root mode=600
      with_fileglob:
        # 这里匹配的是目录下的所有文件,也可以 *.yml 来匹配 yml 结尾的文件
        - /playbooks/files/fooapp/* 

扁平化列表

---
- hosts: ansible
  gather_facts: no
  vars:
      packages_base:
          - [ 'foo-package', 'bar-package' ]
      packages_apps:
          - [ ['one-package', 'two-package' ]]
          - [ ['red-package'], ['blue-package']]
  tasks:
      - name: flattened loop demo
        debug: msg="{{ item }}"
        with_flattened:
            - "{{packages_base}}"
            - "{{packages_apps}}"

使用 with_random_choice 进行随机循环

---
- hosts: test
  gather_facts: no
  tasks:
  - debug: msg="name ---> {{ item }}"
    with_random_choice:
        - "ansible1"
        - "ansible2"
        - "ansible3"
        - "ansible4"

使用 until 条件判断 loops

通过判断一个条件,第某个 task 执行多少次

---
- hosts: all
  gather_facts: no
  tasks:
  - name: debug loops
    shell: cat  /root/Ansbile
    register: host
    untill: host.stdout.startswith("Master")
    retries: 5  # 总共执行 5 次
    delay: 5    # 每次执行的间隔是 5 秒

使用 with_fist_found 进行文件优先匹配 loops

不经常用,这个用法以及更多的 loops 请参考官方文档: https://ansible-tran.readthedocs.io/en/latest/docs/playbooks_loops.html

Playbook lookups

playbook 的 lookups 是让 Ansible 可以从外部拉取数据信息赋值给 Anssible 变量的一种方式。
这个实现的办法就是 Ansible 的 lookups 插件。

目前 Ansible 已经自带的很多的 lookups 插件,接下来只介绍一些常用的。

* file      --> 可以从一个文件中获取数据
* password  --> 可以把传入的内容进行加密处理
* pipe      --> 就是利用了 Python 的 subprocess.Popen 执行命令,之后把执行命令的结果赋值给变量
* redis_kv  --> 从 Redis 数据库中获取数据
* template  --> 和 file 类似,都是读取文件,但是 template 支持读取 jinja 模板文件,并且 loops.j2 文件是每台主机自己的信息,不是控制主机的信息。

示例:

  • file
---
- hosts: ansible
  gather_facts: no
  vars:
      contents: "{{ lookup('file', '/etc/sysconfig/ne---
- hosts: ansible
  gather_facts: no
  vars:
      contents: "{{ lookup('file', '/etc/sysconfig/entwork') }}"
  tasks:
      - name: debug loops
        debug: msg="The contents is  {% for i in contents.split('\n') %} {{ i }} {% endfor %}" twork') }}"
  tasks:
      - name: debug loops
        debug: msg="The contents is  {% for i in contents.split('\n') %} {{ i }} {% endfor %}"    

  • password
---
- hosts: ansible
  gather_facts: no
  vars:
      contents: "{{ lookup('password', 'aa') }}"
  tasks:
      - name: debug loops
        debug: msg="The contents is  {{ contents }}"

  • pipe
---
- hosts: ansible
  gather_facts: no
  vars:
      contents: "{{ lookup('pipe', 'df -P ') }}"
  tasks:
      - name: debug loops
        debug: msg="The contents is  {% for i in contents.split() %} {{ i }} {% endfor %}"

  • redis_kv
vars:
    contents: "{{ lookup('redis_kv', 'redis: //localhost:6379, somekey') }}"
tasks:
      - name: debug loops
        debug: msg="The contents is  {% for i in contents.split('\n') %} {{ i }} {% endfor %}"    

  • template

模板文件内容

➜  ~ cat lookups.j2
worker_processes {{ ansible_processor_cores }}
IPaddress {{ ansible_ent0.ip4.address }}

使用

---
- hosts: ansible
  gather_facts: yes
  vars:
      contents: "{{ lookup('template', './lookups.j2') }}"
  tasks:
      - name: debug loops
        debug: msg="The contents is  {% for i in contents.split() %} {{ i }} {% endfor %}"  

Jinja2 filter

Ansible 默认支持 Jinja2 语言的内置 filter,下面介绍一些常用的。

---
- hosts: all
  gather_facts: no
  vars:
      list: [1,2,3,4,5]
      one: "1"
      str: "string"
  tasks:
      - name: run commands
        shell: df -h
        register: info

      - name: debug pprint filter
        debug: msg="{{ info.stdout | pprint }}"

      - name: debug conditionals filter
        debug: msg="The run commands status is changed"
        when: info | changed

      - name: debug int capitalize filter
        debug: msg="The int value {{ one | int }} The lower value is {{ str | capitalize }}"

      - name: debug default filter
        debug: msg="The Variable value is {{ ansible | default('ansible is not define') }}"
        # default 是假如 ansible 变量没有定义,则采用 default 内的的值作为 ansible 变量的值

      - name: debug  list max and min filter
        debug: msg="The list max value is {{ list | max }} The list min value is {{ list | min }}"

      - name: debug join filter
        debug: msg="The join filter value is {{ list | join("+") }}"

      - name: debug replace and regex_replace filter
        debug: msg="The replace value is {{ str | replace('t','T') }} The regex_replace value is {{ str | regex_replace('.*str(.*)$','\\1') }}"


你可能感兴趣的:(Ansible 不完全手册)