Ansible 实战演练 day8

实战系列

  • 1.调试debug
  • 2.SSH长连接
  • 3.开启SSH的流水线
  • 4.Facts缓存到JSONFILE
  • 5.Facts缓存到Redis
  • 6.执行策略
  • 7.异步
  • 8.使用混合模式的inventory
  • 9.使用inventory脚本
  • 10.目录结构介绍
  • 插件
    • 一、使用插件
      • 1.回调插件介绍
        • 1.1 修改默认的回调插件
        • 1.2 启用其他内置的回调插件
        • 1.3 获取帮助
      • 2.回调插件的类型
      • 3.把返回结果输出到日志中
    • 二、开发自定义插件
      • 1.log_plays插件源码分析
      • 2.开发插件规则
        • 2.1 使用兼容的Python版本编写
        • 2.2 抛出异常错误信息
        • 2.3 妥当处理字符串
        • 2.4 插件配置和文档标准
      • 3.开发回调插件mysql_plays
        • 3.1 准备数据库
        • 3.2 准备表
        • 3.3 编写插件
        • 3.4 保存插件到有效的目录下
        • 3.5 开启使用插件
        • 3.6 关于此插件的使用先决条件等信息
        • 3.7 配置插件使用的选项
        • 3.8 执行 playbook

YAML小手册

1.调试debug

[root@server ~]# ansible dbservers -i hosts -m ping -vvv

[root@server ~]# cat checkhosts.yml 
- hosts: all
  task:
  - name: check hosts
    ping:
[root@server ~]# ansible-playbook -i hosts checkhosts.yml --limit dbservers

Ansible 实战演练 day8_第1张图片

2.SSH长连接

优化ansible的执行速度 —— 修改连接时间

yum povides ssh
yum -y install openssh
yum -y install openssh-clients


[root@server ~]# ssh -V
OpenSSH_7.4p1, OpenSSL 1.0.2k-fips  26 Jan 2017

❤ansible默认的配置文件是在,修改连接时长
vi /etc/ansible/ansible.cfg
ssh_args = -C -o ControlMaster=auto -o ControlPersist=86400s

yum install iproute
ss -ntal 查看22端口建立情况
[root@server ~]# ansible-playbook -i hosts checkhosts.yml 

3.开启SSH的流水线

优化ansible的执行速度 —— 减少put get操作

# vi /etc/ansible/ansible.cfg
pipelining = True

[root@server ~]# ansible-playbook -i hosts checkhosts.yml --limit dbservers -vvv

4.Facts缓存到JSONFILE

facts是获取主机信息的

vi checkhosts.yml
- hosts: all
  tasks:
  - name: check hosts
    ping:
  - name: debug
    debug:
     var: ansible_facts #ansible distribution

[root@server ~]# cat checkhosts.yml 
- hosts: all
  tasks:
  - name: check hosts
    ping:
  - name: debug
    debug:
      var: ansible_facts 
[root@server ~]# ansible-playbook -i hosts checkhosts.yml --limit dbservers

Ansible 实战演练 day8_第2张图片

需求:不想每次获取task,但想用fact变量
解决:fact缓存到本地文件
smart模式,希望收集,如果有缓存,不再收集
因此 gathering 要改为 gathering = smart
Ansible 实战演练 day8_第3张图片

有效期 vi /etc/ansible/ansible.cfg
fact_caching = jsonfile
fact_caching_connection=/dev/shm/ansible_facts_cache/
fact_caching_timeout = 10
Ansible 实战演练 day8_第4张图片

5.Facts缓存到Redis

 

6.执行策略

默认是按照批处理进行执行的,

  • -f 5 并发执行。 指定每次执行5个主机。5个主机结果都返回了再执行5个
  • /etc/ansible/ansible.cfg ,forks ,修改默认

★可以修改策略为free,提高执行效率
vi /etc/ansible/ansible.cfg

  • strategy = free 自由模式,-f 5 ,如果h4先执行完并返回了,那么h6就会补上,接着h3执行完了,h7也会补上。保证队列是5个,交替切换,一直到执行到最后一个
可以通过ip命令来添加网卡检测上述实验
yum provides ip  反查询文件在什么包中。
yum install iproute
Vmware自带

ip addr add 192.168.1.1/16 dev ens33
ip addr add 192.168.1.2/16 dev ens33
ip addr add 192.168.1.3/16 dev ens33
ip addr add 192.168.1.4/16 dev ens33
ip addr add 192.168.1.5/16 dev ens33
ip addr add 192.168.1.6/16 dev ens33
ip addr add 192.168.1.7/16 dev ens33
ip addr add 192.168.1.8/16 dev ens33


# cat hosts
[dbservers]
192.168.1.133

[webservers]
192.168.1.1
192.168.1.2
192.168.1.3
192.168.1.4
192.168.1.5
192.168.1.6
192.168.1.7
192.168.1.8

# cat checkhosts.yml
- hosts: all
  tasks:
  - name: check hosts
    ping:

传递公钥
# cat send-pubkey.yml
- hosts: all
  remote_user: root
  vars:
    ansible_ssh_pass: 123456
  tasks:
  - name: Set authorized key taken from file
    authorized_key:   # 模块
      user: root
      state: present
      key: "{{ lookup('file','/root/.ssh/id_rsa.pub') }}"

# ansible-playbook -i hosts send-pubkey.yml --limit webservers
每次都要检测公钥,如果不想检测
# vi /etc/ansible/ansible.cfg
host_key_checking = False

# ansible-playbook -i hosts checkhosts.yml -f 3

7.异步

ansible dbservers -i hosts -m shell -a “sleep 10”
ansible 命令一旦执行,进程就会卡住,直到主机执行每个节点的任务返回为止,才能进行下一条命令的执行。执行命令时间过长。

需求:放在后台执行,通过一个异步机制得到结果

-B 2 ,表示等待命令2秒,
-B 5 -P 0 , 0表示不去轮询不等待,直接拿到ansible_job_id的值,并且保存结果到/root/.ansible_async/99859864834.20884。 通过job_id获取到stdout_lines
-P 2 ,表示每两秒去查询,直到查询到结果

[root@server ~]# ansible dbservers -i hosts -m shell -a "sleep 2;echo 'hello'"
192.168.1.133 | CHANGED | rc=0 >>
hello
[root@server ~]# ansible dbservers -i hosts -m shell -a "sleep 2;echo 'hello'" -B 2 -P 0
192.168.1.133 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    }, 
    "ansible_job_id": "99859864834.20884", 
    "changed": true, 
    "finished": 0, 
    "results_file": "/root/.ansible_async/99859864834.20884", 
    "started": 1
}

Ansible 实战演练 day8_第5张图片
ansible提供了一个模块async_staus,通过这个模块可以去获取结果

ansible dbservers -i hosts -m async_status -a "jid=99859864834.20884"

Ansible 实战演练 day8_第6张图片 

结合PlayBook运用

# vim async.yml
---
- hosts: dbservers
  remote_user: root

  tasks:
    - name: simulate long running (5 sec),wait for up to 6 sec,poll every 0 sec
      shell: /bin/sleep 5;hostname -i
      async: 6
      poll: 0
      register: job
    - name: show job id
      debug:
        msg: "Job id is {{ job }}"


# ansible-playbook -i hosts async.yml

Ansible 实战演练 day8_第7张图片
 
可得知job_id:309865680283.25332,再写一个获取结果的playbook

# vim getJobResult.yml
- hosts: dbservers
  tasks:
  - name: Get jobs result
    async_status:
     jid: "309865680283.25332"
    register: job_result

  - name: debug job result
    debug:
      var: job_result

# ansible-playbook -i hosts getJobResult.yml 
yum安装方面性能
用这种方式最好:
yum:
  name: [tree,vim]

循环这种是影响性能:
yum:
  name: "{{ item }}"
  loop: [tree,vim]

 

8.使用混合模式的inventory

多个inventory文件,ansible将多个inventory文件合并成一个进行Playbook执行。

(1)ansible-playbook -i 1.yml -i 2.yml -i 3.yml

(2)把1~3.yml文件放到inventory目录,直接 -i inventory
 +
(3)还有种方式修改 vim /etc/ansible/ansible.cfg的inventory
inventory = /root/inventory/
____________________________________________________
02-static.yml 会覆盖 01-static.yml
# mkdir -p /root/inventory/
# cd /root/inventory
# vim 01-static.yml
[webservers]
192.168.1.[1:8]

[allservers:children]
webservers

[allservers:vars]
name = xiguatian

# vim 02-static.yml
[webservers]
192.168.1.[1:8]

[allservers:children]
webservers

[allservers:vars]
name = shark

# ansible all -i inventory -m debug -a "var=name"

Ansible 实战演练 day8_第8张图片

最后执行的生效
# vi /root/inventory/group_vars/all.yml 
name: Guozige
# ansible all -i inventory -m debug -a "var=name"

Ansible 实战演练 day8_第9张图片

9.使用inventory脚本

使用脚本去获取资产清单

清单脚本不限制语言
脚本限制条件

  • 脚本必须接受 --list--host 参数
  • 当使用单个 --list参数调用脚本时,脚本必须输出到标准输出,就是输出到终端,其中包含要管理的所有组的JSON编码的哈希或字典。

每个组的值应该是包含每个主机列表,任何子组和潜在组变量的哈希或字典,或者仅是主机列表:

{
    "group001": {
        "hosts": ["hoste01", "hoste02"],
        "vars": {
            "var1": true
        },
        "children": ["groupe02"]
    },
    "group002": {
        "hosts": ["host003", "host004"],
        "vars": {
            "var2": 500
        },
        "children": []
    }
}

如果组中的任何元素为空,则可以从输出中将其省略

  • 当使用host 参数(其实是上面的主机)进行调用时,脚本必须打印一个空的JSON哈希/字典或含有这个主机变量的哈希/字典,以使其可用于模板和剧本。例如:
{
  "VAR001": "VALUE",
  "VAR002": "VALUE",
}

打印变量是可选的。如果脚本不执行此操作,则应打印一个空的哈希或字典

  • 一个简单的示例
vim get_hosts.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import json
import argparse

def lists():
    dic = {}
    host_list = ['192.168.122.20','192.168.122.21','192.168.122.22']
    hosts_dict = {'hosts': host_list}
    dic['computes'] = hosts_dict
    return json.dumps(dic,indent=4)  # 返回json格式,字典,每个层级都是4个空格

def hosts(name):
    dic = {'ansible_ssh_pass': '12345'}
    return json.dumps(dic)

if __name__ == '__main__':   # if作为执行文件执行的时候,而不是模块导入。
    parser = argparse.ArgumentParser()  # 调用Parser类产生实例
    parser.add_argument('-l','--list',help='host list',action='store_true')  # 固定写法
    parser.add_argument('-H','--host',help='hosts vars')  # 固定写法
    args = vars(parser.parse_args())  # 调用parser模块的处理参数parse_args,会处理前两条添加的参数。 转为一个字典  args = {'host': None,'list': False}

    if args['list']:  # 如果传了list参数,就执行lists()
        print(lists())
    elif args['host']:
        print(hosts(args['host']))
    else:
        parser.print_help()  # 模块的内置方法
  • 改变文件权限为可执行
# chmod 655 /etc/ansible/hosts.py

测试验证

[root@server ansible]# ./host.py -H 192.168.122.20
{"ansible_ssh_pass": "12345"}
[root@server ansible]# ./host.py -l
{
    "computes": {
        "hosts": [
            "192.168.122.20", 
            "192.168.122.21", 
            "192.168.122.22"
        ]
    }
}

Ansible 实战演练 day8_第10张图片
在ansible中使用:

[root@server ansible]# ansible all -i host.py --list-host
  hosts (3):
    192.168.122.20
    192.168.122.21
    192.168.122.22

将host.py移动到inventory目录下,之前在cfg已经指定了默认
[root@server ansible]# cp host.py /root/inventory/
[root@server ansible]# ansible all -i /root/inventory/ --list-host
  hosts (11):
    192.168.122.20
    192.168.122.21
    192.168.122.22
    192.168.1.1
    192.168.1.2
    192.168.1.3
    192.168.1.4
    192.168.1.5
    192.168.1.6
    192.168.1.7
    192.168.1.8


10.目录结构介绍

使用官方建议的目录结构来组织很多role和playbook文件是个很棒的建议。

假如你用role 封装了playbook,并且任务依赖文件或者依赖其他的任务时,建议使用目录结构管理。

假如是一个简单的独立任务,只使用playbook文件即可,这样会方便我们在其他地方进行引用。

下面是官网最佳实战中推荐的目录结构
Ansible 实战演练 day8_第11张图片
Ansible 实战演练 day8_第12张图片

插件

一、使用插件

1.回调插件介绍

1.1 修改默认的回调插件

同时只能有一个回调插件作为主要的管理者,用于输出到屏幕。
如果想替换,应该在这个插件中修改CALLBACK_TYPE = stdout
之后再ansible.cfg 中配置stdout插件。

# vi /etc/ansible/ansible.cfg
stdout_callback = json # 以JSON的格式输出结果

或使用自定义的回调:

stdout_callback = mycallback

默认情况下着仅对playbook生效,如果想让ad-hoc方式生效应该在ansible.cfg文件中做如下设置:

ad-hoc方式:ansible all -i hosts -m shell -a "date +'%F %T'"

# vi /etc/ansible/ansible.cfg
[defaults]
bin_ansible_callbacks = True

未修改默认的回调插件:

想执行一个命令,但也想知道结果什么时间

[root@server ~]# cat hosts
[dbservers]
192.168.1.133

[webservers]
192.168.1.134
192.168.1.1

[allservers:children]
dbservers
webservers

[allservers:vars]
user=tomcat

[root@server ~]# cat remoteDate.yml 
- hosts: all
  gather_facts: no
  tasks:
  - name: test
    shell: date +"%F %T"

无反馈值,通过PlayBook执行
[root@server ~]# ansible-playbook -i hosts checkhosts.yml
Ansible 实战演练 day8_第13张图片无反馈值,通过PlayBook执行
[root@server ~]# ansible-playbook -i hosts remoteDate.yml
Ansible 实战演练 day8_第14张图片

有反馈值,直接通过快捷命令
[root@server ~]# ansible all -i hosts -m shell -a "date +'%F %T'"
Ansible 实战演练 day8_第15张图片


修改默认的回调插件:
同时只能有一个回调插件作为主要的管理者,用于输出到屏幕。
如果想替换,应该在这个插件中修改CALLBACK_TYPE = stdout
之后再ansible.cfg 中配置stdout插件。

# vi /etc/ansible/ansible.cfg
stdout_callback = json # 以JSON的格式输出结果

或使用自定义的回调:

stdout_callback = mycallback

有反馈值,先修改回调插件,再通过PlayBook执行

[root@server ~]# ansible-playbook -i hosts remoteDate.yml 

Ansible 实战演练 day8_第16张图片
默认情况下着仅对playbook生效,如果想让ad-hoc方式生效应该在ansible.cfg文件中做如下设置:

ad-hoc方式:ansible all -i hosts -m shell -a "date +'%F %T'"
[root@server ~]# ansible all -i hosts -m shell -a "date +'%F %T'"
[WARNING]: Unhandled error in Python interpreter discovery for host 192.168.1.1: Failed to connect to the host via ssh: ssh: connect to host 192.168.1.1 port 22: Connection refused
192.168.1.1 | UNREACHABLE! => {
    "changed": false, 
    "msg": "Data could not be sent to remote host \"192.168.1.1\". Make sure this host can be reached over ssh: ssh: connect to host 192.168.1.1 port 22: Connection refused\r\n", 
    "unreachable": true
}
192.168.1.133 | CHANGED | rc=0 >>
2020-07-13 15:13:06
192.168.1.134 | CHANGED | rc=0 >>
2020-07-13 15:13:06

# vi /etc/ansible/ansible.cfg
[defaults]
bin_ansible_callbacks = True

修改之后,再次执行,就能看到json格式的,和playbook效果一样
[root@server ~]# ansible all -i hosts -m shell -a "date +'%F %T'"

 

1.2 启用其他内置的回调插件

大部分情况下,无论是内置的回调插件还是自定义的回调插件,都需要在ansible.cfg添加到白名单中,从而才能启用。

callback_whitelist=timer,mail,profile_roles,custom_callback
  • timer 这个回调插件可以计算整个playbook的运行时间
  • mail这个回调插件可以实现发送邮件的功能
  • profile_roles这个插件是在执行中提添加用时时间
  • custom_callback 是自定义的插件,稍后会讲
注释掉1.2 ansible.cfg所改动的json
vim /etc/ansible/ansible.cfg
#stdout_callback = json

正常情况未启动其他内置的回调插件

[root@server ~]# cat sleep.yml 
- hosts: all
  gather_facts: no
  tasks:
  - name: sleep 2
    command: sleep 2
[root@server ~]# ansible-playbook -i hosts sleep.yml --limit 192.168.1.133

Ansible 实战演练 day8_第17张图片
启动回调插件
timer

vi /etc/ansible/ansible.cfg
callback_whitelist=timer

ansible-playbook -i hosts sleep.yml --limit 192.168.1.133

Ansible 实战演练 day8_第18张图片

vi /etc/ansible/ansible.cfg
callback_whitelist=timer,profile_roles

ansible-playbook -i hosts sleep.yml --limit 192.168.1.133

Ansible 实战演练 day8_第19张图片

1.3 获取帮助

ansible-doc -t callback -l 可以插看当前可用的回调插件列表(callback_whitelist有才可以看得到)
ansible-doc -t callback 可查看具体回调插件的帮助文档

Ansible 实战演练 day8_第20张图片

2.回调插件的类型

回调插件类型在回调插件类中定义:

class CallbackModule(CallbackBase):
  CALLBACK_TYPE = 'notification'

不同的回调类型对于playbook的输出有不一样的效果

-stdout 标准输出类型,用在回调的主管理替

  • aggregate聚合类型,把此类型插件处理的结果和stdout类型插件合并一起输出到标准输出。比如:timerprofile_tasks等。
  • notification通知类型,不参与标准输出,也不影响标准输出插件的正常输出,只是会把执行playbook的返回值写的指定的媒介中。
    比如:log_playsmail。假如自定义把执行playbook的结果输出到数据库中就可以使用此类型。
ansible-doc -t callback mail

查看所有默认的查看类型

grep 'CALLBACK_TYPE =.*' /usr/lib/python2.7/site-packages/ansible/plugins/callback/*.py | cut -d: -f 2 | sort -u

    CALLBACK_TYPE = 'aggregate'
    CALLBACK_TYPE = 'notification'
    CALLBACK_TYPE = 'stdout'

3.把返回结果输出到日志中

内置的回调插件log_plays会将playbook的返回信息输出到var/log/ansible/hosts目录中。

可以在ansible.cfg中配置指定的目录,使用log_folder
比如,把日志存到/tmp/ansible/hosts/目录下在ansible.cfg 文件的最后添加如下配置

# ansible-doc -t callback log_plays
- log_folder
        The folder where log files will be created.
        [Default: /var/log/ansible/hosts]
        set_via:
          env:
          - name: ANSIBLE_LOG_FOLDER
          ini:
          - key: log_folder
            section: callback_log_plays

# vi /etc/ansible/ansible.cfg
bin_ansible_callbacks = True
[callback_log_plays]
log_folder=/tmp/ansible/hosts/

进行配置到白名单中
# vi /etc/ansible/ansible.cfg
callback_whitelist = log_plays

——————————————————————————————————————————————————————————————
# ansible all -i hosts -m ping

# ansible-playbook -i hosts remoteDate.yml
# ls /tmp/ansible/hosts/

Ansible 实战演练 day8_第21张图片
Ansible 实战演练 day8_第22张图片
 

二、开发自定义插件

1.log_plays插件源码分析

获取源码

[root@server ~]# ansible-doc -t callback log_plays
> LOG_PLAYS    (/usr/lib/python2.7/site-packages/ansible/plugins/callback/log_plays.py)

[root@server ~]# cat /usr/lib/python2.7/site-packages/ansible/plugins/callback/log_plays.py
xxxxxxxxx
源码文档所有内容
xxxxxxxxx

DOCUMENTATION = '''
    callback: log_plays # callback名字
'''

class CallbackModule(CallbackBase): # 继承CallbackBase
	def set_options(xx..)    # 重写 
		self.log_folder = self.get_optoin("log_folder")  # log_folder 和上面的option选项要一致。 
	def log(xx..)  # 记录日志
		path = os.path.join(selef.log_folder,host) # 作为绝对路径命名
		msg = to_bytes(xx...) # 转化为Unicode
		with open(path,"ab") as fd:
			fd.write(msg) # b 二进制,不用关心字符编码
	def runner_on_failed(xx..)
		self.og(host,'FAILED',res)  # ansible会在失败的状态下去自动执行。
	def v2    # 2.0开始的版本

继承说明:

class A:
	citype = "BJ"
	def say(self):
		print("hello")

class B(A)
	pass

obj1 = B()
obj1.citye  # 获得 'BJ'
obj1.say()  # 打印hello

重写:
class C(A):
	def say(self):
		print("world")

c = C()
c.say()   # 打印world

2.开发插件规则

  • 用Python编写
  • 引发错误,就是遇到问题后,主动抛出异常
  • 返回以unicode编码的字符串,主要是兼容Jinja2
  • 符合Ansible的配置和文档标准,就是可以通过ansible.cfg 进行配置

2.1 使用兼容的Python版本编写

由于开发出来的插件将在控制器上执行,因此您必须使用兼容版本的Python(Python 2(2.7版)或Python3(3.5版及更高版本)的)进行编写。

2.2 抛出异常错误信息

应该通过引发AnsibleError()或类似的类并返回描述错误的消息来返回插件执行过程中遇到的错误。

将其他异常包装到错误消息中时,应始终使用Ansible的函数to_native来确保跨Python版本的字符串兼容性:

from ansible.module_utils._text import to_native 
from ansible.errors import AnsibleError

try: 
	cause_an_exception()
except Exception as e: 
	raise AnsibleError(' Something happened, this was original exception:%s'% to_native(e))

2.3 妥当处理字符串

您必须将插件返回的所有字符串转换为Python的unicode类型。转换为unicode可确保这些字符串可以通过Jinja2运行。

转换字符串:

from ansible.module_utils._text import to_text 
result_string=to_text(result_string)

2.4 插件配置和文档标准

Ansible的在线帮助文档(ansible-doc)是根据每个模块的源代码中的DOCUMENTATION模块块生成的。该DOCUMENTATION块必须是有效的YAML。

需要为您的插件定义可配置选项,在python文件的部分DOCUMENTATION中对其进行描述。

自Ansible2.4版以来,回调和连接插件已经开始以这种方式声明配置要求了。现在大多数插件类型都执行相同的操作。这种方法可确保插件选项的文档始终是正确的和最新的。
DOCUMENTATION块中的所有字段均为小写。除非另有说明,否则所有字段都是必填字段

DOCUMENTATION = '''
    callback: log_plays
    type: notification
    short_description: write playbook output to log file
    version_added: historical
    description:
      - 此插件的详细描述信息。
      - 使用多条目,不要使用一个较长的语句。
      - 不应该提及模块名称。
    requirements:
     - 必须要求清单
     - 包括最低版本的限制
    options:
      log_folder:
        version_added: '2.9' 此插件添加到 Ansible 时候的当时 Ansible 的版本。
        default: 选项的默认值,如果 required 是 False, 则 default 可以设置
        description: 此选项的作用的详细说明。应该用完整的句子写成。
        env:
          - name: 环境变量的名字
        ini:
          - section: 在 asible.cfg 中的配置块名称 [xx]
            key:  log_folder在对应配置块下面的变量名称 [xx]下对应的变量名
        required: True/False  必需时为 True,如果不设置,就认为
                  不是必须的。
        type: int/str/list 不是必须设置
'''

[root@server ~]# ansible-doc -t callback log_plays
> LOG_PLAYS    (/usr/lib/python2.7/site-packages/ansible/plugins/callback/log_plays.py)

[root@server ~]# cat /usr/lib/python2.7/site-packages/ansible/plugins/callback/log_plays.py

要访问插件中的配置设置,请使用self.get_option("log_folder")

如果需要显式个配置选项设置值,请使用self.set_options()
 

3.开发回调插件mysql_plays

将PlayBook的结果写在mysql里

回调插件会在响应事件时,向Ansible添加新行为。
要创建回调插件,请使用(CallbacksBase)类作为父类创建一个新类:

from ansible.plugins.callback import CallbackBase

class CallbackModule(CallbackBase):
    pass

在 CallbackModule 覆盖 CallbackBase 中的特定方法。

对于打算与Ansible 2.0及更高版本一起使用的插件,您应该仅覆盖以v2开头的方法。
对于可以重写哪些方法呢,可以参阅 lib/ansible/plugins/callback 目录下的 init.py 文件的内容。

我们下面就参考 log_plays 插件编写一个可以将 playbook 的执行结果写如到 MySQL 中的插件。

3.1 准备数据库

[root@server ~]# mysqladmin -u root password '123'
[root@server ~]# mysql -u root -p
Enter password: 

首先要设计一个库和表用于存储结果

mysql> create database if not exists ansible default charset utf8mb4 collate utf8mb4_general_ci;
Query OK, 1 row affected (0.00 sec)
mysql> grant all on ansible.* to root@'%' identified by '123';
Query OK, 0 rows affected, 1 warning (0.00 sec)

3.2 准备表

mysql> use ansible;
mysql> create table playsresult(
       id int auto_increment primary key,
       user varchar(16) not null,
       host varchar(32) not null,
       category varchar(11) not null,
       result text,
       create_time datetime NOT NULL
      );
mysql> show columns from playsresult; 或者 desc playsresult;

+-------------+-------------+------+-----+---------+----------------+
| Field       | Type        | Null | Key | Default | Extra          |
+-------------+-------------+------+-----+---------+----------------+
| id          | int(11)     | NO   | PRI | NULL    | auto_increment |
| user        | varchar(16) | NO   |     | NULL    |                |
| host        | varchar(32) | NO   |     | NULL    |                |
| category    | varchar(11) | NO   |     | NULL    |                |
| result      | text        | YES  |     | NULL    |                |
| create_time | datetime    | NO   |     | NULL    |                |
+-------------+-------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

3.3 编写插件

在log_plays基础上进行修改
#vi /root/.ansible/plugins/callback/mysql_plays.py

# 开发文档
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = '''
    callback: mysql_plays
    type: notification
    short_description: 将 playbook 的执行结果输出到 MySQL 中。
    version_added: historical
    description:
      - 这个回调插件将会把输出存入 MySQL 服务器中。
    requirements:
     - 需要配置到 ansible.cfg 中 Whitelist
     - 可以被访问的 MySQL 服务器实例
     - Python 版本对应的 pymysql 或者 mysqlclient 模块
     - 创表语句(注意:这里的表名需要根据选项中 mysql_table 的值一致)
       create table playsresult(
         id int auto_increment primary key,
         user varchar(16) not null,
         host varchar(32) not null,
         category varchar(11) not null,
         result text,
         create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
        );
    options:
      mysql_host:
        version_added: '2.9'
        default: locallhost
        description: MySQL 服务器 IP或者主机名.
        env:  # 环境变量大写
          - name: ANSIBLE_MYSQL_HOST
        ini:
          - section: callback_mysql_plays
            key: mysql_host
      mysql_port:
        version_added: '2.9'
        default: 3306
        description: MySQL 服务器监听端口.
        env:
          - name: ANSIBLE_MYSQL_PORT
        ini:
          - section: callback_mysql_plays
            key: mysql_port
        type: int
      mysql_user:
        version_added: '2.9'
        default: root
        description: MySQL 服务器登录用户.
        env:
          - name: ANSIBLE_MYSQL_USER
        ini:
          - section: callback_mysql_plays
            key: mysql_user
      mysql_password:
        version_added: '2.9'
        default: '123'
        description: MySQL 服务器登录用户.
        env:
          - name: ANSIBLE_MYSQL_PASSWORD
        ini:
          - section: callback_mysql_plays
            key: mysql_password
      mysql_db:
        version_added: '2.9'
        default: ansible
        description: 存放数据的库名称.
        env:
          - name: ANSIBLE_MYSQL_DB
        ini:
          - section: callback_mysql_plays
            key: db
      mysql_table:
        version_added: '2.9'
        default: playsresult
        description: 存放数据的表名称.
        env:
          - name: ANSIBLE_MYSQL_TABLE
        ini:
          - section: callback_mysql_plays
            key: mysql_table
'''

# 模块导入
import json
import getpass

from ansible.module_utils.common._collections_compat import MutableMapping
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_native


try:
    import pymysql as mysqldb
    pwd = "password"  # 注意这里是password
    database = "db"
except ImportError:
    try:
        import MySQLdb as mysqldb
        pwd = "passwd"
        database = "database"
    except ImportError:
        raise AnsibleError("找不到 pymysql 或 mysqlclient 模块。")

# 核心选项
class CallbackModule(CallbackBase):
    """
    把 playbook 的结果保存到 MySQL 数据库中,默认的库.表是 ansible.playsresult
    """
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'notification'
    CALLBACK_NAME = 'mysql_plays'
    CALLBACK_NEEDS_WHITELIST = True

    TIME_FORMAT = "%b %d %Y %H:%M:%S"
    MSG_FORMAT = "%(now)s - %(category)s - %(data)s\n\n"

    def __init__(self):
        super(CallbackModule, self).__init__()

    def set_options(self, task_keys=None, var_options=None, direct=None):
        """
        用于设置选项和获取选项, 选项包含了自定义的选项
        """
        super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)

        self.mysql_host = self.get_option("mysql_host")
        self.mysql_port = self.get_option("mysql_port")
        self.mysql_user = self.get_option("mysql_user")
        self.mysql_password = self.get_option("mysql_password")
        self.mysql_db = self.get_option("mysql_db")
        self.mysql_table = self.get_option("mysql_table")

        self.user = getpass.getuser()

    def _mysql(self):
        """
        连接数据库,返回数据库对象和游标对象
        """
        db_conn={"host": self.mysql_host, # 自己会去调用
                 "port": self.mysql_port,
                 "user": self.mysql_user,
                 pwd: self.mysql_password,
                 database: self.mysql_db}

        try:
            db = mysqldb.connect(**db_conn)
        except Exception as e:
            raise AnsibleError("%s" % to_native(e))

        cursor= db.cursor()

        return db, cursor


    def _execute_sql(self, host, category, data):
        if isinstance(data, MutableMapping):
            if '_ansible_verbose_override' in data:
                # avoid save extraneous data
                data = 'omitted'
            else:
                data = data.copy()
                invocation = data.pop('invocation', None)
                data = json.dumps(data, cls=AnsibleJSONEncoder)
                if invocation is not None:
                    data = json.dumps(invocation) + " => %s " % data

        sql = """
              insert into {}(host,user,category,result)
              values(%s,%s,%s,%s)
              """.format(self.mysql_table)

        db, cursor = self._mysql()
                               
        try:
            # 执行 sql,记录事件类型和事件结果
            cursor.execute(sql, (host, self.user, category, data))
            db.commit()
        except Exception as e:
            raise AnsibleError("%s" % to_native(e))
        finally:
            cursor.close()
            db.close()

    def runner_on_failed(self, host, res, ignore_errors=False):
        self._execute_sql(host, 'FAILED', res)

    def runner_on_ok(self, host, res):
        self._execute_sql(host, 'OK', res)

    def runner_on_skipped(self, host, item=None):
        self._execute_sql(host, 'SKIPPED', '...')

    def runner_on_unreachable(self, host, res):
        self._execute_sql(host, 'UNREACHABLE', res)

    def runner_on_async_failed(self, host, res, jid):
        self._execute_sql(host, 'ASYNC_FAILED', res)

    def playbook_on_import_for_host(self, host, imported_file):
        self._execute_sql(host, 'IMPORTED', imported_file)

    def playbook_on_not_import_for_host(self, host, missing_file):
        self._execute_sql(host, 'NOTIMPORTED', missing_file)

3.4 保存插件到有效的目录下

[root@server ~]# pwd
/root
[root@server ~]# ls .ansible/plugins/callback/mysql_plays.py
.ansible/plugins/callback/mysql_plays.py

3.5 开启使用插件

添加到白名单

# vim /etc/ansible/ansible.cfg
callback_whitelist=timer,profile_roles,log_plays,mysql_plays   #不能是标准输出类型,标准输出类型是在stdout_callback定义的

Ansible 实战演练 day8_第23张图片

默认此插件仅对 playbook 生效,假如希望在 ad-hoc (快捷命令)中生效,继续打开如下配置,并职位 True

bin_ansible_callbacks = True

在这里插入图片描述

3.6 关于此插件的使用先决条件等信息

#ansible-doc -t callback -l | grep mysql  查看有没有mysql插件
#ansible-doc -t callback mysql_plays  查看有无错误

[root@server ~]# ansible-doc -t callback -l | grep mysql
mysql_plays          将 playbook 的执行结果输出到 MySQL 中。                                                                      
[root@server ~]# ansible-doc -t callback mysql_plays
> MYSQL_PLAYS    (/root/.ansible/plugins/callback/mysql_plays.py)

        这个回调插件将会把输出存入 MySQL 服务器中。

  * This module is maintained by The Ansible Community
OPTIONS (= is mandatory):

- mysql_db
        存放数据的库名称.
        [Default: ansible]
        set_via:
          env:
          - name: ANSIBLE_MYSQL_DB
          ini:
          - key: db
            section: callback_mysql_plays
        
        version_added: 2.9

- mysql_host
        MySQL 服务器 IP或者主机名.
        [Default: locallhost]
        set_via:
          env:
          - name: ANSIBLE_MYSQL_HOST
          ini:
          - key: mysql_host
            section: callback_mysql_plays
        
:...skipping...
> MYSQL_PLAYS    (/root/.ansible/plugins/callback/mysql_plays.py)

        这个回调插件将会把输出存入 MySQL 服务器中。

  * This module is maintained by The Ansible Community
OPTIONS (= is mandatory):

- mysql_db
        存放数据的库名称.
        [Default: ansible]
        set_via:
          env:
          - name: ANSIBLE_MYSQL_DB
          ini:
          - key: db
            section: callback_mysql_plays
        
        version_added: 2.9

- mysql_host
        MySQL 服务器 IP或者主机名.
        [Default: locallhost]
        set_via:
          env:
          - name: ANSIBLE_MYSQL_HOST
          ini:
          - key: mysql_host
            section: callback_mysql_plays
        
        version_added: 2.9

- mysql_password
        MySQL 服务器登录用户.
        [Default: 123]
        set_via:
          env:
          - name: ANSIBLE_MYSQL_PASSWORD
          ini:
          - key: mysql_password
            section: callback_mysql_plays

3.7 配置插件使用的选项

关于限制条件
此插件已经有默认值,如果想修改需在 ansible.cfg 文件的最后添加如下配置

[callback_mysql_plays]
mysql_host = MySQL IP
mysql_port = MySQL 监听端口
mysql_user = MySQL 用户
mysql_password = MySQL 密码
mysql_db = MySQL 库名
mysql_table = MySQL 表名

3.8 执行 playbook

playbook

# vim remoteDate.yml
- hosts: all
  gather_facts: no
  tasks:
  - name: test
    shell: date +"%F %T"

Inventory

# vim hosts
[dbservers]
192.168.1.133

[webservers]
#192.168.1.134
192.168.1.1

[allservers:children]
dbservers
webservers

[allservers:vars]
user=tomcat

执行playbook

# ansible-playbook -i hosts remoteDate.yml

执行之后,查询数据库
Ansible 实战演练 day8_第24张图片
执行之后,查询日志信息
Ansible 实战演练 day8_第25张图片

西瓜真甜

你可能感兴趣的:(#,自动化运维,ansible,linux,自动化运维,Ansible)