diff --git a/lib/ansible/config/manager.py b/lib/ansible/config/manager.py index 545cb7c19b..5270a4fa66 100644 --- a/lib/ansible/config/manager.py +++ b/lib/ansible/config/manager.py @@ -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))) diff --git a/lib/ansible/executor/task_queue_manager.py b/lib/ansible/executor/task_queue_manager.py index 6dbf51c716..faea2aff7f 100644 --- a/lib/ansible/executor/task_queue_manager.py +++ b/lib/ansible/executor/task_queue_manager.py @@ -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]))) diff --git a/lib/ansible/plugins/__init__.py b/lib/ansible/plugins/__init__.py index c89ffd7ef7..cee24dad58 100644 --- a/lib/ansible/plugins/__init__.py +++ b/lib/ansible/plugins/__init__.py @@ -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 diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py index 48a68a25ac..739fb24e48 100644 --- a/lib/ansible/plugins/callback/__init__.py +++ b/lib/ansible/plugins/callback/__init__.py @@ -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")) diff --git a/lib/ansible/plugins/callback/default.py b/lib/ansible/plugins/callback/default.py index 34f4180f54..033cbbde44 100644 --- a/lib/ansible/plugins/callback/default.py +++ b/lib/ansible/plugins/callback/default.py @@ -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)) diff --git a/lib/ansible/plugins/callback/logentries.py b/lib/ansible/plugins/callback/logentries.py index ddb3f096bf..8f0d73b878 100644 --- a/lib/ansible/plugins/callback/logentries.py +++ b/lib/ansible/plugins/callback/logentries.py @@ -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.' - - if warn: - self._display.warning('%s\nDisabling the Logentries callback plugin.' % warn) - - config_path = os.path.abspath(os.path.dirname(__file__)) - config = configparser.ConfigParser() - 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' - - 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.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.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() + # 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.") + + def set_options(self, options): + + super(CallbackModule, self).set_options(options) + + # get options + try: + 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.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 + + 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: @@ -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) diff --git a/lib/ansible/plugins/callback/profile_roles.py b/lib/ansible/plugins/callback/profile_roles.py index 26d86595ac..fd4333ff4d 100644 --- a/lib/ansible/plugins/callback/profile_roles.py +++ b/lib/ansible/plugins/callback/profile_roles.py @@ -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 for a copy of the -# GNU General Public License +# (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) -# -# 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. -# +''' +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) diff --git a/lib/ansible/plugins/callback/profile_tasks.py b/lib/ansible/plugins/callback/profile_tasks.py index e5b57a6275..cb97a2d356 100644 --- a/lib/ansible/plugins/callback/profile_tasks.py +++ b/lib/ansible/plugins/callback/profile_tasks.py @@ -2,29 +2,60 @@ # (C) 2015, Tom Paine, # (C) 2014, Jharrod LaFon, @JharrodLaFon # (C) 2012-2013, Michael DeHaan, -# -# 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 for a copy of the -# GNU General Public License +# (C) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# Provides per-task timing, ongoing playbook elapsed time and -# ordered list of top 20 longest running tasks at end +''' +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: `` () ``" + - 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: > ' +# +# 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] +# ' +''' +# 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'], diff --git a/lib/ansible/plugins/callback/profile_tasks.rst b/lib/ansible/plugins/callback/profile_tasks.rst deleted file mode 100644 index aad84774ea..0000000000 --- a/lib/ansible/plugins/callback/profile_tasks.rst +++ /dev/null @@ -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 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 - - 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+ diff --git a/lib/ansible/plugins/callback/selective.py b/lib/ansible/plugins/callback/selective.py index d0edf46055..d61eb06764 100644 --- a/lib/ansible/plugins/callback/selective.py +++ b/lib/ansible/plugins/callback/selective.py @@ -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 . +# (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 diff --git a/lib/ansible/plugins/callback/skippy.py b/lib/ansible/plugins/callback/skippy.py index 856b424894..522233fe34 100644 --- a/lib/ansible/plugins/callback/skippy.py +++ b/lib/ansible/plugins/callback/skippy.py @@ -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: diff --git a/lib/ansible/plugins/callback/slack.py b/lib/ansible/plugins/callback/slack.py index 13c5b1183f..a4bf4ef8ad 100644 --- a/lib/ansible/plugins/callback/slack.py +++ b/lib/ansible/plugins/callback/slack.py @@ -1,20 +1,45 @@ # (C) 2014-2015, Matt Martz +# (C) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# 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 . - +''' +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)] diff --git a/lib/ansible/plugins/callback/stderr.py b/lib/ansible/plugins/callback/stderr.py index 98e469a0e3..5afde9f92d 100644 --- a/lib/ansible/plugins/callback/stderr.py +++ b/lib/ansible/plugins/callback/stderr.py @@ -1,20 +1,19 @@ # (c) 2017, Frederic Van Espen -# -# 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 . +# (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 diff --git a/lib/ansible/plugins/callback/syslog_json.py b/lib/ansible/plugins/callback/syslog_json.py index 912a64222f..6d0d1446a7 100644 --- a/lib/ansible/plugins/callback/syslog_json.py +++ b/lib/ansible/plugins/callback/syslog_json.py @@ -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 = - and put the plugin in - - 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' diff --git a/lib/ansible/plugins/callback/timer.py b/lib/ansible/plugins/callback/timer.py index ce444badf7..f2657e0b53 100644 --- a/lib/ansible/plugins/callback/timer.py +++ b/lib/ansible/plugins/callback/timer.py @@ -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 diff --git a/lib/ansible/plugins/callback/tree.py b/lib/ansible/plugins/callback/tree.py index e403d99fd2..a0918357b4 100644 --- a/lib/ansible/plugins/callback/tree.py +++ b/lib/ansible/plugins/callback/tree.py @@ -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 . - +# (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 diff --git a/lib/ansible/plugins/connection/network_cli.py b/lib/ansible/plugins/connection/network_cli.py index 365d045c75..0525809f57 100644 --- a/lib/ansible/plugins/connection/network_cli.py +++ b/lib/ansible/plugins/connection/network_cli.py @@ -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: diff --git a/lib/ansible/plugins/connection/ssh.py b/lib/ansible/plugins/connection/ssh.py index 7f6a236359..7280489168 100644 --- a/lib/ansible/plugins/connection/ssh.py +++ b/lib/ansible/plugins/connection/ssh.py @@ -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 + 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 -# self._play_context.private_key_file -# ANSIBLE_SSH_CONTROL_PATH -# ANSIBLE_SSH_CONTROL_PATH_DIR -# DEFAULT_SFTP_BATCH_MODE -# DEFAULT_SCP_IF_SSH + 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) diff --git a/lib/ansible/plugins/loader.py b/lib/ansible/plugins/loader.py index 1302bfbf5d..61721d086f 100644 --- a/lib/ansible/plugins/loader.py +++ b/lib/ansible/plugins/loader.py @@ -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 '''