Initial commit

This commit is contained in:
Ansible Core Team 2020-03-09 09:11:07 +00:00
commit aebc1b03fd
4861 changed files with 812621 additions and 0 deletions

View file

View file

@ -0,0 +1,60 @@
# (c) 2015, Andrew Gaffney <andrew@agaffney.org>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: actionable
type: stdout
short_description: shows only items that need attention
description:
- Use this callback when you dont care about OK nor Skipped.
- This callback suppresses any non Failed or Changed status.
deprecated:
why: The 'default' callback plugin now supports this functionality
removed_in: '2.11'
alternative: "'default' callback plugin with 'display_skipped_hosts = no' and 'display_ok_hosts = no' options"
extends_documentation_fragment:
- default_callback
requirements:
- set as stdout callback in configuration
# Override defaults from 'default' callback plugin
options:
display_skipped_hosts:
name: Show skipped hosts
description: "Toggle to control displaying skipped task/host results in a task"
type: bool
default: no
env:
- name: DISPLAY_SKIPPED_HOSTS
deprecated:
why: environment variables without "ANSIBLE_" prefix are deprecated
version: "2.12"
alternatives: the "ANSIBLE_DISPLAY_SKIPPED_HOSTS" environment variable
- name: ANSIBLE_DISPLAY_SKIPPED_HOSTS
ini:
- key: display_skipped_hosts
section: defaults
display_ok_hosts:
name: Show 'ok' hosts
description: "Toggle to control displaying 'ok' task/host results in a task"
type: bool
default: no
env:
- name: ANSIBLE_DISPLAY_OK_HOSTS
ini:
- key: display_ok_hosts
section: defaults
'''
from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
class CallbackModule(CallbackModule_default):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.actionable'

View file

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
# (c) 2018 Matt Martz <matt@sivel.net>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
callback: cgroup_memory_recap
callback_type: aggregate
requirements:
- whitelist in configuration
- cgroups
short_description: Profiles maximum memory usage of tasks and full execution using cgroups
description:
- This is an ansible callback plugin that profiles maximum memory usage of ansible and individual tasks, and displays a recap at the end using cgroups
notes:
- Requires ansible to be run from within a cgroup, such as with C(cgexec -g memory:ansible_profile ansible-playbook ...)
- This cgroup should only be used by ansible to get accurate results
- To create the cgroup, first use a command such as C(sudo cgcreate -a ec2-user:ec2-user -t ec2-user:ec2-user -g memory:ansible_profile)
options:
max_mem_file:
required: True
description: Path to cgroups C(memory.max_usage_in_bytes) file. Example C(/sys/fs/cgroup/memory/ansible_profile/memory.max_usage_in_bytes)
env:
- name: CGROUP_MAX_MEM_FILE
ini:
- section: callback_cgroupmemrecap
key: max_mem_file
cur_mem_file:
required: True
description: Path to C(memory.usage_in_bytes) file. Example C(/sys/fs/cgroup/memory/ansible_profile/memory.usage_in_bytes)
env:
- name: CGROUP_CUR_MEM_FILE
ini:
- section: callback_cgroupmemrecap
key: cur_mem_file
'''
import time
import threading
from ansible.plugins.callback import CallbackBase
class MemProf(threading.Thread):
"""Python thread for recording memory usage"""
def __init__(self, path, obj=None):
threading.Thread.__init__(self)
self.obj = obj
self.path = path
self.results = []
self.running = True
def run(self):
while self.running:
with open(self.path) as f:
val = f.read()
self.results.append(int(val.strip()) / 1024 / 1024)
time.sleep(0.001)
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.general.cgroup_memory_recap'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super(CallbackModule, self).__init__(display)
self._task_memprof = None
self.task_results = []
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.cgroup_max_file = self.get_option('max_mem_file')
self.cgroup_current_file = self.get_option('cur_mem_file')
with open(self.cgroup_max_file, 'w+') as f:
f.write('0')
def _profile_memory(self, obj=None):
prev_task = None
results = None
try:
self._task_memprof.running = False
results = self._task_memprof.results
prev_task = self._task_memprof.obj
except AttributeError:
pass
if obj is not None:
self._task_memprof = MemProf(self.cgroup_current_file, obj=obj)
self._task_memprof.start()
if results is not None:
self.task_results.append((prev_task, max(results)))
def v2_playbook_on_task_start(self, task, is_conditional):
self._profile_memory(task)
def v2_playbook_on_stats(self, stats):
self._profile_memory()
with open(self.cgroup_max_file) as f:
max_results = int(f.read().strip()) / 1024 / 1024
self._display.banner('CGROUP MEMORY RECAP')
self._display.display('Execution Maximum: %0.2fMB\n\n' % max_results)
for task, memory in self.task_results:
self._display.display('%s (%s): %0.2fMB' % (task.get_name(), task._uuid, memory))

View file

@ -0,0 +1,52 @@
# (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: context_demo
type: aggregate
short_description: demo callback that adds play/task context
description:
- Displays some play and task context along with normal output
- This is mostly for demo purposes
requirements:
- whitelist in configuration
'''
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
"""
This is a very trivial example of how any callback function can get at play and task objects.
play will be 'None' for runner invocations, and task will be None for 'setup' invocations.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.general.context_demo'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, *args, **kwargs):
super(CallbackModule, self).__init__(*args, **kwargs)
self.task = None
self.play = None
def v2_on_any(self, *args, **kwargs):
self._display.display("--- play: {0} task: {1} ---".format(getattr(self.play, 'name', None), self.task))
self._display.display(" --- ARGS ")
for i, a in enumerate(args):
self._display.display(' %s: %s' % (i, a))
self._display.display(" --- KWARGS ")
for k in kwargs:
self._display.display(' %s: %s' % (k, kwargs[k]))
def v2_playbook_on_play_start(self, play):
self.play = play
def v2_playbook_on_task_start(self, task, is_conditional):
self.task = task

View file

@ -0,0 +1,246 @@
# (c) 2018, Ivan Aragones Muniesa <ivan.aragones.muniesa@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
Counter enabled Ansible callback plugin (See DOCUMENTATION for more information)
'''
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible import constants as C
from ansible.plugins.callback import CallbackBase
from ansible.utils.color import colorize, hostcolor
from ansible.template import Templar
from ansible.playbook.task_include import TaskInclude
DOCUMENTATION = '''
callback: counter_enabled
type: stdout
short_description: adds counters to the output items (tasks and hosts/task)
description:
- Use this callback when you need a kind of progress bar on a large environments.
- You will know how many tasks has the playbook to run, and which one is actually running.
- You will know how many hosts may run a task, and which of them is actually running.
extends_documentation_fragment:
- default_callback
requirements:
- set as stdout callback in ansible.cfg (stdout_callback = counter_enabled)
'''
class CallbackModule(CallbackBase):
'''
This is the default callback interface, which simply prints messages
to stdout when new callback events are received.
'''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.counter_enabled'
_task_counter = 1
_task_total = 0
_host_counter = 1
_host_total = 0
def __init__(self):
super(CallbackModule, self).__init__()
self._playbook = ""
self._play = ""
def _all_vars(self, host=None, task=None):
# host and task need to be specified in case 'magic variables' (host vars, group vars, etc)
# need to be loaded as well
return self._play.get_variable_manager().get_vars(
play=self._play,
host=host,
task=task
)
def v2_playbook_on_start(self, playbook):
self._playbook = playbook
def v2_playbook_on_play_start(self, play):
name = play.get_name().strip()
if not name:
msg = u"play"
else:
msg = u"PLAY [%s]" % name
self._play = play
self._display.banner(msg)
self._play = play
self._host_total = len(self._all_vars()['vars']['ansible_play_hosts_all'])
self._task_total = len(self._play.get_tasks()[0])
def v2_playbook_on_stats(self, stats):
self._display.banner("PLAY RECAP")
hosts = sorted(stats.processed.keys())
for host in hosts:
stat = stats.summarize(host)
self._display.display(u"%s : %s %s %s %s %s %s" % (
hostcolor(host, stat),
colorize(u'ok', stat['ok'], C.COLOR_OK),
colorize(u'changed', stat['changed'], C.COLOR_CHANGED),
colorize(u'unreachable', stat['unreachable'], C.COLOR_UNREACHABLE),
colorize(u'failed', stat['failures'], C.COLOR_ERROR),
colorize(u'rescued', stat['rescued'], C.COLOR_OK),
colorize(u'ignored', stat['ignored'], C.COLOR_WARN)),
screen_only=True
)
self._display.display(u"%s : %s %s %s %s %s %s" % (
hostcolor(host, stat, False),
colorize(u'ok', stat['ok'], None),
colorize(u'changed', stat['changed'], None),
colorize(u'unreachable', stat['unreachable'], None),
colorize(u'failed', stat['failures'], None),
colorize(u'rescued', stat['rescued'], None),
colorize(u'ignored', stat['ignored'], None)),
log_only=True
)
self._display.display("", screen_only=True)
# print custom stats
if self._plugin_options.get('show_custom_stats', C.SHOW_CUSTOM_STATS) and stats.custom:
# fallback on constants for inherited plugins missing docs
self._display.banner("CUSTOM STATS: ")
# per host
# TODO: come up with 'pretty format'
for k in sorted(stats.custom.keys()):
if k == '_run':
continue
self._display.display('\t%s: %s' % (k, self._dump_results(stats.custom[k], indent=1).replace('\n', '')))
# print per run custom stats
if '_run' in stats.custom:
self._display.display("", screen_only=True)
self._display.display('\tRUN: %s' % self._dump_results(stats.custom['_run'], indent=1).replace('\n', ''))
self._display.display("", screen_only=True)
def v2_playbook_on_task_start(self, task, is_conditional):
args = ''
# args can be specified as no_log in several places: in the task or in
# the argument spec. We can check whether the task is no_log but the
# argument spec can't be because that is only run on the target
# machine and we haven't run it there yet at this time.
#
# So we give people a config option to affect display of the args so
# that they can secure this if they feel that their stdout is insecure
# (shoulder surfing, logging stdout straight to a file, etc).
if not task.no_log and C.DISPLAY_ARGS_TO_STDOUT:
args = ', '.join(('%s=%s' % a for a in task.args.items()))
args = ' %s' % args
self._display.banner("TASK %d/%d [%s%s]" % (self._task_counter, self._task_total, task.get_name().strip(), args))
if self._display.verbosity >= 2:
path = task.get_path()
if path:
self._display.display("task path: %s" % path, color=C.COLOR_DEBUG)
self._host_counter = 0
self._task_counter += 1
def v2_runner_on_ok(self, result):
self._host_counter += 1
delegated_vars = result._result.get('_ansible_delegated_vars', None)
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
self._print_task_banner(result._task)
if isinstance(result._task, TaskInclude):
return
elif result._result.get('changed', False):
if delegated_vars:
msg = "changed: %d/%d [%s -> %s]" % (self._host_counter, self._host_total, result._host.get_name(), delegated_vars['ansible_host'])
else:
msg = "changed: %d/%d [%s]" % (self._host_counter, self._host_total, result._host.get_name())
color = C.COLOR_CHANGED
else:
if delegated_vars:
msg = "ok: %d/%d [%s -> %s]" % (self._host_counter, self._host_total, result._host.get_name(), delegated_vars['ansible_host'])
else:
msg = "ok: %d/%d [%s]" % (self._host_counter, self._host_total, result._host.get_name())
color = C.COLOR_OK
self._handle_warnings(result._result)
if result._task.loop and 'results' in result._result:
self._process_items(result)
else:
self._clean_results(result._result, result._task.action)
if self._run_is_verbose(result):
msg += " => %s" % (self._dump_results(result._result),)
self._display.display(msg, color=color)
def v2_runner_on_failed(self, result, ignore_errors=False):
self._host_counter += 1
delegated_vars = result._result.get('_ansible_delegated_vars', None)
self._clean_results(result._result, result._task.action)
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
self._print_task_banner(result._task)
self._handle_exception(result._result)
self._handle_warnings(result._result)
if result._task.loop and 'results' in result._result:
self._process_items(result)
else:
if delegated_vars:
self._display.display("fatal: %d/%d [%s -> %s]: FAILED! => %s" % (self._host_counter, self._host_total,
result._host.get_name(), delegated_vars['ansible_host'],
self._dump_results(result._result)),
color=C.COLOR_ERROR)
else:
self._display.display("fatal: %d/%d [%s]: FAILED! => %s" % (self._host_counter, self._host_total,
result._host.get_name(), self._dump_results(result._result)),
color=C.COLOR_ERROR)
if ignore_errors:
self._display.display("...ignoring", color=C.COLOR_SKIP)
def v2_runner_on_skipped(self, result):
self._host_counter += 1
if self._plugin_options.get('show_skipped_hosts', C.DISPLAY_SKIPPED_HOSTS): # fallback on constants for inherited plugins missing docs
self._clean_results(result._result, result._task.action)
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
self._print_task_banner(result._task)
if result._task.loop and 'results' in result._result:
self._process_items(result)
else:
msg = "skipping: %d/%d [%s]" % (self._host_counter, self._host_total, result._host.get_name())
if self._run_is_verbose(result):
msg += " => %s" % self._dump_results(result._result)
self._display.display(msg, color=C.COLOR_SKIP)
def v2_runner_on_unreachable(self, result):
self._host_counter += 1
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
self._print_task_banner(result._task)
delegated_vars = result._result.get('_ansible_delegated_vars', None)
if delegated_vars:
self._display.display("fatal: %d/%d [%s -> %s]: UNREACHABLE! => %s" % (self._host_counter, self._host_total,
result._host.get_name(), delegated_vars['ansible_host'],
self._dump_results(result._result)),
color=C.COLOR_UNREACHABLE)
else:
self._display.display("fatal: %d/%d [%s]: UNREACHABLE! => %s" % (self._host_counter, self._host_total,
result._host.get_name(), self._dump_results(result._result)),
color=C.COLOR_UNREACHABLE)

501
plugins/callback/dense.py Normal file
View file

@ -0,0 +1,501 @@
# (c) 2016, Dag Wieers <dag@wieers.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: dense
type: stdout
short_description: minimal stdout output
extends_documentation_fragment:
- default_callback
description:
- When in verbose mode it will act the same as the default callback
author:
- Dag Wieers (@dagwieers)
requirements:
- set as stdout in configuration
'''
HAS_OD = False
try:
from collections import OrderedDict
HAS_OD = True
except ImportError:
pass
from ansible.module_utils.six import binary_type, text_type
from ansible.module_utils.common._collections_compat import MutableMapping, MutableSequence
from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
from ansible.utils.color import colorize, hostcolor
from ansible.utils.display import Display
import sys
display = Display()
# Design goals:
#
# + On screen there should only be relevant stuff
# - How far are we ? (during run, last line)
# - What issues occurred
# - What changes occurred
# - Diff output (in diff-mode)
#
# + If verbosity increases, act as default output
# So that users can easily switch to default for troubleshooting
#
# + Rewrite the output during processing
# - We use the cursor to indicate where in the task we are.
# Output after the prompt is the output of the previous task.
# - If we would clear the line at the start of a task, there would often
# be no information at all, so we leave it until it gets updated
#
# + Use the same color-conventions of Ansible
#
# + Ensure the verbose output (-v) is also dense.
# Remove information that is not essential (eg. timestamps, status)
# TODO:
#
# + Properly test for terminal capabilities, and fall back to default
# + Modify Ansible mechanism so we don't need to use sys.stdout directly
# + Find an elegant solution for progress bar line wrapping
# FIXME: Importing constants as C simply does not work, beats me :-/
# from ansible import constants as C
class C:
COLOR_HIGHLIGHT = 'white'
COLOR_VERBOSE = 'blue'
COLOR_WARN = 'bright purple'
COLOR_ERROR = 'red'
COLOR_DEBUG = 'dark gray'
COLOR_DEPRECATE = 'purple'
COLOR_SKIP = 'cyan'
COLOR_UNREACHABLE = 'bright red'
COLOR_OK = 'green'
COLOR_CHANGED = 'yellow'
# Taken from Dstat
class vt100:
black = '\033[0;30m'
darkred = '\033[0;31m'
darkgreen = '\033[0;32m'
darkyellow = '\033[0;33m'
darkblue = '\033[0;34m'
darkmagenta = '\033[0;35m'
darkcyan = '\033[0;36m'
gray = '\033[0;37m'
darkgray = '\033[1;30m'
red = '\033[1;31m'
green = '\033[1;32m'
yellow = '\033[1;33m'
blue = '\033[1;34m'
magenta = '\033[1;35m'
cyan = '\033[1;36m'
white = '\033[1;37m'
blackbg = '\033[40m'
redbg = '\033[41m'
greenbg = '\033[42m'
yellowbg = '\033[43m'
bluebg = '\033[44m'
magentabg = '\033[45m'
cyanbg = '\033[46m'
whitebg = '\033[47m'
reset = '\033[0;0m'
bold = '\033[1m'
reverse = '\033[2m'
underline = '\033[4m'
clear = '\033[2J'
# clearline = '\033[K'
clearline = '\033[2K'
save = '\033[s'
restore = '\033[u'
save_all = '\0337'
restore_all = '\0338'
linewrap = '\033[7h'
nolinewrap = '\033[7l'
up = '\033[1A'
down = '\033[1B'
right = '\033[1C'
left = '\033[1D'
colors = dict(
ok=vt100.darkgreen,
changed=vt100.darkyellow,
skipped=vt100.darkcyan,
ignored=vt100.cyanbg + vt100.red,
failed=vt100.darkred,
unreachable=vt100.red,
)
states = ('skipped', 'ok', 'changed', 'failed', 'unreachable')
class CallbackModule_dense(CallbackModule_default):
'''
This is the dense callback interface, where screen estate is still valued.
'''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'dense'
def __init__(self):
# From CallbackModule
self._display = display
if HAS_OD:
self.disabled = False
self.super_ref = super(CallbackModule, self)
self.super_ref.__init__()
# Attributes to remove from results for more density
self.removed_attributes = (
# 'changed',
'delta',
# 'diff',
'end',
'failed',
'failed_when_result',
'invocation',
'start',
'stdout_lines',
)
# Initiate data structures
self.hosts = OrderedDict()
self.keep = False
self.shown_title = False
self.count = dict(play=0, handler=0, task=0)
self.type = 'foo'
# Start immediately on the first line
sys.stdout.write(vt100.reset + vt100.save + vt100.clearline)
sys.stdout.flush()
else:
display.warning("The 'dense' callback plugin requires OrderedDict which is not available in this version of python, disabling.")
self.disabled = True
def __del__(self):
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
def _add_host(self, result, status):
name = result._host.get_name()
# Add a new status in case a failed task is ignored
if status == 'failed' and result._task.ignore_errors:
status = 'ignored'
# Check if we have to update an existing state (when looping over items)
if name not in self.hosts:
self.hosts[name] = dict(state=status)
elif states.index(self.hosts[name]['state']) < states.index(status):
self.hosts[name]['state'] = status
# Store delegated hostname, if needed
delegated_vars = result._result.get('_ansible_delegated_vars', None)
if delegated_vars:
self.hosts[name]['delegate'] = delegated_vars['ansible_host']
# Print progress bar
self._display_progress(result)
# # Ensure that tasks with changes/failures stay on-screen, and during diff-mode
# if status in ['changed', 'failed', 'unreachable'] or (result.get('_diff_mode', False) and result._resultget('diff', False)):
# Ensure that tasks with changes/failures stay on-screen
if status in ['changed', 'failed', 'unreachable']:
self.keep = True
if self._display.verbosity == 1:
# Print task title, if needed
self._display_task_banner()
self._display_results(result, status)
def _clean_results(self, result):
# Remove non-essential attributes
for attr in self.removed_attributes:
if attr in result:
del(result[attr])
# Remove empty attributes (list, dict, str)
for attr in result.copy():
if isinstance(result[attr], (MutableSequence, MutableMapping, binary_type, text_type)):
if not result[attr]:
del(result[attr])
def _handle_exceptions(self, result):
if 'exception' in result:
# Remove the exception from the result so it's not shown every time
del result['exception']
if self._display.verbosity == 1:
return "An exception occurred during task execution. To see the full traceback, use -vvv."
def _display_progress(self, result=None):
# Always rewrite the complete line
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.nolinewrap + vt100.underline)
sys.stdout.write('%s %d:' % (self.type, self.count[self.type]))
sys.stdout.write(vt100.reset)
sys.stdout.flush()
# Print out each host in its own status-color
for name in self.hosts:
sys.stdout.write(' ')
if self.hosts[name].get('delegate', None):
sys.stdout.write(self.hosts[name]['delegate'] + '>')
sys.stdout.write(colors[self.hosts[name]['state']] + name + vt100.reset)
sys.stdout.flush()
# if result._result.get('diff', False):
# sys.stdout.write('\n' + vt100.linewrap)
sys.stdout.write(vt100.linewrap)
# self.keep = True
def _display_task_banner(self):
if not self.shown_title:
self.shown_title = True
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.underline)
sys.stdout.write('%s %d: %s' % (self.type, self.count[self.type], self.task.get_name().strip()))
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
sys.stdout.flush()
else:
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline)
self.keep = False
def _display_results(self, result, status):
# Leave the previous task on screen (as it has changes/errors)
if self._display.verbosity == 0 and self.keep:
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
else:
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline)
self.keep = False
self._clean_results(result._result)
dump = ''
if result._task.action == 'include':
return
elif status == 'ok':
return
elif status == 'ignored':
dump = self._handle_exceptions(result._result)
elif status == 'failed':
dump = self._handle_exceptions(result._result)
elif status == 'unreachable':
dump = result._result['msg']
if not dump:
dump = self._dump_results(result._result)
if result._task.loop and 'results' in result._result:
self._process_items(result)
else:
sys.stdout.write(colors[status] + status + ': ')
delegated_vars = result._result.get('_ansible_delegated_vars', None)
if delegated_vars:
sys.stdout.write(vt100.reset + result._host.get_name() + '>' + colors[status] + delegated_vars['ansible_host'])
else:
sys.stdout.write(result._host.get_name())
sys.stdout.write(': ' + dump + '\n')
sys.stdout.write(vt100.reset + vt100.save + vt100.clearline)
sys.stdout.flush()
if status == 'changed':
self._handle_warnings(result._result)
def v2_playbook_on_play_start(self, play):
# Leave the previous task on screen (as it has changes/errors)
if self._display.verbosity == 0 and self.keep:
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline + vt100.bold)
else:
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.bold)
# Reset at the start of each play
self.keep = False
self.count.update(dict(handler=0, task=0))
self.count['play'] += 1
self.play = play
# Write the next play on screen IN UPPERCASE, and make it permanent
name = play.get_name().strip()
if not name:
name = 'unnamed'
sys.stdout.write('PLAY %d: %s' % (self.count['play'], name.upper()))
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
sys.stdout.flush()
def v2_playbook_on_task_start(self, task, is_conditional):
# Leave the previous task on screen (as it has changes/errors)
if self._display.verbosity == 0 and self.keep:
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline + vt100.underline)
else:
# Do not clear line, since we want to retain the previous output
sys.stdout.write(vt100.restore + vt100.reset + vt100.underline)
# Reset at the start of each task
self.keep = False
self.shown_title = False
self.hosts = OrderedDict()
self.task = task
self.type = 'task'
# Enumerate task if not setup (task names are too long for dense output)
if task.get_name() != 'setup':
self.count['task'] += 1
# Write the next task on screen (behind the prompt is the previous output)
sys.stdout.write('%s %d.' % (self.type, self.count[self.type]))
sys.stdout.write(vt100.reset)
sys.stdout.flush()
def v2_playbook_on_handler_task_start(self, task):
# Leave the previous task on screen (as it has changes/errors)
if self._display.verbosity == 0 and self.keep:
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline + vt100.underline)
else:
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline + vt100.underline)
# Reset at the start of each handler
self.keep = False
self.shown_title = False
self.hosts = OrderedDict()
self.task = task
self.type = 'handler'
# Enumerate handler if not setup (handler names may be too long for dense output)
if task.get_name() != 'setup':
self.count[self.type] += 1
# Write the next task on screen (behind the prompt is the previous output)
sys.stdout.write('%s %d.' % (self.type, self.count[self.type]))
sys.stdout.write(vt100.reset)
sys.stdout.flush()
def v2_playbook_on_cleanup_task_start(self, task):
# TBD
sys.stdout.write('cleanup.')
sys.stdout.flush()
def v2_runner_on_failed(self, result, ignore_errors=False):
self._add_host(result, 'failed')
def v2_runner_on_ok(self, result):
if result._result.get('changed', False):
self._add_host(result, 'changed')
else:
self._add_host(result, 'ok')
def v2_runner_on_skipped(self, result):
self._add_host(result, 'skipped')
def v2_runner_on_unreachable(self, result):
self._add_host(result, 'unreachable')
def v2_runner_on_include(self, included_file):
pass
def v2_runner_on_file_diff(self, result, diff):
sys.stdout.write(vt100.bold)
self.super_ref.v2_runner_on_file_diff(result, diff)
sys.stdout.write(vt100.reset)
def v2_on_file_diff(self, result):
sys.stdout.write(vt100.bold)
self.super_ref.v2_on_file_diff(result)
sys.stdout.write(vt100.reset)
# Old definition in v2.0
def v2_playbook_item_on_ok(self, result):
self.v2_runner_item_on_ok(result)
def v2_runner_item_on_ok(self, result):
if result._result.get('changed', False):
self._add_host(result, 'changed')
else:
self._add_host(result, 'ok')
# Old definition in v2.0
def v2_playbook_item_on_failed(self, result):
self.v2_runner_item_on_failed(result)
def v2_runner_item_on_failed(self, result):
self._add_host(result, 'failed')
# Old definition in v2.0
def v2_playbook_item_on_skipped(self, result):
self.v2_runner_item_on_skipped(result)
def v2_runner_item_on_skipped(self, result):
self._add_host(result, 'skipped')
def v2_playbook_on_no_hosts_remaining(self):
if self._display.verbosity == 0 and self.keep:
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
else:
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline)
self.keep = False
sys.stdout.write(vt100.white + vt100.redbg + 'NO MORE HOSTS LEFT')
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
sys.stdout.flush()
def v2_playbook_on_include(self, included_file):
pass
def v2_playbook_on_stats(self, stats):
if self._display.verbosity == 0 and self.keep:
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
else:
sys.stdout.write(vt100.restore + vt100.reset + vt100.clearline)
# In normal mode screen output should be sufficient, summary is redundant
if self._display.verbosity == 0:
return
sys.stdout.write(vt100.bold + vt100.underline)
sys.stdout.write('SUMMARY')
sys.stdout.write(vt100.restore + vt100.reset + '\n' + vt100.save + vt100.clearline)
sys.stdout.flush()
hosts = sorted(stats.processed.keys())
for h in hosts:
t = stats.summarize(h)
self._display.display(
u"%s : %s %s %s %s %s %s" % (
hostcolor(h, t),
colorize(u'ok', t['ok'], C.COLOR_OK),
colorize(u'changed', t['changed'], C.COLOR_CHANGED),
colorize(u'unreachable', t['unreachable'], C.COLOR_UNREACHABLE),
colorize(u'failed', t['failures'], C.COLOR_ERROR),
colorize(u'rescued', t['rescued'], C.COLOR_OK),
colorize(u'ignored', t['ignored'], C.COLOR_WARN),
),
screen_only=True
)
# When using -vv or higher, simply do the default action
if display.verbosity >= 2 or not HAS_OD:
CallbackModule = CallbackModule_default
else:
CallbackModule = CallbackModule_dense

View file

@ -0,0 +1,75 @@
# (c) 2012-2014, 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)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: full_skip
type: stdout
short_description: suppresses tasks if all hosts skipped
description:
- Use this plugin when you do not care about any output for tasks that were completely skipped
deprecated:
why: The 'default' callback plugin now supports this functionality
removed_in: '2.11'
alternative: "'default' callback plugin with 'display_skipped_hosts = no' option"
extends_documentation_fragment:
- default_callback
requirements:
- set as stdout in configuration
'''
from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
class CallbackModule(CallbackModule_default):
'''
This is the default callback interface, which simply prints messages
to stdout when new callback events are received.
'''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.full_skip'
def v2_runner_on_skipped(self, result):
self.outlines = []
def v2_playbook_item_on_skipped(self, result):
self.outlines = []
def v2_runner_item_on_skipped(self, result):
self.outlines = []
def v2_runner_on_failed(self, result, ignore_errors=False):
self.display()
super(CallbackModule, self).v2_runner_on_failed(result, ignore_errors)
def v2_playbook_on_task_start(self, task, is_conditional):
self.outlines = []
self.outlines.append("TASK [%s]" % task.get_name().strip())
if self._display.verbosity >= 2:
path = task.get_path()
if path:
self.outlines.append("task path: %s" % path)
def v2_playbook_item_on_ok(self, result):
self.display()
super(CallbackModule, self).v2_playbook_item_on_ok(result)
def v2_runner_on_ok(self, result):
self.display()
super(CallbackModule, self).v2_runner_on_ok(result)
def display(self):
if len(self.outlines) == 0:
return
(first, rest) = self.outlines[0], self.outlines[1:]
self._display.banner(first)
for line in rest:
self._display.display(line)
self.outlines = []

227
plugins/callback/hipchat.py Normal file
View file

@ -0,0 +1,227 @@
# (C) 2014, Matt Martz <matt@sivel.net>
# (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: hipchat
callback_type: notification
requirements:
- whitelist in configuration.
- prettytable (python lib)
short_description: post task events to hipchat
description:
- This callback plugin sends status updates to a HipChat channel during playbook execution.
- Before 2.4 only environment variables were available for configuring this plugin.
options:
token:
description: HipChat API token for v1 or v2 API.
required: True
env:
- name: HIPCHAT_TOKEN
ini:
- section: callback_hipchat
key: token
api_version:
description: HipChat API version, v1 or v2.
required: False
default: v1
env:
- name: HIPCHAT_API_VERSION
ini:
- section: callback_hipchat
key: api_version
room:
description: HipChat room to post in.
default: ansible
env:
- name: HIPCHAT_ROOM
ini:
- section: callback_hipchat
key: room
from:
description: Name to post as
default: ansible
env:
- name: HIPCHAT_FROM
ini:
- section: callback_hipchat
key: from
notify:
description: Add notify flag to important messages
type: bool
default: True
env:
- name: HIPCHAT_NOTIFY
ini:
- section: callback_hipchat
key: notify
'''
import os
import json
try:
import prettytable
HAS_PRETTYTABLE = True
except ImportError:
HAS_PRETTYTABLE = False
from ansible.plugins.callback import CallbackBase
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.urls import open_url
class CallbackModule(CallbackBase):
"""This is an example ansible callback plugin that sends status
updates to a HipChat channel during playbook execution.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.hipchat'
CALLBACK_NEEDS_WHITELIST = True
API_V1_URL = 'https://api.hipchat.com/v1/rooms/message'
API_V2_URL = 'https://api.hipchat.com/v2/'
def __init__(self):
super(CallbackModule, self).__init__()
if not HAS_PRETTYTABLE:
self.disabled = True
self._display.warning('The `prettytable` python module is not installed. '
'Disabling the HipChat callback plugin.')
self.printed_playbook = False
self.playbook_name = None
self.play = None
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.token = self.get_option('token')
self.api_version = self.get_option('api_version')
self.from_name = self.get_option('from')
self.allow_notify = self.get_option('notify')
self.room = self.get_option('room')
if self.token is None:
self.disabled = True
self._display.warning('HipChat token could not be loaded. The HipChat '
'token can be provided using the `HIPCHAT_TOKEN` '
'environment variable.')
# Pick the request handler.
if self.api_version == 'v2':
self.send_msg = self.send_msg_v2
else:
self.send_msg = self.send_msg_v1
def send_msg_v2(self, msg, msg_format='text', color='yellow', notify=False):
"""Method for sending a message to HipChat"""
headers = {'Authorization': 'Bearer %s' % self.token, 'Content-Type': 'application/json'}
body = {}
body['room_id'] = self.room
body['from'] = self.from_name[:15] # max length is 15
body['message'] = msg
body['message_format'] = msg_format
body['color'] = color
body['notify'] = self.allow_notify and notify
data = json.dumps(body)
url = self.API_V2_URL + "room/{room_id}/notification".format(room_id=self.room)
try:
response = open_url(url, data=data, headers=headers, method='POST')
return response.read()
except Exception as ex:
self._display.warning('Could not submit message to hipchat: {0}'.format(ex))
def send_msg_v1(self, msg, msg_format='text', color='yellow', notify=False):
"""Method for sending a message to HipChat"""
params = {}
params['room_id'] = self.room
params['from'] = self.from_name[:15] # max length is 15
params['message'] = msg
params['message_format'] = msg_format
params['color'] = color
params['notify'] = int(self.allow_notify and notify)
url = ('%s?auth_token=%s' % (self.API_V1_URL, self.token))
try:
response = open_url(url, data=urlencode(params))
return response.read()
except Exception as ex:
self._display.warning('Could not submit message to hipchat: {0}'.format(ex))
def v2_playbook_on_play_start(self, play):
"""Display Playbook and play start messages"""
self.play = play
name = play.name
# This block sends information about a playbook when it starts
# The playbook object is not immediately available at
# playbook_on_start so we grab it via the play
#
# Displays info about playbook being started by a person on an
# inventory, as well as Tags, Skip Tags and Limits
if not self.printed_playbook:
self.playbook_name, _ = os.path.splitext(
os.path.basename(self.play.playbook.filename))
host_list = self.play.playbook.inventory.host_list
inventory = os.path.basename(os.path.realpath(host_list))
self.send_msg("%s: Playbook initiated by %s against %s" %
(self.playbook_name,
self.play.playbook.remote_user,
inventory), notify=True)
self.printed_playbook = True
subset = self.play.playbook.inventory._subset
skip_tags = self.play.playbook.skip_tags
self.send_msg("%s:\nTags: %s\nSkip Tags: %s\nLimit: %s" %
(self.playbook_name,
', '.join(self.play.playbook.only_tags),
', '.join(skip_tags) if skip_tags else None,
', '.join(subset) if subset else subset))
# This is where we actually say we are starting a play
self.send_msg("%s: Starting play: %s" %
(self.playbook_name, name))
def playbook_on_stats(self, stats):
"""Display info about playbook statistics"""
hosts = sorted(stats.processed.keys())
t = prettytable.PrettyTable(['Host', 'Ok', 'Changed', 'Unreachable',
'Failures'])
failures = False
unreachable = False
for h in hosts:
s = stats.summarize(h)
if s['failures'] > 0:
failures = True
if s['unreachable'] > 0:
unreachable = True
t.add_row([h] + [s[k] for k in ['ok', 'changed', 'unreachable',
'failures']])
self.send_msg("%s: Playbook complete" % self.playbook_name,
notify=True)
if failures or unreachable:
color = 'red'
self.send_msg("%s: Failures detected" % self.playbook_name,
color=color, notify=True)
else:
color = 'green'
self.send_msg("/code %s:\n%s" % (self.playbook_name, t), color=color)

117
plugins/callback/jabber.py Normal file
View file

@ -0,0 +1,117 @@
# Copyright (C) 2016 maxn nikolaev.makc@gmail.com
# Copyright (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: jabber
type: notification
short_description: post task events to a jabber server
description:
- The chatty part of ChatOps with a Hipchat server as a target
- This callback plugin sends status updates to a HipChat channel during playbook execution.
requirements:
- xmpp (python lib https://github.com/ArchipelProject/xmpppy)
options:
server:
description: connection info to jabber server
required: True
env:
- name: JABBER_SERV
user:
description: Jabber user to authenticate as
required: True
env:
- name: JABBER_USER
password:
description: Password for the user to the jabber server
required: True
env:
- name: JABBER_PASS
to:
description: chat identifier that will receive the message
required: True
env:
- name: JABBER_TO
'''
import os
HAS_XMPP = True
try:
import xmpp
except ImportError:
HAS_XMPP = False
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.jabber'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super(CallbackModule, self).__init__(display=display)
if not HAS_XMPP:
self._display.warning("The required python xmpp library (xmpppy) is not installed. "
"pip install git+https://github.com/ArchipelProject/xmpppy")
self.disabled = True
self.serv = os.getenv('JABBER_SERV')
self.j_user = os.getenv('JABBER_USER')
self.j_pass = os.getenv('JABBER_PASS')
self.j_to = os.getenv('JABBER_TO')
if (self.j_user or self.j_pass or self.serv or self.j_to) is None:
self.disabled = True
self._display.warning('Jabber CallBack wants the JABBER_SERV, JABBER_USER, JABBER_PASS and JABBER_TO environment variables')
def send_msg(self, msg):
"""Send message"""
jid = xmpp.JID(self.j_user)
client = xmpp.Client(self.serv, debug=[])
client.connect(server=(self.serv, 5222))
client.auth(jid.getNode(), self.j_pass, resource=jid.getResource())
message = xmpp.Message(self.j_to, msg)
message.setAttr('type', 'chat')
client.send(message)
client.disconnect()
def v2_runner_on_ok(self, result):
self._clean_results(result._result, result._task.action)
self.debug = self._dump_results(result._result)
def v2_playbook_on_task_start(self, task, is_conditional):
self.task = task
def v2_playbook_on_play_start(self, play):
"""Display Playbook and play start messages"""
self.play = play
name = play.name
self.send_msg("Ansible starting play: %s" % (name))
def playbook_on_stats(self, stats):
name = self.play
hosts = sorted(stats.processed.keys())
failures = False
unreachable = False
for h in hosts:
s = stats.summarize(h)
if s['failures'] > 0:
failures = True
if s['unreachable'] > 0:
unreachable = True
if failures or unreachable:
out = self.debug
self.send_msg("%s: Failures detected \n%s \nHost: %s\n Failed at:\n%s" % (name, self.task, h, out))
else:
out = self.debug
self.send_msg("Great! \n Playbook %s completed:\n%s \n Last task debug:\n %s" % (name, s, out))

View file

@ -0,0 +1,108 @@
# (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
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:
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
'''
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 = 'community.general.log_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.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)

207
plugins/callback/logdna.py Normal file
View file

@ -0,0 +1,207 @@
# (c) 2018, Samir Musali <samir.musali@logdna.com>
# 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: logdna
callback_type: aggregate
short_description: Sends playbook logs to LogDNA
description:
- This callback will report logs from playbook actions, tasks, and events to LogDNA (https://app.logdna.com)
requirements:
- LogDNA Python Library (https://github.com/logdna/python)
- whitelisting in configuration
options:
conf_key:
required: True
description: LogDNA Ingestion Key
type: string
env:
- name: LOGDNA_INGESTION_KEY
ini:
- section: callback_logdna
key: conf_key
plugin_ignore_errors:
required: False
description: Whether to ignore errors on failing or not
type: boolean
env:
- name: ANSIBLE_IGNORE_ERRORS
ini:
- section: callback_logdna
key: plugin_ignore_errors
default: False
conf_hostname:
required: False
description: Alternative Host Name; the current host name by default
type: string
env:
- name: LOGDNA_HOSTNAME
ini:
- section: callback_logdna
key: conf_hostname
conf_tags:
required: False
description: Tags
type: string
env:
- name: LOGDNA_TAGS
ini:
- section: callback_logdna
key: conf_tags
default: ansible
'''
import logging
import json
import socket
from uuid import getnode
from ansible.plugins.callback import CallbackBase
from ansible.parsing.ajson import AnsibleJSONEncoder
try:
from logdna import LogDNAHandler
HAS_LOGDNA = True
except ImportError:
HAS_LOGDNA = False
# Getting MAC Address of system:
def get_mac():
mac = "%012x" % getnode()
return ":".join(map(lambda index: mac[index:index + 2], range(int(len(mac) / 2))))
# Getting hostname of system:
def get_hostname():
return str(socket.gethostname()).split('.local')[0]
# Getting IP of system:
def get_ip():
try:
return socket.gethostbyname(get_hostname())
except Exception:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('10.255.255.255', 1))
IP = s.getsockname()[0]
except Exception:
IP = '127.0.0.1'
finally:
s.close()
return IP
# Is it JSON?
def isJSONable(obj):
try:
json.dumps(obj, sort_keys=True, cls=AnsibleJSONEncoder)
return True
except Exception:
return False
# LogDNA Callback Module:
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 0.1
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.general.logdna'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super(CallbackModule, self).__init__(display=display)
self.disabled = True
self.playbook_name = None
self.playbook = None
self.conf_key = None
self.plugin_ignore_errors = None
self.conf_hostname = None
self.conf_tags = None
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.conf_key = self.get_option('conf_key')
self.plugin_ignore_errors = self.get_option('plugin_ignore_errors')
self.conf_hostname = self.get_option('conf_hostname')
self.conf_tags = self.get_option('conf_tags')
self.mac = get_mac()
self.ip = get_ip()
if self.conf_hostname is None:
self.conf_hostname = get_hostname()
self.conf_tags = self.conf_tags.split(',')
if HAS_LOGDNA:
self.log = logging.getLogger('logdna')
self.log.setLevel(logging.INFO)
self.options = {'hostname': self.conf_hostname, 'mac': self.mac, 'index_meta': True}
self.log.addHandler(LogDNAHandler(self.conf_key, self.options))
self.disabled = False
else:
self.disabled = True
self._display.warning('WARNING:\nPlease, install LogDNA Python Package: `pip install logdna`')
def metaIndexing(self, meta):
invalidKeys = []
ninvalidKeys = 0
for key, value in meta.items():
if not isJSONable(value):
invalidKeys.append(key)
ninvalidKeys += 1
if ninvalidKeys > 0:
for key in invalidKeys:
del meta[key]
meta['__errors'] = 'These keys have been sanitized: ' + ', '.join(invalidKeys)
return meta
def sanitizeJSON(self, data):
try:
return json.loads(json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder))
except Exception:
return {'warnings': ['JSON Formatting Issue', json.dumps(data, sort_keys=True, cls=AnsibleJSONEncoder)]}
def flush(self, log, options):
if HAS_LOGDNA:
self.log.info(json.dumps(log), options)
def sendLog(self, host, category, logdata):
options = {'app': 'ansible', 'meta': {'playbook': self.playbook_name, 'host': host, 'category': category}}
logdata['info'].pop('invocation', None)
warnings = logdata['info'].pop('warnings', None)
if warnings is not None:
self.flush({'warn': warnings}, options)
self.flush(logdata, options)
def v2_playbook_on_start(self, playbook):
self.playbook = playbook
self.playbook_name = playbook._file_name
def v2_playbook_on_stats(self, stats):
result = dict()
for host in stats.processed.keys():
result[host] = stats.summarize(host)
self.sendLog(self.conf_hostname, 'STATS', {'info': self.sanitizeJSON(result)})
def runner_on_failed(self, host, res, ignore_errors=False):
if self.plugin_ignore_errors:
ignore_errors = self.plugin_ignore_errors
self.sendLog(host, 'FAILED', {'info': self.sanitizeJSON(res), 'ignore_errors': ignore_errors})
def runner_on_ok(self, host, res):
self.sendLog(host, 'OK', {'info': self.sanitizeJSON(res)})
def runner_on_unreachable(self, host, res):
self.sendLog(host, 'UNREACHABLE', {'info': self.sanitizeJSON(res)})
def runner_on_async_failed(self, host, res, jid):
self.sendLog(host, 'ASYNC_FAILED', {'info': self.sanitizeJSON(res), 'job_id': jid})
def runner_on_async_ok(self, host, res, jid):
self.sendLog(host, 'ASYNC_OK', {'info': self.sanitizeJSON(res), 'job_id': jid})

View file

@ -0,0 +1,329 @@
# (c) 2015, Logentries.com, Jimmy Tang <jimmy.tang@logentries.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: logentries
type: notification
short_description: Sends events to Logentries
description:
- This callback plugin will generate JSON objects and send them to Logentries via TCP for auditing/debugging purposes.
- Before 2.4, if you wanted to use an ini configuration, the file must be placed in the same directory as this plugin and named logentries.ini
- In 2.4 and above you can just put it in the main Ansible configuration file.
requirements:
- whitelisting in configuration
- certifi (python library)
- flatdict (python library), if you want to use the 'flatten' option
options:
api:
description: URI to the Logentries API
env:
- name: LOGENTRIES_API
default: data.logentries.com
ini:
- section: callback_logentries
key: api
port:
description: HTTP port to use when connecting to the API
env:
- name: LOGENTRIES_PORT
default: 80
ini:
- section: callback_logentries
key: port
tls_port:
description: Port to use when connecting to the API when TLS is enabled
env:
- name: LOGENTRIES_TLS_PORT
default: 443
ini:
- section: callback_logentries
key: tls_port
token:
description: The logentries "TCP token"
env:
- name: LOGENTRIES_ANSIBLE_TOKEN
required: True
ini:
- section: callback_logentries
key: token
use_tls:
description:
- Toggle to decide whether to use TLS to encrypt the communications with the API server
env:
- name: LOGENTRIES_USE_TLS
default: False
type: boolean
ini:
- section: callback_logentries
key: use_tls
flatten:
description: flatten complex data structures into a single dictionary with complex keys
type: boolean
default: False
env:
- name: LOGENTRIES_FLATTEN
ini:
- section: callback_logentries
key: flatten
'''
EXAMPLES = '''
examples: >
To enable, add this to your ansible.cfg file in the defaults block
[defaults]
callback_whitelist = logentries
Either set the environment variables
export LOGENTRIES_API=data.logentries.com
export LOGENTRIES_PORT=10000
export LOGENTRIES_ANSIBLE_TOKEN=dd21fc88-f00a-43ff-b977-e3a4233c53af
Or in the main Ansible config file
[callback_logentries]
api = data.logentries.com
port = 10000
tls_port = 20000
use_tls = no
token = dd21fc88-f00a-43ff-b977-e3a4233c53af
flatten = False
'''
import os
import socket
import random
import time
import uuid
try:
import certifi
HAS_CERTIFI = True
except ImportError:
HAS_CERTIFI = False
try:
import flatdict
HAS_FLATDICT = True
except ImportError:
HAS_FLATDICT = False
from ansible.module_utils._text import to_bytes, to_text
from ansible.plugins.callback import CallbackBase
# Todo:
# * Better formatting of output before sending out to logentries data/api nodes.
class PlainTextSocketAppender(object):
def __init__(self, display, LE_API='data.logentries.com', LE_PORT=80, LE_TLS_PORT=443):
self.LE_API = LE_API
self.LE_PORT = LE_PORT
self.LE_TLS_PORT = LE_TLS_PORT
self.MIN_DELAY = 0.1
self.MAX_DELAY = 10
# Error message displayed when an incorrect Token has been detected
self.INVALID_TOKEN = "\n\nIt appears the LOGENTRIES_TOKEN parameter you entered is incorrect!\n\n"
# Unicode Line separator character \u2028
self.LINE_SEP = u'\u2028'
self._display = display
self._conn = None
def open_connection(self):
self._conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._conn.connect((self.LE_API, self.LE_PORT))
def reopen_connection(self):
self.close_connection()
root_delay = self.MIN_DELAY
while True:
try:
self.open_connection()
return
except Exception as e:
self._display.vvvv(u"Unable to connect to Logentries: %s" % to_text(e))
root_delay *= 2
if root_delay > self.MAX_DELAY:
root_delay = self.MAX_DELAY
wait_for = root_delay + random.uniform(0, root_delay)
try:
self._display.vvvv("sleeping %s before retry" % wait_for)
time.sleep(wait_for)
except KeyboardInterrupt:
raise
def close_connection(self):
if self._conn is not None:
self._conn.close()
def put(self, data):
# Replace newlines with Unicode line separator
# for multi-line events
data = to_text(data, errors='surrogate_or_strict')
multiline = data.replace(u'\n', self.LINE_SEP)
multiline += u"\n"
# Send data, reconnect if needed
while True:
try:
self._conn.send(to_bytes(multiline, errors='surrogate_or_strict'))
except socket.error:
self.reopen_connection()
continue
break
self.close_connection()
try:
import ssl
HAS_SSL = True
except ImportError: # for systems without TLS support.
SocketAppender = PlainTextSocketAppender
HAS_SSL = False
else:
class TLSSocketAppender(PlainTextSocketAppender):
def open_connection(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock = ssl.wrap_socket(
sock=sock,
keyfile=None,
certfile=None,
server_side=False,
cert_reqs=ssl.CERT_REQUIRED,
ssl_version=getattr(
ssl, 'PROTOCOL_TLSv1_2', ssl.PROTOCOL_TLSv1),
ca_certs=certifi.where(),
do_handshake_on_connect=True,
suppress_ragged_eofs=True, )
sock.connect((self.LE_API, self.LE_TLS_PORT))
self._conn = sock
SocketAppender = TLSSocketAppender
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.logentries'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
# TODO: allow for alternate posting methods (REST/UDP/agent/etc)
super(CallbackModule, self).__init__()
# verify dependencies
if not HAS_SSL:
self._display.warning("Unable to import ssl module. Will send over port 80.")
if not HAS_CERTIFI:
self.disabled = True
self._display.warning('The `certifi` python module is not installed.\nDisabling the Logentries callback plugin.')
self.le_jobid = str(uuid.uuid4())
# FIXME: make configurable, move to options
self.timeout = 10
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)
# get options
try:
self.api_url = self.get_option('api')
self.api_port = self.get_option('port')
self.api_tls_port = self.get_option('tls_port')
self.use_tls = self.get_option('use_tls')
self.flatten = self.get_option('flatten')
except KeyError as e:
self._display.warning(u"Missing option for Logentries callback plugin: %s" % to_text(e))
self.disabled = True
try:
self.token = self.get_option('token')
except KeyError as e:
self._display.warning('Logentries token was not provided, this is required for this callback to operate, disabling')
self.disabled = True
if self.flatten and not HAS_FLATDICT:
self.disabled = True
self._display.warning('You have chosen to flatten and the `flatdict` python module is not installed.\nDisabling the Logentries callback plugin.')
self._initialize_connections()
def _initialize_connections(self):
if not self.disabled:
if self.use_tls:
self._display.vvvv("Connecting to %s:%s with TLS" % (self.api_url, self.api_tls_port))
self._appender = TLSSocketAppender(display=self._display, LE_API=self.api_url, LE_TLS_PORT=self.api_tls_port)
else:
self._display.vvvv("Connecting to %s:%s" % (self.api_url, self.api_port))
self._appender = PlainTextSocketAppender(display=self._display, LE_API=self.api_url, LE_PORT=self.api_port)
self._appender.reopen_connection()
def emit_formatted(self, record):
if self.flatten:
results = flatdict.FlatDict(record)
self.emit(self._dump_results(results))
else:
self.emit(self._dump_results(record))
def emit(self, record):
msg = record.rstrip('\n')
msg = "{0} {1}".format(self.token, msg)
self._appender.put(msg)
self._display.vvvv("Sent event to logentries")
def _set_info(self, host, res):
return {'le_jobid': self.le_jobid, 'hostname': host, 'results': res}
def runner_on_ok(self, host, res):
results = self._set_info(host, res)
results['status'] = 'OK'
self.emit_formatted(results)
def runner_on_failed(self, host, res, ignore_errors=False):
results = self._set_info(host, res)
results['status'] = 'FAILED'
self.emit_formatted(results)
def runner_on_skipped(self, host, item=None):
results = self._set_info(host, item)
del results['results']
results['status'] = 'SKIPPED'
self.emit_formatted(results)
def runner_on_unreachable(self, host, res):
results = self._set_info(host, res)
results['status'] = 'UNREACHABLE'
self.emit_formatted(results)
def runner_on_async_failed(self, host, res, jid):
results = self._set_info(host, res)
results['jid'] = jid
results['status'] = 'ASYNC_FAILED'
self.emit_formatted(results)
def v2_playbook_on_play_start(self, play):
results = {}
results['le_jobid'] = self.le_jobid
results['started_by'] = os.getlogin()
if play.name:
results['play'] = play.name
results['hosts'] = play.hosts
self.emit_formatted(results)
def playbook_on_stats(self, stats):
""" close connection """
self._appender.close_connection()

View file

@ -0,0 +1,228 @@
# (C) 2016, Ievgen Khmelenko <ujenmr@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: logstash
type: notification
short_description: Sends events to Logstash
description:
- This callback will report facts and task events to Logstash https://www.elastic.co/products/logstash
requirements:
- whitelisting in configuration
- logstash (python library)
options:
server:
description: Address of the Logstash server
env:
- name: LOGSTASH_SERVER
default: localhost
port:
description: Port on which logstash is listening
env:
- name: LOGSTASH_PORT
default: 5000
type:
description: Message type
env:
- name: LOGSTASH_TYPE
default: ansible
'''
import os
import json
import socket
import uuid
from datetime import datetime
import logging
try:
import logstash
HAS_LOGSTASH = True
except ImportError:
HAS_LOGSTASH = False
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
"""
ansible logstash callback plugin
ansible.cfg:
callback_plugins = <path_to_callback_plugins_folder>
callback_whitelist = logstash
and put the plugin in <path_to_callback_plugins_folder>
logstash config:
input {
tcp {
port => 5000
codec => json
}
}
Requires:
python-logstash
This plugin makes use of the following environment variables:
LOGSTASH_SERVER (optional): defaults to localhost
LOGSTASH_PORT (optional): defaults to 5000
LOGSTASH_TYPE (optional): defaults to ansible
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.general.logstash'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
super(CallbackModule, self).__init__()
if not HAS_LOGSTASH:
self.disabled = True
self._display.warning("The required python-logstash is not installed. "
"pip install python-logstash")
else:
self.logger = logging.getLogger('python-logstash-logger')
self.logger.setLevel(logging.DEBUG)
self.handler = logstash.TCPLogstashHandler(
os.getenv('LOGSTASH_SERVER', 'localhost'),
int(os.getenv('LOGSTASH_PORT', 5000)),
version=1,
message_type=os.getenv('LOGSTASH_TYPE', 'ansible')
)
self.logger.addHandler(self.handler)
self.hostname = socket.gethostname()
self.session = str(uuid.uuid1())
self.errors = 0
self.start_time = datetime.utcnow()
def v2_playbook_on_start(self, playbook):
self.playbook = playbook._file_name
data = {
'status': "OK",
'host': self.hostname,
'session': self.session,
'ansible_type': "start",
'ansible_playbook': self.playbook,
}
self.logger.info("ansible start", extra=data)
def v2_playbook_on_stats(self, stats):
end_time = datetime.utcnow()
runtime = end_time - self.start_time
summarize_stat = {}
for host in stats.processed.keys():
summarize_stat[host] = stats.summarize(host)
if self.errors == 0:
status = "OK"
else:
status = "FAILED"
data = {
'status': status,
'host': self.hostname,
'session': self.session,
'ansible_type': "finish",
'ansible_playbook': self.playbook,
'ansible_playbook_duration': runtime.total_seconds(),
'ansible_result': json.dumps(summarize_stat),
}
self.logger.info("ansible stats", extra=data)
def v2_runner_on_ok(self, result, **kwargs):
data = {
'status': "OK",
'host': self.hostname,
'session': self.session,
'ansible_type': "task",
'ansible_playbook': self.playbook,
'ansible_host': result._host.name,
'ansible_task': result._task,
'ansible_result': self._dump_results(result._result)
}
self.logger.info("ansible ok", extra=data)
def v2_runner_on_skipped(self, result, **kwargs):
data = {
'status': "SKIPPED",
'host': self.hostname,
'session': self.session,
'ansible_type': "task",
'ansible_playbook': self.playbook,
'ansible_task': result._task,
'ansible_host': result._host.name
}
self.logger.info("ansible skipped", extra=data)
def v2_playbook_on_import_for_host(self, result, imported_file):
data = {
'status': "IMPORTED",
'host': self.hostname,
'session': self.session,
'ansible_type': "import",
'ansible_playbook': self.playbook,
'ansible_host': result._host.name,
'imported_file': imported_file
}
self.logger.info("ansible import", extra=data)
def v2_playbook_on_not_import_for_host(self, result, missing_file):
data = {
'status': "NOT IMPORTED",
'host': self.hostname,
'session': self.session,
'ansible_type': "import",
'ansible_playbook': self.playbook,
'ansible_host': result._host.name,
'missing_file': missing_file
}
self.logger.info("ansible import", extra=data)
def v2_runner_on_failed(self, result, **kwargs):
data = {
'status': "FAILED",
'host': self.hostname,
'session': self.session,
'ansible_type': "task",
'ansible_playbook': self.playbook,
'ansible_host': result._host.name,
'ansible_task': result._task,
'ansible_result': self._dump_results(result._result)
}
self.errors += 1
self.logger.error("ansible failed", extra=data)
def v2_runner_on_unreachable(self, result, **kwargs):
data = {
'status': "UNREACHABLE",
'host': self.hostname,
'session': self.session,
'ansible_type': "task",
'ansible_playbook': self.playbook,
'ansible_host': result._host.name,
'ansible_task': result._task,
'ansible_result': self._dump_results(result._result)
}
self.logger.error("ansible unreachable", extra=data)
def v2_runner_on_async_failed(self, result, **kwargs):
data = {
'status': "FAILED",
'host': self.hostname,
'session': self.session,
'ansible_type': "task",
'ansible_playbook': self.playbook,
'ansible_host': result._host.name,
'ansible_task': result._task,
'ansible_result': self._dump_results(result._result)
}
self.errors += 1
self.logger.error("ansible async", extra=data)

227
plugins/callback/mail.py Normal file
View file

@ -0,0 +1,227 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2012, Dag Wieers <dag@wieers.com>
# 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: mail
type: notification
short_description: Sends failure events via email
description:
- This callback will report failures via email
author:
- Dag Wieers (@dagwieers)
requirements:
- whitelisting in configuration
options:
mta:
description: Mail Transfer Agent, server that accepts SMTP
env:
- name: SMTPHOST
ini:
- section: callback_mail
key: smtphost
default: localhost
mtaport:
description: Mail Transfer Agent Port, port at which server SMTP
ini:
- section: callback_mail
key: smtpport
default: 25
to:
description: Mail recipient
ini:
- section: callback_mail
key: to
default: root
sender:
description: Mail sender
ini:
- section: callback_mail
key: sender
cc:
description: CC'd recipient
ini:
- section: callback_mail
key: cc
bcc:
description: BCC'd recipient
ini:
- section: callback_mail
key: bcc
note:
- "TODO: expand configuration options now that plugins can leverage Ansible's configuration"
'''
import json
import os
import re
import smtplib
from ansible.module_utils.six import string_types
from ansible.module_utils._text import to_bytes
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
''' This Ansible callback plugin mails errors to interested parties. '''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.mail'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super(CallbackModule, self).__init__(display=display)
self.sender = None
self.to = 'root'
self.smtphost = os.getenv('SMTPHOST', 'localhost')
self.smtpport = 25
self.cc = None
self.bcc = None
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.sender = self.get_option('sender')
self.to = self.get_option('to')
self.smtphost = self.get_option('mta')
self.smtpport = int(self.get_option('mtaport'))
self.cc = self.get_option('cc')
self.bcc = self.get_option('bcc')
def mail(self, subject='Ansible error mail', body=None):
if body is None:
body = subject
smtp = smtplib.SMTP(self.smtphost, port=self.smtpport)
b_sender = to_bytes(self.sender)
b_to = to_bytes(self.to)
b_cc = to_bytes(self.cc)
b_bcc = to_bytes(self.bcc)
b_subject = to_bytes(subject)
b_body = to_bytes(body)
b_content = b'From: %s\n' % b_sender
b_content += b'To: %s\n' % b_to
if self.cc:
b_content += b'Cc: %s\n' % b_cc
b_content += b'Subject: %s\n\n' % b_subject
b_content += b_body
b_addresses = b_to.split(b',')
if self.cc:
b_addresses += b_cc.split(b',')
if self.bcc:
b_addresses += b_bcc.split(b',')
for b_address in b_addresses:
smtp.sendmail(b_sender, b_address, b_content)
smtp.quit()
def subject_msg(self, multiline, failtype, linenr):
return '%s: %s' % (failtype, multiline.strip('\r\n').splitlines()[linenr])
def indent(self, multiline, indent=8):
return re.sub('^', ' ' * indent, multiline, flags=re.MULTILINE)
def body_blob(self, multiline, texttype):
''' Turn some text output in a well-indented block for sending in a mail body '''
intro = 'with the following %s:\n\n' % texttype
blob = ''
for line in multiline.strip('\r\n').splitlines():
blob += '%s\n' % line
return intro + self.indent(blob) + '\n'
def mail_result(self, result, failtype):
host = result._host.get_name()
if not self.sender:
self.sender = '"Ansible: %s" <root>' % host
# Add subject
if self.itembody:
subject = self.itemsubject
elif result._result.get('failed_when_result') is True:
subject = "Failed due to 'failed_when' condition"
elif result._result.get('msg'):
subject = self.subject_msg(result._result['msg'], failtype, 0)
elif result._result.get('stderr'):
subject = self.subject_msg(result._result['stderr'], failtype, -1)
elif result._result.get('stdout'):
subject = self.subject_msg(result._result['stdout'], failtype, -1)
elif result._result.get('exception'): # Unrelated exceptions are added to output :-/
subject = self.subject_msg(result._result['exception'], failtype, -1)
else:
subject = '%s: %s' % (failtype, result._task.name or result._task.action)
# Make playbook name visible (e.g. in Outlook/Gmail condensed view)
body = 'Playbook: %s\n' % os.path.basename(self.playbook._file_name)
if result._task.name:
body += 'Task: %s\n' % result._task.name
body += 'Module: %s\n' % result._task.action
body += 'Host: %s\n' % host
body += '\n'
# Add task information (as much as possible)
body += 'The following task failed:\n\n'
if 'invocation' in result._result:
body += self.indent('%s: %s\n' % (result._task.action, json.dumps(result._result['invocation']['module_args'], indent=4)))
elif result._task.name:
body += self.indent('%s (%s)\n' % (result._task.name, result._task.action))
else:
body += self.indent('%s\n' % result._task.action)
body += '\n'
# Add item / message
if self.itembody:
body += self.itembody
elif result._result.get('failed_when_result') is True:
body += "due to the following condition:\n\n" + self.indent('failed_when:\n- ' + '\n- '.join(result._task.failed_when)) + '\n\n'
elif result._result.get('msg'):
body += self.body_blob(result._result['msg'], 'message')
# Add stdout / stderr / exception / warnings / deprecations
if result._result.get('stdout'):
body += self.body_blob(result._result['stdout'], 'standard output')
if result._result.get('stderr'):
body += self.body_blob(result._result['stderr'], 'error output')
if result._result.get('exception'): # Unrelated exceptions are added to output :-/
body += self.body_blob(result._result['exception'], 'exception')
if result._result.get('warnings'):
for i in range(len(result._result.get('warnings'))):
body += self.body_blob(result._result['warnings'][i], 'exception %d' % (i + 1))
if result._result.get('deprecations'):
for i in range(len(result._result.get('deprecations'))):
body += self.body_blob(result._result['deprecations'][i], 'exception %d' % (i + 1))
body += 'and a complete dump of the error:\n\n'
body += self.indent('%s: %s' % (failtype, json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4)))
self.mail(subject=subject, body=body)
def v2_playbook_on_start(self, playbook):
self.playbook = playbook
self.itembody = ''
def v2_runner_on_failed(self, result, ignore_errors=False):
if ignore_errors:
return
self.mail_result(result, 'Failed')
def v2_runner_on_unreachable(self, result):
self.mail_result(result, 'Unreachable')
def v2_runner_on_async_failed(self, result):
self.mail_result(result, 'Async failure')
def v2_runner_item_on_failed(self, result):
# Pass item information to task failure
self.itemsubject = result._result['msg']
self.itembody += self.body_blob(json.dumps(result._result, cls=AnsibleJSONEncoder, indent=4), "failed item dump '%(item)s'" % result._result)

188
plugins/callback/nrdp.py Normal file
View file

@ -0,0 +1,188 @@
# -*- coding: utf-8 -*-
# (c) 2018 Remi Verchere <remi@verchere.fr>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: nrdp
type: notification
author: "Remi VERCHERE (@rverchere)"
short_description: post task result to a nagios server through nrdp
description:
- this callback send playbook result to nagios
- nagios shall use NRDP to recive passive events
- the passive check is sent to a dedicated host/service for ansible
options:
url:
description: url of the nrdp server
required: True
env:
- name : NRDP_URL
ini:
- section: callback_nrdp
key: url
validate_certs:
description: (bool) validate the SSL certificate of the nrdp server. (For HTTPS url)
env:
- name: NRDP_VALIDATE_CERTS
ini:
- section: callback_nrdp
key: validate_nrdp_certs
- section: callback_nrdp
key: validate_certs
default: False
aliases: [ validate_nrdp_certs ]
token:
description: token to be allowed to push nrdp events
required: True
env:
- name: NRDP_TOKEN
ini:
- section: callback_nrdp
key: token
hostname:
description: hostname where the passive check is linked to
required: True
env:
- name : NRDP_HOSTNAME
ini:
- section: callback_nrdp
key: hostname
servicename:
description: service where the passive check is linked to
required: True
env:
- name : NRDP_SERVICENAME
ini:
- section: callback_nrdp
key: servicename
'''
import os
import json
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.urls import open_url
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
'''
send ansible-playbook to Nagios server using nrdp protocol
'''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.nrdp'
CALLBACK_NEEDS_WHITELIST = True
# Nagios states
OK = 0
WARNING = 1
CRITICAL = 2
UNKNOWN = 3
def __init__(self):
super(CallbackModule, self).__init__()
self.printed_playbook = False
self.playbook_name = None
self.play = None
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.url = self.get_option('url')
if not self.url.endswith('/'):
self.url += '/'
self.token = self.get_option('token')
self.hostname = self.get_option('hostname')
self.servicename = self.get_option('servicename')
self.validate_nrdp_certs = self.get_option('validate_certs')
if (self.url or self.token or self.hostname or
self.servicename) is None:
self._display.warning("NRDP callback wants the NRDP_URL,"
" NRDP_TOKEN, NRDP_HOSTNAME,"
" NRDP_SERVICENAME"
" environment variables'."
" The NRDP callback plugin is disabled.")
self.disabled = True
def _send_nrdp(self, state, msg):
'''
nrpd service check send XMLDATA like this:
<?xml version='1.0'?>
<checkresults>
<checkresult type='service'>
<hostname>somehost</hostname>
<servicename>someservice</servicename>
<state>1</state>
<output>WARNING: Danger Will Robinson!|perfdata</output>
</checkresult>
</checkresults>
'''
xmldata = "<?xml version='1.0'?>\n"
xmldata += "<checkresults>\n"
xmldata += "<checkresult type='service'>\n"
xmldata += "<hostname>%s</hostname>\n" % self.hostname
xmldata += "<servicename>%s</servicename>\n" % self.servicename
xmldata += "<state>%d</state>\n" % state
xmldata += "<output>%s</output>\n" % msg
xmldata += "</checkresult>\n"
xmldata += "</checkresults>\n"
body = {
'cmd': 'submitcheck',
'token': self.token,
'XMLDATA': bytes(xmldata)
}
try:
response = open_url(self.url,
data=urlencode(body),
method='POST',
validate_certs=self.validate_nrdp_certs)
return response.read()
except Exception as ex:
self._display.warning("NRDP callback cannot send result {0}".format(ex))
def v2_playbook_on_play_start(self, play):
'''
Display Playbook and play start messages
'''
self.play = play
def v2_playbook_on_stats(self, stats):
'''
Display info about playbook statistics
'''
name = self.play
gstats = ""
hosts = sorted(stats.processed.keys())
critical = warning = 0
for host in hosts:
stat = stats.summarize(host)
gstats += "'%s_ok'=%d '%s_changed'=%d \
'%s_unreachable'=%d '%s_failed'=%d " % \
(host, stat['ok'], host, stat['changed'],
host, stat['unreachable'], host, stat['failures'])
# Critical when failed tasks or unreachable host
critical += stat['failures']
critical += stat['unreachable']
# Warning when changed tasks
warning += stat['changed']
msg = "%s | %s" % (name, gstats)
if critical:
# Send Critical
self._send_nrdp(self.CRITICAL, msg)
elif warning:
# Send Warning
self._send_nrdp(self.WARNING, msg)
else:
# Send OK
self._send_nrdp(self.OK, msg)

29
plugins/callback/null.py Normal file
View file

@ -0,0 +1,29 @@
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: 'null'
callback_type: stdout
requirements:
- set as main display callback
short_description: Don't display stuff to screen
description:
- This callback prevents outputing events to screen
'''
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
'''
This callback wont print messages to stdout when new callback events are received.
'''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.null'

1
plugins/callback/osx_say.py Symbolic link
View file

@ -0,0 +1 @@
say.py

113
plugins/callback/say.py Normal file
View file

@ -0,0 +1,113 @@
# (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)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: say
type: notification
requirements:
- whitelisting in configuration
- the '/usr/bin/say' command line program (standard on macOS) or 'espeak' command line program
short_description: notify using software speech synthesizer
description:
- This plugin will use the 'say' or 'espeak' program to "speak" about play events.
notes:
- In 2.8, this callback has been renamed from C(osx_say) into M(say).
'''
import distutils.spawn
import platform
import subprocess
import os
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
"""
makes Ansible much more exciting.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.say'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
super(CallbackModule, self).__init__()
self.FAILED_VOICE = None
self.REGULAR_VOICE = None
self.HAPPY_VOICE = None
self.LASER_VOICE = None
self.synthesizer = distutils.spawn.find_executable('say')
if not self.synthesizer:
self.synthesizer = distutils.spawn.find_executable('espeak')
if self.synthesizer:
self.FAILED_VOICE = 'klatt'
self.HAPPY_VOICE = 'f5'
self.LASER_VOICE = 'whisper'
elif platform.system() != 'Darwin':
# 'say' binary available, it might be GNUstep tool which doesn't support 'voice' parameter
self._display.warning("'say' executable found but system is '%s': ignoring voice parameter" % platform.system())
else:
self.FAILED_VOICE = 'Zarvox'
self.REGULAR_VOICE = 'Trinoids'
self.HAPPY_VOICE = 'Cellos'
self.LASER_VOICE = 'Princess'
# plugin disable itself if say is not present
# ansible will not call any callback if disabled is set to True
if not self.synthesizer:
self.disabled = True
self._display.warning("Unable to find either 'say' or 'espeak' executable, plugin %s disabled" % os.path.basename(__file__))
def say(self, msg, voice):
cmd = [self.synthesizer, msg]
if voice:
cmd.extend(('-v', voice))
subprocess.call(cmd)
def runner_on_failed(self, host, res, ignore_errors=False):
self.say("Failure on host %s" % host, self.FAILED_VOICE)
def runner_on_ok(self, host, res):
self.say("pew", self.LASER_VOICE)
def runner_on_skipped(self, host, item=None):
self.say("pew", self.LASER_VOICE)
def runner_on_unreachable(self, host, res):
self.say("Failure on host %s" % host, self.FAILED_VOICE)
def runner_on_async_ok(self, host, res, jid):
self.say("pew", self.LASER_VOICE)
def runner_on_async_failed(self, host, res, jid):
self.say("Failure on host %s" % host, self.FAILED_VOICE)
def playbook_on_start(self):
self.say("Running Playbook", self.REGULAR_VOICE)
def playbook_on_notify(self, host, handler):
self.say("pew", self.LASER_VOICE)
def playbook_on_task_start(self, name, is_conditional):
if not is_conditional:
self.say("Starting task: %s" % name, self.REGULAR_VOICE)
else:
self.say("Notifying task: %s" % name, self.REGULAR_VOICE)
def playbook_on_setup(self):
self.say("Gathering facts", self.REGULAR_VOICE)
def playbook_on_play_start(self, name):
self.say("Starting play: %s" % name, self.HAPPY_VOICE)
def playbook_on_stats(self, stats):
self.say("Play complete", self.HAPPY_VOICE)

View file

@ -0,0 +1,275 @@
# (c) Fastly, inc 2016
# (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: selective
callback_type: stdout
requirements:
- set as main display callback
short_description: only print certain tasks
description:
- This callback only prints tasks that have been tagged with `print_action` or that have failed.
This allows operators to focus on the tasks that provide value only.
- Tasks that are not printed are placed with a '.'.
- If you increase verbosity all tasks are printed.
options:
nocolor:
default: False
description: This setting allows suppressing colorizing output
env:
- name: ANSIBLE_NOCOLOR
- name: ANSIBLE_SELECTIVE_DONT_COLORIZE
ini:
- section: defaults
key: nocolor
type: boolean
'''
EXAMPLES = """
- debug: msg="This will not be printed"
- debug: msg="But this will"
tags: [print_action]
"""
import difflib
from ansible import constants as C
from ansible.plugins.callback import CallbackBase
from ansible.module_utils._text import to_text
from ansible.utils.color import codeCodes
DONT_COLORIZE = False
COLORS = {
'normal': '\033[0m',
'ok': '\033[{0}m'.format(codeCodes[C.COLOR_OK]),
'bold': '\033[1m',
'not_so_bold': '\033[1m\033[34m',
'changed': '\033[{0}m'.format(codeCodes[C.COLOR_CHANGED]),
'failed': '\033[{0}m'.format(codeCodes[C.COLOR_ERROR]),
'endc': '\033[0m',
'skipped': '\033[{0}m'.format(codeCodes[C.COLOR_SKIP]),
}
def dict_diff(prv, nxt):
"""Return a dict of keys that differ with another config object."""
keys = set(prv.keys() + nxt.keys())
result = {}
for k in keys:
if prv.get(k) != nxt.get(k):
result[k] = (prv.get(k), nxt.get(k))
return result
def colorize(msg, color):
"""Given a string add necessary codes to format the string."""
if DONT_COLORIZE:
return msg
else:
return '{0}{1}{2}'.format(COLORS[color], msg, COLORS['endc'])
class CallbackModule(CallbackBase):
"""selective.py callback plugin."""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.selective'
def __init__(self, display=None):
"""selective.py callback plugin."""
super(CallbackModule, self).__init__(display)
self.last_skipped = False
self.last_task_name = None
self.printed_last_task = False
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)
global DONT_COLORIZE
DONT_COLORIZE = self.get_option('nocolor')
def _print_task(self, task_name=None):
if task_name is None:
task_name = self.last_task_name
if not self.printed_last_task:
self.printed_last_task = True
line_length = 120
if self.last_skipped:
print()
msg = colorize("# {0} {1}".format(task_name,
'*' * (line_length - len(task_name))), 'bold')
print(msg)
def _indent_text(self, text, indent_level):
lines = text.splitlines()
result_lines = []
for l in lines:
result_lines.append("{0}{1}".format(' ' * indent_level, l))
return '\n'.join(result_lines)
def _print_diff(self, diff, indent_level):
if isinstance(diff, dict):
try:
diff = '\n'.join(difflib.unified_diff(diff['before'].splitlines(),
diff['after'].splitlines(),
fromfile=diff.get('before_header',
'new_file'),
tofile=diff['after_header']))
except AttributeError:
diff = dict_diff(diff['before'], diff['after'])
if diff:
diff = colorize(str(diff), 'changed')
print(self._indent_text(diff, indent_level + 4))
def _print_host_or_item(self, host_or_item, changed, msg, diff, is_host, error, stdout, stderr):
if is_host:
indent_level = 0
name = colorize(host_or_item.name, 'not_so_bold')
else:
indent_level = 4
if isinstance(host_or_item, dict):
if 'key' in host_or_item.keys():
host_or_item = host_or_item['key']
name = colorize(to_text(host_or_item), 'bold')
if error:
color = 'failed'
change_string = colorize('FAILED!!!', color)
else:
color = 'changed' if changed else 'ok'
change_string = colorize("changed={0}".format(changed), color)
msg = colorize(msg, color)
line_length = 120
spaces = ' ' * (40 - len(name) - indent_level)
line = "{0} * {1}{2}- {3}".format(' ' * indent_level, name, spaces, change_string)
if len(msg) < 50:
line += ' -- {0}'.format(msg)
print("{0} {1}---------".format(line, '-' * (line_length - len(line))))
else:
print("{0} {1}".format(line, '-' * (line_length - len(line))))
print(self._indent_text(msg, indent_level + 4))
if diff:
self._print_diff(diff, indent_level)
if stdout:
stdout = colorize(stdout, 'failed')
print(self._indent_text(stdout, indent_level + 4))
if stderr:
stderr = colorize(stderr, 'failed')
print(self._indent_text(stderr, indent_level + 4))
def v2_playbook_on_play_start(self, play):
"""Run on start of the play."""
pass
def v2_playbook_on_task_start(self, task, **kwargs):
"""Run when a task starts."""
self.last_task_name = task.get_name()
self.printed_last_task = False
def _print_task_result(self, result, error=False, **kwargs):
"""Run when a task finishes correctly."""
if 'print_action' in result._task.tags or error or self._display.verbosity > 1:
self._print_task()
self.last_skipped = False
msg = to_text(result._result.get('msg', '')) or\
to_text(result._result.get('reason', ''))
stderr = [result._result.get('exception', None),
result._result.get('module_stderr', None)]
stderr = "\n".join([e for e in stderr if e]).strip()
self._print_host_or_item(result._host,
result._result.get('changed', False),
msg,
result._result.get('diff', None),
is_host=True,
error=error,
stdout=result._result.get('module_stdout', None),
stderr=stderr.strip(),
)
if 'results' in result._result:
for r in result._result['results']:
failed = 'failed' in r
stderr = [r.get('exception', None), r.get('module_stderr', None)]
stderr = "\n".join([e for e in stderr if e]).strip()
self._print_host_or_item(r['item'],
r.get('changed', False),
to_text(r.get('msg', '')),
r.get('diff', None),
is_host=False,
error=failed,
stdout=r.get('module_stdout', None),
stderr=stderr.strip(),
)
else:
self.last_skipped = True
print('.', end="")
def v2_playbook_on_stats(self, stats):
"""Display info about playbook statistics."""
print()
self.printed_last_task = False
self._print_task('STATS')
hosts = sorted(stats.processed.keys())
for host in hosts:
s = stats.summarize(host)
if s['failures'] or s['unreachable']:
color = 'failed'
elif s['changed']:
color = 'changed'
else:
color = 'ok'
msg = '{0} : ok={1}\tchanged={2}\tfailed={3}\tunreachable={4}\trescued={5}\tignored={6}'.format(
host, s['ok'], s['changed'], s['failures'], s['unreachable'], s['rescued'], s['ignored'])
print(colorize(msg, color))
def v2_runner_on_skipped(self, result, **kwargs):
"""Run when a task is skipped."""
if self._display.verbosity > 1:
self._print_task()
self.last_skipped = False
line_length = 120
spaces = ' ' * (31 - len(result._host.name) - 4)
line = " * {0}{1}- {2}".format(colorize(result._host.name, 'not_so_bold'),
spaces,
colorize("skipped", 'skipped'),)
reason = result._result.get('skipped_reason', '') or \
result._result.get('skip_reason', '')
if len(reason) < 50:
line += ' -- {0}'.format(reason)
print("{0} {1}---------".format(line, '-' * (line_length - len(line))))
else:
print("{0} {1}".format(line, '-' * (line_length - len(line))))
print(self._indent_text(reason, 8))
print(reason)
def v2_runner_on_ok(self, result, **kwargs):
self._print_task_result(result, error=False, **kwargs)
def v2_runner_on_failed(self, result, **kwargs):
self._print_task_result(result, error=True, **kwargs)
def v2_runner_on_unreachable(self, result, **kwargs):
self._print_task_result(result, error=True, **kwargs)
v2_playbook_on_handler_task_start = v2_playbook_on_task_start

250
plugins/callback/slack.py Normal file
View file

@ -0,0 +1,250 @@
# (C) 2014-2015, Matt Martz <matt@sivel.net>
# (C) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: slack
callback_type: notification
requirements:
- whitelist in configuration
- prettytable (python library)
short_description: Sends play events to a Slack channel
description:
- This is an ansible callback plugin that sends status updates to a Slack channel during playbook execution.
- Before 2.4 only environment variables were available for configuring this plugin
options:
webhook_url:
required: True
description: Slack Webhook URL
env:
- name: SLACK_WEBHOOK_URL
ini:
- section: callback_slack
key: webhook_url
channel:
default: "#ansible"
description: Slack room to post in.
env:
- name: SLACK_CHANNEL
ini:
- section: callback_slack
key: channel
username:
description: Username to post as.
env:
- name: SLACK_USERNAME
default: ansible
ini:
- section: callback_slack
key: username
validate_certs:
description: validate the SSL certificate of the Slack server. (For HTTPS URLs)
env:
- name: SLACK_VALIDATE_CERTS
ini:
- section: callback_slack
key: validate_certs
default: True
type: bool
'''
import json
import os
import uuid
from ansible import context
from ansible.module_utils._text import to_text
from ansible.module_utils.urls import open_url
from ansible.plugins.callback import CallbackBase
try:
import prettytable
HAS_PRETTYTABLE = True
except ImportError:
HAS_PRETTYTABLE = False
class CallbackModule(CallbackBase):
"""This is an ansible callback plugin that sends status
updates to a Slack channel during playbook execution.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
CALLBACK_NAME = 'community.general.slack'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super(CallbackModule, self).__init__(display=display)
if not HAS_PRETTYTABLE:
self.disabled = True
self._display.warning('The `prettytable` python module is not '
'installed. Disabling the Slack callback '
'plugin.')
self.playbook_name = None
# This is a 6 character identifier provided with each message
# This makes it easier to correlate messages when there are more
# than 1 simultaneous playbooks running
self.guid = uuid.uuid4().hex[:6]
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.webhook_url = self.get_option('webhook_url')
self.channel = self.get_option('channel')
self.username = self.get_option('username')
self.show_invocation = (self._display.verbosity > 1)
self.validate_certs = self.get_option('validate_certs')
if self.webhook_url is None:
self.disabled = True
self._display.warning('Slack Webhook URL was not provided. The '
'Slack Webhook URL can be provided using '
'the `SLACK_WEBHOOK_URL` environment '
'variable.')
def send_msg(self, attachments):
headers = {
'Content-type': 'application/json',
}
payload = {
'channel': self.channel,
'username': self.username,
'attachments': attachments,
'parse': 'none',
'icon_url': ('https://cdn2.hubspot.net/hub/330046/'
'file-449187601-png/ansible_badge.png'),
}
data = json.dumps(payload)
self._display.debug(data)
self._display.debug(self.webhook_url)
try:
response = open_url(self.webhook_url, data=data, validate_certs=self.validate_certs,
headers=headers)
return response.read()
except Exception as e:
self._display.warning(u'Could not submit message to Slack: %s' %
to_text(e))
def v2_playbook_on_start(self, playbook):
self.playbook_name = os.path.basename(playbook._file_name)
title = [
'*Playbook initiated* (_%s_)' % self.guid
]
invocation_items = []
if context.CLIARGS and self.show_invocation:
tags = context.CLIARGS['tags']
skip_tags = context.CLIARGS['skip_tags']
extra_vars = context.CLIARGS['extra_vars']
subset = context.CLIARGS['subset']
inventory = [os.path.abspath(i) for i in context.CLIARGS['inventory']]
invocation_items.append('Inventory: %s' % ', '.join(inventory))
if tags and tags != ['all']:
invocation_items.append('Tags: %s' % ', '.join(tags))
if skip_tags:
invocation_items.append('Skip Tags: %s' % ', '.join(skip_tags))
if subset:
invocation_items.append('Limit: %s' % subset)
if extra_vars:
invocation_items.append('Extra Vars: %s' %
' '.join(extra_vars))
title.append('by *%s*' % context.CLIARGS['remote_user'])
title.append('\n\n*%s*' % self.playbook_name)
msg_items = [' '.join(title)]
if invocation_items:
msg_items.append('```\n%s\n```' % '\n'.join(invocation_items))
msg = '\n'.join(msg_items)
attachments = [{
'fallback': msg,
'fields': [
{
'value': msg
}
],
'color': 'warning',
'mrkdwn_in': ['text', 'fallback', 'fields'],
}]
self.send_msg(attachments=attachments)
def v2_playbook_on_play_start(self, play):
"""Display Play start messages"""
name = play.name or 'Play name not specified (%s)' % play._uuid
msg = '*Starting play* (_%s_)\n\n*%s*' % (self.guid, name)
attachments = [
{
'fallback': msg,
'text': msg,
'color': 'warning',
'mrkdwn_in': ['text', 'fallback', 'fields'],
}
]
self.send_msg(attachments=attachments)
def v2_playbook_on_stats(self, stats):
"""Display info about playbook statistics"""
hosts = sorted(stats.processed.keys())
t = prettytable.PrettyTable(['Host', 'Ok', 'Changed', 'Unreachable',
'Failures', 'Rescued', 'Ignored'])
failures = False
unreachable = False
for h in hosts:
s = stats.summarize(h)
if s['failures'] > 0:
failures = True
if s['unreachable'] > 0:
unreachable = True
t.add_row([h] + [s[k] for k in ['ok', 'changed', 'unreachable',
'failures', 'rescued', 'ignored']])
attachments = []
msg_items = [
'*Playbook Complete* (_%s_)' % self.guid
]
if failures or unreachable:
color = 'danger'
msg_items.append('\n*Failed!*')
else:
color = 'good'
msg_items.append('\n*Success!*')
msg_items.append('```\n%s\n```' % t)
msg = '\n'.join(msg_items)
attachments.append({
'fallback': msg,
'fields': [
{
'value': msg
}
],
'color': color,
'mrkdwn_in': ['text', 'fallback', 'fields']
})
self.send_msg(attachments=attachments)

230
plugins/callback/splunk.py Normal file
View file

@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: splunk
type: aggregate
short_description: Sends task result events to Splunk HTTP Event Collector
author: "Stuart Hirst <support@convergingdata.com>"
description:
- This callback plugin will send task results as JSON formatted events to a Splunk HTTP collector.
- The companion Splunk Monitoring & Diagnostics App is available here "https://splunkbase.splunk.com/app/4023/"
- Credit to "Ryan Currah (@ryancurrah)" for original source upon which this is based.
requirements:
- Whitelisting this callback plugin
- 'Create a HTTP Event Collector in Splunk'
- 'Define the url and token in ansible.cfg'
options:
url:
description: URL to the Splunk HTTP collector source
env:
- name: SPLUNK_URL
ini:
- section: callback_splunk
key: url
authtoken:
description: Token to authenticate the connection to the Splunk HTTP collector
env:
- name: SPLUNK_AUTHTOKEN
ini:
- section: callback_splunk
key: authtoken
'''
EXAMPLES = '''
examples: >
To enable, add this to your ansible.cfg file in the defaults block
[defaults]
callback_whitelist = splunk
Set the environment variable
export SPLUNK_URL=http://mysplunkinstance.datapaas.io:8088/services/collector/event
export SPLUNK_AUTHTOKEN=f23blad6-5965-4537-bf69-5b5a545blabla88
Set the ansible.cfg variable in the callback_splunk block
[callback_splunk]
url = http://mysplunkinstance.datapaas.io:8088/services/collector/event
authtoken = f23blad6-5965-4537-bf69-5b5a545blabla88
'''
import json
import uuid
import socket
import getpass
from datetime import datetime
from os.path import basename
from ansible.module_utils.urls import open_url
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
class SplunkHTTPCollectorSource(object):
def __init__(self):
self.ansible_check_mode = False
self.ansible_playbook = ""
self.ansible_version = ""
self.session = str(uuid.uuid4())
self.host = socket.gethostname()
self.ip_address = socket.gethostbyname(socket.gethostname())
self.user = getpass.getuser()
def send_event(self, url, authtoken, state, result, runtime):
if result._task_fields['args'].get('_ansible_check_mode') is True:
self.ansible_check_mode = True
if result._task_fields['args'].get('_ansible_version'):
self.ansible_version = \
result._task_fields['args'].get('_ansible_version')
if result._task._role:
ansible_role = str(result._task._role)
else:
ansible_role = None
if 'args' in result._task_fields:
del result._task_fields['args']
data = {}
data['uuid'] = result._task._uuid
data['session'] = self.session
data['status'] = state
data['timestamp'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S '
'+0000')
data['host'] = self.host
data['ip_address'] = self.ip_address
data['user'] = self.user
data['runtime'] = runtime
data['ansible_version'] = self.ansible_version
data['ansible_check_mode'] = self.ansible_check_mode
data['ansible_host'] = result._host.name
data['ansible_playbook'] = self.ansible_playbook
data['ansible_role'] = ansible_role
data['ansible_task'] = result._task_fields
data['ansible_result'] = result._result
# This wraps the json payload in and outer json event needed by Splunk
jsondata = json.dumps(data, cls=AnsibleJSONEncoder, sort_keys=True)
jsondata = '{"event":' + jsondata + "}"
open_url(
url,
jsondata,
headers={
'Content-type': 'application/json',
'Authorization': 'Splunk ' + authtoken
},
method='POST'
)
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.general.splunk'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super(CallbackModule, self).__init__(display=display)
self.start_datetimes = {} # Collect task start times
self.url = None
self.authtoken = None
self.splunk = SplunkHTTPCollectorSource()
def _runtime(self, result):
return (
datetime.utcnow() -
self.start_datetimes[result._task._uuid]
).total_seconds()
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.url = self.get_option('url')
if self.url is None:
self.disabled = True
self._display.warning('Splunk HTTP collector source URL was '
'not provided. The Splunk HTTP collector '
'source URL can be provided using the '
'`SPLUNK_URL` environment variable or '
'in the ansible.cfg file.')
self.authtoken = self.get_option('authtoken')
if self.authtoken is None:
self.disabled = True
self._display.warning('Splunk HTTP collector requires an authentication'
'token. The Splunk HTTP collector '
'authentication token can be provided using the '
'`SPLUNK_AUTHTOKEN` environment variable or '
'in the ansible.cfg file.')
def v2_playbook_on_start(self, playbook):
self.splunk.ansible_playbook = basename(playbook._file_name)
def v2_playbook_on_task_start(self, task, is_conditional):
self.start_datetimes[task._uuid] = datetime.utcnow()
def v2_playbook_on_handler_task_start(self, task):
self.start_datetimes[task._uuid] = datetime.utcnow()
def v2_runner_on_ok(self, result, **kwargs):
self.splunk.send_event(
self.url,
self.authtoken,
'OK',
result,
self._runtime(result)
)
def v2_runner_on_skipped(self, result, **kwargs):
self.splunk.send_event(
self.url,
self.authtoken,
'SKIPPED',
result,
self._runtime(result)
)
def v2_runner_on_failed(self, result, **kwargs):
self.splunk.send_event(
self.url,
self.authtoken,
'FAILED',
result,
self._runtime(result)
)
def runner_on_async_failed(self, result, **kwargs):
self.splunk.send_event(
self.url,
self.authtoken,
'FAILED',
result,
self._runtime(result)
)
def v2_runner_on_unreachable(self, result, **kwargs):
self.splunk.send_event(
self.url,
self.authtoken,
'UNREACHABLE',
result,
self._runtime(result)
)

View file

@ -0,0 +1,70 @@
# (c) 2017, Frederic Van Espen <github@freh.be>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: stderr
callback_type: stdout
requirements:
- set as main display callback
short_description: Splits output, sending failed tasks to stderr
deprecated:
why: The 'default' callback plugin now supports this functionality
removed_in: '2.11'
alternative: "'default' callback plugin with 'display_failed_stderr = yes' option"
extends_documentation_fragment:
- default_callback
description:
- This is the stderr callback plugin, it behaves like the default callback plugin but sends error output to stderr.
- Also it does not output skipped host/task/item status
'''
from ansible import constants as C
from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
class CallbackModule(CallbackModule_default):
'''
This is the stderr callback plugin, which reuses the default
callback plugin but sends error output to stderr.
'''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.stderr'
def __init__(self):
self.super_ref = super(CallbackModule, self)
self.super_ref.__init__()
def v2_runner_on_failed(self, result, ignore_errors=False):
delegated_vars = result._result.get('_ansible_delegated_vars', None)
self._clean_results(result._result, result._task.action)
if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
self._print_task_banner(result._task)
self._handle_exception(result._result, use_stderr=True)
self._handle_warnings(result._result)
if result._task.loop and 'results' in result._result:
self._process_items(result)
else:
if delegated_vars:
self._display.display("fatal: [%s -> %s]: FAILED! => %s" % (result._host.get_name(), delegated_vars['ansible_host'],
self._dump_results(result._result)), color=C.COLOR_ERROR,
stderr=True)
else:
self._display.display("fatal: [%s]: FAILED! => %s" % (result._host.get_name(), self._dump_results(result._result)),
color=C.COLOR_ERROR, stderr=True)
if ignore_errors:
self._display.display("...ignoring", color=C.COLOR_SKIP)

View file

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: sumologic
type: aggregate
short_description: Sends task result events to Sumologic
author: "Ryan Currah (@ryancurrah)"
description:
- This callback plugin will send task results as JSON formatted events to a Sumologic HTTP collector source
requirements:
- Whitelisting this callback plugin
- 'Create a HTTP collector source in Sumologic and specify a custom timestamp format of C(yyyy-MM-dd HH:mm:ss ZZZZ) and a custom timestamp locator
of C("timestamp": "(.*)")'
options:
url:
description: URL to the Sumologic HTTP collector source
env:
- name: SUMOLOGIC_URL
ini:
- section: callback_sumologic
key: url
'''
EXAMPLES = '''
examples: >
To enable, add this to your ansible.cfg file in the defaults block
[defaults]
callback_whitelist = sumologic
Set the environment variable
export SUMOLOGIC_URL=https://endpoint1.collection.us2.sumologic.com/receiver/v1/http/R8moSv1d8EW9LAUFZJ6dbxCFxwLH6kfCdcBfddlfxCbLuL-BN5twcTpMk__pYy_cDmp==
Set the ansible.cfg variable in the callback_sumologic block
[callback_sumologic]
url = https://endpoint1.collection.us2.sumologic.com/receiver/v1/http/R8moSv1d8EW9LAUFZJ6dbxCFxwLH6kfCdcBfddlfxCbLuL-BN5twcTpMk__pYy_cDmp==
'''
import json
import uuid
import socket
import getpass
from datetime import datetime
from os.path import basename
from ansible.module_utils.urls import open_url
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
class SumologicHTTPCollectorSource(object):
def __init__(self):
self.ansible_check_mode = False
self.ansible_playbook = ""
self.ansible_version = ""
self.session = str(uuid.uuid4())
self.host = socket.gethostname()
self.ip_address = socket.gethostbyname(socket.gethostname())
self.user = getpass.getuser()
def send_event(self, url, state, result, runtime):
if result._task_fields['args'].get('_ansible_check_mode') is True:
self.ansible_check_mode = True
if result._task_fields['args'].get('_ansible_version'):
self.ansible_version = \
result._task_fields['args'].get('_ansible_version')
if result._task._role:
ansible_role = str(result._task._role)
else:
ansible_role = None
if 'args' in result._task_fields:
del result._task_fields['args']
data = {}
data['uuid'] = result._task._uuid
data['session'] = self.session
data['status'] = state
data['timestamp'] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S '
'+0000')
data['host'] = self.host
data['ip_address'] = self.ip_address
data['user'] = self.user
data['runtime'] = runtime
data['ansible_version'] = self.ansible_version
data['ansible_check_mode'] = self.ansible_check_mode
data['ansible_host'] = result._host.name
data['ansible_playbook'] = self.ansible_playbook
data['ansible_role'] = ansible_role
data['ansible_task'] = result._task_fields
data['ansible_result'] = result._result
open_url(
url,
data=json.dumps(data, cls=AnsibleJSONEncoder, sort_keys=True),
headers={
'Content-type': 'application/json',
'X-Sumo-Host': data['ansible_host']
},
method='POST'
)
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.general.sumologic'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super(CallbackModule, self).__init__(display=display)
self.start_datetimes = {} # Collect task start times
self.url = None
self.sumologic = SumologicHTTPCollectorSource()
def _runtime(self, result):
return (
datetime.utcnow() -
self.start_datetimes[result._task._uuid]
).total_seconds()
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.url = self.get_option('url')
if self.url is None:
self.disabled = True
self._display.warning('Sumologic HTTP collector source URL was '
'not provided. The Sumologic HTTP collector '
'source URL can be provided using the '
'`SUMOLOGIC_URL` environment variable or '
'in the ansible.cfg file.')
def v2_playbook_on_start(self, playbook):
self.sumologic.ansible_playbook = basename(playbook._file_name)
def v2_playbook_on_task_start(self, task, is_conditional):
self.start_datetimes[task._uuid] = datetime.utcnow()
def v2_playbook_on_handler_task_start(self, task):
self.start_datetimes[task._uuid] = datetime.utcnow()
def v2_runner_on_ok(self, result, **kwargs):
self.sumologic.send_event(
self.url,
'OK',
result,
self._runtime(result)
)
def v2_runner_on_skipped(self, result, **kwargs):
self.sumologic.send_event(
self.url,
'SKIPPED',
result,
self._runtime(result)
)
def v2_runner_on_failed(self, result, **kwargs):
self.sumologic.send_event(
self.url,
'FAILED',
result,
self._runtime(result)
)
def runner_on_async_failed(self, result, **kwargs):
self.sumologic.send_event(
self.url,
'FAILED',
result,
self._runtime(result)
)
def v2_runner_on_unreachable(self, result, **kwargs):
self.sumologic.send_event(
self.url,
'UNREACHABLE',
result,
self._runtime(result)
)

View file

@ -0,0 +1,104 @@
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: syslog_json
callback_type: notification
requirements:
- whitelist in configuration
short_description: sends JSON events to syslog
description:
- This plugin logs ansible-playbook and ansible runs to a syslog server in JSON format
- Before 2.9 only environment variables were available for configuration
options:
server:
description: syslog server that will receive the event
env:
- name: SYSLOG_SERVER
default: localhost
ini:
- section: callback_syslog_json
key: syslog_server
port:
description: port on which the syslog server is listening
env:
- name: SYSLOG_PORT
default: 514
ini:
- section: callback_syslog_json
key: syslog_port
facility:
description: syslog facility to log as
env:
- name: SYSLOG_FACILITY
default: user
ini:
- section: callback_syslog_json
key: syslog_facility
'''
import os
import json
import logging
import logging.handlers
import socket
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
"""
logs ansible-playbook and ansible runs to a syslog server in json format
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'community.general.syslog_json'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self):
super(CallbackModule, self).__init__()
self.set_options()
syslog_host = self.get_option("server")
syslog_port = int(self.get_option("port"))
syslog_facility = self.get_option("facility")
self.logger = logging.getLogger('ansible logger')
self.logger.setLevel(logging.DEBUG)
self.handler = logging.handlers.SysLogHandler(
address=(syslog_host, syslog_port),
facility=syslog_facility
)
self.logger.addHandler(self.handler)
self.hostname = socket.gethostname()
def runner_on_failed(self, host, res, ignore_errors=False):
self.logger.error('%s ansible-command: task execution FAILED; host: %s; message: %s', self.hostname, host, self._dump_results(res))
def runner_on_ok(self, host, res):
self.logger.info('%s ansible-command: task execution OK; host: %s; message: %s', self.hostname, host, self._dump_results(res))
def runner_on_skipped(self, host, item=None):
self.logger.info('%s ansible-command: task execution SKIPPED; host: %s; message: %s', self.hostname, host, 'skipped')
def runner_on_unreachable(self, host, res):
self.logger.error('%s ansible-command: task execution UNREACHABLE; host: %s; message: %s', self.hostname, host, self._dump_results(res))
def runner_on_async_failed(self, host, res, jid):
self.logger.error('%s ansible-command: task execution FAILED; host: %s; message: %s', self.hostname, host, self._dump_results(res))
def playbook_on_import_for_host(self, host, imported_file):
self.logger.info('%s ansible-command: playbook IMPORTED; host: %s; message: imported file %s', self.hostname, host, imported_file)
def playbook_on_not_import_for_host(self, host, missing_file):
self.logger.info('%s ansible-command: playbook NOT IMPORTED; host: %s; message: missing file %s', self.hostname, host, missing_file)

246
plugins/callback/unixy.py Normal file
View file

@ -0,0 +1,246 @@
# Copyright: (c) 2017, Allyson Bowles <@akatch>
# Copyright: (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: unixy
type: stdout
author: Allyson Bowles <@akatch>
short_description: condensed Ansible output
description:
- Consolidated Ansible output in the style of LINUX/UNIX startup logs.
extends_documentation_fragment:
- default_callback
requirements:
- set as stdout in configuration
'''
from os.path import basename
from ansible import constants as C
from ansible import context
from ansible.module_utils._text import to_text
from ansible.utils.color import colorize, hostcolor
from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
class CallbackModule(CallbackModule_default):
'''
Design goals:
- Print consolidated output that looks like a *NIX startup log
- Defaults should avoid displaying unnecessary information wherever possible
TODOs:
- Only display task names if the task runs on at least one host
- Add option to display all hostnames on a single line in the appropriate result color (failures may have a separate line)
- Consolidate stats display
- Display whether run is in --check mode
- Don't show play name if no hosts found
'''
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.unixy'
def _run_is_verbose(self, result):
return ((self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and '_ansible_verbose_override' not in result._result)
def _get_task_display_name(self, task):
self.task_display_name = None
display_name = task.get_name().strip().split(" : ")
task_display_name = display_name[-1]
if task_display_name.startswith("include"):
return
else:
self.task_display_name = task_display_name
def _preprocess_result(self, result):
self.delegated_vars = result._result.get('_ansible_delegated_vars', None)
self._handle_exception(result._result, use_stderr=self.display_failed_stderr)
self._handle_warnings(result._result)
def _process_result_output(self, result, msg):
task_host = result._host.get_name()
task_result = "%s %s" % (task_host, msg)
if self._run_is_verbose(result):
task_result = "%s %s: %s" % (task_host, msg, self._dump_results(result._result, indent=4))
return task_result
if self.delegated_vars:
task_delegate_host = self.delegated_vars['ansible_host']
task_result = "%s -> %s %s" % (task_host, task_delegate_host, msg)
if result._result.get('msg') and result._result.get('msg') != "All items completed":
task_result += " | msg: " + to_text(result._result.get('msg'))
if result._result.get('stdout'):
task_result += " | stdout: " + result._result.get('stdout')
if result._result.get('stderr'):
task_result += " | stderr: " + result._result.get('stderr')
return task_result
def v2_playbook_on_task_start(self, task, is_conditional):
self._get_task_display_name(task)
if self.task_display_name is not None:
self._display.display("%s..." % self.task_display_name)
def v2_playbook_on_handler_task_start(self, task):
self._get_task_display_name(task)
if self.task_display_name is not None:
self._display.display("%s (via handler)... " % self.task_display_name)
def v2_playbook_on_play_start(self, play):
name = play.get_name().strip()
if name and play.hosts:
msg = u"\n- %s on hosts: %s -" % (name, ",".join(play.hosts))
else:
msg = u"---"
self._display.display(msg)
def v2_runner_on_skipped(self, result, ignore_errors=False):
if self.display_skipped_hosts:
self._preprocess_result(result)
display_color = C.COLOR_SKIP
msg = "skipped"
task_result = self._process_result_output(result, msg)
self._display.display(" " + task_result, display_color)
else:
return
def v2_runner_on_failed(self, result, ignore_errors=False):
self._preprocess_result(result)
display_color = C.COLOR_ERROR
msg = "failed"
item_value = self._get_item_label(result._result)
if item_value:
msg += " | item: %s" % (item_value,)
task_result = self._process_result_output(result, msg)
self._display.display(" " + task_result, display_color, stderr=self.display_failed_stderr)
def v2_runner_on_ok(self, result, msg="ok", display_color=C.COLOR_OK):
self._preprocess_result(result)
result_was_changed = ('changed' in result._result and result._result['changed'])
if result_was_changed:
msg = "done"
item_value = self._get_item_label(result._result)
if item_value:
msg += " | item: %s" % (item_value,)
display_color = C.COLOR_CHANGED
task_result = self._process_result_output(result, msg)
self._display.display(" " + task_result, display_color)
elif self.display_ok_hosts:
task_result = self._process_result_output(result, msg)
self._display.display(" " + task_result, display_color)
def v2_runner_item_on_skipped(self, result):
self.v2_runner_on_skipped(result)
def v2_runner_item_on_failed(self, result):
self.v2_runner_on_failed(result)
def v2_runner_item_on_ok(self, result):
self.v2_runner_on_ok(result)
def v2_runner_on_unreachable(self, result):
self._preprocess_result(result)
msg = "unreachable"
display_color = C.COLOR_UNREACHABLE
task_result = self._process_result_output(result, msg)
self._display.display(" " + task_result, display_color, stderr=self.display_failed_stderr)
def v2_on_file_diff(self, result):
if result._task.loop and 'results' in result._result:
for res in result._result['results']:
if 'diff' in res and res['diff'] and res.get('changed', False):
diff = self._get_diff(res['diff'])
if diff:
self._display.display(diff)
elif 'diff' in result._result and result._result['diff'] and result._result.get('changed', False):
diff = self._get_diff(result._result['diff'])
if diff:
self._display.display(diff)
def v2_playbook_on_stats(self, stats):
self._display.display("\n- Play recap -", screen_only=True)
hosts = sorted(stats.processed.keys())
for h in hosts:
# TODO how else can we display these?
t = stats.summarize(h)
self._display.display(u" %s : %s %s %s %s %s %s" % (
hostcolor(h, t),
colorize(u'ok', t['ok'], C.COLOR_OK),
colorize(u'changed', t['changed'], C.COLOR_CHANGED),
colorize(u'unreachable', t['unreachable'], C.COLOR_UNREACHABLE),
colorize(u'failed', t['failures'], C.COLOR_ERROR),
colorize(u'rescued', t['rescued'], C.COLOR_OK),
colorize(u'ignored', t['ignored'], C.COLOR_WARN)),
screen_only=True
)
self._display.display(u" %s : %s %s %s %s %s %s" % (
hostcolor(h, t, False),
colorize(u'ok', t['ok'], None),
colorize(u'changed', t['changed'], None),
colorize(u'unreachable', t['unreachable'], None),
colorize(u'failed', t['failures'], None),
colorize(u'rescued', t['rescued'], None),
colorize(u'ignored', t['ignored'], None)),
log_only=True
)
if stats.custom and self.show_custom_stats:
self._display.banner("CUSTOM STATS: ")
# per host
# TODO: come up with 'pretty format'
for k in sorted(stats.custom.keys()):
if k == '_run':
continue
self._display.display('\t%s: %s' % (k, self._dump_results(stats.custom[k], indent=1).replace('\n', '')))
# print per run custom stats
if '_run' in stats.custom:
self._display.display("", screen_only=True)
self._display.display('\tRUN: %s' % self._dump_results(stats.custom['_run'], indent=1).replace('\n', ''))
self._display.display("", screen_only=True)
def v2_playbook_on_no_hosts_matched(self):
self._display.display(" No hosts found!", color=C.COLOR_DEBUG)
def v2_playbook_on_no_hosts_remaining(self):
self._display.display(" Ran out of hosts!", color=C.COLOR_ERROR)
def v2_playbook_on_start(self, playbook):
# TODO display whether this run is happening in check mode
self._display.display("Executing playbook %s" % basename(playbook._file_name))
# show CLI arguments
if self._display.verbosity > 3:
if context.CLIARGS.get('args'):
self._display.display('Positional arguments: %s' % ' '.join(context.CLIARGS['args']),
color=C.COLOR_VERBOSE, screen_only=True)
for argument in (a for a in context.CLIARGS if a != 'args'):
val = context.CLIARGS[argument]
if val:
self._display.vvvv('%s: %s' % (argument, val))
def v2_runner_retry(self, result):
msg = " Retrying... (%d of %d)" % (result._result['attempts'], result._result['retries'])
if self._run_is_verbose(result):
msg += "Result was: %s" % self._dump_results(result._result)
self._display.display(msg, color=C.COLOR_DEBUG)

129
plugins/callback/yaml.py Normal file
View file

@ -0,0 +1,129 @@
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
callback: yaml
type: stdout
short_description: yaml-ized Ansible screen output
description:
- Ansible output that can be quite a bit easier to read than the
default JSON formatting.
extends_documentation_fragment:
- default_callback
requirements:
- set as stdout in configuration
'''
import yaml
import json
import re
import string
import sys
from ansible.module_utils._text import to_bytes, to_text
from ansible.module_utils.six import string_types
from ansible.parsing.yaml.dumper import AnsibleDumper
from ansible.plugins.callback import CallbackBase, strip_internal_keys, module_response_deepcopy
from ansible.plugins.callback.default import CallbackModule as Default
# from http://stackoverflow.com/a/15423007/115478
def should_use_block(value):
"""Returns true if string should be in block format"""
for c in u"\u000a\u000d\u001c\u001d\u001e\u0085\u2028\u2029":
if c in value:
return True
return False
def my_represent_scalar(self, tag, value, style=None):
"""Uses block style for multi-line strings"""
if style is None:
if should_use_block(value):
style = '|'
# we care more about readable than accuracy, so...
# ...no trailing space
value = value.rstrip()
# ...and non-printable characters
value = ''.join(x for x in value if x in string.printable)
# ...tabs prevent blocks from expanding
value = value.expandtabs()
# ...and odd bits of whitespace
value = re.sub(r'[\x0b\x0c\r]', '', value)
# ...as does trailing space
value = re.sub(r' +\n', '\n', value)
else:
style = self.default_style
node = yaml.representer.ScalarNode(tag, value, style=style)
if self.alias_key is not None:
self.represented_objects[self.alias_key] = node
return node
class CallbackModule(Default):
"""
Variation of the Default output which uses nicely readable YAML instead
of JSON for printing results.
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'stdout'
CALLBACK_NAME = 'community.general.yaml'
def __init__(self):
super(CallbackModule, self).__init__()
yaml.representer.BaseRepresenter.represent_scalar = my_represent_scalar
def _dump_results(self, result, indent=None, sort_keys=True, keep_invocation=False):
if result.get('_ansible_no_log', False):
return json.dumps(dict(censored="The output has been hidden due to the fact that 'no_log: true' was specified for this result"))
# All result keys stating with _ansible_ are internal, so remove them from the result before we output anything.
abridged_result = strip_internal_keys(module_response_deepcopy(result))
# remove invocation unless specifically wanting it
if not keep_invocation and self._display.verbosity < 3 and 'invocation' in result:
del abridged_result['invocation']
# remove diff information from screen output
if self._display.verbosity < 3 and 'diff' in result:
del abridged_result['diff']
# remove exception from screen output
if 'exception' in abridged_result:
del abridged_result['exception']
dumped = ''
# put changed and skipped into a header line
if 'changed' in abridged_result:
dumped += 'changed=' + str(abridged_result['changed']).lower() + ' '
del abridged_result['changed']
if 'skipped' in abridged_result:
dumped += 'skipped=' + str(abridged_result['skipped']).lower() + ' '
del abridged_result['skipped']
# if we already have stdout, we don't need stdout_lines
if 'stdout' in abridged_result and 'stdout_lines' in abridged_result:
abridged_result['stdout_lines'] = '<omitted>'
# if we already have stderr, we don't need stderr_lines
if 'stderr' in abridged_result and 'stderr_lines' in abridged_result:
abridged_result['stderr_lines'] = '<omitted>'
if abridged_result:
dumped += '\n'
dumped += to_text(yaml.dump(abridged_result, allow_unicode=True, width=1000, Dumper=AnsibleDumper, default_flow_style=False))
# indent by a couple of spaces
dumped = '\n '.join(dumped.split('\n')).rstrip()
return dumped
def _serialize_diff(self, diff):
return to_text(yaml.dump(diff, allow_unicode=True, width=1000, Dumper=AnsibleDumper, default_flow_style=False))