allow config for callbaks and some fixes

* only complain about ini deprecation if value is set
 * set plugin config for stdout and other types
 * updated plugin docs, moved several plugins to new config
 * finished ssh docs
 * fixed some issues seen in plugins while modifying docs
 * placeholder for 'required'
 * callbacks must use _plugin_options as _options already in use

(cherry picked from commit 869a318492)
pull/29947/head
Brian Coca 7 years ago committed by Toshio Kuratomi
parent 61315cd3c5
commit 34db3cd9be

@ -215,6 +215,15 @@ class ConfigManager(object):
''' Load YAML Config Files in order, check merge flags, keep origin of settings'''
pass
def get_plugin_options(self, plugin_type, name, variables=None):
options = {}
defs = self.get_configuration_definitions(plugin_type, name)
for option in defs:
options[option] = self.get_config_value(option, plugin_type=plugin_type, plugin_name=name, variables=variables)
return options
def get_configuration_definitions(self, plugin_type=None, name=None):
''' just list the possible settings, either base or for specific plugins or plugin '''
@ -224,7 +233,7 @@ class ConfigManager(object):
elif name is None:
ret = self._plugins.get(plugin_type, {})
else:
ret = {name: self._plugins.get(plugin_type, {}).get(name, {})}
ret = self._plugins.get(plugin_type, {}).get(name, {})
return ret
@ -287,7 +296,7 @@ class ConfigManager(object):
for ini_entry in defs[config]['ini']:
value = get_ini_config_value(self._parser, ini_entry)
origin = cfile
if 'deprecated' in ini_entry:
if value is not None and 'deprecated' in ini_entry:
self.DEPRECATED.append(('[%s]%s' % (ini_entry['section'], ini_entry['key']), ini_entry['deprecated']))
except Exception as e:
sys.stderr.write("Error while loading ini config %s: %s" % (cfile, to_native(e)))

@ -176,6 +176,7 @@ class TaskQueueManager:
raise AnsibleError("Invalid callback for stdout specified: %s" % self._stdout_callback)
else:
self._stdout_callback = callback_loader.get(self._stdout_callback)
self._stdout_callback.set_options(C.config.get_plugin_options('callback', self._stdout_callback._load_name))
stdout_callback_loaded = True
else:
raise AnsibleError("callback must be an instance of CallbackBase or the name of a callback plugin")
@ -198,7 +199,9 @@ class TaskQueueManager:
C.DEFAULT_CALLBACK_WHITELIST is None or callback_name not in C.DEFAULT_CALLBACK_WHITELIST)):
continue
self._callback_plugins.append(callback_plugin())
callback_obj = callback_plugin()
callback_obj .set_options(C.config.get_plugin_options('callback', callback_plugin._load_name))
self._callback_plugins.append(callback_obj)
self._callbacks_loaded = True
@ -366,4 +369,4 @@ class TaskQueueManager:
display.warning(u"Failure using method (%s) in callback plugin (%s): %s" % (to_text(method_name), to_text(callback_plugin), to_text(e)))
from traceback import format_tb
from sys import exc_info
display.debug('Callback Exception: \n' + ' '.join(format_tb(exc_info()[2])))
display.vvv('Callback Exception: \n' + ' '.join(format_tb(exc_info()[2])))

@ -24,7 +24,7 @@ __metaclass__ = type
from abc import ABCMeta
from ansible import constants as C
from ansible.module_utils.six import with_metaclass
from ansible.module_utils.six import with_metaclass, string_types
try:
from __main__ import display
@ -39,22 +39,29 @@ PLUGIN_PATH_CACHE = {}
def get_plugin_class(obj):
return obj.__class__.__name__.lower().replace('module', '')
if isinstance(obj, string_types):
return obj.lower().replace('module', '')
else:
return obj.__class__.__name__.lower().replace('module', '')
class AnsiblePlugin(with_metaclass(ABCMeta, object)):
def __init__(self):
self.options = {}
self._options = {}
def get_option(self, option, hostvars=None):
if option not in self.options:
if option not in self._options:
option_value = C.config.get_config_value(option, plugin_type=get_plugin_class(self), plugin_name=self.name, variables=hostvars)
self.set_option(option, option_value)
return self.options.get(option)
return self._options.get(option)
def set_option(self, option, value):
self.options[option] = value
self._options[option] = value
def set_options(self, options):
self.options = options
self._options = options
def _check_required(self):
# FIXME: standarize required check based on config
pass

@ -26,6 +26,7 @@ import warnings
from copy import deepcopy
from ansible import constants as C
from ansible.plugins import AnsiblePlugin
from ansible.module_utils._text import to_text
from ansible.utils.color import stringc
from ansible.vars.manager import strip_internal_keys
@ -45,7 +46,7 @@ except ImportError:
__all__ = ["CallbackBase"]
class CallbackBase:
class CallbackBase(AnsiblePlugin):
'''
This is a base ansible callback class that does nothing. New callbacks should
@ -53,7 +54,8 @@ class CallbackBase:
custom actions.
'''
def __init__(self, display=None):
def __init__(self, display=None, options=None):
if display:
self._display = display
else:
@ -70,9 +72,18 @@ class CallbackBase:
version = getattr(self, 'CALLBACK_VERSION', '1.0')
self._display.vvvv('Loading callback plugin %s of type %s, v%s from %s' % (name, ctype, version, __file__))
self.disabled = False
self._plugin_options = {}
if options is not None:
self.set_options(options)
''' helper for callbacks, so they don't all have to include deepcopy '''
_copy_result = deepcopy
def set_options(self, options):
self._plugin_options = options
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"))

@ -14,17 +14,17 @@ DOCUMENTATION:
show_skipped_hosts:
name: Show skipped hosts
description: "Toggle to control displaying skipped task/host results in a task"
default: True
env:
- name: DISPLAY_SKIPPED_HOSTS
ini:
- key: display_skipped_hosts
section: defaults
type: boolean
default: True
show_custom_stats:
name: Show custom stats
default: False
description: 'This adds the custom stats set via the set_stats plugin to the play recap'
default: False
env:
- name: ANSIBLE_SHOW_CUSTOM_STATS
ini:
@ -119,7 +119,7 @@ class CallbackModule(CallbackBase):
self._display.display(msg, color=color)
def v2_runner_on_skipped(self, result):
if C.DISPLAY_SKIPPED_HOSTS:
if self._plugin_options['show_skipped_hosts']:
delegated_vars = result._result.get('_ansible_delegated_vars', None)
self._clean_results(result._result, result._task.action)
@ -248,7 +248,7 @@ class CallbackModule(CallbackBase):
self._display.display(msg + " (item=%s) => %s" % (self._get_item(result._result), self._dump_results(result._result)), color=C.COLOR_ERROR)
def v2_runner_item_on_skipped(self, result):
if C.DISPLAY_SKIPPED_HOSTS:
if self._plugin_options['show_skipped_hosts']:
self._clean_results(result._result, result._task.action)
msg = "skipping: [%s] => (item=%s) " % (result._host.get_name(), self._get_item(result._result))
if (self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and '_ansible_verbose_override' not in result._result:
@ -287,7 +287,7 @@ class CallbackModule(CallbackBase):
self._display.display("", screen_only=True)
# print custom stats
if C.SHOW_CUSTOM_STATS and stats.custom:
if self._plugin_options['show_custom_stats'] and stats.custom:
self._display.banner("CUSTOM STATS: ")
# per host
# TODO: come up with 'pretty format'
@ -308,11 +308,11 @@ class CallbackModule(CallbackBase):
self._display.banner("PLAYBOOK: %s" % basename(playbook._file_name))
if self._display.verbosity > 3:
if self._options is not None:
for option in dir(self._options):
if self._plugin_options is not None:
for option in dir(self._plugin_options):
if option.startswith('_') or option in ['read_file', 'ensure_value', 'read_module']:
continue
val = getattr(self._options, option)
val = getattr(self._plugin_options, option)
if val:
self._display.vvvv('%s: %s' % (option, val))

@ -8,13 +8,14 @@ DOCUMENTATION:
type: notification
short_description: Sends events to Logentries
description:
- This callback plugin will generate JSON objects and send them to Logentries for auditing/debugging purposes.
- If you want to use an ini configuration, the file must be placed in the same directory as this plugin and named logentries.ini
- 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.
version_added: "2.0"
requirements:
- whitelisting in configuration
- certifi (python library)
- flatdict (pytnon library)
- flatdict (pytnon library), if you want to use the 'flatten' option
options:
api:
description: URI to the Logentries API
@ -22,7 +23,7 @@ DOCUMENTATION:
- name: LOGENTRIES_API
default: data.logentries.com
ini:
- section: defaults
- section: callback_logentries
key: api
port:
description: Http port to use when connecting to the API
@ -30,7 +31,7 @@ DOCUMENTATION:
- name: LOGENTRIES_PORT
default: 80
ini:
- section: defaults
- section: callback_logentries
key: port
tls_port:
description: Port to use when connecting to the API when TLS is enabled
@ -38,15 +39,15 @@ DOCUMENTATION:
- name: LOGENTRIES_TLS_PORT
default: 443
ini:
- section: defaults
- section: callback_logentries
key: tls_port
token:
description: the authentication token
description: The logentries "TCP token"
env:
- name: LOGENTRIES_ANSIBLE_TOKEN
required: True
ini:
- section: defaults
- section: callback_logentries
key: token
use_tls:
description:
@ -56,7 +57,7 @@ DOCUMENTATION:
default: False
type: boolean
ini:
- section: defaults
- section: callback_logentries
key: use_tls
flatten:
description: flatten complex data structures into a single dictionary with complex keys
@ -65,7 +66,7 @@ DOCUMENTATION:
env:
- name: LOGENTRIES_FLATTEN
ini:
- section: defaults
- section: callback_logentries
key: flatten
EXAMPLES: >
To enable, add this to your ansible.cfg file in the defaults block
@ -78,8 +79,8 @@ EXAMPLES: >
export LOGENTRIES_PORT=10000
export LOGENTRIES_ANSIBLE_TOKEN=dd21fc88-f00a-43ff-b977-e3a4233c53af
Or create a logentries.ini config file that sites next to the plugin with the following contents
[logentries]
Or in the main Ansible config file
[callback_logentries]
api = data.logentries.com
port = 10000
tls_port = 20000
@ -108,8 +109,8 @@ try:
except ImportError:
HAS_FLATDICT = False
from ansible.module_utils.six.moves import configparser
from ansible.module_utils._text import to_bytes, to_text
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_bytes, to_text, to_native
from ansible.plugins.callback import CallbackBase
"""
Todo:
@ -118,11 +119,7 @@ Todo:
class PlainTextSocketAppender(object):
def __init__(self,
verbose=True,
LE_API='data.logentries.com',
LE_PORT=80,
LE_TLS_PORT=443):
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
@ -134,7 +131,7 @@ class PlainTextSocketAppender(object):
# Unicode Line separator character \u2028
self.LINE_SEP = u'\u2028'
self.verbose = verbose
self._display = display
self._conn = None
def open_connection(self):
@ -149,9 +146,8 @@ class PlainTextSocketAppender(object):
try:
self.open_connection()
return
except Exception:
if self.verbose:
self._display.warning("Unable to connect to Logentries")
except Exception as e:
self._display.vvvv("Unable to connect to Logentries: %s" % str(e))
root_delay *= 2
if (root_delay > self.MAX_DELAY):
@ -160,6 +156,7 @@ class PlainTextSocketAppender(object):
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
@ -221,91 +218,76 @@ class CallbackModule(CallbackBase):
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.")
warn = ''
if not HAS_CERTIFI:
self.disabled = True
warn += 'The `certifi` python module is not installed.'
self._display.warning('The `certifi` python module is not installed.\nDisabling the Logentries callback plugin.')
if not HAS_FLATDICT:
self.disabled = True
warn += 'The `flatdict` python module is not installed.'
self.le_jobid = str(uuid.uuid4())
# FIXME: remove when done testing
# initialize configurable
self.api_url = 'data.logentries.com'
self.api_port = 80
self.api_tls_port = 443
self.use_tls = False
self.flatten = False
self.token = None
# FIXME: make configurable, move to options
self.timeout = 10
# FIXME: remove testing
# self.set_options({'api': 'data.logentries.com', 'port': 80,
# 'tls_port': 10000, 'use_tls': True, 'flatten': False, 'token': 'ae693734-4c5b-4a44-8814-1d2feb5c8241'})
def set_option(self, name, value):
raise AnsibleError("The Logentries callabck plugin does not suport setting individual options.")
if warn:
self._display.warning('%s\nDisabling the Logentries callback plugin.' % warn)
def set_options(self, options):
config_path = os.path.abspath(os.path.dirname(__file__))
config = configparser.ConfigParser()
super(CallbackModule, self).set_options(options)
# get options
try:
config.readfp(open(os.path.join(config_path, 'logentries.ini')))
if config.has_option('logentries', 'api'):
self.api_uri = config.get('logentries', 'api')
if config.has_option('logentries', 'port'):
self.api_port = config.getint('logentries', 'port')
if config.has_option('logentries', 'tls_port'):
self.api_tls_port = config.getint('logentries', 'tls_port')
if config.has_option('logentries', 'use_tls'):
self.use_tls = config.getboolean('logentries', 'use_tls')
if config.has_option('logentries', 'token'):
self.token = config.get('logentries', 'token')
if config.has_option('logentries', 'flatten'):
self.flatten = config.getboolean('logentries', 'flatten')
except:
self.api_uri = os.getenv('LOGENTRIES_API')
if self.api_uri is None:
self.api_uri = 'data.logentries.com'
self.api_url = self._plugin_options['api']
self.api_port = self._plugin_options['port']
self.api_tls_port = self._plugin_options['tls_port']
self.use_tls = self._plugin_options['use_tls']
self.flatten = self._plugin_options['flatten']
except KeyError as e:
self._display.warning("Missing option for Logentries callback plugin: %s" % to_native(e))
self.disabled = True
try:
self.api_port = int(os.getenv('LOGENTRIES_PORT'))
if self.api_port is None:
self.api_port = 80
except TypeError:
self.api_port = 80
try:
self.token = self._plugin_options['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
try:
self.api_tls_port = int(os.getenv('LOGENTRIES_TLS_PORT'))
if self.api_tls_port is None:
self.api_tls_port = 443
except TypeError:
self.api_tls_port = 443
# this just needs to be set to use TLS
self.use_tls = os.getenv('LOGENTRIES_USE_TLS')
if self.use_tls is None:
self.use_tls = False
elif self.use_tls.lower() in ['yes', 'true']:
self.use_tls = True
self.token = os.getenv('LOGENTRIES_ANSIBLE_TOKEN')
if self.token is None:
self.disabled = True
self._display.warning('Logentries token could not be loaded. The logentries token can be provided using the `LOGENTRIES_TOKEN` environment '
'variable')
self.flatten = os.getenv('LOGENTRIES_FLATTEN')
if self.flatten is None:
self.flatten = False
elif self.flatten.lower() in ['yes', 'true']:
self.flatten = True
self.verbose = False
self.timeout = 10
self.le_jobid = str(uuid.uuid4())
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.')
if self.use_tls:
self._appender = TLSSocketAppender(verbose=self.verbose,
LE_API=self.api_uri,
LE_TLS_PORT=self.api_tls_port)
else:
self._appender = PlainTextSocketAppender(verbose=self.verbose,
LE_API=self.api_uri,
LE_PORT=self.api_port)
self._appender.reopen_connection()
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:
@ -318,43 +300,34 @@ class CallbackModule(CallbackBase):
msg = record.rstrip('\n')
msg = "{} {}".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 = {}
results['le_jobid'] = self.le_jobid
results['hostname'] = host
results['results'] = 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 = {}
results['le_jobid'] = self.le_jobid
results['hostname'] = host
results['results'] = res
results = self._set_info(host, res)
results['status'] = 'FAILED'
self.emit_formatted(results)
def runner_on_skipped(self, host, item=None):
results = {}
results['le_jobid'] = self.le_jobid
results['hostname'] = host
results = self._set_info(host, item)
del results['results']
results['status'] = 'SKIPPED'
self.emit_formatted(results)
def runner_on_unreachable(self, host, res):
results = {}
results['le_jobid'] = self.le_jobid
results['hostname'] = host
results['results'] = res
results = self._set_info(host, res)
results['status'] = 'UNREACHABLE'
self.emit_formatted(results)
def runner_on_async_failed(self, host, res, jid):
results = {}
results['le_jobid'] = self.le_jobid
results['hostname'] = host
results['results'] = res
results = self._set_info(host, res)
results['jid'] = jid
results['status'] = 'ASYNC_FAILED'
self.emit_formatted(results)

@ -1,23 +1,20 @@
# (C) 2017, Tennis Smith, http://github.com/gamename
#
# This file 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.
#
# File 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.
#
# See <http://www.gnu.org/licenses/> for a copy of the
# GNU General Public License
#
# This will track the use of each role during the life of a playbook's
# execution. The total time spent in each role will be printed at the
# end.
#
# (c) 2017, Tennis Smith, http://github.com/gamename
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
DOCUMENTATION:
callback: profile_roles
type: aggregate
short_description: adds timing information to roles
version_added: "2.4"
description:
- This callback module provides profiling for ansible roles.
requirements:
- whitelisting in configuration
'''
# Make coding more python3-ish
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
@ -117,11 +114,9 @@ class CallbackModule(CallbackBase):
# Print the timings starting with the largest one
for result in self.totals.most_common():
msg = u"{0:-<70}{1:->9}".format(result[0] + u' ',
u' {0:.02f}s'.format(result[1]))
msg = u"{0:-<70}{1:->9}".format(result[0] + u' ', u' {0:.02f}s'.format(result[1]))
self._display.display(msg)
msg_total = u"{0:-<70}{1:->9}".format(u'total ',
u' {0:.02f}s'.format(total_time))
msg_total = u"{0:-<70}{1:->9}".format(u'total ', u' {0:.02f}s'.format(total_time))
self._display.display(filled("", fchar="~"))
self._display.display(msg_total)

@ -2,29 +2,60 @@
# (C) 2015, Tom Paine, <github@aioue.net>
# (C) 2014, Jharrod LaFon, @JharrodLaFon
# (C) 2012-2013, 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)
'''
DOCUMENTATION:
callback: profile_tasks
type: aggregate
short_description: adds time information to tasks
version_added: "2.0"
description:
- Ansible callback plugin for timing individual tasks and overall execution time.
- "Mashup of 2 excellent original works: https://github.com/jlafon/ansible-profile,
https://github.com/junaid18183/ansible_home/blob/master/ansible_plugins/callback_plugins/timestamp.py.old"
- "Format: ``<task start timestamp> (<length of previous task>) <current elapsed playbook execution time>``"
- It also lists the top/bottom time consuming tasks in the summary (configurable)
- Before 2.4 only the environment variables were available for configuration.
requirements:
- whitelisting in configuration
options:
output_limit:
description: Number of tasks to display in the summary
default: 20
env:
- name: PROFILE_TASKS_TASK_OUTPUT_LIMIT
ini:
- section: callback_profile_tasks
key: task_output_limit
sort_order:
description: Adjust the sorting output of summary tasks
choices: ['descending', 'ascending', 'none']
default: 'descending'
env:
- name: PROFILE_TASKS_SORT_ORDER
ini:
- section: callback_profile_tasks
key: sort_order
#EXAMPLES: > '
#
# This file 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.
# TASK: [ensure messaging security group exists] ********************************
# Thursday 11 June 2017 22:50:53 +0100 (0:00:00.721) 0:00:05.322 *********
# ok: [localhost]
#
# File 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.
#
# See <http://www.gnu.org/licenses/> for a copy of the
# GNU General Public License
# Provides per-task timing, ongoing playbook elapsed time and
# ordered list of top 20 longest running tasks at end
# TASK: [ensure db security group exists] ***************************************
# Thursday 11 June 2017 22:50:54 +0100 (0:00:00.558) 0:00:05.880 *********
# changed: [localhost]
# '
'''
# Make coding more python3-ish
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import collections
import os
import time
from ansible.module_utils.six.moves import reduce
@ -82,19 +113,32 @@ class CallbackModule(CallbackBase):
def __init__(self):
self.stats = collections.OrderedDict()
self.current = None
self.sort_order = os.getenv('PROFILE_TASKS_SORT_ORDER', True)
self.task_output_limit = os.getenv('PROFILE_TASKS_TASK_OUTPUT_LIMIT', 20)
if self.sort_order == 'ascending':
self.sort_order = False
if self.task_output_limit == 'all':
self.task_output_limit = None
else:
self.task_output_limit = int(self.task_output_limit)
self.sort_order = None
self.task_output_limit = None
super(CallbackModule, self).__init__()
def set_options(self, options):
super(CallbackModule, self).set_options(options)
self.sort_order = self._plugin_options['sort_order']
if self.sort_order is not None:
if self.sort_order == 'ascending':
self.sort_order = False
elif self.sort_order == 'descending':
self.sort_order = True
elif self.sort_order == 'none':
self.sort_order = None
self.task_output_limit = self._plugin_options['output_limit']
if self.task_output_limit is not None:
if self.task_output_limit == 'all':
self.task_output_limit = None
else:
self.task_output_limit = int(self.task_output_limit)
def _record_task(self, task):
"""
Logs the start of each task
@ -126,7 +170,7 @@ class CallbackModule(CallbackBase):
results = self.stats.items()
# Sort the tasks by the specified sort
if self.sort_order != 'none':
if self.sort_order is not None:
results = sorted(
self.stats.items(),
key=lambda x: x[1]['time'],

@ -1,75 +0,0 @@
profile\_tasks.py
=================
Ansible plugin for timing individual tasks and overall execution time.
Mashup of 2 excellent original works:
- https://github.com/jlafon/ansible-profile
- https://github.com/junaid18183/ansible_home/blob/master/ansible_plugins/callback_plugins/timestamp.py.old
Usage
-----
Add ``profile_tasks`` to the ``callback_whitelist`` in ``ansible.cfg``.
Run playbooks as normal.
Certain options are configurable using environment variables. You can specify ``ascending`` or ``none`` for
the environment variable ``PROFILE_TASKS_SORT_ORDER`` to adjust sorting output. If you want to see more than
20 tasks in the output you can set ``PROFILE_TASKS_TASK_OUTPUT_LIMIT`` to any number, or the special value
``all`` to get a list of all tasks.
Features
--------
Tasks
~~~~~
Ongoing timing of each task as it happens.
| Format:
| ``<task start timestamp> (<length of previous task>) <current elapsed playbook execution time>``
Task output example:
.. code:: shell
TASK: [ensure messaging security group exists] ********************************
Thursday 11 June 2017 22:50:53 +0100 (0:00:00.721) 0:00:05.322 *********
ok: [localhost]
TASK: [ensure db security group exists] ***************************************
Thursday 11 June 2017 22:50:54 +0100 (0:00:00.558) 0:00:05.880 *********
changed: [localhost]
Play Recap
~~~~~~~~~~
Recap includes ending timestamp, total playbook execution time and a
sorted list of the top longest running tasks.
No more wondering how old the results in a terminal window are.
.. code:: shell
ansible <args here>
<normal output here>
PLAY RECAP ********************************************************************
Thursday 11 June 2016 22:51:00 +0100 (0:00:01.011) 0:00:43.247 *********
===============================================================================
old_and_slow : install tons of packages -------------------------------- 20.03s
/home/bob/ansible/roles/old_and_slow/tasks/main.yml:4 -------------------------
db : second task to run ------------------------------------------------- 2.03s
/home/bob/ansible/roles/db/tasks/main.yml:4 -----------------------------------
setup ------------------------------------------------------------------- 0.42s
None --------------------------------------------------------------------------
www : first task to run ------------------------------------------------- 0.03s
/home/bob/ansible/roles/www/tasks/main.yml:1 ----------------------------------
fast_task : first task to run ------------------------------------------- 0.01s
/home/bob/ansible/roles/fast_task.yml:1 ---------------------------------------
Compatibility
-------------
Ansible 2.0+

@ -1,61 +1,59 @@
# (c) Fastly, inc 2016
#
# 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/>.
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
selective.py callback plugin.
This callback only prints tasks that have been tagged with `print_action` or that have failed.
Tasks that are not printed are placed with a '.'.
For example:
- debug: msg="This will not be printed"
- debug: msg="But this will"
tags: [print_action]"
This allows operators to focus on the tasks that provide value only.
If you increase verbosity all tasks are printed.
DOCUMENTATION:
callback: selective
callback_type: stdout
requirements:
- set as main display callback
short_description: only print certain tasks
version_added: "2.4"
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]
"""
from __future__ import (absolute_import, division, print_function)
import difflib
import os
from ansible import constants as C
from ansible.plugins.callback import CallbackBase
from ansible.module_utils._text import to_text
__metaclass__ = type
DONT_COLORIZE = False
COLORS = {
'normal': '\033[0m',
'ok': '\033[92m',
'ok': C.COLOR_OK,
'bold': '\033[1m',
'not_so_bold': '\033[1m\033[34m',
'changed': '\033[93m',
'failed': '\033[91m',
'changed': C.COLOR_CHANGED,
'failed': C.COLOR_ERROR,
'endc': '\033[0m',
'skipped': '\033[96m',
'skipped': C.COLOR_SKIP,
}
DONT_COLORIZE = os.getenv('ANSIBLE_SELECTIVE_DONT_COLORIZE', default=False)
def dict_diff(prv, nxt):
"""Return a dict of keys that differ with another config object."""
@ -89,6 +87,13 @@ class CallbackModule(CallbackBase):
self.last_task_name = None
self.printed_last_task = False
def set_options(self, options):
super(CallbackModule, self).set_options(options)
global DONT_COLORIZE
DONT_COLORIZE = self._plugin_options['nocolor']
def _print_task(self, task_name=None):
if task_name is None:
task_name = self.last_task_name

@ -6,7 +6,8 @@
DOCUMENTATION:
callback: skippy
callback_type: stdout
requires: set as display
requirements:
- set as main display callback
short_description: Ansible screen output that ignores skipped status
version_added: "2.0"
description:

@ -1,20 +1,45 @@
# (C) 2014-2015, Matt Martz <matt@sivel.net>
# 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/>.
# (C) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
DOCUMENTATION:
callback: slack
callback_type: notification
requirements:
- whitelist in configuration
- prettytable (python library)
short_description: Sends play events to a Slack channel
version_added: "2.1"
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
'''
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
@ -42,17 +67,6 @@ except ImportError:
class CallbackModule(CallbackBase):
"""This is an ansible callback plugin that sends status
updates to a Slack channel during playbook execution.
This plugin makes use of the following environment variables:
SLACK_WEBHOOK_URL (required): Slack Webhook URL
SLACK_CHANNEL (optional): Slack room to post in. Default: #ansible
SLACK_USERNAME (optional): Username to post as. Default: ansible
SLACK_INVOCATION (optional): Show command line invocation
details. Default: False
Requires:
prettytable
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'notification'
@ -64,9 +78,9 @@ class CallbackModule(CallbackBase):
self.disabled = False
if cli:
self._options = cli.options
self._plugin_options = cli.options
else:
self._options = None
self._plugin_options = None
super(CallbackModule, self).__init__(display=display)
@ -76,13 +90,10 @@ class CallbackModule(CallbackBase):
'installed. Disabling the Slack callback '
'plugin.')
self.webhook_url = os.getenv('SLACK_WEBHOOK_URL')
self.channel = os.getenv('SLACK_CHANNEL', '#ansible')
self.username = os.getenv('SLACK_USERNAME', 'ansible')
self.show_invocation = boolean(
os.getenv('SLACK_INVOCATION', self._display.verbosity > 1),
strict=False
)
self.webhook_url = self._plugin_options['webook_url']
self.channel = self._plugin_options['channel']
self.username = self._plugin_options['username']
self.show_invocation = (self._display.verbosity > 1)
if self.webhook_url is None:
self.disabled = True
@ -91,12 +102,13 @@ class CallbackModule(CallbackBase):
'the `SLACK_WEBHOOK_URL` environment '
'variable.')
self.playbook_name = None
else:
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]
# 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 send_msg(self, attachments):
payload = {
@ -125,13 +137,13 @@ class CallbackModule(CallbackBase):
'*Playbook initiated* (_%s_)' % self.guid
]
invocation_items = []
if self._options and self.show_invocation:
tags = self._options.tags
skip_tags = self._options.skip_tags
extra_vars = self._options.extra_vars
subset = self._options.subset
if self._plugin_options and self.show_invocation:
tags = self._plugin_options.tags
skip_tags = self._plugin_options.skip_tags
extra_vars = self._plugin_options.extra_vars
subset = self._plugin_options.subset
inventory = os.path.basename(
os.path.realpath(self._options.inventory)
os.path.realpath(self._plugin_options.inventory)
)
invocation_items.append('Inventory: %s' % inventory)
@ -145,7 +157,7 @@ class CallbackModule(CallbackBase):
invocation_items.append('Extra Vars: %s' %
' '.join(extra_vars))
title.append('by *%s*' % self._options.remote_user)
title.append('by *%s*' % self._plugin_options.remote_user)
title.append('\n\n*%s*' % self.playbook_name)
msg_items = [' '.join(title)]

@ -1,20 +1,19 @@
# (c) 2017, Frederic Van Espen <github@freh.be>
#
# 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/>.
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
DOCUMENTATION:
callback: stderr
callback_type: stdout
requirements:
- set as main display callback
short_description: Splits output, sending failed tasks to stderr
version_added: "2.4"
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
'''
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

@ -1,3 +1,44 @@
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
DOCUMENTATION:
callback: syslog_json
callback_type: notification
requirements:
- whietlist in configuration
short_description: sends JSON events to syslog
version_added: "1.9"
description:
- This plugin logs ansible-playbook and ansible runs to a syslog server in JSON format
- Before 2.4 only environment variables were available for configuration
options:
server:
description: syslog server that will recieve the event
env:
- name: SYSLOG_SERVER
default: localhost
ini:
- section: callback_syslog_json
key: syslog_server
port:
description: prot on which the syslog server is listening
env:
- name: SYSLOG_PORT
default: 514
ini:
- section: callback_syslog_json
key: syslog_port
facility:
description: syslog facitliy to log as
env:
- name: SYSLOG_FACILITY
default: user
ini:
- section: callback_syslog_json
key: syslog_facility
'''
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
@ -16,15 +57,8 @@ from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
"""
logs ansible-playbook and ansible runs to a syslog server in json format
make sure you have in ansible.cfg:
callback_plugins = <path_to_callback_plugins_folder>
and put the plugin in <path_to_callback_plugins_folder>
This plugin makes use of the following environment variables:
SYSLOG_SERVER (optional): defaults to localhost
SYSLOG_PORT (optional): defaults to 514
SYSLOG_FACILITY (optional): defaults to user
"""
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'syslog_json'

@ -1,3 +1,16 @@
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
DOCUMENTATION:
callback: timer
callback_type: aggregate
requirements:
- whitelist in configuration
short_description: Adds time to play stats
version_added: "2.0"
description:
- This callback just adds total play duration to the play stats.
'''
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

@ -1,20 +1,18 @@
# (c) 2012-2014, Ansible, Inc
#
# 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/>.
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
'''
DOCUMENTATION:
callback: tree
callback_type: notification
requirements:
- invoked in the command line
short_description: Save host events to files
version_added: "2.0"
description:
- "This callback is used by the Ansible (adhoc) command line option `-t|--tree`"
- This produces a JSON dump of events in a directory, a file for each host, the directory used MUST be passed as a commadn line option.
'''
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

@ -28,6 +28,11 @@ DOCUMENTATION:
private_key_file:
description:
- Key or certificate file used for authentication
ini:
- section: defaults
key: private_key_file
env:
- name: ANSIBLE_PRIVATE_KEY_FILE
vars:
- name: ansible_private_key_file
timeout:

@ -69,10 +69,11 @@ DOCUMENTATION:
description: Extra exclusive to the 'ssh' CLI
vars:
- name: ansible_ssh_extra_args
ssh_retries:
retries:
# constant: ANSIBLE_SSH_RETRIES
description: Number of attempts to connect.
default: 3
type: integer
env:
- name: ANSIBLE_SSH_RETRIES
ini:
@ -118,14 +119,54 @@ DOCUMENTATION:
- {key: pipelining, section: ssh_connection}
type: boolean
vars: [{name: ansible_ssh_pipelining}]
# TODO:
# ANSIBLE_SSH_RETRIES
# self._play_context.private_key_file
# ANSIBLE_SSH_CONTROL_PATH
# ANSIBLE_SSH_CONTROL_PATH_DIR
# DEFAULT_SFTP_BATCH_MODE
# DEFAULT_SCP_IF_SSH
private_key_file:
description:
- Path to private key file to use for authentication
ini:
- section: defaults
key: private_key_file
env:
- name: ANSIBLE_PRIVATE_KEY_FILE
vars:
- name: ansible_private_key_file
- name: ansible_ssh_private_key_file
control_path:
default: null
description:
- This is the location to save ssh's ControlPath sockets, it uses ssh's variable substitution.
- Since 2.3, if null, ansible will generate a unique hash. Use `%(directory)s` to indicate where to use the control dir path setting.
env:
- name: ANSIBLE_SSH_CONTROL_PATH
ini:
- key: control_path
section: ssh_connection
control_path_dir:
default: ~/.ansible/cp
description:
- This sets the directory to use for ssh control path if the control path setting is null.
- Also, provides the `%(directory)s` variable for the control path setting.
env:
- name: ANSIBLE_SSH_CONTROL_PATH_DIR
ini:
- section: ssh_connection
key: control_path_dir
sftp_batch_mode:
default: True
description: 'TODO: write it'
env: [{name: ANSIBLE_SFTP_BATCH_MODE}]
ini:
- {key: sftp_batch_mode, section: ssh_connection}
type: boolean
scp_if_ssh:
default: smart
description:
- "Prefered method to use when transfering files over ssh"
- When set to smart, Ansible will try them until one succeeds or they all fail
- If set to True, it will force 'scp', if False it will use 'sftp'
env: [{name: ANSIBLE_SCP_IF_SSH}]
ini:
- {key: scp_if_ssh, section: ssh_connection}
'''
from __future__ import (absolute_import, division, print_function)

@ -205,14 +205,17 @@ class PluginLoader:
''' Reads plugin docs to find configuration setting definitions, to push to config manager for later use '''
# plugins w/o class name don't support config
if self.class_name and self.class_name in ('Connection'):
# FIXME: expand from just connection
type_name = get_plugin_class(self)
dstring = read_docstring(path, verbose=False, ignore_errors=False)
if dstring.get('doc', False):
if 'options' in dstring['doc'] and isinstance(dstring['doc']['options'], dict):
C.config.initialize_plugin_configuration_definitions(type_name, name, dstring['doc']['options'])
display.debug('Loaded config def from plugin (%s/%s)' % (type_name, name))
if self.class_name:
type_name = get_plugin_class(self.class_name)
# FIXME: expand from just connection and callback
if type_name in ('connection', 'callback'):
dstring = read_docstring(path, verbose=False, ignore_errors=False)
if dstring.get('doc', False):
if 'options' in dstring['doc'] and isinstance(dstring['doc']['options'], dict):
C.config.initialize_plugin_configuration_definitions(type_name, name, dstring['doc']['options'])
display.debug('Loaded config def from plugin (%s/%s)' % (type_name, name))
def add_directory(self, directory, with_subdir=False):
''' Adds an additional directory to the search path '''

Loading…
Cancel
Save