文章目录
一、使用插件
1、回调插件介绍
修改默认的回调插件
同时只能有一个回调插件作为主要的管理者,用于输出到屏幕。
在 ansible.cfg 中配置 stdout 插件
[defaults]
stdout_callback = json # playbook以 JSON 的格式输出结果
默认情况下这仅对 playbook 生效
如果想让 ad-hoc 方式生效应该在 ansible.cfg 文件中做如下设置
[defaults]
bin_ansible_callbacks = True #Ad-hoc以JSON格式输出
设置成功后,随意执行之前的一个 playbook 就会看到输出结果是 JSON 格式的数据
启用其他内置的回调插件
大部分情况下,无论是内置的回调插件还是自定义的回调插件,都需要在 ansible.cfg 中添加到白名单中,从而才能启用
callback_whitelist = timer, mail, profile_roles, custom_callback
timer 这个回调插件可以计算整个 playbook 的运行时间
mail 这个回调插件可以实现发送邮件的功能
profile_roles 这个插件是在执行中提添加用时时间
custom_callback 是自定义的插件,稍后会讲
获取帮助
ansible-doc -t callback -l 可以查看当前可用的回调插件列表
ansible-doc -t callback 可查看具体回调插件的帮助文档
ansible-doc -t callback timer
2、回调插件的类型
回调插件类型在回调插件类中定义
class CallbackModule(CallbackBase):
CALLBACK_TYPE = ‘notification’
不同的回调类型对于 playbook 的输出有不一样的效果
stdout 标准输出类型,用在回调的主管理者
aggregate` 聚合类型, 把此类型插件处理的结果和 stdout 类型插件合并一起输出到标准输出。比如 timer, profile_tasks 等。
notification 通知类型,不参与标准输出,也不影响标准输出插件的正常输出,只是会把执行 playbook 的返回值写的指定的媒介中。
比如: log_plays,mail。假如自定义把执行playbook 的结果输出到数据库中就可以使用此类型。
查看所有默认的查看类型
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 文件的最后添加如下配置
[callback_log_plays]
log_folder=/tmp/ansible/hosts/
配置到白名单
ansible.cfg
callback_whitelist = log_plays
二、开发自定义插件
1、log_plays插件源码分析
# (C) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: log_plays
type: notification
short_description: write playbook output to log file
version_added: historical
description:
- This callback writes playbook output to a file per host in the `/var/log/ansible/hosts` directory
requirements:
- Whitelist in configuration
- A writeable /var/log/ansible/hosts directory by the user executing Ansible on the controller
options:
log_folder:
version_added: '2.9'
default: /var/log/ansible/hosts
description: The folder where log files will be created.
env:
- name: ANSIBLE_LOG_FOLDER
ini:
- section: callback_log_plays
key: log_folder
'''
# 上面的log_folder标题和key保持一致,和下面导入的名字一样, self.log_folder = self.get_option("log_folder")
import os
import time
import json
from ansible.utils.path import makedirs_safe
from ansible.module_utils._text import to_bytes
from ansible.module_utils.common._collections_compat import MutableMapping
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
# NOTE: in Ansible 1.2 or later general logging is available without
# this plugin, just set ANSIBLE_LOG_PATH as an environment variable
# or log_path in the DEFAULTS section of your ansible configuration
# file. This callback is an example of per hosts logging for those
# that want it.
class CallbackModule(CallbackBase):
"""
logs playbook results, per host, in /var/log/ansible/hosts
"""
CALLBACK_VERSION = 2.0 # 插件版本,自定义的
CALLBACK_TYPE = 'notification' # 插件类型,固定写法
CALLBACK_NAME = 'log_plays' # 插件名称
CALLBACK_NEEDS_WHITELIST = True # 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.log_folder = self.get_option("log_folder")
if not os.path.exists(self.log_folder):
makedirs_safe(self.log_folder)
def log(self, host, category, data):
if isinstance(data, MutableMapping):
if '_ansible_verbose_override' in data:
# avoid logging 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
# 文件的路径
path = os.path.join(self.log_folder, host)
# 记录当前时间
now = time.strftime(self.TIME_FORMAT, time.localtime())
# 定义要记录的消息
msg = to_bytes(self.MSG_FORMAT % dict(now=now, category=category, data=data))
# 把消息写的指定好的文件中
with open(path, "ab") as fd:
fd.write(msg)
# 下面的代码不用改
def runner_on_failed(self, host, res, ignore_errors=False):
self.log(host, 'FAILED', res)
def runner_on_ok(self, host, res):
self.log(host, 'OK', res)
def runner_on_skipped(self, host, item=None):
self.log(host, 'SKIPPED', '...')
def runner_on_unreachable(self, host, res):
self.log(host, 'UNREACHABLE', res)
def runner_on_async_failed(self, host, res, jid):
self.log(host, 'ASYNC_FAILED', res)
def playbook_on_import_for_host(self, host, imported_file):
self.log(host, 'IMPORTED', imported_file)
def playbook_on_not_import_for_host(self, host, missing_file):
self.log(host, 'NOTIMPORTED', missing_file)
2、开发插件规则
插件配置和文档标准
Ansible的在线帮助文档是根据每个模块的源代码中的DOCUMENTATION
模块生成的。该DOCUMENTATION
块必须是有效的YAML。
需要为您的插件定义可配置选项,在python文件的部分 DOCUMENTATION
中对其进行描述。
自Ansible 2.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 中的配置块名称
key: log_folder在对应配置块下面的变量名称
required: True/False 必需时为 True,如果不设置,就认为
不是必须的。
type: int/str/list 不是必须的
'''
要使用插件中的配置项,请使用self.get_option("log_folder")
。
3、开发回调插件mysql_plays
我们下面就参考 log_plays
插件编写一个可以将 playbook 的执行结果写如到 MySQL 中的插件。
3.1 准备数据库
首先要设计一个库和表用于存储结果
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 ansible@'%' identified by '123!';
Query OK, 0 rows affected, 1 warning (0.00 sec)
3.2 准备表
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 DEFAULT CURRENT_TIMESTAMP
);
3.3 编写插件
#coding:utf-8
# (C) 2020,kakaops, <kakaops@aliyun.com><WeChat:y86000153>
# (c) 2020 Ansible Custom Plugin Project
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: ansible
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: mysql_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"
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()
# getpass获取当前登录用户
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)
请注意,CALLBACK_VERSION
和CALLBACK_NAME
定义是Ansible 2.0版及更高版本正确运行的插件所必需的。
3.4 保存插件到有效的目录下
没有就创建一个
把插件保存为 mysql_plays.py
文件,并存到ansible 控制节点的如下目录下: ~/.ansible/plugins/callback/
[root@qfedu.com ~]# pwd
/root
[root@qfedu.com ~]# ls .ansible/plugins/callback/mysql_plays.py
.ansible/plugins/callback/mysql_plays.py
或者 /usr/share/ansible/plugins/callback
3.5开启使用插件
在 ansible.cfg 中编辑如下配置
callback_whitelist = mysql_plays
如果还使用了其他插件,请用英文的逗号分开。
比如
callback_whitelist = timer, mysql_plays
默认此插件仅对 playbook 生效,假如希望在 ad-hoc (快捷命令)中生效,继续打开如下配置,并职位 True
bin_ansible_callbacks = True
3.6 关于此插件的使用先决条件等信息
在做好以上步骤后,使用如下方式获取帮助
验证配置的正确性
[root@qfedu.com ~]# ansible-doc -t callback -l |grep mysql_plays
mysql_plays 将 playbook 的执行结果输出到 MySQL 中。
查看帮助文档
[root@qfedu.com ~]# 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
version_added: 2.9
- mysql_password
MySQL 服务器登录用户.
[Default: QFedu123!]
set_via:
env:
- name: ANSIBLE_MYSQL_PASSWORD
ini:
- key: mysql_password
section: callback_mysql_plays
version_added: 2.9
- mysql_port
MySQL 服务器监听端口.
[Default: 3306]
set_via:
env:
- name: ANSIBLE_MYSQL_PORT
ini:
- key: mysql_port
section: callback_mysql_plays
type: int
version_added: 2.9
- mysql_table
存放数据的表名称.
[Default: playsresult]
set_via:
env:
- name: ANSIBLE_MYSQL_TABLE
ini:
- key: mysql_table
section: callback_mysql_plays
version_added: 2.9
- mysql_user
MySQL 服务器登录用户.
[Default: ansible]
set_via:
env:
- name: ANSIBLE_MYSQL_USER
ini:
- key: mysql_user
section: callback_mysql_plays
version_added: 2.9
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 );
METADATA:
status:
- preview
supported_by: community
TYPE: notification
3.6 配置插件使用的选项
关于限制条件
此插件已经有默认值,如果想修改需在 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.7 执行 playbook
playbook**
- hosts: all
gather_facts: no
tasks:
- name: test
shell: date +"%F %T"
Inventory
[root@qfedu.com ~]#[dbservers]
172.18.0.3
[webservers]
172.18.0.4
172.18.0.5
[allservers:children]
dbservers
webservers
执行playbook
[root@qfedu.com ~]# ansible-playbook -i hosts remoteDate.yml
PLAY [all] *************************************************
TASK [test] *************************************************
fatal: [172.18.0.5]: UNREACHABLE! => {“changed”: false, “msg”: “Failed to connect to the host via ssh: ssh: connect to host 172.18.0.5 port 22: Connection refused”, “unreachable”: true}
changed: [172.18.0.3]
changed: [172.18.0.4]
PLAY RECAP ************************************************************************************************************
172.18.0.3 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
172.18.0.4 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
172.18.0.5 : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0
查询数据库**
> 为了输出效果,已对输出信息做成修改
mysql> select * from playsresult\G
************ 11. row ************
id: 21
user: root
host: 172.18.0.5
category: UNREACHABLE
result: {
"msg": "Failed to connect to the host via ssh: ssh: connect to host 172.18.0.5 port 22: Connection refused",
"unreachable": true,
"changed": false}
create_time: 2020-04-24 01:34:46
************ 12. row ************
id: 22
user: root
host: 172.18.0.3
category: OK
result: {
"module_args": {"warn": true,
"executable": null,
"_uses_shell": true,
"strip_empty_ends": true,
"_raw_params": "date +\"%F %T\"",
"removes": null,
"argv": null,
"creates": null,
"chdir": null,
"stdin_add_newline": true,
"stdin": null
}
} => {"stderr_lines": [],
"cmd": "date +\"%F %T\"",
"end": "2020-04-24 01:34:46.762027",
"_ansible_no_log": false,
"stdout": "2020-04-24 01:34:46",
"changed": true,
"rc": 0,
"start": "2020-04-24 01:34:46.518139",
"stderr": "",
"delta": "0:00:00.243888",
"stdout_lines": ["2020-04-24 01:34:46"],
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
}
}
create_time: 2020-04-24 01:34:46
************ 13. row ************
id: 23
user: root
host: 172.18.0.4
category: OK
result: {
"module_args": {"warn": true,
"executable": null,
"_uses_shell": true, "strip_empty_ends": true,
"_raw_params": "date +\"%F %T\"",
"removes": null,
"argv": null,
"creates": null,
"chdir": null,
"stdin_add_newline": true,
"stdin": null
}
} => {"stderr_lines": [],
"cmd": "date +\"%F %T\"",
"end": "2020-04-24 01:34:46.767316",
"_ansible_no_log": false,
"stdout": "2020-04-24 01:34:46",
"changed": true,
"rc": 0,
"start": "2020-04-24 01:34:46.528226",
"stderr": "",
"delta": "0:00:00.239090",
"stdout_lines": ["2020-04-24 01:34:46"],
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"}
}
create_time: 2020-04-24 01:34:46