Ansible Config part2 (#27448)

* Ansible Config part2

- made dump_me nicer, added note this is not prod
- moved internal key removal function to vars
- carry tracebacks in errors we can now show tracebacks for plugins on vvv
- show inventory plugin tracebacks on vvv
- minor fixes to cg groups plugin
- draft config from plugin docs
- made search path warning 'saner' (top level dirs only)
- correctly display config entries and others
- removed unneeded code
- commented out some conn plugin specific from base.yml
- also deprecated sudo/su
- updated ssh conn docs
- shared get option method for connection plugins
- note about needing eval for defaults
- tailored yaml ext
- updated strategy entry
- for connection pliugins, options load on plugin load
- allow for long types in definitions
- better display in ansible-doc
- cleaned up/updated source docs and base.yml
- added many descriptions
- deprecated include toggles as include is
- draft backwards compat get_config
- fixes to ansible-config, added --only-changed
- some code reoorg
- small license headers
- show default in doc type
- pushed module utils details to 5vs
- work w/o config file
- PEPE ATE!
- moved loader to it's own file
- fixed rhn_register test
- fixed boto requirement in make tests
- I ate Pepe
- fixed dynamic eval of defaults
- better doc code

skip ipaddr filter tests when missing netaddr
removed devnull string from config
better becoem resolution

* killed extra space with extreeme prejudice

cause its an affront against all that is holy that 2 spaces touch each other!

shippable timing out on some images, but merging as it passes most
pull/25089/merge
Brian Coca 7 years ago committed by GitHub
parent 8b617aaef5
commit f921369445

@ -45,7 +45,7 @@ from ansible.module_utils.six import PY3
from ansible.module_utils.six.moves import cPickle from ansible.module_utils.six.moves import cPickle
from ansible.module_utils.connection import send_data, recv_data from ansible.module_utils.connection import send_data, recv_data
from ansible.playbook.play_context import PlayContext from ansible.playbook.play_context import PlayContext
from ansible.plugins import connection_loader from ansible.plugins.loader import connection_loader
from ansible.utils.path import unfrackpath, makedirs_safe from ansible.utils.path import unfrackpath, makedirs_safe
from ansible.errors import AnsibleConnectionFailure from ansible.errors import AnsibleConnectionFailure
from ansible.utils.display import Display from ansible.utils.display import Display

@ -29,7 +29,7 @@ from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.parsing.splitter import parse_kv from ansible.parsing.splitter import parse_kv
from ansible.playbook.play import Play from ansible.playbook.play import Play
from ansible.plugins import get_all_plugin_loaders from ansible.plugins.loader import get_all_plugin_loaders
try: try:
from __main__ import display from __main__ import display
@ -105,6 +105,9 @@ class AdHocCLI(CLI):
(sshpass, becomepass) = self.ask_passwords() (sshpass, becomepass) = self.ask_passwords()
passwords = {'conn_pass': sshpass, 'become_pass': becomepass} passwords = {'conn_pass': sshpass, 'become_pass': becomepass}
# dynamically load any plugins
get_all_plugin_loaders()
loader, inventory, variable_manager = self._play_prereqs(self.options) loader, inventory, variable_manager = self._play_prereqs(self.options)
no_hosts = False no_hosts = False
@ -138,13 +141,6 @@ class AdHocCLI(CLI):
if self.options.module_name in ('include', 'include_role'): if self.options.module_name in ('include', 'include_role'):
raise AnsibleOptionsError("'%s' is not a valid action for ad-hoc commands" % self.options.module_name) raise AnsibleOptionsError("'%s' is not a valid action for ad-hoc commands" % self.options.module_name)
# dynamically load any plugins from the playbook directory
for name, obj in get_all_plugin_loaders():
if obj.subdir:
plugin_path = os.path.join('.', obj.subdir)
if os.path.isdir(plugin_path):
obj.add_directory(plugin_path)
play_ds = self._play_ds(pattern, self.options.seconds, self.options.poll_interval) play_ds = self._play_ds(pattern, self.options.seconds, self.options.poll_interval)
play = Play().load(play_ds, variable_manager=variable_manager, loader=loader) play = Play().load(play_ds, variable_manager=variable_manager, loader=loader)

@ -26,8 +26,7 @@ import sys
import yaml import yaml
from ansible.cli import CLI from ansible.cli import CLI
from ansible.config.data import Setting from ansible.config.manager import ConfigManager, Setting
from ansible.config.manager import ConfigManager
from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.errors import AnsibleError, AnsibleOptionsError
from ansible.module_utils._text import to_native, to_text from ansible.module_utils._text import to_native, to_text
from ansible.parsing.yaml.dumper import AnsibleDumper from ansible.parsing.yaml.dumper import AnsibleDumper
@ -68,6 +67,8 @@ class ConfigCLI(CLI):
if self.action == "list": if self.action == "list":
self.parser.set_usage("usage: %prog list [options] ") self.parser.set_usage("usage: %prog list [options] ")
if self.action == "dump": if self.action == "dump":
self.parser.add_option('--only-changed', dest='only_changed', action='store_true',
help="Only show configurations that have changed from the default")
self.parser.set_usage("usage: %prog dump [options] [-c ansible.cfg]") self.parser.set_usage("usage: %prog dump [options] [-c ansible.cfg]")
elif self.action == "view": elif self.action == "view":
self.parser.set_usage("usage: %prog view [options] [-c ansible.cfg] ") self.parser.set_usage("usage: %prog view [options] [-c ansible.cfg] ")
@ -154,14 +155,15 @@ class ConfigCLI(CLI):
''' '''
list all current configs reading lib/constants.py and shows env and config file setting names list all current configs reading lib/constants.py and shows env and config file setting names
''' '''
self.pager(to_text(yaml.dump(self.config.initial_defs, Dumper=AnsibleDumper), errors='surrogate_or_strict')) self.pager(to_text(yaml.dump(self.config.get_configuration_definitions(), Dumper=AnsibleDumper), errors='surrogate_or_strict'))
def execute_dump(self): def execute_dump(self):
''' '''
Shows the current settings, merges ansible.cfg if specified Shows the current settings, merges ansible.cfg if specified
''' '''
# FIXME: deal with plugins, not just base config
text = [] text = []
defaults = self.config.initial_defs.copy() defaults = self.config.get_configuration_definitions().copy()
for setting in self.config.data.get_settings(): for setting in self.config.data.get_settings():
if setting.name in defaults: if setting.name in defaults:
defaults[setting.name] = setting defaults[setting.name] = setting
@ -176,6 +178,7 @@ class ConfigCLI(CLI):
else: else:
color = 'green' color = 'green'
msg = "%s(%s) = %s" % (setting, 'default', defaults[setting].get('default')) msg = "%s(%s) = %s" % (setting, 'default', defaults[setting].get('default'))
if not self.options.only_changed or color == 'yellow':
text.append(stringc(msg, color)) text.append(stringc(msg, color))
self.pager(to_text('\n'.join(text), errors='surrogate_or_strict')) self.pager(to_text('\n'.join(text), errors='surrogate_or_strict'))

@ -44,7 +44,7 @@ from ansible.module_utils._text import to_native, to_text
from ansible.module_utils.parsing.convert_bool import boolean from ansible.module_utils.parsing.convert_bool import boolean
from ansible.parsing.splitter import parse_kv from ansible.parsing.splitter import parse_kv
from ansible.playbook.play import Play from ansible.playbook.play import Play
from ansible.plugins import module_loader from ansible.plugins.loader import module_loader
from ansible.utils import plugin_docs from ansible.utils import plugin_docs
from ansible.utils.color import stringc from ansible.utils.color import stringc

@ -30,7 +30,7 @@ from ansible.cli import CLI
from ansible.errors import AnsibleError, AnsibleOptionsError from ansible.errors import AnsibleError, AnsibleOptionsError
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.parsing.yaml.dumper import AnsibleDumper from ansible.parsing.yaml.dumper import AnsibleDumper
from ansible.plugins import module_loader, action_loader, lookup_loader, callback_loader, cache_loader, connection_loader, strategy_loader, PluginLoader from ansible.plugins.loader import module_loader, action_loader, lookup_loader, callback_loader, cache_loader, connection_loader, strategy_loader, PluginLoader
from ansible.utils import plugin_docs from ansible.utils import plugin_docs
try: try:
from __main__ import display from __main__ import display
@ -66,7 +66,8 @@ class DocCLI(CLI):
self.parser.add_option("-a", "--all", action="store_true", default=False, dest='all_plugins', self.parser.add_option("-a", "--all", action="store_true", default=False, dest='all_plugins',
help='Show documentation for all plugins') help='Show documentation for all plugins')
self.parser.add_option("-t", "--type", action="store", default='module', dest='type', type='choice', self.parser.add_option("-t", "--type", action="store", default='module', dest='type', type='choice',
help='Choose which plugin type', choices=['cache', 'callback', 'connection', 'inventory', 'lookup', 'module', 'strategy']) help='Choose which plugin type (defaults to "module")',
choices=['cache', 'callback', 'connection', 'inventory', 'lookup', 'module', 'strategy'])
super(DocCLI, self).parse() super(DocCLI, self).parse()
@ -99,6 +100,10 @@ class DocCLI(CLI):
for i in self.options.module_path.split(os.pathsep): for i in self.options.module_path.split(os.pathsep):
loader.add_directory(i) loader.add_directory(i)
# save only top level paths for errors
search_paths = DocCLI.print_paths(loader)
loader._paths = None # reset so we can use subdirs below
# list plugins for type # list plugins for type
if self.options.list_dir: if self.options.list_dir:
paths = loader._get_paths() paths = loader._get_paths()
@ -125,7 +130,7 @@ class DocCLI(CLI):
# if the plugin lives in a non-python file (eg, win_X.ps1), require the corresponding python file for docs # if the plugin lives in a non-python file (eg, win_X.ps1), require the corresponding python file for docs
filename = loader.find_plugin(plugin, mod_type='.py', ignore_deprecated=True) filename = loader.find_plugin(plugin, mod_type='.py', ignore_deprecated=True)
if filename is None: if filename is None:
display.warning("%s %s not found in %s\n" % (plugin_type, plugin, DocCLI.print_paths(loader))) display.warning("%s %s not found in:\n%s\n" % (plugin_type, plugin, search_paths))
continue continue
if any(filename.endswith(x) for x in C.BLACKLIST_EXTS): if any(filename.endswith(x) for x in C.BLACKLIST_EXTS):
@ -255,7 +260,7 @@ class DocCLI(CLI):
# Uses a list to get the order right # Uses a list to get the order right
ret = [] ret = []
for i in finder._get_paths(): for i in finder._get_paths(subdirs=False):
if i not in ret: if i not in ret:
ret.append(i) ret.append(i)
return os.pathsep.join(ret) return os.pathsep.join(ret)
@ -288,6 +293,9 @@ class DocCLI(CLI):
return "\n".join(text) return "\n".join(text)
def _dump_yaml(self, struct, indent):
return CLI.tty_ify('\n'.join([indent + line for line in yaml.dump(struct, default_flow_style=False, Dumper=AnsibleDumper).split('\n')]))
def add_fields(self, text, fields, limit, opt_indent): def add_fields(self, text, fields, limit, opt_indent):
for o in sorted(fields): for o in sorted(fields):
@ -322,123 +330,109 @@ class DocCLI(CLI):
del opt['choices'] del opt['choices']
default = '' default = ''
if 'default' in opt or not required: if 'default' in opt or not required:
default = "[Default: " + str(opt.pop('default', '(null)')) + "]" default = "[Default: %s" % str(opt.pop('default', '(null)')) + "]"
text.append(textwrap.fill(CLI.tty_ify(aliases + choices + default), limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) text.append(textwrap.fill(CLI.tty_ify(aliases + choices + default), limit, initial_indent=opt_indent, subsequent_indent=opt_indent))
if 'options' in opt: if 'options' in opt:
text.append(opt_indent + "options:\n") text.append("%soptions:\n" % opt_indent)
self.add_fields(text, opt['options'], limit, opt_indent + opt_indent) self.add_fields(text, opt.pop('options'), limit, opt_indent + opt_indent)
text.append('')
del opt['options']
if 'spec' in opt: if 'spec' in opt:
text.append(opt_indent + "spec:\n") text.append("%sspec:\n" % opt_indent)
self.add_fields(text, opt['spec'], limit, opt_indent + opt_indent) self.add_fields(text, opt.pop('spec'), limit, opt_indent + opt_indent)
text.append('')
del opt['spec'] conf = {}
for config in ('env', 'ini', 'yaml', 'vars'):
for conf in ('config', 'env_vars', 'host_vars'): if config in opt and opt[config]:
if conf in opt: conf[config] = opt.pop(config)
text.append(textwrap.fill(CLI.tty_ify("%s: " % conf), limit, initial_indent=opt_indent, subsequent_indent=opt_indent))
for entry in opt[conf]: if conf:
if isinstance(entry, dict): text.append(self._dump_yaml({'set_via': conf}, opt_indent))
pre = " -"
for key in entry:
text.append(textwrap.fill(CLI.tty_ify("%s %s: %s" % (pre, key, entry[key])),
limit, initial_indent=opt_indent, subsequent_indent=opt_indent))
pre = " "
else:
text.append(textwrap.fill(CLI.tty_ify(" - %s" % entry), limit, initial_indent=opt_indent, subsequent_indent=opt_indent))
del opt[conf]
# unspecified keys for k in sorted(opt):
for k in opt:
if k.startswith('_'): if k.startswith('_'):
continue continue
if isinstance(opt[k], string_types): if isinstance(opt[k], string_types):
text.append(textwrap.fill(CLI.tty_ify("%s: %s" % (k, opt[k])), limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) text.append('%s%s: %s' % (opt_indent, k, textwrap.fill(CLI.tty_ify(opt[k]), limit - (len(k) + 2), subsequent_indent=opt_indent)))
elif isinstance(opt[k], (list, dict)): elif isinstance(opt[k], (list, tuple)):
text.append(textwrap.fill(CLI.tty_ify("%s: %s" % (k, yaml.dump(opt[k], Dumper=AnsibleDumper, default_flow_style=False))), text.append(CLI.tty_ify('%s%s: %s' % (opt_indent, k, ', '.join(opt[k]))))
limit, initial_indent=opt_indent, subsequent_indent=opt_indent))
else: else:
display.vv("Skipping %s key cuase we don't know how to handle eet" % k) text.append(self._dump_yaml({k: opt[k]}, opt_indent))
text.append('')
def get_man_text(self, doc): def get_man_text(self, doc):
IGNORE = frozenset(['module', 'docuri', 'version_added', 'short_description', 'now_date'])
opt_indent = " " opt_indent = " "
text = [] text = []
text.append("> %s (%s)\n" % (doc[self.options.type].upper(), doc['filename']))
text.append("> %s (%s)\n" % (doc[self.options.type].upper(), doc.pop('filename')))
pad = display.columns * 0.20 pad = display.columns * 0.20
limit = max(display.columns - int(pad), 70) limit = max(display.columns - int(pad), 70)
if isinstance(doc['description'], list): if isinstance(doc['description'], list):
desc = " ".join(doc['description']) desc = " ".join(doc.pop('description'))
else: else:
desc = doc['description'] desc = doc.pop('description')
text.append("%s\n" % textwrap.fill(CLI.tty_ify(desc), limit, initial_indent=" ", subsequent_indent=" ")) text.append("%s\n" % textwrap.fill(CLI.tty_ify(desc), limit, initial_indent=opt_indent, subsequent_indent=opt_indent))
if 'deprecated' in doc and doc['deprecated'] is not None and len(doc['deprecated']) > 0: if 'deprecated' in doc and doc['deprecated'] is not None and len(doc['deprecated']) > 0:
text.append("DEPRECATED: \n%s\n" % doc['deprecated']) text.append("DEPRECATED: \n%s\n" % doc.pop('deprecated'))
if 'action' in doc and doc['action']: if doc.pop('action', False):
text.append(" * note: %s\n" % "This module has a corresponding action plugin.") text.append(" * note: %s\n" % "This module has a corresponding action plugin.")
if 'options' in doc and doc['options']: if 'options' in doc and doc['options']:
text.append("Options (= is mandatory):\n") text.append("OPTIONS (= is mandatory):\n")
self.add_fields(text, doc['options'], limit, opt_indent) self.add_fields(text, doc.pop('options'), limit, opt_indent)
text.append('') text.append('')
if 'notes' in doc and doc['notes'] and len(doc['notes']) > 0: if 'notes' in doc and doc['notes'] and len(doc['notes']) > 0:
text.append("Notes:") text.append("NOTES:")
for note in doc['notes']: for note in doc['notes']:
text.append(textwrap.fill(CLI.tty_ify(note), limit - 6, initial_indent=" * ", subsequent_indent=opt_indent)) text.append(textwrap.fill(CLI.tty_ify(note), limit - 6, initial_indent=opt_indent[:-2] + "* ", subsequent_indent=opt_indent))
text.append('')
del doc['notes']
if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0: if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0:
req = ", ".join(doc['requirements']) req = ", ".join(doc.pop('requirements'))
text.append("Requirements:%s\n" % textwrap.fill(CLI.tty_ify(req), limit - 16, initial_indent=" ", subsequent_indent=opt_indent)) text.append("REQUIREMENTS:%s\n" % textwrap.fill(CLI.tty_ify(req), limit - 16, initial_indent=" ", subsequent_indent=opt_indent))
if 'examples' in doc and len(doc['examples']) > 0:
text.append("Example%s:\n" % ('' if len(doc['examples']) < 2 else 's'))
for ex in doc['examples']:
text.append("%s\n" % (ex['code']))
if 'plainexamples' in doc and doc['plainexamples'] is not None: if 'plainexamples' in doc and doc['plainexamples'] is not None:
text.append("EXAMPLES:\n") text.append("EXAMPLES:")
if isinstance(doc['plainexamples'], string_types): if isinstance(doc['plainexamples'], string_types):
text.append(doc['plainexamples']) text.append(doc.pop('plainexamples').strip())
else: else:
text.append(yaml.dump(doc['plainexamples'], indent=2, default_flow_style=False)) text.append(yaml.dump(doc.pop('plainexamples'), indent=2, default_flow_style=False))
text.append('')
if 'returndocs' in doc and doc['returndocs'] is not None: if 'returndocs' in doc and doc['returndocs'] is not None:
text.append("RETURN VALUES:\n") text.append("RETURN VALUES:\n")
if isinstance(doc['returndocs'], string_types): if isinstance(doc['returndocs'], string_types):
text.append(doc['returndocs']) text.append(doc.pop('returndocs'))
else: else:
text.append(yaml.dump(doc['returndocs'], indent=2, default_flow_style=False)) text.append(yaml.dump(doc.pop('returndocs'), indent=2, default_flow_style=False))
text.append('') text.append('')
maintainers = set() # Control rest of keys on verbosity (3 == full, 0 only adds small list)
if 'author' in doc: rest = []
if isinstance(doc['author'], string_types): if self.options.verbosity >= 3:
maintainers.add(doc['author']) rest = doc
else: elif 'author' in doc:
maintainers.update(doc['author']) rest = ['author']
if 'maintainers' in doc: # Generic handler
if isinstance(doc['maintainers'], string_types): for k in sorted(rest):
maintainers.add(doc['author']) if k in IGNORE or not doc[k]:
continue
if isinstance(doc[k], string_types):
text.append('%s: %s' % (k.upper(), textwrap.fill(CLI.tty_ify(doc[k]), limit - (len(k) + 2), subsequent_indent=opt_indent)))
elif isinstance(doc[k], (list, tuple)):
text.append('%s: %s' % (k.upper(), ', '.join(doc[k])))
else: else:
maintainers.update(doc['author']) text.append(self._dump_yaml({k.upper(): doc[k]}, opt_indent))
text.append('MAINTAINERS: ' + ', '.join(maintainers))
text.append('') text.append('')
if 'metadata' in doc and doc['metadata']:
text.append("METADATA:")
for k in doc['metadata']:
if isinstance(k, list):
text.append("\t%s: %s" % (k.capitalize(), ", ".join(doc['metadata'][k])))
else:
text.append("\t%s: %s" % (k.capitalize(), doc['metadata'][k]))
text.append('')
return "\n".join(text) return "\n".join(text)

@ -31,7 +31,7 @@ import time
from ansible.cli import CLI from ansible.cli import CLI
from ansible.errors import AnsibleOptionsError from ansible.errors import AnsibleOptionsError
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.plugins import module_loader from ansible.plugins.loader import module_loader
from ansible.utils.cmd_functions import run_cmd from ansible.utils.cmd_functions import run_cmd
try: try:

@ -1,27 +1,10 @@
# (c) 2017, Ansible by Red Hat, inc # Copyright (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 <http://www.gnu.org/licenses/>.
# Make coding more python3-ish # Make coding more python3-ish
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from collections import namedtuple
Setting = namedtuple('Setting','name value origin')
class ConfigData(object): class ConfigData(object):
@ -59,3 +42,4 @@ class ConfigData(object):
if plugin.name not in self._plugins[plugin.type]: if plugin.name not in self._plugins[plugin.type]:
self._plugins[plugin.type][plugin.name] = {} self._plugins[plugin.type][plugin.name] = {}
self._plugins[plugin.type][plugin.name][setting.name] = setting self._plugins[plugin.type][plugin.name][setting.name] = setting

@ -1,19 +1,5 @@
# (c) 2017, Ansible by Red Hat, inc # Copyright (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 <http://www.gnu.org/licenses/>.
# Make coding more python3-ish # Make coding more python3-ish
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
@ -24,7 +10,9 @@ import sys
import tempfile import tempfile
import yaml import yaml
from ansible.config.data import ConfigData, Setting from collections import namedtuple
from ansible.config.data import ConfigData
from ansible.errors import AnsibleOptionsError, AnsibleError from ansible.errors import AnsibleOptionsError, AnsibleError
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.module_utils.six.moves import configparser from ansible.module_utils.six.moves import configparser
@ -34,131 +22,169 @@ from ansible.parsing.quoting import unquote
from ansible.utils.path import unfrackpath from ansible.utils.path import unfrackpath
from ansible.utils.path import makedirs_safe from ansible.utils.path import makedirs_safe
Plugin = namedtuple('Plugin','name type')
Setting = namedtuple('Setting','name value origin')
def resolve_path(path): # FIXME: see if we can unify in module_utils with similar function used by argspec
def ensure_type(value, value_type):
''' return a configuration variable with casting
:arg value: The value to ensure correct typing of
:kwarg value_type: The type of the value. This can be any of the following strings:
:boolean: sets the value to a True or False value
:integer: Sets the value to an integer or raises a ValueType error
:float: Sets the value to a float or raises a ValueType error
:list: Treats the value as a comma separated list. Split the value
and return it as a python list.
:none: Sets the value to None
:path: Expands any environment variables and tilde's in the value.
:tmp_path: Create a unique temporary directory inside of the directory
specified by value and return its path.
:pathlist: Treat the value as a typical PATH string. (On POSIX, this
means colon separated strings.) Split the value and then expand
each part for environment variables and tildes.
'''
if value_type:
value_type = value_type.lower()
if value_type in ('boolean', 'bool'):
value = boolean(value, strict=False)
elif value:
if value_type in ('integer', 'int'):
value = int(value)
elif value_type == 'float':
value = float(value)
elif value_type == 'list':
if isinstance(value, string_types):
value = [x.strip() for x in value.split(',')]
elif value_type == 'none':
if value == "None":
value = None
elif value_type == 'path':
value = resolve_path(value)
elif value_type in ('tmp', 'temppath', 'tmppath'):
value = resolve_path(value)
if not os.path.exists(value):
makedirs_safe(value, 0o700)
prefix = 'ansible-local-%s' % os.getpid()
value = tempfile.mkdtemp(prefix=prefix, dir=value)
elif value_type == 'pathlist':
if isinstance(value, string_types):
value = [resolve_path(x) for x in value.split(os.pathsep)]
# defaults to string types
elif isinstance(value, string_types):
value = unquote(value)
return to_text(value, errors='surrogate_or_strict', nonstring='passthru')
# FIXME: see if this can live in utils/path
def resolve_path(path):
''' resolve relative or 'varaible' paths '''
if '{{CWD}}' in path: # allow users to force CWD using 'magic' {{CWD}} if '{{CWD}}' in path: # allow users to force CWD using 'magic' {{CWD}}
path = path.replace('{{CWD}}', os.getcwd()) path = path.replace('{{CWD}}', os.getcwd())
return unfrackpath(path, follow=False) return unfrackpath(path, follow=False)
# FIXME: generic file type?
def get_config_type(cfile):
def get_ini_config(p, entries): ftype = None
if cfile is not None:
ext = os.path.splitext(cfile)[-1]
if ext in ('.ini', '.cfg'):
ftype = 'ini'
elif ext in ('.yaml', '.yml'):
ftype = 'yaml'
else:
raise AnsibleOptionsError("Unsupported configuration file extension for %s: %s" % (cfile, to_native(ext)))
return ftype
# FIXME: can move to module_utils for use for ini plugins also?
def get_ini_config_value(p, entry):
''' returns the value of last ini entry found ''' ''' returns the value of last ini entry found '''
value = None value = None
if p is not None: if p is not None:
for entry in entries:
try: try:
value = p.get(entry.get('section','defaults'), entry.get('key',''), raw=True) value = p.get(entry.get('section','defaults'), entry.get('key',''), raw=True)
except: except: # FIXME: actually report issues here
pass pass
return value return value
class ConfigManager(object): class ConfigManager(object):
UNABLE = []
DEPRECATED = []
def __init__(self, conf_file=None): def __init__(self, conf_file=None):
self._base_defs = {}
self._plugins = {}
self._parser = None
self._config_file = conf_file
self.data = ConfigData() self.data = ConfigData()
#FIXME: make dynamic?
bconfig_def = to_bytes('%s/data/config.yml' % os.path.dirname(__file__)) #FIXME: make dynamic? scan for more? make it's own method?
# Create configuration definitions from source
bconfig_def = to_bytes('%s/base.yml' % os.path.dirname(__file__))
if os.path.exists(bconfig_def): if os.path.exists(bconfig_def):
with open(bconfig_def, 'rb') as config_def: with open(bconfig_def, 'rb') as config_def:
self.initial_defs = yaml.safe_load(config_def) self._base_defs = yaml.safe_load(config_def)
else: else:
raise AnsibleError("Missing base configuration definition file (bad install?): %s" % to_native(bconfig_def)) raise AnsibleError("Missing base configuration definition file (bad install?): %s" % to_native(bconfig_def))
ftype = None if self._config_file is None:
if conf_file is None:
# set config using ini # set config using ini
conf_file = self.find_ini_config_file() self._config_file = self._find_ini_config_file()
ftype = 'ini'
else: if self._config_file:
ext = os.path.splitext(conf_file)[-1] if os.path.exists(self._config_file):
if ext in ('.ini', '.cfg'): # initialize parser and read config
ftype = 'ini' self._parse_config_file()
elif ext in ('.yaml', '.yml'):
ftype = 'yaml'
else:
raise AnsibleOptionsError("Unsupported configuration file extension: \n{0}".format(ext))
self.parse_config(conf_file, ftype) # update constants
self.update_config_data()
def parse_config(self, cfile, ftype): def _parse_config_file(self, cfile=None):
''' return flat configuration settings from file(s) '''
# TODO: take list of files with merge/nomerge # TODO: take list of files with merge/nomerge
parser = None if cfile is None:
if cfile: cfile = self._config_file
ftype = get_config_type(cfile)
if cfile is not None:
if ftype == 'ini': if ftype == 'ini':
parser = configparser.ConfigParser() self._parser = configparser.ConfigParser()
try: try:
parser.read(cfile) self._parser.read(cfile)
except configparser.Error as e: except configparser.Error as e:
raise AnsibleOptionsError("Error reading config file: \n{0}".format(e)) raise AnsibleOptionsError("Error reading config file (%s): %s" % (cfile, to_native(e)))
elif ftype == 'yaml': # FIXME: this should eventually handle yaml config files
with open(cfile, 'rb') as config_stream: #elif ftype == 'yaml':
parser = yaml.safe_load(config_stream) # with open(cfile, 'rb') as config_stream:
# self._parser = yaml.safe_load(config_stream)
else: else:
raise AnsibleOptionsError("Unsupported configuration file type: \n{0}".format(ftype)) raise AnsibleOptionsError("Unsupported configuration file type: %s" % to_native(ftype))
self.update_config(cfile, self.initial_defs, parser, ftype)
def update_config(self, configfile, defs, parser, ftype):
# update the constant for config file
self.data.update_setting(Setting('CONFIG_FILE', configfile, ''))
origin = None
# env and config defs can have several entries, ordered in list from lowest to highest precedence
for config in self.initial_defs:
value = None
# env vars are highest precedence
if defs[config].get('env'):
try:
for env_var in defs[config]['env']:
env_value = os.environ.get(env_var.get('name'), None)
if env_value is not None: # only set if env var is defined
value = env_value
origin = 'env: %s' % env_var.get('name')
except:
sys.stderr.write("Error while loading environment configs for %s\n" % config)
# try config file entries next
if value is None and defs[config].get(ftype):
if ftype == 'ini':
# load from ini config
try:
value = get_ini_config(parser, defs[config]['ini'])
origin = configfile
except Exception as e:
sys.stderr.write("Error while loading ini config %s: %s" % (configfile, str(e)))
elif ftype == 'yaml':
# FIXME: break down key from defs (. notation???)
key = 'name'
value = parser.get(key)
origin = configfile
# set default if we got here w/o a value
if value is None:
value = defs[config].get('default')
origin = 'default'
# ensure correct type
try:
value = self.ensure_type(value, defs[config].get('value_type'))
except:
sys.stderr.write("Unable to set correct type for %s, skipping" % config)
continue
# set the constant
self.data.update_setting(Setting(config, value, origin))
def _find_yaml_config_files(self):
''' Load YAML Config Files in order, check merge flags, keep origin of settings'''
pass
def find_ini_config_file(self): def _find_ini_config_file(self):
''' Load Config File order(first found is used): ENV, CWD, HOME, /etc/ansible ''' ''' Load INI Config File order(first found is used): ENV, CWD, HOME, /etc/ansible '''
# FIXME: eventually deprecate ini configs
path0 = os.getenv("ANSIBLE_CONFIG", None) path0 = os.getenv("ANSIBLE_CONFIG", None)
if path0 is not None: if path0 is not None:
@ -180,57 +206,163 @@ class ConfigManager(object):
return path return path
def ensure_type(self, value, value_type): def get_configuration_definitions(self, plugin_type=None, name=None):
''' return a configuration variable with casting ''' just list the possible settings, either base or for specific plugins or plugin '''
:arg value: The value to ensure correct typing of
:kwarg value_type: The type of the value. This can be any of the following strings:
:boolean: sets the value to a True or False value
:integer: Sets the value to an integer or raises a ValueType error
:float: Sets the value to a float or raises a ValueType error
:list: Treats the value as a comma separated list. Split the value
and return it as a python list.
:none: Sets the value to None
:path: Expands any environment variables and tilde's in the value.
:tmp_path: Create a unique temporary directory inside of the directory
specified by value and return its path.
:pathlist: Treat the value as a typical PATH string. (On POSIX, this
means colon separated strings.) Split the value and then expand
each part for environment variables and tildes.
'''
if value_type == 'boolean':
value = boolean(value, strict=False)
elif value: ret = {}
if value_type == 'integer': if plugin_type is None:
value = int(value) ret = self._base_defs
elif name is None:
ret = self._plugins.get(plugin_type, {})
else:
ret = {name: self._plugins.get(plugin_type, {}).get(name, {})}
elif value_type == 'float': return ret
value = float(value)
elif value_type == 'list': def _loop_entries(self, container, entry_list):
if isinstance(value, string_types): ''' repeat code for value entry assignment '''
value = [x.strip() for x in value.split(',')]
elif value_type == 'none':
if value == "None":
value = None value = None
origin = None
for entry in entry_list:
name = entry.get('name')
temp_value = container.get(name, None)
if temp_value is not None: # only set if env var is defined
value = temp_value
origin = name
elif value_type == 'path': # deal with deprecation of setting source, if used
value = resolve_path(value) #FIXME: if entry.get('deprecated'):
elif value_type == 'tmppath': return value, origin
value = resolve_path(value)
if not os.path.exists(value):
makedirs_safe(value, 0o700)
prefix = 'ansible-local-%s' % os.getpid()
value = tempfile.mkdtemp(prefix=prefix, dir=value)
elif value_type == 'pathlist': def get_config_value(self, config, cfile=None, plugin_type=None, plugin_name=None, variables=None):
if isinstance(value, string_types): ''' wrapper '''
value = [resolve_path(x) for x in value.split(os.pathsep)] value, _drop = self.get_config_value_and_origin(config, cfile=cfile, plugin_type=plugin_type, plugin_name=plugin_name, variables=variables)
return value
elif isinstance(value, string_types): def get_config_value_and_origin(self, config, cfile=None, plugin_type=None, plugin_name=None, variables=None):
value = unquote(value) ''' Given a config key figure out the actual value and report on the origin of the settings '''
return to_text(value, errors='surrogate_or_strict', nonstring='passthru') if cfile is None:
cfile = self._config_file
# Note: sources that are lists listed in low to high precedence (last one wins)
value = None
defs = {}
if plugin_type is None:
defs = self._base_defs
elif plugin_name is None:
defs = self._plugins[plugin_type]
else:
defs = self._plugins[plugin_type][plugin_name]
# Use 'variable overrides' if present, highest precedence, but only present when querying running play
if variables:
value, origin = self._loop_entries(variables, defs[config]['vars'])
origin = 'var: %s' % origin
# env vars are next precedence
if value is None and defs[config].get('env'):
value, origin = self._loop_entries(os.environ, defs[config]['env'])
origin = 'env: %s' % origin
# try config file entries next, if we have one
if value is None and cfile is not None:
ftype = get_config_type(cfile)
if ftype and defs[config].get(ftype):
if ftype == 'ini':
# load from ini config
try: # FIXME: generaelize _loop_entries to allow for files also
for ini_entry in defs[config]['ini']:
value = get_ini_config_value(self._parser, ini_entry)
origin = cfile
#FIXME: if ini_entry.get('deprecated'):
except Exception as e:
sys.stderr.write("Error while loading ini config %s: %s" % (cfile, to_native(e)))
elif ftype == 'yaml':
pass # FIXME: implement, also , break down key from defs (. notation???)
origin = cfile
'''
# for plugins, try using existing constants, this is for backwards compatiblity
if plugin_name and defs[config].get('constants'):
value, origin = self._loop_entries(self.data, defs[config]['constants'])
origin = 'constant: %s' % origin
'''
# set default if we got here w/o a value
if value is None:
value = defs[config].get('default')
origin = 'default'
# FIXME: moved eval to constants as this does not have access to previous vars
if plugin_type is None and isinstance(value, string_types) and (value.startswith('eval(') and value.endswith(')')):
return value, origin
#default_value = defs[config].get('default')
#if plugin_type is None and isinstance(default_value, string_types) and (default_value.startswith('eval(') and default_value.endswith(')')):
# try:
# eval_string = default_value.replace('eval(', '', 1)[:-1]
# value = eval(eval_string) # FIXME: safe eval?
# except:
# value = default_value
#else:
# value = default_value
# ensure correct type
try:
value = ensure_type(value, defs[config].get('type'))
except Exception as e:
self.UNABLE.append(config)
# deal with deprecation of the setting
if defs[config].get('deprecated') and origin != 'default':
self.DEPRECATED.append((config, defs[config].get('deprecated')))
return value, origin
def update_plugin_config(self, plugin_type, name, defs):
''' really: update constants '''
# no sense?
self.initialize_plugin_configuration_definitions(plugin_type, name, defs)
self.update_config_data(defs)
def initialize_plugin_configuration_definitions(self, plugin_type, name, defs):
if plugin_type not in self._plugins:
self._plugins[plugin_type] = {}
self._plugins[plugin_type][name] = defs
def update_config_data(self, defs=None, configfile=None):
''' really: update constants '''
if defs is None:
defs = self._base_defs
if configfile is None:
configfile = self._config_file
if not isinstance(defs, dict):
raise AnsibleOptionsError("Invalid configuration definition type: %s for %s" % (type(defs), defs))
# update the constant for config file
self.data.update_setting(Setting('CONFIG_FILE', configfile, ''))
origin = None
# env and config defs can have several entries, ordered in list from lowest to highest precedence
for config in defs:
if not isinstance(defs[config], dict):
raise AnsibleOptionsError("Invalid configuration definition '%s': type is %s" % (to_native(config), type(defs[config])))
# get value and origin
value, origin = self.get_config_value_and_origin(config, configfile)
# set the constant
self.data.update_setting(Setting(config, value, origin))
# FIXME: find better way to do this by passing back to where display is available
if self.UNABLE:
sys.stderr.write("Unable to set correct type for:\n\t%s\n" % '\n\t'.join(self.UNABLE))
if self.DEPRECATED:
for k, reason in self.DEPRECATED:
sys.stderr.write("[DEPRECATED] %s: %(why)s. It will be removed in %(version)s. As alternative %(alternative)s", (k, reason))

@ -1,46 +1,58 @@
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> # Copyright (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
# # Copyright (c) 2017 Ansible Project
# This file is part of Ansible # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# 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/>.
# Make coding more python3-ish # Make coding more python3-ish
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import os # used to set lang
from string import ascii_letters, digits from string import ascii_letters, digits
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.module_utils.parsing.convert_bool import boolean, BOOLEANS_TRUE from ansible.module_utils.parsing.convert_bool import boolean, BOOLEANS_TRUE
from ansible.module_utils.six import string_types
from ansible.config.manager import ConfigManager from ansible.config.manager import ConfigManager
_config = ConfigManager() def _deprecated(msg):
''' display is not guaranteed here, nor it being the full class, but try anyways, fallback to sys.stderr.write '''
# Generate constants from config try:
for setting in _config.data.get_settings(): from __main__ import display
vars()[setting.name] = setting.value display.deprecated(msg, version='2.8')
except:
import sys
sys.stderr.write('[DEPRECATED] %s, to be removed in 2.8' % msg)
def mk_boolean(value): def mk_boolean(value):
''' moved to module_utils''' ''' moved to module_utils'''
# We don't have a display here so we can't call deprecated _deprecated('ansible.constants.mk_boolean() is deprecated. Use ansible.module_utils.parsing.convert_bool.boolean() instead')
# display.deprecated('ansible.constants.mk_boolean() is deprecated. Use ansible.module_utils.parsing.convert_bool.boolean() instead', version='2.8')
return boolean(value, strict=False) return boolean(value, strict=False)
def get_config(parser, section, key, env_var, default_value, value_type=None, expand_relative_paths=False):
''' kept for backwarsd compatibility, but deprecated '''
_deprecated('ansible.constants.get_config() is deprecated. There is new config API, see porting docs.')
import os
# ### CONSTANTS ### yes, actual ones value = None
# small reconstruction of the old code env/ini/default
value = os.environ.get(env_var, None)
if value is None:
try:
value = config.get_ini_config(parser, [{'key': key, 'section': section}])
except:
pass
if value is None:
value = default_value
try:
value = config.ensure_type(value, value_type)
except:
pass
return value
### CONSTANTS ### yes, actual ones
BLACKLIST_EXTS = ('.pyc', '.pyo', '.swp', '.bak', '~', '.rpm', '.md', '.txt') BLACKLIST_EXTS = ('.pyc', '.pyo', '.swp', '.bak', '~', '.rpm', '.md', '.txt')
BECOME_METHODS = ['sudo', 'su', 'pbrun', 'pfexec', 'doas', 'dzdo', 'ksu', 'runas', 'pmrun'] BECOME_METHODS = ['sudo', 'su', 'pbrun', 'pfexec', 'doas', 'dzdo', 'ksu', 'runas', 'pmrun']
BECOME_ERROR_STRINGS = { BECOME_ERROR_STRINGS = {
@ -79,3 +91,22 @@ RESTRICTED_RESULT_KEYS = ['ansible_rsync_path', 'ansible_playbook_python']
TREE_DIR = None TREE_DIR = None
VAULT_VERSION_MIN = 1.0 VAULT_VERSION_MIN = 1.0
VAULT_VERSION_MAX = 1.0 VAULT_VERSION_MAX = 1.0
### POPULATE SETTINGS FROM CONFIG ###
config = ConfigManager()
# Generate constants from config
for setting in config.data.get_settings():
# FIXME: find better way to do in manager class and/or ensure types
if isinstance(setting.value, string_types) and (setting.value.startswith('eval(') and setting.value.endswith(')')):
try:
eval_string = setting.value.replace('eval(', '', 1)[:-1]
vars()[setting.name] = eval(eval_string) # FIXME: safe eval?
continue
except:
pass
vars()[setting.name] = setting.value

@ -20,6 +20,8 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from collections import Sequence from collections import Sequence
import traceback
import sys
from ansible.errors.yaml_strings import ( from ansible.errors.yaml_strings import (
YAML_COMMON_DICT_ERROR, YAML_COMMON_DICT_ERROR,
@ -68,6 +70,8 @@ class AnsibleError(Exception):
self.message += '\nexception type: %s' % to_native(type(orig_exc)) self.message += '\nexception type: %s' % to_native(type(orig_exc))
self.message += '\nexception: %s' % to_native(orig_exc) self.message += '\nexception: %s' % to_native(orig_exc)
self.tb = ''.join(traceback.format_tb(sys.exc_info()[2]))
def __str__(self): def __str__(self):
return self.message return self.message

@ -36,7 +36,7 @@ from ansible.release import __version__, __author__
from ansible import constants as C from ansible import constants as C
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils._text import to_bytes, to_text
from ansible.plugins import module_utils_loader, ps_module_utils_loader from ansible.plugins.loader import module_utils_loader, ps_module_utils_loader
from ansible.plugins.shell.powershell import async_watchdog, async_wrapper, become_wrapper, leaf_exec, exec_wrapper from ansible.plugins.shell.powershell import async_watchdog, async_wrapper, become_wrapper, leaf_exec, exec_wrapper
# Must import strategy and use write_locks from there # Must import strategy and use write_locks from there
# If we import write_locks directly then we end up binding a # If we import write_locks directly then we end up binding a
@ -579,7 +579,7 @@ def recursive_finder(name, data, py_module_names, py_module_cache, zf):
zf.writestr(os.path.join("ansible/module_utils", zf.writestr(os.path.join("ansible/module_utils",
py_module_file_name), py_module_cache[py_module_name][0]) py_module_file_name), py_module_cache[py_module_name][0])
display.vvv("Using module_utils file %s" % py_module_cache[py_module_name][1]) display.vvvvv("Using module_utils file %s" % py_module_cache[py_module_name][1])
# Add the names of the files we're scheduling to examine in the loop to # Add the names of the files we're scheduling to examine in the loop to
# py_module_names so that we don't re-examine them in the next pass # py_module_names so that we don't re-examine them in the next pass

@ -731,6 +731,7 @@ class TaskExecutor:
conn_type = self._play_context.connection conn_type = self._play_context.connection
connection = self._shared_loader_obj.connection_loader.get(conn_type, self._play_context, self._new_stdin) connection = self._shared_loader_obj.connection_loader.get(conn_type, self._play_context, self._new_stdin)
self._play_context.set_options_from_plugin(connection)
if not connection: if not connection:
raise AnsibleError("the connection plugin '%s' was not found" % conn_type) raise AnsibleError("the connection plugin '%s' was not found" % conn_type)

@ -31,7 +31,7 @@ from ansible.module_utils.six import string_types
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.playbook.block import Block from ansible.playbook.block import Block
from ansible.playbook.play_context import PlayContext from ansible.playbook.play_context import PlayContext
from ansible.plugins import callback_loader, strategy_loader, module_loader from ansible.plugins.loader import callback_loader, strategy_loader, module_loader
from ansible.plugins.callback import CallbackBase from ansible.plugins.callback import CallbackBase
from ansible.template import Templar from ansible.template import Templar
from ansible.utils.helpers import pct_to_int from ansible.utils.helpers import pct_to_int

@ -30,7 +30,7 @@ from ansible.inventory.data import InventoryData
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils._text import to_bytes, to_text
from ansible.parsing.utils.addresses import parse_address from ansible.parsing.utils.addresses import parse_address
from ansible.plugins import PluginLoader from ansible.plugins.loader import PluginLoader
from ansible.utils.path import unfrackpath from ansible.utils.path import unfrackpath
try: try:
@ -260,14 +260,15 @@ class InventoryManager(object):
display.vvv(u'Parsed %s inventory source with %s plugin' % (to_text(source), plugin_name)) display.vvv(u'Parsed %s inventory source with %s plugin' % (to_text(source), plugin_name))
break break
except AnsibleParserError as e: except AnsibleParserError as e:
failures.append(u'\n* Failed to parse %s with %s inventory plugin: %s\n' % (to_text(source), plugin_name, to_text(e))) failures.append({'src': source, 'plugin': plugin_name, 'exc': e})
else: else:
display.debug(u'%s did not meet %s requirements' % (to_text(source), plugin_name)) display.debug(u'%s did not meet %s requirements' % (to_text(source), plugin_name))
else: else:
if failures: if failures:
# only if no plugin processed files should we show errors. # only if no plugin processed files should we show errors.
for fail in failures: for fail in failures:
display.warning(fail) display.warning(u'\n* Failed to parse %s with %s inventory plugin: %s' % (to_text(fail['src']), fail['plugin'], to_text(fail['exc'])))
display.vvv(fail['exc'].tb)
if not parsed: if not parsed:
display.warning(u"Unable to parse %s as an inventory source" % to_text(source)) display.warning(u"Unable to parse %s as an inventory source" % to_text(source))

@ -23,7 +23,7 @@ from ansible.errors import AnsibleParserError, AnsibleError
from ansible.module_utils.six import iteritems, string_types from ansible.module_utils.six import iteritems, string_types
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.parsing.splitter import parse_kv, split_args from ansible.parsing.splitter import parse_kv, split_args
from ansible.plugins import module_loader, action_loader from ansible.plugins.loader import module_loader, action_loader
from ansible.template import Templar from ansible.template import Templar

@ -0,0 +1,83 @@
# Copyright (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
import ast
import yaml
from ansible.parsing.yaml.loader import AnsibleLoader
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
def read_docstring(filename, verbose=True, ignore_errors=True):
"""
Search for assignment of the DOCUMENTATION and EXAMPLES variables in the given file.
Parse DOCUMENTATION from YAML and return the YAML doc or None together with EXAMPLES, as plain text.
"""
data = {
'doc': None,
'plainexamples': None,
'returndocs': None,
'metadata': None
}
string_to_vars = {
'DOCUMENTATION': 'doc',
'EXAMPLES': 'plainexamples',
'RETURN': 'returndocs',
'ANSIBLE_METADATA': 'metadata'
}
try:
M = ast.parse(''.join(open(filename)))
try:
display.debug('Attempt first docstring is yaml docs')
docstring = yaml.load(M.body[0].value.s)
for string in string_to_vars.keys():
if string in docstring:
data[string_to_vars[string]] = docstring[string]
display.debug('assigned :%s' % string_to_vars[string])
except Exception as e:
display.debug('failed docstring parsing: %s' % str(e))
if 'docs' not in data or not data['docs']:
display.debug('Fallback to vars parsing')
for child in M.body:
if isinstance(child, ast.Assign):
for t in child.targets:
try:
theid = t.id
except AttributeError:
# skip errors can happen when trying to use the normal code
display.warning("Failed to assign id for %s on %s, skipping" % (t, filename))
continue
if theid in string_to_vars:
varkey = string_to_vars[theid]
if isinstance(child.value, ast.Dict):
data[varkey] = ast.literal_eval(child.value)
else:
if theid in ['DOCUMENTATION', 'ANSIBLE_METADATA']:
# string should be yaml
data[varkey] = AnsibleLoader(child.value.s, file_name=filename).get_single_data()
else:
# not yaml, should be a simple string
data[varkey] = child.value.s
display.debug('assigned :%s' % varkey)
except:
if verbose:
display.error("unable to parse %s" % filename)
if not ignore_errors:
raise
return data

@ -26,7 +26,7 @@ from ansible.errors import AnsibleParserError
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.playbook.play import Play from ansible.playbook.play import Play
from ansible.playbook.playbook_include import PlaybookInclude from ansible.playbook.playbook_include import PlaybookInclude
from ansible.plugins import get_all_plugin_loaders from ansible.plugins.loader import get_all_plugin_loaders
try: try:
from __main__ import display from __main__ import display

@ -197,9 +197,10 @@ class Base(with_metaclass(BaseMeta, object)):
self.vars = dict() self.vars = dict()
def dump_me(self, depth=0): def dump_me(self, depth=0):
''' this is never called from production code, it is here to be used when debugging as a 'complex print' '''
if depth == 0: if depth == 0:
print("DUMPING OBJECT ------------------------------------------------------") display.debug("DUMPING OBJECT ------------------------------------------------------")
print("%s- %s (%s, id=%s)" % (" " * depth, self.__class__.__name__, self, id(self))) display.debug("%s- %s (%s, id=%s)" % (" " * depth, self.__class__.__name__, self, id(self)))
if hasattr(self, '_parent') and self._parent: if hasattr(self, '_parent') and self._parent:
self._parent.dump_me(depth + 2) self._parent.dump_me(depth + 2)
dep_chain = self._parent.get_dep_chain() dep_chain = self._parent.get_dep_chain()

@ -36,6 +36,7 @@ from ansible.module_utils._text import to_bytes
from ansible.module_utils.parsing.convert_bool import boolean from ansible.module_utils.parsing.convert_bool import boolean
from ansible.playbook.attribute import FieldAttribute from ansible.playbook.attribute import FieldAttribute
from ansible.playbook.base import Base from ansible.playbook.base import Base
from ansible.plugins import get_plugin_class
from ansible.utils.ssh_functions import check_for_controlpersist from ansible.utils.ssh_functions import check_for_controlpersist
@ -54,31 +55,47 @@ __all__ = ['PlayContext']
# in variable names. # in variable names.
MAGIC_VARIABLE_MAPPING = dict( MAGIC_VARIABLE_MAPPING = dict(
accelerate_port=('ansible_accelerate_port', ),
# base
connection=('ansible_connection', ), connection=('ansible_connection', ),
module_compression=('ansible_module_compression', ),
shell=('ansible_shell_type', ),
executable=('ansible_shell_executable', ),
remote_tmp_dir=('ansible_remote_tmp', ),
# connection common
remote_addr=('ansible_ssh_host', 'ansible_host'), remote_addr=('ansible_ssh_host', 'ansible_host'),
remote_user=('ansible_ssh_user', 'ansible_user'), remote_user=('ansible_ssh_user', 'ansible_user'),
remote_tmp_dir=('ansible_remote_tmp', ), password=('ansible_ssh_pass', 'ansible_password'),
port=('ansible_ssh_port', 'ansible_port'), port=('ansible_ssh_port', 'ansible_port'),
pipelining=('ansible_ssh_pipelining', 'ansible_pipelining'),
timeout=('ansible_ssh_timeout', 'ansible_timeout'), timeout=('ansible_ssh_timeout', 'ansible_timeout'),
ssh_executable=('ansible_ssh_executable', ),
accelerate_port=('ansible_accelerate_port', ),
password=('ansible_ssh_pass', 'ansible_password'),
private_key_file=('ansible_ssh_private_key_file', 'ansible_private_key_file'), private_key_file=('ansible_ssh_private_key_file', 'ansible_private_key_file'),
pipelining=('ansible_ssh_pipelining', 'ansible_pipelining'),
shell=('ansible_shell_type', ), # networking modules
network_os=('ansible_network_os', ), network_os=('ansible_network_os', ),
# ssh TODO: remove
ssh_executable=('ansible_ssh_executable', ),
ssh_common_args=('ansible_ssh_common_args', ),
sftp_extra_args=('ansible_sftp_extra_args', ),
scp_extra_args=('ansible_scp_extra_args', ),
ssh_extra_args=('ansible_ssh_extra_args', ),
ssh_transfer_method=('ansible_ssh_transfer_method', ),
# docker TODO: remove
docker_extra_args=('ansible_docker_extra_args', ),
# become
become=('ansible_become', ), become=('ansible_become', ),
become_method=('ansible_become_method', ), become_method=('ansible_become_method', ),
become_user=('ansible_become_user', ), become_user=('ansible_become_user', ),
become_pass=('ansible_become_password', 'ansible_become_pass'), become_pass=('ansible_become_password', 'ansible_become_pass'),
become_exe=('ansible_become_exe', ), become_exe=('ansible_become_exe', ),
become_flags=('ansible_become_flags', ), become_flags=('ansible_become_flags', ),
ssh_common_args=('ansible_ssh_common_args', ),
docker_extra_args=('ansible_docker_extra_args', ), # deprecated
sftp_extra_args=('ansible_sftp_extra_args', ),
scp_extra_args=('ansible_scp_extra_args', ),
ssh_extra_args=('ansible_ssh_extra_args', ),
ssh_transfer_method=('ansible_ssh_transfer_method', ),
sudo=('ansible_sudo', ), sudo=('ansible_sudo', ),
sudo_user=('ansible_sudo_user', ), sudo_user=('ansible_sudo_user', ),
sudo_pass=('ansible_sudo_password', 'ansible_sudo_pass'), sudo_pass=('ansible_sudo_password', 'ansible_sudo_pass'),
@ -89,10 +106,9 @@ MAGIC_VARIABLE_MAPPING = dict(
su_pass=('ansible_su_password', 'ansible_su_pass'), su_pass=('ansible_su_password', 'ansible_su_pass'),
su_exe=('ansible_su_exe', ), su_exe=('ansible_su_exe', ),
su_flags=('ansible_su_flags', ), su_flags=('ansible_su_flags', ),
executable=('ansible_shell_executable', ),
module_compression=('ansible_module_compression', ),
) )
# TODO: needs to be configurable
b_SU_PROMPT_LOCALIZATIONS = [ b_SU_PROMPT_LOCALIZATIONS = [
to_bytes('Password'), to_bytes('Password'),
to_bytes('암호'), to_bytes('암호'),
@ -135,7 +151,7 @@ TASK_ATTRIBUTE_OVERRIDES = (
'become_method', 'become_method',
'become_flags', 'become_flags',
'connection', 'connection',
'docker_extra_args', 'docker_extra_args', # TODO: remove
'delegate_to', 'delegate_to',
'no_log', 'no_log',
'remote_user', 'remote_user',
@ -143,6 +159,11 @@ TASK_ATTRIBUTE_OVERRIDES = (
RESET_VARS = ( RESET_VARS = (
'ansible_connection', 'ansible_connection',
'ansible_user',
'ansible_host',
'ansible_port',
# TODO: ???
'ansible_docker_extra_args', 'ansible_docker_extra_args',
'ansible_ssh_host', 'ansible_ssh_host',
'ansible_ssh_pass', 'ansible_ssh_pass',
@ -151,9 +172,6 @@ RESET_VARS = (
'ansible_ssh_private_key_file', 'ansible_ssh_private_key_file',
'ansible_ssh_pipelining', 'ansible_ssh_pipelining',
'ansible_ssh_executable', 'ansible_ssh_executable',
'ansible_user',
'ansible_host',
'ansible_port',
) )
@ -165,47 +183,59 @@ class PlayContext(Base):
connection/authentication information. connection/authentication information.
''' '''
# base
_module_compression = FieldAttribute(isa='string', default=C.DEFAULT_MODULE_COMPRESSION)
_shell = FieldAttribute(isa='string')
_executable = FieldAttribute(isa='string', default=C.DEFAULT_EXECUTABLE)
# connection fields, some are inherited from Base: # connection fields, some are inherited from Base:
# (connection, port, remote_user, environment, no_log) # (connection, port, remote_user, environment, no_log)
_docker_extra_args = FieldAttribute(isa='string')
_remote_addr = FieldAttribute(isa='string') _remote_addr = FieldAttribute(isa='string')
_remote_tmp_dir = FieldAttribute(isa='string', default=C.DEFAULT_REMOTE_TMP) _remote_tmp_dir = FieldAttribute(isa='string', default=C.DEFAULT_REMOTE_TMP)
_password = FieldAttribute(isa='string') _password = FieldAttribute(isa='string')
_private_key_file = FieldAttribute(isa='string', default=C.DEFAULT_PRIVATE_KEY_FILE)
_timeout = FieldAttribute(isa='int', default=C.DEFAULT_TIMEOUT) _timeout = FieldAttribute(isa='int', default=C.DEFAULT_TIMEOUT)
_shell = FieldAttribute(isa='string')
_network_os = FieldAttribute(isa='string')
_connection_user = FieldAttribute(isa='string') _connection_user = FieldAttribute(isa='string')
_private_key_file = FieldAttribute(isa='string', default=C.DEFAULT_PRIVATE_KEY_FILE)
_pipelining = FieldAttribute(isa='bool', default=C.ANSIBLE_PIPELINING)
# networking modules
_network_os = FieldAttribute(isa='string')
# docker FIXME: remove these
_docker_extra_args = FieldAttribute(isa='string')
# ssh # FIXME: remove these
_ssh_executable = FieldAttribute(isa='string', default=C.ANSIBLE_SSH_EXECUTABLE)
_ssh_args = FieldAttribute(isa='string', default=C.ANSIBLE_SSH_ARGS) _ssh_args = FieldAttribute(isa='string', default=C.ANSIBLE_SSH_ARGS)
_ssh_common_args = FieldAttribute(isa='string') _ssh_common_args = FieldAttribute(isa='string')
_sftp_extra_args = FieldAttribute(isa='string') _sftp_extra_args = FieldAttribute(isa='string')
_scp_extra_args = FieldAttribute(isa='string') _scp_extra_args = FieldAttribute(isa='string')
_ssh_extra_args = FieldAttribute(isa='string') _ssh_extra_args = FieldAttribute(isa='string')
_ssh_executable = FieldAttribute(isa='string', default=C.ANSIBLE_SSH_EXECUTABLE)
_ssh_transfer_method = FieldAttribute(isa='string', default=C.DEFAULT_SSH_TRANSFER_METHOD) _ssh_transfer_method = FieldAttribute(isa='string', default=C.DEFAULT_SSH_TRANSFER_METHOD)
# ???
_connection_lockfd = FieldAttribute(isa='int') _connection_lockfd = FieldAttribute(isa='int')
_pipelining = FieldAttribute(isa='bool', default=C.ANSIBLE_SSH_PIPELINING)
# accelerate FIXME: remove as soon as deprecation period expires
_accelerate = FieldAttribute(isa='bool', default=False) _accelerate = FieldAttribute(isa='bool', default=False)
_accelerate_ipv6 = FieldAttribute(isa='bool', default=False, always_post_validate=True) _accelerate_ipv6 = FieldAttribute(isa='bool', default=False, always_post_validate=True)
_accelerate_port = FieldAttribute(isa='int', default=C.ACCELERATE_PORT, always_post_validate=True) _accelerate_port = FieldAttribute(isa='int', default=C.ACCELERATE_PORT, always_post_validate=True)
_executable = FieldAttribute(isa='string', default=C.DEFAULT_EXECUTABLE)
_module_compression = FieldAttribute(isa='string', default=C.DEFAULT_MODULE_COMPRESSION)
# privilege escalation fields # privilege escalation fields
_become = FieldAttribute(isa='bool') _become = FieldAttribute(isa='bool')
_become_method = FieldAttribute(isa='string') _become_method = FieldAttribute(isa='string')
_become_user = FieldAttribute(isa='string') _become_user = FieldAttribute(isa='string')
_become_pass = FieldAttribute(isa='string') _become_pass = FieldAttribute(isa='string')
_become_exe = FieldAttribute(isa='string') _become_exe = FieldAttribute(isa='string', default=C.DEFAULT_BECOME_EXE)
_become_flags = FieldAttribute(isa='string') _become_flags = FieldAttribute(isa='string', default=C.DEFAULT_BECOME_FLAGS)
_prompt = FieldAttribute(isa='string') _prompt = FieldAttribute(isa='string')
# backwards compatibility fields for sudo/su # DEPRECATED: backwards compatibility fields for sudo/su
_sudo_exe = FieldAttribute(isa='string') _sudo_exe = FieldAttribute(isa='string', default=C.DEFAULT_SUDO_EXE)
_sudo_flags = FieldAttribute(isa='string') _sudo_flags = FieldAttribute(isa='string', default=C.DEFAULT_SUDO_FLAGS)
_sudo_pass = FieldAttribute(isa='string') _sudo_pass = FieldAttribute(isa='string')
_su_exe = FieldAttribute(isa='string') _su_exe = FieldAttribute(isa='string', default=C.DEFAULT_SU_EXE)
_su_flags = FieldAttribute(isa='string') _su_flags = FieldAttribute(isa='string', default=C.DEFAULT_SU_FLAGS)
_su_pass = FieldAttribute(isa='string') _su_pass = FieldAttribute(isa='string')
# general flags # general flags
@ -277,6 +307,22 @@ class PlayContext(Base):
if play.force_handlers is not None: if play.force_handlers is not None:
self.force_handlers = play.force_handlers self.force_handlers = play.force_handlers
def set_options_from_plugin(self, plugin):
# generic derived from connection plugin
# get options for plugins
options = C.config.get_configuration_definitions(get_plugin_class(plugin), plugin._load_name)
for option in options:
if option:
flag = options[option].get('name')
if flag:
setattr(self, flag, self.connection.get_option(flag))
# TODO: made irrelavent by above
# get ssh options FIXME: make these common to all connections
# for flag in ('ssh_common_args', 'docker_extra_args', 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args'):
# setattr(self, flag, getattr(options, flag, ''))
def set_options(self, options): def set_options(self, options):
''' '''
Configures this connection information instance with data from Configures this connection information instance with data from
@ -291,12 +337,10 @@ class PlayContext(Base):
self.check_mode = boolean(options.check, strict=False) self.check_mode = boolean(options.check, strict=False)
# get ssh options FIXME: make these common to all connections
for flag in ['ssh_common_args', 'docker_extra_args', 'sftp_extra_args', 'scp_extra_args', 'ssh_extra_args']:
setattr(self, flag, getattr(options, flag, ''))
# general flags (should we move out?) # general flags (should we move out?)
for flag in ['connection', 'remote_user', 'private_key_file', 'verbosity', 'force_handlers', 'step', 'start_at_task', 'diff']: # for flag in ('connection', 'remote_user', 'private_key_file', 'verbosity', 'force_handlers', 'step', 'start_at_task', 'diff'):
# should only be 'non plugin' flags
for flag in ('connection', 'private_key_file', 'verbosity', 'force_handlers', 'step', 'start_at_task', 'diff'):
attribute = getattr(options, flag, False) attribute = getattr(options, flag, False)
if attribute: if attribute:
setattr(self, flag, attribute) setattr(self, flag, attribute)
@ -492,22 +536,18 @@ class PlayContext(Base):
command = success_cmd command = success_cmd
# set executable to use for the privilege escalation method, with various overrides # set executable to use for the privilege escalation method, with various overrides
exe = ( exe = self.become_method
self.become_exe or for myexe in (getattr(self, '%s_exe' % self.become_method, None), self.become_exe):
getattr(self, '%s_exe' % self.become_method, None) or if myexe:
C.DEFAULT_BECOME_EXE or exe = myexe
getattr(C, 'DEFAULT_%s_EXE' % self.become_method.upper(), None) or break
self.become_method
)
# set flags to use for the privilege escalation method, with various overrides # set flags to use for the privilege escalation method, with various overrides
flags = ( flags = ''
self.become_flags or for myflag in (getattr(self, '%s_flags' % self.become_method, None), self.become_flags):
getattr(self, '%s_flags' % self.become_method, None) or if myflag is not None:
C.DEFAULT_BECOME_FLAGS or flags = myflag
getattr(C, 'DEFAULT_%s_FLAGS' % self.become_method.upper(), None) or break
''
)
if self.become_method == 'sudo': if self.become_method == 'sudo':
# If we have a password, we run sudo with a randomly-generated # If we have a password, we run sudo with a randomly-generated

@ -31,7 +31,7 @@ from ansible.playbook.conditional import Conditional
from ansible.playbook.helpers import load_list_of_blocks from ansible.playbook.helpers import load_list_of_blocks
from ansible.playbook.role.metadata import RoleMetadata from ansible.playbook.role.metadata import RoleMetadata
from ansible.playbook.taggable import Taggable from ansible.playbook.taggable import Taggable
from ansible.plugins import get_all_plugin_loaders from ansible.plugins.loader import get_all_plugin_loaders
from ansible.utils.vars import combine_vars from ansible.utils.vars import combine_vars

@ -26,7 +26,7 @@ from ansible.module_utils.six import iteritems, string_types
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.parsing.mod_args import ModuleArgsParser from ansible.parsing.mod_args import ModuleArgsParser
from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping, AnsibleUnicode from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping, AnsibleUnicode
from ansible.plugins import lookup_loader from ansible.plugins.loader import lookup_loader
from ansible.playbook.attribute import FieldAttribute from ansible.playbook.attribute import FieldAttribute
from ansible.playbook.base import Base from ansible.playbook.base import Base
from ansible.playbook.become import Become from ansible.playbook.become import Become

@ -21,18 +21,10 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import glob from abc import ABCMeta
import imp
import os
import os.path
import sys
import warnings
from collections import defaultdict
from ansible import constants as C from ansible import constants as C
from ansible.module_utils._text import to_text from ansible.module_utils.six import with_metaclass
try: try:
from __main__ import display from __main__ import display
@ -46,537 +38,11 @@ PATH_CACHE = {}
PLUGIN_PATH_CACHE = {} PLUGIN_PATH_CACHE = {}
def get_all_plugin_loaders(): def get_plugin_class(obj):
return [(name, obj) for (name, obj) in globals().items() if isinstance(obj, PluginLoader)] return obj.__class__.__name__.lower().replace('module', '')
class PluginLoader:
'''
PluginLoader loads plugins from the configured plugin directories.
It searches for plugins by iterating through the combined list of
play basedirs, configured paths, and the python path.
The first match is used.
'''
def __init__(self, class_name, package, config, subdir, aliases={}, required_base_class=None):
self.class_name = class_name
self.base_class = required_base_class
self.package = package
self.subdir = subdir
self.aliases = aliases
if config and not isinstance(config, list):
config = [config]
elif not config:
config = []
self.config = config
if class_name not in MODULE_CACHE:
MODULE_CACHE[class_name] = {}
if class_name not in PATH_CACHE:
PATH_CACHE[class_name] = None
if class_name not in PLUGIN_PATH_CACHE:
PLUGIN_PATH_CACHE[class_name] = defaultdict(dict)
self._module_cache = MODULE_CACHE[class_name]
self._paths = PATH_CACHE[class_name]
self._plugin_path_cache = PLUGIN_PATH_CACHE[class_name]
self._extra_dirs = []
self._searched_paths = set()
def __setstate__(self, data):
'''
Deserializer.
'''
class_name = data.get('class_name')
package = data.get('package')
config = data.get('config')
subdir = data.get('subdir')
aliases = data.get('aliases')
base_class = data.get('base_class')
PATH_CACHE[class_name] = data.get('PATH_CACHE')
PLUGIN_PATH_CACHE[class_name] = data.get('PLUGIN_PATH_CACHE')
self.__init__(class_name, package, config, subdir, aliases, base_class)
self._extra_dirs = data.get('_extra_dirs', [])
self._searched_paths = data.get('_searched_paths', set())
def __getstate__(self):
'''
Serializer.
'''
return dict(
class_name=self.class_name,
base_class=self.base_class,
package=self.package,
config=self.config,
subdir=self.subdir,
aliases=self.aliases,
_extra_dirs=self._extra_dirs,
_searched_paths=self._searched_paths,
PATH_CACHE=PATH_CACHE[self.class_name],
PLUGIN_PATH_CACHE=PLUGIN_PATH_CACHE[self.class_name],
)
def format_paths(self, paths):
''' Returns a string suitable for printing of the search path '''
# Uses a list to get the order right
ret = []
for i in paths:
if i not in ret:
ret.append(i)
return os.pathsep.join(ret)
def print_paths(self):
return self.format_paths(self._get_paths())
def _all_directories(self, dir):
results = []
results.append(dir)
for root, subdirs, files in os.walk(dir, followlinks=True):
if '__init__.py' in files:
for x in subdirs:
results.append(os.path.join(root, x))
return results
def _get_package_paths(self, subdirs=True):
''' Gets the path of a Python package '''
if not self.package:
return []
if not hasattr(self, 'package_path'):
m = __import__(self.package)
parts = self.package.split('.')[1:]
for parent_mod in parts:
m = getattr(m, parent_mod)
self.package_path = os.path.dirname(m.__file__)
if subdirs:
return self._all_directories(self.package_path)
return [self.package_path]
def _get_paths(self, subdirs=True):
''' Return a list of paths to search for plugins in '''
# FIXME: This is potentially buggy if subdirs is sometimes True and
# sometimes False. In current usage, everything calls this with
# subdirs=True except for module_utils_loader which always calls it
# with subdirs=False. So there currently isn't a problem with this
# caching.
if self._paths is not None:
return self._paths
ret = self._extra_dirs[:]
# look in any configured plugin paths, allow one level deep for subcategories
if self.config is not None:
for path in self.config:
path = os.path.realpath(os.path.expanduser(path))
if subdirs:
contents = glob.glob("%s/*" % path) + glob.glob("%s/*/*" % path)
for c in contents:
if os.path.isdir(c) and c not in ret:
ret.append(c)
if path not in ret:
ret.append(path)
# look for any plugins installed in the package subtree
# Note package path always gets added last so that every other type of
# path is searched before it.
ret.extend(self._get_package_paths(subdirs=subdirs))
# HACK: because powershell modules are in the same directory
# hierarchy as other modules we have to process them last. This is
# because powershell only works on windows but the other modules work
# anywhere (possibly including windows if the correct language
# interpreter is installed). the non-powershell modules can have any
# file extension and thus powershell modules are picked up in that.
# The non-hack way to fix this is to have powershell modules be
# a different PluginLoader/ModuleLoader. But that requires changing
# other things too (known thing to change would be PATHS_CACHE,
# PLUGIN_PATHS_CACHE, and MODULE_CACHE. Since those three dicts key
# on the class_name and neither regular modules nor powershell modules
# would have class_names, they would not work as written.
reordered_paths = []
win_dirs = []
for path in ret:
if path.endswith('windows'):
win_dirs.append(path)
else:
reordered_paths.append(path)
reordered_paths.extend(win_dirs)
# cache and return the result
self._paths = reordered_paths
return reordered_paths
def add_directory(self, directory, with_subdir=False):
''' Adds an additional directory to the search path '''
directory = os.path.realpath(directory)
if directory is not None:
if with_subdir:
directory = os.path.join(directory, self.subdir)
if directory not in self._extra_dirs:
# append the directory and invalidate the path cache
self._extra_dirs.append(directory)
self._paths = None
def find_plugin(self, name, mod_type='', ignore_deprecated=False):
''' Find a plugin named name '''
if mod_type:
suffix = mod_type
elif self.class_name:
# Ansible plugins that run in the controller process (most plugins)
suffix = '.py'
else:
# Only Ansible Modules. Ansible modules can be any executable so
# they can have any suffix
suffix = ''
# The particular cache to look for modules within. This matches the
# requested mod_type
pull_cache = self._plugin_path_cache[suffix]
try:
return pull_cache[name]
except KeyError:
# Cache miss. Now let's find the plugin
pass
# TODO: Instead of using the self._paths cache (PATH_CACHE) and
# self._searched_paths we could use an iterator. Before enabling that
# we need to make sure we don't want to add additional directories
# (add_directory()) once we start using the iterator. Currently, it
# looks like _get_paths() never forces a cache refresh so if we expect
# additional directories to be added later, it is buggy.
for path in (p for p in self._get_paths() if p not in self._searched_paths and os.path.isdir(p)):
try:
full_paths = (os.path.join(path, f) for f in os.listdir(path))
except OSError as e:
display.warning("Error accessing plugin paths: %s" % to_text(e))
for full_path in (f for f in full_paths if os.path.isfile(f) and not f.endswith('__init__.py')):
full_name = os.path.basename(full_path)
# HACK: We have no way of executing python byte
# compiled files as ansible modules so specifically exclude them
# FIXME: I believe this is only correct for modules and
# module_utils. For all other plugins we want .pyc and .pyo should
# bew valid
if full_path.endswith(('.pyc', '.pyo')):
continue
splitname = os.path.splitext(full_name)
base_name = splitname[0]
try:
extension = splitname[1]
except IndexError:
extension = ''
# Module found, now enter it into the caches that match
# this file
if base_name not in self._plugin_path_cache['']:
self._plugin_path_cache[''][base_name] = full_path
if full_name not in self._plugin_path_cache['']:
self._plugin_path_cache[''][full_name] = full_path
if base_name not in self._plugin_path_cache[extension]:
self._plugin_path_cache[extension][base_name] = full_path
if full_name not in self._plugin_path_cache[extension]:
self._plugin_path_cache[extension][full_name] = full_path
self._searched_paths.add(path)
try:
return pull_cache[name]
except KeyError:
# Didn't find the plugin in this directory. Load modules from
# the next one
pass
# if nothing is found, try finding alias/deprecated
if not name.startswith('_'):
alias_name = '_' + name
# We've already cached all the paths at this point
if alias_name in pull_cache:
if not ignore_deprecated and not os.path.islink(pull_cache[alias_name]):
display.deprecated('%s is kept for backwards compatibility '
'but usage is discouraged. The module '
'documentation details page may explain '
'more about this rationale.' %
name.lstrip('_'))
return pull_cache[alias_name]
return None
def has_plugin(self, name):
''' Checks if a plugin named name exists '''
return self.find_plugin(name) is not None
__contains__ = has_plugin
def _load_module_source(self, name, path):
# avoid collisions across plugins
full_name = '.'.join([self.package, name])
if full_name in sys.modules:
# Avoids double loading, See https://github.com/ansible/ansible/issues/13110
return sys.modules[full_name]
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
with open(path, 'rb') as module_file:
module = imp.load_source(full_name, path, module_file)
return module
def get(self, name, *args, **kwargs):
''' instantiates a plugin of the given name using arguments '''
found_in_cache = True
class_only = kwargs.pop('class_only', False)
if name in self.aliases:
name = self.aliases[name]
path = self.find_plugin(name)
if path is None:
return None
if path not in self._module_cache:
self._module_cache[path] = self._load_module_source(name, path)
found_in_cache = False
obj = getattr(self._module_cache[path], self.class_name)
if self.base_class:
# The import path is hardcoded and should be the right place,
# so we are not expecting an ImportError.
module = __import__(self.package, fromlist=[self.base_class])
# Check whether this obj has the required base class.
try:
plugin_class = getattr(module, self.base_class)
except AttributeError:
return None
if not issubclass(obj, plugin_class):
return None
self._display_plugin_load(self.class_name, name, self._searched_paths, path,
found_in_cache=found_in_cache, class_only=class_only)
if not class_only:
try:
obj = obj(*args, **kwargs)
except TypeError as e:
if "abstract" in e.args[0]:
# Abstract Base Class. The found plugin file does not
# fully implement the defined interface.
return None
raise
# set extra info on the module, in case we want it later
setattr(obj, '_original_path', path)
setattr(obj, '_load_name', name)
return obj
def _display_plugin_load(self, class_name, name, searched_paths, path, found_in_cache=None, class_only=None):
msg = 'Loading %s \'%s\' from %s' % (class_name, os.path.basename(name), path)
if len(searched_paths) > 1:
msg = '%s (searched paths: %s)' % (msg, self.format_paths(searched_paths))
if found_in_cache or class_only:
msg = '%s (found_in_cache=%s, class_only=%s)' % (msg, found_in_cache, class_only)
display.debug(msg)
def all(self, *args, **kwargs):
''' instantiates all plugins with the same arguments '''
path_only = kwargs.pop('path_only', False)
class_only = kwargs.pop('class_only', False)
all_matches = []
found_in_cache = True
for i in self._get_paths():
all_matches.extend(glob.glob(os.path.join(i, "*.py")))
for path in sorted(all_matches, key=lambda match: os.path.basename(match)):
name, _ = os.path.splitext(path)
if '__init__' in name:
continue
if path_only:
yield path
continue
if path not in self._module_cache:
self._module_cache[path] = self._load_module_source(name, path)
found_in_cache = False
try:
obj = getattr(self._module_cache[path], self.class_name)
except AttributeError as e:
display.warning("Skipping plugin (%s) as it seems to be invalid: %s" % (path, to_text(e)))
continue
if self.base_class:
# The import path is hardcoded and should be the right place,
# so we are not expecting an ImportError.
module = __import__(self.package, fromlist=[self.base_class])
# Check whether this obj has the required base class.
try:
plugin_class = getattr(module, self.base_class)
except AttributeError:
continue
if not issubclass(obj, plugin_class):
continue
self._display_plugin_load(self.class_name, name, self._searched_paths, path, found_in_cache=found_in_cache, class_only=class_only)
if not class_only:
try:
obj = obj(*args, **kwargs)
except TypeError as e:
display.warning("Skipping plugin (%s) as it seems to be incomplete: %s" % (path, to_text(e)))
# set extra info on the module, in case we want it later
setattr(obj, '_original_path', path)
setattr(obj, '_load_name', name)
yield obj
action_loader = PluginLoader(
'ActionModule',
'ansible.plugins.action',
C.DEFAULT_ACTION_PLUGIN_PATH,
'action_plugins',
required_base_class='ActionBase',
)
cache_loader = PluginLoader(
'CacheModule',
'ansible.plugins.cache',
C.DEFAULT_CACHE_PLUGIN_PATH,
'cache_plugins',
)
callback_loader = PluginLoader(
'CallbackModule',
'ansible.plugins.callback',
C.DEFAULT_CALLBACK_PLUGIN_PATH,
'callback_plugins',
)
connection_loader = PluginLoader(
'Connection',
'ansible.plugins.connection',
C.DEFAULT_CONNECTION_PLUGIN_PATH,
'connection_plugins',
aliases={'paramiko': 'paramiko_ssh'},
required_base_class='ConnectionBase',
)
shell_loader = PluginLoader(
'ShellModule',
'ansible.plugins.shell',
'shell_plugins',
'shell_plugins',
)
module_loader = PluginLoader(
'',
'ansible.modules',
C.DEFAULT_MODULE_PATH,
'library',
)
module_utils_loader = PluginLoader(
'',
'ansible.module_utils',
C.DEFAULT_MODULE_UTILS_PATH,
'module_utils',
)
# NB: dedicated loader is currently necessary because PS module_utils expects "with subdir" lookup where
# regular module_utils doesn't. This can be revisited once we have more granular loaders.
ps_module_utils_loader = PluginLoader(
'',
'ansible.module_utils',
C.DEFAULT_MODULE_UTILS_PATH,
'module_utils',
)
lookup_loader = PluginLoader(
'LookupModule',
'ansible.plugins.lookup',
C.DEFAULT_LOOKUP_PLUGIN_PATH,
'lookup_plugins',
required_base_class='LookupBase',
)
filter_loader = PluginLoader(
'FilterModule',
'ansible.plugins.filter',
C.DEFAULT_FILTER_PLUGIN_PATH,
'filter_plugins',
)
test_loader = PluginLoader(
'TestModule',
'ansible.plugins.test',
C.DEFAULT_TEST_PLUGIN_PATH,
'test_plugins'
)
fragment_loader = PluginLoader(
'ModuleDocFragment',
'ansible.utils.module_docs_fragments',
os.path.join(os.path.dirname(__file__), 'module_docs_fragments'),
'',
)
strategy_loader = PluginLoader(
'StrategyModule',
'ansible.plugins.strategy',
C.DEFAULT_STRATEGY_PLUGIN_PATH,
'strategy_plugins',
required_base_class='StrategyBase',
)
terminal_loader = PluginLoader(
'TerminalModule',
'ansible.plugins.terminal',
'terminal_plugins',
'terminal_plugins'
)
vars_loader = PluginLoader(
'VarsModule',
'ansible.plugins.vars',
C.DEFAULT_VARS_PLUGIN_PATH,
'vars_plugins',
)
cliconf_loader = PluginLoader( class AnsiblePlugin(with_metaclass(ABCMeta, object)):
'Cliconf',
'ansible.plugins.cliconf',
'cliconf_plugins',
'cliconf_plugins',
required_base_class='CliconfBase'
)
netconf_loader = PluginLoader( def get_option(self, option):
'Netconf', return C.get_plugin_option(get_plugin_class(self), self.name, option)
'ansible.plugins.netconf',
'netconf_plugins',
'netconf_plugins',
required_base_class='NetconfBase'
)

@ -40,6 +40,7 @@ from ansible.parsing.utils.jsonify import jsonify
from ansible.playbook.play_context import MAGIC_VARIABLE_MAPPING from ansible.playbook.play_context import MAGIC_VARIABLE_MAPPING
from ansible.release import __version__ from ansible.release import __version__
from ansible.utils.unsafe_proxy import wrap_var from ansible.utils.unsafe_proxy import wrap_var
from ansible.vars.manager import remove_internal_keys
try: try:
@ -743,7 +744,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
tmpdir_delete = (not data.pop("_ansible_suppress_tmpdir_delete", False) and wrap_async) tmpdir_delete = (not data.pop("_ansible_suppress_tmpdir_delete", False) and wrap_async)
# remove internal keys # remove internal keys
self._remove_internal_keys(data) remove_internal_keys(data)
# cleanup tmp? # cleanup tmp?
if (self._play_context.become and self._play_context.become_user != 'root') and not persist_files and delete_remote_tmp or tmpdir_delete: if (self._play_context.become and self._play_context.become_user != 'root') and not persist_files and delete_remote_tmp or tmpdir_delete:
@ -766,17 +767,6 @@ class ActionBase(with_metaclass(ABCMeta, object)):
display.debug("done with _execute_module (%s, %s)" % (module_name, module_args)) display.debug("done with _execute_module (%s, %s)" % (module_name, module_args))
return data return data
def _remove_internal_keys(self, data):
for key in list(data.keys()):
if key.startswith('_ansible_') and key != '_ansible_parsed' or key in C.INTERNAL_RESULT_KEYS:
display.warning("Removed unexpected internal key in module return: %s = %s" % (key, data[key]))
del data[key]
# remove bad/empty internal keys
for key in ['warnings', 'deprecations']:
if key in data and not data[key]:
del data[key]
def _clean_returned_data(self, data): def _clean_returned_data(self, data):
remove_keys = set() remove_keys = set()
fact_keys = set(data.keys()) fact_keys = set(data.keys())
@ -817,7 +807,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
display.warning("Removed restricted key from module data: %s = %s" % (r_key, r_val)) display.warning("Removed restricted key from module data: %s = %s" % (r_key, r_val))
del data[r_key] del data[r_key]
self._remove_internal_keys(data) remove_internal_keys(data)
def _parse_returned_data(self, res): def _parse_returned_data(self, res):
try: try:

@ -26,7 +26,7 @@ from ansible import constants as C
from ansible.module_utils.basic import AnsibleFallbackNotFound from ansible.module_utils.basic import AnsibleFallbackNotFound
from ansible.module_utils.junos import junos_argument_spec from ansible.module_utils.junos import junos_argument_spec
from ansible.module_utils.six import iteritems from ansible.module_utils.six import iteritems
from ansible.plugins import connection_loader, module_loader from ansible.plugins.loader import connection_loader, module_loader
from ansible.plugins.action.normal import ActionModule as _ActionModule from ansible.plugins.action.normal import ActionModule as _ActionModule
from ansible.module_utils.connection import Connection from ansible.module_utils.connection import Connection

@ -26,7 +26,7 @@ from ansible.module_utils._text import to_text
from ansible.module_utils.parsing.convert_bool import boolean from ansible.module_utils.parsing.convert_bool import boolean
from ansible.playbook.play_context import MAGIC_VARIABLE_MAPPING from ansible.playbook.play_context import MAGIC_VARIABLE_MAPPING
from ansible.plugins.action import ActionBase from ansible.plugins.action import ActionBase
from ansible.plugins import connection_loader from ansible.plugins.loader import connection_loader
class ActionModule(ActionBase): class ActionModule(ActionBase):

@ -27,7 +27,7 @@ from ansible import constants as C
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.module_utils.six import with_metaclass from ansible.module_utils.six import with_metaclass
from ansible.module_utils._text import to_bytes from ansible.module_utils._text import to_bytes
from ansible.plugins import cache_loader from ansible.plugins.loader import cache_loader
try: try:
from __main__ import display from __main__ import display

@ -15,6 +15,17 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
'''
DOCUMENTATION:
callback: json
short_description: Ansbile screen output asjson
version_added: "2.2"
description:
- This callback converts all events into JSON output
type: stdout
plugin_api_version: "2.0"
'''
# Make coding more python3-ish # Make coding more python3-ish
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type

@ -23,14 +23,15 @@ import fcntl
import gettext import gettext
import os import os
import shlex import shlex
from abc import ABCMeta, abstractmethod, abstractproperty from abc import abstractmethod, abstractproperty
from functools import wraps from functools import wraps
from ansible import constants as C from ansible import constants as C
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.module_utils.six import string_types, with_metaclass from ansible.module_utils.six import string_types
from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils._text import to_bytes, to_text
from ansible.plugins import shell_loader from ansible.plugins import AnsiblePlugin
from ansible.plugins.loader import shell_loader
try: try:
from __main__ import display from __main__ import display
@ -53,7 +54,7 @@ def ensure_connect(func):
return wrapped return wrapped
class ConnectionBase(with_metaclass(ABCMeta, object)): class ConnectionBase(AnsiblePlugin):
''' '''
A base class for connections to contain common code. A base class for connections to contain common code.
''' '''

@ -29,16 +29,24 @@ DOCUMENTATION:
author: Tomas Tomecek (ttomecek@redhat.com) author: Tomas Tomecek (ttomecek@redhat.com)
version_added: 2.4 version_added: 2.4
options: options:
remote_addr:
description:
- The ID of the container you want to access.
default: inventory_hostname
config:
vars:
- name: ansible_host
remote_user: remote_user:
description: description:
- User specified via name or ID which is used to execute commands inside the container. - User specified via name or ID which is used to execute commands inside the container.
config: config:
ini:
- section: defaults - section: defaults
key: remote_user key: remote_user
env_vars: env:
- ANSIBLE_REMOTE_USER - name: ANSIBLE_REMOTE_USER
host_vars: vars:
- ansible_user - name: ansible_user
""" """
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)

@ -25,7 +25,7 @@ import json
from ansible import constants as C from ansible import constants as C
from ansible.errors import AnsibleConnectionFailure, AnsibleError from ansible.errors import AnsibleConnectionFailure, AnsibleError
from ansible.module_utils._text import to_bytes, to_native, to_text from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.plugins import netconf_loader from ansible.plugins.loader import netconf_loader
from ansible.plugins.connection import ConnectionBase, ensure_connect from ansible.plugins.connection import ConnectionBase, ensure_connect
from ansible.utils.jsonrpc import Rpc from ansible.utils.jsonrpc import Rpc

@ -31,8 +31,7 @@ from ansible import constants as C
from ansible.errors import AnsibleConnectionFailure from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils.six import BytesIO, binary_type from ansible.module_utils.six import BytesIO, binary_type
from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils._text import to_bytes, to_text
from ansible.plugins import cliconf_loader from ansible.plugins.loader import cliconf_loader, terminal_loader
from ansible.plugins import terminal_loader
from ansible.plugins.connection.paramiko_ssh import Connection as _Connection from ansible.plugins.connection.paramiko_ssh import Connection as _Connection
from ansible.utils.jsonrpc import Rpc from ansible.utils.jsonrpc import Rpc

@ -26,73 +26,122 @@ DOCUMENTATION:
author: ansible (@core) author: ansible (@core)
version_added: historical version_added: historical
options: options:
_host: host:
description: Hostname/ip to connect to. description: Hostname/ip to connect to.
default: inventory_hostname default: inventory_hostname
host_vars: vars:
- ansible_host - name: ansible_host
- ansible_ssh_host - name: ansible_ssh_host
_host_key_checking: host_key_checking:
type: bool constants:
- name: HOST_KEY_CHECKING
description: Determines if ssh should check host keys description: Determines if ssh should check host keys
config: type: boolean
ini:
- section: defaults - section: defaults
key: 'host_key_checking' key: 'host_key_checking'
env_vars: env:
- ANSIBLE_HOST_KEY_CHECKING - name: ANSIBLE_HOST_KEY_CHECKING
_password: password:
description: Authentication password for the C(remote_user). Can be supplied as CLI option. description: Authentication password for the C(remote_user). Can be supplied as CLI option.
host_vars: vars:
- ansible_password - name: ansible_password
- ansible_ssh_pass - name: ansible_ssh_pass
_ssh_args: ssh_args:
description: Arguments to pass to all ssh cli tools description: Arguments to pass to all ssh cli tools
default: '-C -o ControlMaster=auto -o ControlPersist=60s' default: '-C -o ControlMaster=auto -o ControlPersist=60s'
config: ini:
- section: 'ssh_connection' - section: 'ssh_connection'
key: 'ssh_args' key: 'ssh_args'
env_vars: env:
- ANSIBLE_SSH_ARGS - name: ANSIBLE_SSH_ARGS
_ssh_common_args: ssh_common_args:
description: Common extra args for ssh CLI tools description: Common extra args for all ssh CLI tools
host_vars: vars:
- ansible_ssh_common_args - name: ansible_ssh_common_args
_scp_extra_args: ssh_executable:
default: ssh
description:
- This defines the location of the ssh binary. It defaults to `ssh` which will use the first ssh binary available in $PATH.
- This option is usually not required, it might be useful when access to system ssh is restricted,
or when using ssh wrappers to connect to remote hosts.
env: [{name: ANSIBLE_SSH_EXECUTABLE}]
ini:
- {key: ssh_executable, section: ssh_connection}
yaml: {key: ssh_connection.ssh_executable}
const:
- name: ANSIBLE_SSH_EXECUTABLE
version_added: "2.2"
scp_extra_args:
description: Extra exclusive to the 'scp' CLI description: Extra exclusive to the 'scp' CLI
host_vars: vars:
- ansible_scp_extra_args - name: ansible_scp_extra_args
_sftp_extra_args: sftp_extra_args:
description: Extra exclusive to the 'sftp' CLI description: Extra exclusive to the 'sftp' CLI
host_vars: vars:
- ansible_sftp_extra_args - name: ansible_sftp_extra_args
_ssh_extra_args: ssh_extra_args:
description: Extra exclusive to the 'ssh' CLI description: Extra exclusive to the 'ssh' CLI
host_vars: vars:
- ansible_ssh_extra_args - name: ansible_ssh_extra_args
ssh_retries:
# constant: ANSIBLE_SSH_RETRIES
description: Number of attempts to connect.
default: 3
env:
- name: ANSIBLE_SSH_RETRIES
ini:
- section: connection
key: retries
- section: ssh_connection
key: retries
port: port:
description: Remote port to connect to. description: Remote port to connect to.
type: int type: int
config: default: 22
ini:
- section: defaults - section: defaults
key: remote_port key: remote_port
default: 22 env:
env_vars: - name: ANSIBLE_REMOTE_PORT
- ANSIBLE_REMOTE_PORT vars:
host_vars: - name: ansible_port
- ansible_port - name: ansible_ssh_port
- ansible_ssh_port
remote_user: remote_user:
description: description:
- User name with which to login to the remote server, normally set by the remote_user keyword. - User name with which to login to the remote server, normally set by the remote_user keyword.
- If no user is supplied, Ansible will let the ssh client binary choose the user as it normally - If no user is supplied, Ansible will let the ssh client binary choose the user as it normally
config: ini:
- section: defaults - section: defaults
key: remote_user key: remote_user
env_vars: env:
- ANSIBLE_REMOTE_USER - name: ANSIBLE_REMOTE_USER
host_vars: vars:
- ansible_user - name: ansible_user
- ansible_ssh_user - name: ansible_ssh_user
pipelining:
default: ANSIBLE_PIPELINING
description:
- Pipelining reduces the number of SSH operations required to execute a module on the remote server,
by executing many Ansible modules without actual file transfer.
- This can result in a very significant performance improvement when enabled.
- However this conflicts with privilege escalation (become).
For example, when using sudo operations you must first disable 'requiretty' in the sudoers file for the target hosts,
which is why this feature is disabled by default.
env: [{name: ANSIBLE_SSH_PIPELINING}]
ini:
- {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
''' '''
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)

@ -25,10 +25,9 @@ DOCUMENTATION:
- Uses a YAML configuration file to identify group and the Jinja2 expressions that qualify a host for membership. - Uses a YAML configuration file to identify group and the Jinja2 expressions that qualify a host for membership.
- Only variables already in inventory are available for expressions (no facts). - Only variables already in inventory are available for expressions (no facts).
- Failed expressions will be ignored (assumes vars were missing). - Failed expressions will be ignored (assumes vars were missing).
EXAMPLES: EXAMPLES: | # inventory.config file in YAML format
# inventory.config file in YAML format plugin: constructed_groups
plugin: constructed_groups groups:
groups:
# simple name matching # simple name matching
webservers: inventory_hostname.startswith('web') webservers: inventory_hostname.startswith('web')
@ -77,7 +76,7 @@ class InventoryModule(BaseInventoryPlugin):
def parse(self, inventory, loader, path, cache=False): def parse(self, inventory, loader, path, cache=False):
''' parses the inventory file ''' ''' parses the inventory file '''
super(InventoryModule, self).parse(inventory, loader, path) super(InventoryModule, self).parse(inventory, loader, path, cache=True)
try: try:
data = self.loader.load_from_file(path) data = self.loader.load_from_file(path)
@ -94,19 +93,19 @@ class InventoryModule(BaseInventoryPlugin):
for host in inventory.hosts: for host in inventory.hosts:
# get available variables to templar # get available variables to templar
hostvars = host.get_vars() hostvars = inventory.hosts[host].get_vars()
if host.name in inventory.cache: # adds facts if cache is active if host in inventory.cache: # adds facts if cache is active
hostvars = combine_vars(hostvars, inventory.cache[host.name]) hostvars = combine_vars(hostvars, inventory.cache[host])
templar.set_available_variables(hostvars) templar.set_available_variables(hostvars)
# process each 'group entry' # process each 'group entry'
for group_name, expression in data.get('groups', {}): for group_name in data.get('groups', {}):
conditional = u"{%% if %s %%} True {%% else %%} False {%% endif %%}" % expression conditional = u"{%% if %s %%} True {%% else %%} False {%% endif %%}" % data['groups'][group_name]
result = templar.template(conditional) result = templar.template(conditional)
if result and bool(result): if result and bool(result):
# ensure group exists # ensure group exists
inventory.add_group(group_name) inventory.add_group(group_name)
# add host to group # add host to group
inventory.add_child(group_name, host.name) inventory.add_child(group_name, host)
except Exception as e: except Exception as e:
raise AnsibleParserError("failed to parse %s: %s " % (to_native(path), to_native(e))) raise AnsibleParserError("failed to parse %s: %s " % (to_native(path), to_native(e)))

@ -21,17 +21,19 @@ DOCUMENTATION:
version_added: "2.4" version_added: "2.4"
short_description: Uses a specifically YAML file as inventory source. short_description: Uses a specifically YAML file as inventory source.
description: description:
- YAML based inventory, starts with the 'all' group and has hosts/vars/children entries. - "YAML based inventory, starts with the 'all' group and has hosts/vars/children entries."
- Host entries can have sub-entries defined, which will be treated as variables. - Host entries can have sub-entries defined, which will be treated as variables.
- Vars entries are normal group vars. - Vars entries are normal group vars.
- Children are 'child groups', which can also have their own vars/hosts/children and so on. - "Children are 'child groups', which can also have their own vars/hosts/children and so on."
- File MUST have a valid extension: yaml, yml, json. - File MUST have a valid extension, defined in configuration
notes: notes:
- It takes the place of the previously hardcoded YAML inventory. - It takes the place of the previously hardcoded YAML inventory.
- To function it requires being whitelisted in configuration. - To function it requires being whitelisted in configuration.
options: options:
_yaml_extensions: yaml_extensions:
description: list of 'valid' extensions for files containing YAML description: list of 'valid' extensions for files containing YAML
type: list
default: ['.yaml', '.yml', '.json']
EXAMPLES: EXAMPLES:
all: # keys must be unique, i.e. only one 'hosts' per group all: # keys must be unique, i.e. only one 'hosts' per group
hosts: hosts:

@ -0,0 +1,588 @@
# (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.com>
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> and others
# (c) 2017, Toshio Kuratomi <tkuratomi@ansible.com>
#
# 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/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import glob
import imp
import os
import os.path
import sys
import warnings
from collections import defaultdict
from ansible import constants as C
from ansible.plugins import get_plugin_class, MODULE_CACHE, PATH_CACHE, PLUGIN_PATH_CACHE
from ansible.module_utils._text import to_text
from ansible.parsing.plugin_docs import read_docstring
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
def get_all_plugin_loaders():
return [(name, obj) for (name, obj) in globals().items() if isinstance(obj, PluginLoader)]
class PluginLoader:
'''
PluginLoader loads plugins from the configured plugin directories.
It searches for plugins by iterating through the combined list of
play basedirs, configured paths, and the python path.
The first match is used.
'''
def __init__(self, class_name, package, config, subdir, aliases={}, required_base_class=None):
self.class_name = class_name
self.base_class = required_base_class
self.package = package
self.subdir = subdir
self.aliases = aliases
if config and not isinstance(config, list):
config = [config]
elif not config:
config = []
self.config = config
if class_name not in MODULE_CACHE:
MODULE_CACHE[class_name] = {}
if class_name not in PATH_CACHE:
PATH_CACHE[class_name] = None
if class_name not in PLUGIN_PATH_CACHE:
PLUGIN_PATH_CACHE[class_name] = defaultdict(dict)
self._module_cache = MODULE_CACHE[class_name]
self._paths = PATH_CACHE[class_name]
self._plugin_path_cache = PLUGIN_PATH_CACHE[class_name]
self._extra_dirs = []
self._searched_paths = set()
def __setstate__(self, data):
'''
Deserializer.
'''
class_name = data.get('class_name')
package = data.get('package')
config = data.get('config')
subdir = data.get('subdir')
aliases = data.get('aliases')
base_class = data.get('base_class')
PATH_CACHE[class_name] = data.get('PATH_CACHE')
PLUGIN_PATH_CACHE[class_name] = data.get('PLUGIN_PATH_CACHE')
self.__init__(class_name, package, config, subdir, aliases, base_class)
self._extra_dirs = data.get('_extra_dirs', [])
self._searched_paths = data.get('_searched_paths', set())
def __getstate__(self):
'''
Serializer.
'''
return dict(
class_name=self.class_name,
base_class=self.base_class,
package=self.package,
config=self.config,
subdir=self.subdir,
aliases=self.aliases,
_extra_dirs=self._extra_dirs,
_searched_paths=self._searched_paths,
PATH_CACHE=PATH_CACHE[self.class_name],
PLUGIN_PATH_CACHE=PLUGIN_PATH_CACHE[self.class_name],
)
def format_paths(self, paths):
''' Returns a string suitable for printing of the search path '''
# Uses a list to get the order right
ret = []
for i in paths:
if i not in ret:
ret.append(i)
return os.pathsep.join(ret)
def print_paths(self):
return self.format_paths(self._get_paths(subdirs=False))
def _all_directories(self, dir):
results = []
results.append(dir)
for root, subdirs, files in os.walk(dir, followlinks=True):
if '__init__.py' in files:
for x in subdirs:
results.append(os.path.join(root, x))
return results
def _get_package_paths(self, subdirs=True):
''' Gets the path of a Python package '''
if not self.package:
return []
if not hasattr(self, 'package_path'):
m = __import__(self.package)
parts = self.package.split('.')[1:]
for parent_mod in parts:
m = getattr(m, parent_mod)
self.package_path = os.path.dirname(m.__file__)
if subdirs:
return self._all_directories(self.package_path)
return [self.package_path]
def _get_paths(self, subdirs=True):
''' Return a list of paths to search for plugins in '''
# FIXME: This is potentially buggy if subdirs is sometimes True and sometimes False.
# In current usage, everything calls this with subdirs=True except for module_utils_loader and ansible-doc
# which always calls it with subdirs=False. So there currently isn't a problem with this caching.
if self._paths is not None:
return self._paths
ret = self._extra_dirs[:]
# look in any configured plugin paths, allow one level deep for subcategories
if self.config is not None:
for path in self.config:
path = os.path.realpath(os.path.expanduser(path))
if subdirs:
contents = glob.glob("%s/*" % path) + glob.glob("%s/*/*" % path)
for c in contents:
if os.path.isdir(c) and c not in ret:
ret.append(c)
if path not in ret:
ret.append(path)
# look for any plugins installed in the package subtree
# Note package path always gets added last so that every other type of
# path is searched before it.
ret.extend(self._get_package_paths(subdirs=subdirs))
# HACK: because powershell modules are in the same directory
# hierarchy as other modules we have to process them last. This is
# because powershell only works on windows but the other modules work
# anywhere (possibly including windows if the correct language
# interpreter is installed). the non-powershell modules can have any
# file extension and thus powershell modules are picked up in that.
# The non-hack way to fix this is to have powershell modules be
# a different PluginLoader/ModuleLoader. But that requires changing
# other things too (known thing to change would be PATHS_CACHE,
# PLUGIN_PATHS_CACHE, and MODULE_CACHE. Since those three dicts key
# on the class_name and neither regular modules nor powershell modules
# would have class_names, they would not work as written.
reordered_paths = []
win_dirs = []
for path in ret:
if path.endswith('windows'):
win_dirs.append(path)
else:
reordered_paths.append(path)
reordered_paths.extend(win_dirs)
# cache and return the result
self._paths = reordered_paths
return reordered_paths
def _load_config_defs(self, name, path):
''' 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))
def add_directory(self, directory, with_subdir=False):
''' Adds an additional directory to the search path '''
directory = os.path.realpath(directory)
if directory is not None:
if with_subdir:
directory = os.path.join(directory, self.subdir)
if directory not in self._extra_dirs:
# append the directory and invalidate the path cache
self._extra_dirs.append(directory)
self._paths = None
def find_plugin(self, name, mod_type='', ignore_deprecated=False):
''' Find a plugin named name '''
if mod_type:
suffix = mod_type
elif self.class_name:
# Ansible plugins that run in the controller process (most plugins)
suffix = '.py'
else:
# Only Ansible Modules. Ansible modules can be any executable so
# they can have any suffix
suffix = ''
# The particular cache to look for modules within. This matches the
# requested mod_type
pull_cache = self._plugin_path_cache[suffix]
try:
return pull_cache[name]
except KeyError:
# Cache miss. Now let's find the plugin
pass
# TODO: Instead of using the self._paths cache (PATH_CACHE) and
# self._searched_paths we could use an iterator. Before enabling that
# we need to make sure we don't want to add additional directories
# (add_directory()) once we start using the iterator. Currently, it
# looks like _get_paths() never forces a cache refresh so if we expect
# additional directories to be added later, it is buggy.
for path in (p for p in self._get_paths() if p not in self._searched_paths and os.path.isdir(p)):
try:
full_paths = (os.path.join(path, f) for f in os.listdir(path))
except OSError as e:
display.warning("Error accessing plugin paths: %s" % to_text(e))
for full_path in (f for f in full_paths if os.path.isfile(f) and not f.endswith('__init__.py')):
full_name = os.path.basename(full_path)
# HACK: We have no way of executing python byte compiled files as ansible modules so specifically exclude them
# FIXME: I believe this is only correct for modules and module_utils.
# For all other plugins we want .pyc and .pyo should be valid
if full_path.endswith(('.pyc', '.pyo')):
continue
splitname = os.path.splitext(full_name)
base_name = splitname[0]
try:
extension = splitname[1]
except IndexError:
extension = ''
# Module found, now enter it into the caches that match this file
if base_name not in self._plugin_path_cache['']:
self._plugin_path_cache[''][base_name] = full_path
if full_name not in self._plugin_path_cache['']:
self._plugin_path_cache[''][full_name] = full_path
if base_name not in self._plugin_path_cache[extension]:
self._plugin_path_cache[extension][base_name] = full_path
if full_name not in self._plugin_path_cache[extension]:
self._plugin_path_cache[extension][full_name] = full_path
self._searched_paths.add(path)
try:
return pull_cache[name]
except KeyError:
# Didn't find the plugin in this directory. Load modules from the next one
pass
# if nothing is found, try finding alias/deprecated
if not name.startswith('_'):
alias_name = '_' + name
# We've already cached all the paths at this point
if alias_name in pull_cache:
if not ignore_deprecated and not os.path.islink(pull_cache[alias_name]):
# FIXME: this is not always the case, some are just aliases
display.deprecated('%s is kept for backwards compatibility but usage is discouraged. '
'The module documentation details page may explain more about this rationale.' % name.lstrip('_'))
return pull_cache[alias_name]
return None
def has_plugin(self, name):
''' Checks if a plugin named name exists '''
return self.find_plugin(name) is not None
__contains__ = has_plugin
def _load_module_source(self, name, path):
# avoid collisions across plugins
full_name = '.'.join([self.package, name])
if full_name in sys.modules:
# Avoids double loading, See https://github.com/ansible/ansible/issues/13110
return sys.modules[full_name]
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
with open(path, 'rb') as module_file:
module = imp.load_source(full_name, path, module_file)
return module
def _update_object(self, obj, name, path):
# load plugin config data
self._load_config_defs(name, path)
# set extra info on the module, in case we want it later
setattr(obj, '_original_path', path)
setattr(obj, '_load_name', name)
def get(self, name, *args, **kwargs):
''' instantiates a plugin of the given name using arguments '''
found_in_cache = True
class_only = kwargs.pop('class_only', False)
if name in self.aliases:
name = self.aliases[name]
path = self.find_plugin(name)
if path is None:
return None
if path not in self._module_cache:
self._module_cache[path] = self._load_module_source(name, path)
found_in_cache = False
obj = getattr(self._module_cache[path], self.class_name)
if self.base_class:
# The import path is hardcoded and should be the right place,
# so we are not expecting an ImportError.
module = __import__(self.package, fromlist=[self.base_class])
# Check whether this obj has the required base class.
try:
plugin_class = getattr(module, self.base_class)
except AttributeError:
return None
if not issubclass(obj, plugin_class):
return None
self._display_plugin_load(self.class_name, name, self._searched_paths, path, found_in_cache=found_in_cache, class_only=class_only)
if not class_only:
try:
obj = obj(*args, **kwargs)
except TypeError as e:
if "abstract" in e.args[0]:
# Abstract Base Class. The found plugin file does not
# fully implement the defined interface.
return None
raise
self._update_object(obj, name, path)
return obj
def _display_plugin_load(self, class_name, name, searched_paths, path, found_in_cache=None, class_only=None):
msg = 'Loading %s \'%s\' from %s' % (class_name, os.path.basename(name), path)
if len(searched_paths) > 1:
msg = '%s (searched paths: %s)' % (msg, self.format_paths(searched_paths))
if found_in_cache or class_only:
msg = '%s (found_in_cache=%s, class_only=%s)' % (msg, found_in_cache, class_only)
display.debug(msg)
def all(self, *args, **kwargs):
''' instantiates all plugins with the same arguments '''
path_only = kwargs.pop('path_only', False)
class_only = kwargs.pop('class_only', False)
all_matches = []
found_in_cache = True
for i in self._get_paths():
all_matches.extend(glob.glob(os.path.join(i, "*.py")))
for path in sorted(all_matches, key=lambda match: os.path.basename(match)):
name = os.path.basename(os.path.splitext(path)[0])
if '__init__' in name:
continue
if path_only:
yield path
continue
if path not in self._module_cache:
self._module_cache[path] = self._load_module_source(name, path)
found_in_cache = False
try:
obj = getattr(self._module_cache[path], self.class_name)
except AttributeError as e:
display.warning("Skipping plugin (%s) as it seems to be invalid: %s" % (path, to_text(e)))
continue
if self.base_class:
# The import path is hardcoded and should be the right place,
# so we are not expecting an ImportError.
module = __import__(self.package, fromlist=[self.base_class])
# Check whether this obj has the required base class.
try:
plugin_class = getattr(module, self.base_class)
except AttributeError:
continue
if not issubclass(obj, plugin_class):
continue
self._display_plugin_load(self.class_name, name, self._searched_paths, path, found_in_cache=found_in_cache, class_only=class_only)
if not class_only:
try:
obj = obj(*args, **kwargs)
except TypeError as e:
display.warning("Skipping plugin (%s) as it seems to be incomplete: %s" % (path, to_text(e)))
self._update_object(obj, name, path)
yield obj
action_loader = PluginLoader(
'ActionModule',
'ansible.plugins.action',
C.DEFAULT_ACTION_PLUGIN_PATH,
'action_plugins',
required_base_class='ActionBase',
)
cache_loader = PluginLoader(
'CacheModule',
'ansible.plugins.cache',
C.DEFAULT_CACHE_PLUGIN_PATH,
'cache_plugins',
)
callback_loader = PluginLoader(
'CallbackModule',
'ansible.plugins.callback',
C.DEFAULT_CALLBACK_PLUGIN_PATH,
'callback_plugins',
)
connection_loader = PluginLoader(
'Connection',
'ansible.plugins.connection',
C.DEFAULT_CONNECTION_PLUGIN_PATH,
'connection_plugins',
aliases={'paramiko': 'paramiko_ssh'},
required_base_class='ConnectionBase',
)
shell_loader = PluginLoader(
'ShellModule',
'ansible.plugins.shell',
'shell_plugins',
'shell_plugins',
)
module_loader = PluginLoader(
'',
'ansible.modules',
C.DEFAULT_MODULE_PATH,
'library',
)
module_utils_loader = PluginLoader(
'',
'ansible.module_utils',
C.DEFAULT_MODULE_UTILS_PATH,
'module_utils',
)
# NB: dedicated loader is currently necessary because PS module_utils expects "with subdir" lookup where
# regular module_utils doesn't. This can be revisited once we have more granular loaders.
ps_module_utils_loader = PluginLoader(
'',
'ansible.module_utils',
C.DEFAULT_MODULE_UTILS_PATH,
'module_utils',
)
lookup_loader = PluginLoader(
'LookupModule',
'ansible.plugins.lookup',
C.DEFAULT_LOOKUP_PLUGIN_PATH,
'lookup_plugins',
required_base_class='LookupBase',
)
filter_loader = PluginLoader(
'FilterModule',
'ansible.plugins.filter',
C.DEFAULT_FILTER_PLUGIN_PATH,
'filter_plugins',
)
test_loader = PluginLoader(
'TestModule',
'ansible.plugins.test',
C.DEFAULT_TEST_PLUGIN_PATH,
'test_plugins'
)
fragment_loader = PluginLoader(
'ModuleDocFragment',
'ansible.utils.module_docs_fragments',
os.path.join(os.path.dirname(__file__), 'module_docs_fragments'),
'',
)
strategy_loader = PluginLoader(
'StrategyModule',
'ansible.plugins.strategy',
C.DEFAULT_STRATEGY_PLUGIN_PATH,
'strategy_plugins',
required_base_class='StrategyBase',
)
terminal_loader = PluginLoader(
'TerminalModule',
'ansible.plugins.terminal',
'terminal_plugins',
'terminal_plugins'
)
vars_loader = PluginLoader(
'VarsModule',
'ansible.plugins.vars',
C.DEFAULT_VARS_PLUGIN_PATH,
'vars_plugins',
)
cliconf_loader = PluginLoader(
'Cliconf',
'ansible.plugins.cliconf',
'cliconf_plugins',
'cliconf_plugins',
required_base_class='CliconfBase'
)
netconf_loader = PluginLoader(
'Netconf',
'ansible.plugins.netconf',
'netconf_plugins',
'netconf_plugins',
required_base_class='NetconfBase'
)

@ -29,20 +29,24 @@ DOCUMENTATION:
description: description:
- the list of keys to lookup on the etcd server - the list of keys to lookup on the etcd server
type: list type: list
element_type: string elements: string
required: True required: True
_etcd_url: _etcd_url:
description: description:
- Environment variable with the url for the etcd server - Environment variable with the url for the etcd server
default: 'http://127.0.0.1:4001' default: 'http://127.0.0.1:4001'
env_vars: env:
- name: ANSIBLE_ETCD_URL - name: ANSIBLE_ETCD_URL
yaml:
- key: etcd.url
_etcd_version: _etcd_version:
description: description:
- Environment variable with the etcd protocol version - Environment variable with the etcd protocol version
default: 'v1' default: 'v1'
env_vars: env:
- name: ANSIBLE_ETCD_VERSION - name: ANSIBLE_ETCD_VERSION
yaml:
- key: etcd.version
EXAMPLES: EXAMPLES:
- name: "a value from a locally running etcd" - name: "a value from a locally running etcd"
debug: msg={{ lookup('etcd', 'foo/bar') }} debug: msg={{ lookup('etcd', 'foo/bar') }}
@ -50,10 +54,11 @@ EXAMPLES:
- name: "a values from a folder on a locally running etcd" - name: "a values from a folder on a locally running etcd"
debug: msg={{ lookup('etcd', 'foo') }} debug: msg={{ lookup('etcd', 'foo') }}
RETURN: RETURN:
_list: _raw:
description: description:
- list of values associated with input keys - list of values associated with input keys
type: strings type: list
elements: strings
''' '''
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type

@ -40,7 +40,7 @@ from ansible.playbook.helpers import load_list_of_blocks
from ansible.playbook.included_file import IncludedFile from ansible.playbook.included_file import IncludedFile
from ansible.playbook.task_include import TaskInclude from ansible.playbook.task_include import TaskInclude
from ansible.playbook.role_include import IncludeRole from ansible.playbook.role_include import IncludeRole
from ansible.plugins import action_loader, connection_loader, filter_loader, lookup_loader, module_loader, test_loader from ansible.plugins.loader import action_loader, connection_loader, filter_loader, lookup_loader, module_loader, test_loader
from ansible.template import Templar from ansible.template import Templar
from ansible.utils.vars import combine_vars from ansible.utils.vars import combine_vars
from ansible.vars.manager import strip_internal_keys from ansible.vars.manager import strip_internal_keys
@ -899,6 +899,7 @@ class StrategyBase:
msg = "ending play" msg = "ending play"
elif meta_action == 'reset_connection': elif meta_action == 'reset_connection':
connection = connection_loader.get(play_context.connection, play_context, os.devnull) connection = connection_loader.get(play_context.connection, play_context, os.devnull)
play_context.set_options_from_plugin(connection)
if connection: if connection:
connection.reset() connection.reset()
msg = 'reset connection' msg = 'reset connection'

@ -34,7 +34,7 @@ import time
from ansible import constants as C from ansible import constants as C
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.playbook.included_file import IncludedFile from ansible.playbook.included_file import IncludedFile
from ansible.plugins import action_loader from ansible.plugins.loader import action_loader
from ansible.plugins.strategy import StrategyBase from ansible.plugins.strategy import StrategyBase
from ansible.template import Templar from ansible.template import Templar
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text

@ -38,7 +38,7 @@ from ansible.module_utils._text import to_text
from ansible.playbook.block import Block from ansible.playbook.block import Block
from ansible.playbook.included_file import IncludedFile from ansible.playbook.included_file import IncludedFile
from ansible.playbook.task import Task from ansible.playbook.task import Task
from ansible.plugins import action_loader from ansible.plugins.loader import action_loader
from ansible.plugins.strategy import StrategyBase from ansible.plugins.strategy import StrategyBase
from ansible.template import Templar from ansible.template import Templar

@ -45,7 +45,7 @@ from ansible import constants as C
from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleUndefinedVariable from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleUndefinedVariable
from ansible.module_utils.six import string_types, text_type from ansible.module_utils.six import string_types, text_type
from ansible.module_utils._text import to_native, to_text, to_bytes from ansible.module_utils._text import to_native, to_text, to_bytes
from ansible.plugins import filter_loader, lookup_loader, test_loader from ansible.plugins.loader import filter_loader, lookup_loader, test_loader
from ansible.template.safe_eval import safe_eval from ansible.template.safe_eval import safe_eval
from ansible.template.template import AnsibleJ2Template from ansible.template.template import AnsibleJ2Template
from ansible.template.vars import AnsibleJ2Vars from ansible.template.vars import AnsibleJ2Vars

@ -24,7 +24,7 @@ import sys
from ansible import constants as C from ansible import constants as C
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.module_utils.six.moves import builtins from ansible.module_utils.six.moves import builtins
from ansible.plugins import filter_loader, test_loader from ansible.plugins.loader import filter_loader, test_loader
def safe_eval(expr, locals={}, include_exceptions=False): def safe_eval(expr, locals={}, include_exceptions=False):

@ -20,15 +20,12 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import ast
import yaml
from collections import MutableMapping, MutableSet, MutableSequence from collections import MutableMapping, MutableSet, MutableSequence
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.parsing.metadata import extract_metadata from ansible.parsing.plugin_docs import read_docstring
from ansible.parsing.yaml.loader import AnsibleLoader from ansible.parsing.yaml.loader import AnsibleLoader
from ansible.plugins import fragment_loader from ansible.plugins.loader import fragment_loader
try: try:
from __main__ import display from __main__ import display
@ -93,94 +90,13 @@ def add_fragments(doc, filename):
def get_docstring(filename, verbose=False): def get_docstring(filename, verbose=False):
""" """
Search for assignment of the DOCUMENTATION and EXAMPLES variables DOCUMENTATION can be extended using documentation fragments loaded by the PluginLoader from the module_docs_fragments directory.
in the given file.
Parse DOCUMENTATION from YAML and return the YAML doc or None
together with EXAMPLES, as plain text.
DOCUMENTATION can be extended using documentation fragments
loaded by the PluginLoader from the module_docs_fragments
directory.
""" """
# FIXME: Should refactor this so that we have a docstring parsing data = read_docstring(filename, verbose=verbose)
# function and a separate variable parsing function
# Can have a function one higher that invokes whichever is needed
#
# Should look roughly like this:
# get_plugin_doc(filename, verbose=False)
# documentation = extract_docstring(plugin_ast, identifier, verbose=False)
# if not documentation and not (filter or test):
# documentation = extract_variables(plugin_ast)
# documentation['metadata'] = extract_metadata(plugin_ast)
data = {
'doc': None,
'plainexamples': None,
'returndocs': None,
'metadata': None
}
string_to_vars = {
'DOCUMENTATION': 'doc',
'EXAMPLES': 'plainexamples',
'RETURN': 'returndocs',
}
try:
b_module_data = open(filename, 'rb').read()
M = ast.parse(b_module_data)
try:
display.debug('Attempt first docstring is yaml docs')
docstring = yaml.load(M.body[0].value.s)
for string in string_to_vars.keys():
if string in docstring:
data[string_to_vars[string]] = docstring[string]
display.debug('assigned :%s' % string_to_vars[string])
except Exception as e:
display.debug('failed docstring parsing: %s' % str(e))
if 'docs' not in data or not data['docs']:
display.debug('Fallback to vars parsing')
for child in M.body:
if isinstance(child, ast.Assign):
for t in child.targets:
try:
theid = t.id
except AttributeError:
# skip errors can happen when trying to use the normal code
display.warning("Failed to assign id for %s on %s, skipping" % (t, filename))
continue
if theid in string_to_vars:
varkey = string_to_vars[theid]
if isinstance(child.value, ast.Dict):
data[varkey] = ast.literal_eval(child.value)
else:
if theid == 'DOCUMENTATION':
# string should be yaml
data[varkey] = AnsibleLoader(child.value.s, file_name=filename).get_single_data()
else:
# not yaml, should be a simple string
data[varkey] = child.value.s
display.debug('assigned :%s' % varkey)
# Metadata is per-file rather than per-plugin/function
data['metadata'] = extract_metadata(module_ast=M)[0]
# add fragments to documentation # add fragments to documentation
if data['doc']: if data.get('doc', False):
add_fragments(data['doc'], filename) add_fragments(data['doc'], filename)
# remove version
if data['metadata']:
for x in ('version', 'metadata_version'):
if x in data['metadata']:
del data['metadata'][x]
except Exception as e:
display.error("unable to parse %s" % filename)
if verbose is True:
display.display("unable to parse %s" % filename)
raise
return data['doc'], data['plainexamples'], data['returndocs'], data['metadata'] return data['doc'], data['plainexamples'], data['returndocs'], data['metadata']

@ -37,7 +37,7 @@ from ansible.inventory.host import Host
from ansible.inventory.helpers import sort_groups, get_group_vars from ansible.inventory.helpers import sort_groups, get_group_vars
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.module_utils.six import iteritems, string_types, text_type from ansible.module_utils.six import iteritems, string_types, text_type
from ansible.plugins import lookup_loader, vars_loader from ansible.plugins.loader import lookup_loader, vars_loader
from ansible.plugins.cache import FactCache from ansible.plugins.cache import FactCache
from ansible.template import Templar from ansible.template import Templar
from ansible.utils.listify import listify_lookup_plugin_terms from ansible.utils.listify import listify_lookup_plugin_terms
@ -86,6 +86,21 @@ def strip_internal_keys(dirty):
return clean return clean
def remove_internal_keys(data):
'''
More nuanced version of strip_internal_keys
'''
for key in list(data.keys()):
if (key.startswith('_ansible_') and key != '_ansible_parsed') or key in C.INTERNAL_RESULT_KEYS:
display.warning("Removed unexpected internal key in module return: %s = %s" % (key, data[key]))
del data[key]
# remove bad/empty internal keys
for key in ['warnings', 'deprecations']:
if key in data and not data[key]:
del data[key]
class VariableManager: class VariableManager:
def __init__(self, loader=None, inventory=None): def __init__(self, loader=None, inventory=None):

@ -190,8 +190,7 @@ setup(
'galaxy/data/*/*/.*', 'galaxy/data/*/*/.*',
'galaxy/data/*/*/*.*', 'galaxy/data/*/*/*.*',
'galaxy/data/*/tests/inventory', 'galaxy/data/*/tests/inventory',
'config/data/*.yaml', 'config/base.yml',
'config/data/*.yml',
], ],
}, },
classifiers=[ classifiers=[

@ -21,20 +21,19 @@ def ansible_environment(args, color=True):
if not path.startswith(ansible_path + os.pathsep): if not path.startswith(ansible_path + os.pathsep):
path = ansible_path + os.pathsep + path path = ansible_path + os.pathsep + path
ansible_config = '/dev/null'
if os.path.isfile('test/integration/%s.cfg' % args.command):
ansible_config = os.path.abspath('test/integration/%s.cfg' % args.command)
ansible = dict( ansible = dict(
ANSIBLE_FORCE_COLOR='%s' % 'true' if args.color and color else 'false', ANSIBLE_FORCE_COLOR='%s' % 'true' if args.color and color else 'false',
ANSIBLE_DEPRECATION_WARNINGS='false', ANSIBLE_DEPRECATION_WARNINGS='false',
ANSIBLE_CONFIG=ansible_config,
ANSIBLE_HOST_KEY_CHECKING='false', ANSIBLE_HOST_KEY_CHECKING='false',
PYTHONPATH=os.path.abspath('lib'), PYTHONPATH=os.path.abspath('lib'),
PAGER='/bin/cat', PAGER='/bin/cat',
PATH=path, PATH=path,
) )
if os.path.isfile('test/integration/%s.cfg' % args.command):
ansible_config = os.path.abspath('test/integration/%s.cfg' % args.command)
ansible['ANSIBLE_CONFIG'] = ansible_config
env.update(ansible) env.update(ansible)
if args.debug: if args.debug:

@ -1,4 +1,5 @@
lib/ansible/cli/config.py lib/ansible/cli/config.py
lib/ansible/constants.py
lib/ansible/config/data.py lib/ansible/config/data.py
lib/ansible/config/manager.py lib/ansible/config/manager.py
lib/ansible/modules/cloud/amazon/_ec2_ami_search.py lib/ansible/modules/cloud/amazon/_ec2_ami_search.py

@ -24,7 +24,7 @@ from ansible.compat.tests.mock import patch, MagicMock
from ansible.errors import AnsibleError, AnsibleParserError from ansible.errors import AnsibleError, AnsibleParserError
from ansible.executor.task_executor import TaskExecutor from ansible.executor.task_executor import TaskExecutor
from ansible.playbook.play_context import PlayContext from ansible.playbook.play_context import PlayContext
from ansible.plugins import action_loader, lookup_loader from ansible.plugins.loader import action_loader, lookup_loader
from ansible.parsing.yaml.objects import AnsibleUnicode from ansible.parsing.yaml.objects import AnsibleUnicode
from units.mock.loader import DictDataLoader from units.mock.loader import DictDataLoader

@ -16,14 +16,20 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
import pytest import pytest
import boto3
import os import os
import json import json
import collections import collections
from . placebo_fixtures import placeboify, maybe_sleep from . placebo_fixtures import placeboify, maybe_sleep
from nose.plugins.skip import SkipTest
from ansible.modules.cloud.amazon import data_pipeline from ansible.modules.cloud.amazon import data_pipeline
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
try:
import boto3
except ImportError:
raise SkipTest("test_api_gateway.py requires the `boto3` and `botocore` modules")
@pytest.fixture(scope='module') @pytest.fixture(scope='module')
def dp_setup(): def dp_setup():

@ -3,6 +3,7 @@ import json
from ansible.compat.tests import unittest from ansible.compat.tests import unittest
from ansible.compat.tests.mock import PropertyMock, patch, mock_open from ansible.compat.tests.mock import PropertyMock, patch, mock_open
from ansible.module_utils import basic from ansible.module_utils import basic
from ansible.module_utils._text import to_native
from ansible.module_utils.six.moves import xmlrpc_client from ansible.module_utils.six.moves import xmlrpc_client
from ansible.modules.packaging.os import rhn_register from ansible.modules.packaging.os import rhn_register
@ -96,7 +97,7 @@ class TestRhnRegister(unittest.TestCase):
orig_import = __import__ orig_import = __import__
with patch('__builtin__.__import__', side_effect=mock_import): with patch('__builtin__.__import__', side_effect=mock_import):
rhn = self.module.Rhn() rhn = self.module.Rhn()
self.assertEqual('123456789', rhn.systemid) self.assertEqual('123456789', to_native(rhn.systemid))
def test_without_required_parameters(self): def test_without_required_parameters(self):
"""Failure must occurs when all parameters are missing""" """Failure must occurs when all parameters are missing"""

@ -20,6 +20,11 @@ __metaclass__ = type
from ansible.compat.tests import unittest from ansible.compat.tests import unittest
from ansible.plugins.filter.ipaddr import (ipaddr, _netmask_query, nthhost, next_nth_usable, from ansible.plugins.filter.ipaddr import (ipaddr, _netmask_query, nthhost, next_nth_usable,
previous_nth_usable, network_in_usable, network_in_network) previous_nth_usable, network_in_usable, network_in_network)
try:
import netaddr
except ImportError:
from nose.plugins.skip import SkipTest
raise SkipTest("This test requires the `netaddr` python library")
class TestIpFilter(unittest.TestCase): class TestIpFilter(unittest.TestCase):

@ -29,7 +29,7 @@ from ansible.compat.tests.mock import mock_open, patch
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.module_utils.six import text_type from ansible.module_utils.six import text_type
from ansible.module_utils.six.moves import builtins from ansible.module_utils.six.moves import builtins
from ansible.plugins import PluginLoader from ansible.plugins.loader import PluginLoader
from ansible.plugins.lookup import password from ansible.plugins.lookup import password
from ansible.utils import encrypt from ansible.utils import encrypt

@ -24,7 +24,7 @@ import os
from ansible.compat.tests import BUILTINS, unittest from ansible.compat.tests import BUILTINS, unittest
from ansible.compat.tests.mock import mock_open, patch, MagicMock from ansible.compat.tests.mock import mock_open, patch, MagicMock
from ansible.plugins import MODULE_CACHE, PATH_CACHE, PLUGIN_PATH_CACHE, PluginLoader from ansible.plugins.loader import MODULE_CACHE, PATH_CACHE, PLUGIN_PATH_CACHE, PluginLoader
class TestErrors(unittest.TestCase): class TestErrors(unittest.TestCase):

Loading…
Cancel
Save