Fix for user defined modules not overriding modules from core.
This fix takes into account that powershell modules are somewhat different than regular modules and have to be kept separate.
This commit is contained in:
parent
f61fb9787d
commit
18e2ee16ef
4 changed files with 128 additions and 61 deletions
|
@ -26,10 +26,9 @@ import inspect
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from ansible import constants as C
|
from ansible import constants as C
|
||||||
from ansible.utils.unicode import to_unicode
|
|
||||||
from ansible import errors
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from __main__ import display
|
from __main__ import display
|
||||||
|
@ -37,6 +36,7 @@ except ImportError:
|
||||||
from ansible.utils.display import Display
|
from ansible.utils.display import Display
|
||||||
display = Display()
|
display = Display()
|
||||||
|
|
||||||
|
# Global so that all instances of a PluginLoader will share the caches
|
||||||
MODULE_CACHE = {}
|
MODULE_CACHE = {}
|
||||||
PATH_CACHE = {}
|
PATH_CACHE = {}
|
||||||
PLUGIN_PATH_CACHE = {}
|
PLUGIN_PATH_CACHE = {}
|
||||||
|
@ -68,7 +68,7 @@ class PluginLoader:
|
||||||
if not class_name in PATH_CACHE:
|
if not class_name in PATH_CACHE:
|
||||||
PATH_CACHE[class_name] = None
|
PATH_CACHE[class_name] = None
|
||||||
if not class_name in PLUGIN_PATH_CACHE:
|
if not class_name in PLUGIN_PATH_CACHE:
|
||||||
PLUGIN_PATH_CACHE[class_name] = {}
|
PLUGIN_PATH_CACHE[class_name] = defaultdict(dict)
|
||||||
|
|
||||||
self._module_cache = MODULE_CACHE[class_name]
|
self._module_cache = MODULE_CACHE[class_name]
|
||||||
self._paths = PATH_CACHE[class_name]
|
self._paths = PATH_CACHE[class_name]
|
||||||
|
@ -169,9 +169,30 @@ class PluginLoader:
|
||||||
# look for any plugins installed in the package subtree
|
# look for any plugins installed in the package subtree
|
||||||
ret.extend(self._get_package_paths())
|
ret.extend(self._get_package_paths())
|
||||||
|
|
||||||
|
# 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
|
# cache and return the result
|
||||||
self._paths = ret
|
self._paths = reordered_paths
|
||||||
return ret
|
return reordered_paths
|
||||||
|
|
||||||
|
|
||||||
def add_directory(self, directory, with_subdir=False):
|
def add_directory(self, directory, with_subdir=False):
|
||||||
|
@ -187,55 +208,90 @@ class PluginLoader:
|
||||||
self._extra_dirs.append(directory)
|
self._extra_dirs.append(directory)
|
||||||
self._paths = None
|
self._paths = None
|
||||||
|
|
||||||
def find_plugin(self, name, suffixes=None):
|
def find_plugin(self, name, mod_type=''):
|
||||||
''' Find a plugin named name '''
|
''' Find a plugin named name '''
|
||||||
|
|
||||||
if not suffixes:
|
# The particular cache to look for modules within. This matches the
|
||||||
if self.class_name:
|
# requested mod_type
|
||||||
suffixes = ['.py']
|
pull_cache = self._plugin_path_cache[mod_type]
|
||||||
|
try:
|
||||||
|
return pull_cache[name]
|
||||||
|
except KeyError:
|
||||||
|
# Cache miss. Now let's find the plugin
|
||||||
|
pass
|
||||||
|
|
||||||
|
if mod_type:
|
||||||
|
suffix = mod_type
|
||||||
|
elif self.class_name:
|
||||||
|
# Ansible plugins that run in the controller process (most plugins)
|
||||||
|
suffix = '.py'
|
||||||
else:
|
else:
|
||||||
suffixes = ['.py', '']
|
# Only Ansible Modules. Ansible modules can be any executable so
|
||||||
|
# they can have any suffix
|
||||||
|
suffix = ''
|
||||||
|
|
||||||
potential_names = frozenset('%s%s' % (name, s) for s in suffixes)
|
### FIXME:
|
||||||
for full_name in potential_names:
|
# Instead of using the self._paths cache (PATH_CACHE) and
|
||||||
if full_name in self._plugin_path_cache:
|
# self._searched_paths we could use an iterator. Before enabling that
|
||||||
return self._plugin_path_cache[full_name]
|
# we need to make sure we don't want to add additional directories
|
||||||
|
# (add_directory()) once we start using the iterator. Currently, it
|
||||||
found = None
|
# looks like _get_paths() never forces a cache refresh so if we expect
|
||||||
for path in [p for p in self._get_paths() if p not in self._searched_paths]:
|
# additional directories to be added later, it is buggy.
|
||||||
if os.path.isdir(path):
|
for path in (p for p in self._get_paths() if p not in self._searched_paths and os.path.isdir(p)):
|
||||||
try:
|
try:
|
||||||
full_paths = (os.path.join(path, f) for f in os.listdir(path))
|
full_paths = (os.path.join(path, f) for f in os.listdir(path))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
display.warning("Error accessing plugin paths: %s" % str(e))
|
display.warning("Error accessing plugin paths: %s" % str(e))
|
||||||
for full_path in (f for f in full_paths if os.path.isfile(f)):
|
|
||||||
for suffix in suffixes:
|
for full_path in (f for f in full_paths if os.path.isfile(f) and not f.endswith('__init__.py')):
|
||||||
if full_path.endswith(suffix):
|
|
||||||
full_name = os.path.basename(full_path)
|
full_name = os.path.basename(full_path)
|
||||||
break
|
|
||||||
else: # Yes, this is a for-else: http://bit.ly/1ElPkyg
|
# HACK: We have no way of executing python byte
|
||||||
|
# compiled files as ansible modules so specifically exclude them
|
||||||
|
if full_path.endswith(('.pyc', '.pyo')):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if full_name not in self._plugin_path_cache:
|
splitname = os.path.splitext(full_name)
|
||||||
self._plugin_path_cache[full_name] = full_path
|
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)
|
self._searched_paths.add(path)
|
||||||
for full_name in potential_names:
|
try:
|
||||||
if full_name in self._plugin_path_cache:
|
return pull_cache[name]
|
||||||
return self._plugin_path_cache[full_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 nothing is found, try finding alias/deprecated
|
||||||
if not name.startswith('_'):
|
if not name.startswith('_'):
|
||||||
for alias_name in ('_%s' % n for n in potential_names):
|
alias_name = '_' + name
|
||||||
# We've already cached all the paths at this point
|
# We've already cached all the paths at this point
|
||||||
if alias_name in self._plugin_path_cache:
|
if alias_name in pull_cache:
|
||||||
if not os.path.islink(self._plugin_path_cache[alias_name]):
|
if not os.path.islink(pull_cache[alias_name]):
|
||||||
display.deprecated('%s is kept for backwards compatibility '
|
display.deprecated('%s is kept for backwards compatibility '
|
||||||
'but usage is discouraged. The module '
|
'but usage is discouraged. The module '
|
||||||
'documentation details page may explain '
|
'documentation details page may explain '
|
||||||
'more about this rationale.' %
|
'more about this rationale.' %
|
||||||
name.lstrip('_'))
|
name.lstrip('_'))
|
||||||
return self._plugin_path_cache[alias_name]
|
return pull_cache[alias_name]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -69,30 +69,36 @@ class ActionBase:
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Search module path(s) for named module.
|
# Search module path(s) for named module.
|
||||||
module_suffixes = getattr(self._connection, 'default_suffixes', None)
|
for mod_type in self._connection.module_implementation_preferences:
|
||||||
|
|
||||||
# Check to determine if PowerShell modules are supported, and apply
|
# Check to determine if PowerShell modules are supported, and apply
|
||||||
# some fixes (hacks) to module name + args.
|
# some fixes (hacks) to module name + args.
|
||||||
if module_suffixes and '.ps1' in module_suffixes:
|
if mod_type == '.ps1':
|
||||||
# Use Windows versions of stat/file/copy modules when called from
|
# win_stat, win_file, and win_copy are not just like their
|
||||||
# within other action plugins.
|
# python counterparts but they are compatible enough for our
|
||||||
|
# internal usage
|
||||||
if module_name in ('stat', 'file', 'copy') and self._task.action != module_name:
|
if module_name in ('stat', 'file', 'copy') and self._task.action != module_name:
|
||||||
module_name = 'win_%s' % module_name
|
module_name = 'win_%s' % module_name
|
||||||
|
|
||||||
# Remove extra quotes surrounding path parameters before sending to module.
|
# Remove extra quotes surrounding path parameters before sending to module.
|
||||||
if module_name in ('win_stat', 'win_file', 'win_copy', 'slurp') and module_args and hasattr(self._connection._shell, '_unquote'):
|
if module_name in ('win_stat', 'win_file', 'win_copy', 'slurp') and module_args and hasattr(self._connection._shell, '_unquote'):
|
||||||
for key in ('src', 'dest', 'path'):
|
for key in ('src', 'dest', 'path'):
|
||||||
if key in module_args:
|
if key in module_args:
|
||||||
module_args[key] = self._connection._shell._unquote(module_args[key])
|
module_args[key] = self._connection._shell._unquote(module_args[key])
|
||||||
|
|
||||||
module_path = self._shared_loader_obj.module_loader.find_plugin(module_name, module_suffixes)
|
module_path = self._shared_loader_obj.module_loader.find_plugin(module_name, mod_type)
|
||||||
if module_path is None:
|
if module_path:
|
||||||
|
break
|
||||||
|
else: # This is a for-else: http://bit.ly/1ElPkyg
|
||||||
|
# FIXME: Why is it necessary to look for the windows version?
|
||||||
|
# Shouldn't all modules be installed?
|
||||||
|
#
|
||||||
# Use Windows version of ping module to check module paths when
|
# Use Windows version of ping module to check module paths when
|
||||||
# using a connection that supports .ps1 suffixes.
|
# using a connection that supports .ps1 suffixes.
|
||||||
if module_suffixes and '.ps1' in module_suffixes:
|
if '.ps1' in self._connection.module_implementation_preferences:
|
||||||
ping_module = 'win_ping'
|
ping_module = 'win_ping'
|
||||||
else:
|
else:
|
||||||
ping_module = 'ping'
|
ping_module = 'ping'
|
||||||
module_path2 = self._shared_loader_obj.module_loader.find_plugin(ping_module, module_suffixes)
|
module_path2 = self._shared_loader_obj.module_loader.find_plugin(ping_module, self._connection.module_implementation_preferences)
|
||||||
if module_path2 is not None:
|
if module_path2 is not None:
|
||||||
raise AnsibleError("The module %s was not found in configured module paths" % (module_name))
|
raise AnsibleError("The module %s was not found in configured module paths" % (module_name))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -57,6 +57,10 @@ class ConnectionBase(with_metaclass(ABCMeta, object)):
|
||||||
|
|
||||||
has_pipelining = False
|
has_pipelining = False
|
||||||
become_methods = C.BECOME_METHODS
|
become_methods = C.BECOME_METHODS
|
||||||
|
# When running over this connection type, prefer modules written in a certain language
|
||||||
|
# as discovered by the specified file extension. An empty string as the
|
||||||
|
# language means any language.
|
||||||
|
module_implementation_preferences = ('',)
|
||||||
|
|
||||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||||
# All these hasattrs allow subclasses to override these parameters
|
# All these hasattrs allow subclasses to override these parameters
|
||||||
|
|
|
@ -52,10 +52,11 @@ from ansible.utils.unicode import to_bytes, to_unicode
|
||||||
class Connection(ConnectionBase):
|
class Connection(ConnectionBase):
|
||||||
'''WinRM connections over HTTP/HTTPS.'''
|
'''WinRM connections over HTTP/HTTPS.'''
|
||||||
|
|
||||||
|
module_implementation_preferences = ('.ps1', '')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
self.has_pipelining = False
|
self.has_pipelining = False
|
||||||
self.default_suffixes = ['.ps1', '']
|
|
||||||
self.protocol = None
|
self.protocol = None
|
||||||
self.shell_id = None
|
self.shell_id = None
|
||||||
self.delegate = None
|
self.delegate = None
|
||||||
|
|
Loading…
Reference in a new issue