diff --git a/lib/ansible/executor/connection_info.py b/lib/ansible/executor/connection_info.py index 9e91cd09eaf..bf78cf63a5b 100644 --- a/lib/ansible/executor/connection_info.py +++ b/lib/ansible/executor/connection_info.py @@ -29,6 +29,20 @@ from ansible.errors import AnsibleError __all__ = ['ConnectionInformation'] +# the magic variable mapping dictionary below is used to translate +# host/inventory variables to fields in the ConnectionInformation +# object. The dictionary values are tuples, to account for aliases +# in variable names. + +MAGIC_VARIABLE_MAPPING = dict( + connection = ('ansible_connection',), + remote_addr = ('ansible_ssh_host', 'ansible_host'), + remote_user = ('ansible_ssh_user', 'ansible_user'), + port = ('ansible_ssh_port', 'ansible_port'), + password = ('ansible_ssh_pass', 'ansible_password'), + private_key_file = ('ansible_ssh_private_key_file', 'ansible_private_key_file'), + shell = ('ansible_shell_type',), +) class ConnectionInformation: @@ -51,6 +65,7 @@ class ConnectionInformation: self.port = None self.private_key_file = C.DEFAULT_PRIVATE_KEY_FILE self.timeout = C.DEFAULT_TIMEOUT + self.shell = None # privilege escalation self.become = None @@ -170,7 +185,7 @@ class ConnectionInformation: else: setattr(self, field, value) - def set_task_override(self, task): + def set_task_and_host_override(self, task, host): ''' Sets attributes from the task if they are set, which will override those from the play. @@ -179,12 +194,22 @@ class ConnectionInformation: new_info = ConnectionInformation() new_info.copy(self) + # loop through a subset of attributes on the task object and set + # connection fields based on their values for attr in ('connection', 'remote_user', 'become', 'become_user', 'become_pass', 'become_method', 'environment', 'no_log'): if hasattr(task, attr): attr_val = getattr(task, attr) if attr_val: setattr(new_info, attr, attr_val) + # finally, use the MAGIC_VARIABLE_MAPPING dictionary to update this + # connection info object with 'magic' variables from inventory + variables = host.get_vars() + for (attr, variable_names) in MAGIC_VARIABLE_MAPPING.iteritems(): + for variable_name in variable_names: + if variable_name in variables: + setattr(new_info, attr, variables[variable_name]) + return new_info def make_become_cmd(self, cmd, executable, become_settings=None): diff --git a/lib/ansible/executor/process/worker.py b/lib/ansible/executor/process/worker.py index d8e8960fe40..e1488ebcb1d 100644 --- a/lib/ansible/executor/process/worker.py +++ b/lib/ansible/executor/process/worker.py @@ -111,7 +111,7 @@ class WorkerProcess(multiprocessing.Process): # apply the given task's information to the connection info, # which may override some fields already set by the play or # the options specified on the command line - new_connection_info = connection_info.set_task_override(task) + new_connection_info = connection_info.set_task_and_host_override(task=task, host=host) # execute the task and build a TaskResult from the result debug("running TaskExecutor() for %s/%s" % (host, task)) diff --git a/lib/ansible/plugins/__init__.py b/lib/ansible/plugins/__init__.py index 36b5c3d0334..8d23ae796cb 100644 --- a/lib/ansible/plugins/__init__.py +++ b/lib/ansible/plugins/__init__.py @@ -55,9 +55,10 @@ class PluginLoader: The first match is used. ''' - def __init__(self, class_name, package, config, subdir, aliases={}): + 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.config = config self.subdir = subdir @@ -87,11 +88,12 @@ class PluginLoader: 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) + 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()) @@ -102,6 +104,7 @@ class PluginLoader: return dict( class_name = self.class_name, + base_class = self.base_class, package = self.package, config = self.config, subdir = self.subdir, @@ -268,9 +271,13 @@ class PluginLoader: self._module_cache[path] = imp.load_source('.'.join([self.package, name]), path) if kwargs.get('class_only', False): - return getattr(self._module_cache[path], self.class_name) + obj = getattr(self._module_cache[path], self.class_name) else: - return getattr(self._module_cache[path], self.class_name)(*args, **kwargs) + obj = getattr(self._module_cache[path], self.class_name)(*args, **kwargs) + if self.base_class and self.base_class not in [base.__name__ for base in obj.__class__.__bases__]: + return None + + return obj def all(self, *args, **kwargs): ''' instantiates all plugins with the same arguments ''' @@ -291,6 +298,9 @@ class PluginLoader: else: obj = getattr(self._module_cache[path], self.class_name)(*args, **kwargs) + if self.base_class and self.base_class not in [base.__name__ for base in obj.__class__.__bases__]: + continue + # set extra info on the module, in case we want it later setattr(obj, '_original_path', path) yield obj @@ -299,21 +309,22 @@ action_loader = PluginLoader( 'ActionModule', 'ansible.plugins.action', C.DEFAULT_ACTION_PLUGIN_PATH, - 'action_plugins' + 'action_plugins', + required_base_class='ActionBase', ) cache_loader = PluginLoader( 'CacheModule', 'ansible.plugins.cache', C.DEFAULT_CACHE_PLUGIN_PATH, - 'cache_plugins' + 'cache_plugins', ) callback_loader = PluginLoader( 'CallbackModule', 'ansible.plugins.callback', C.DEFAULT_CALLBACK_PLUGIN_PATH, - 'callback_plugins' + 'callback_plugins', ) connection_loader = PluginLoader( @@ -321,7 +332,8 @@ connection_loader = PluginLoader( 'ansible.plugins.connections', C.DEFAULT_CONNECTION_PLUGIN_PATH, 'connection_plugins', - aliases={'paramiko': 'paramiko_ssh'} + aliases={'paramiko': 'paramiko_ssh'}, + required_base_class='ConnectionBase', ) shell_loader = PluginLoader( @@ -335,28 +347,29 @@ module_loader = PluginLoader( '', 'ansible.modules', C.DEFAULT_MODULE_PATH, - 'library' + 'library', ) lookup_loader = PluginLoader( 'LookupModule', 'ansible.plugins.lookup', C.DEFAULT_LOOKUP_PLUGIN_PATH, - 'lookup_plugins' + 'lookup_plugins', + required_base_class='LookupBase', ) vars_loader = PluginLoader( 'VarsModule', 'ansible.plugins.vars', C.DEFAULT_VARS_PLUGIN_PATH, - 'vars_plugins' + 'vars_plugins', ) filter_loader = PluginLoader( 'FilterModule', 'ansible.plugins.filter', C.DEFAULT_FILTER_PLUGIN_PATH, - 'filter_plugins' + 'filter_plugins', ) fragment_loader = PluginLoader( @@ -371,4 +384,5 @@ strategy_loader = PluginLoader( 'ansible.plugins.strategies', None, 'strategy_plugins', + required_base_class='StrategyBase', ) diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index 83c129687ec..d6861118b2f 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -34,6 +34,7 @@ from ansible.parsing.utils.jsonify import jsonify from ansible.plugins import shell_loader from ansible.utils.debug import debug +from ansible.utils.unicode import to_bytes class ActionBase: @@ -51,21 +52,21 @@ class ActionBase: self._loader = loader self._templar = templar self._shared_loader_obj = shared_loader_obj - self._shell = self.get_shell() + + # load the shell plugin for this action/connection + if self._connection_info.shell: + shell_type = self._connection_info.shell + elif hasattr(connection, '_shell'): + shell_type = getattr(connection, '_shell') + else: + shell_type = os.path.basename(C.DEFAULT_EXECUTABLE) + + self._shell = shell_loader.get(shell_type) + if not self._shell: + raise AnsibleError("Invalid shell type specified (%s), or the plugin for that shell type is missing." % shell_type) self._supports_check_mode = True - def get_shell(self): - - if hasattr(self._connection, '_shell'): - shell_plugin = getattr(self._connection, '_shell', '') - else: - shell_plugin = shell_loader.get(os.path.basename(C.DEFAULT_EXECUTABLE)) - if shell_plugin is None: - shell_plugin = shell_loader.get('sh') - - return shell_plugin - def _configure_module(self, module_name, module_args): ''' Handles the loading and templating of the module code through the @@ -201,18 +202,13 @@ class ActionBase: Copies the module data out to the temporary module path. ''' - if type(data) == dict: + if isinstance(data, dict): data = jsonify(data) afd, afile = tempfile.mkstemp() afo = os.fdopen(afd, 'w') try: - # FIXME: is this still necessary? - #if not isinstance(data, unicode): - # #ensure the data is valid UTF-8 - # data = data.decode('utf-8') - #else: - # data = data.encode('utf-8') + data = to_bytes(data, errors='strict') afo.write(data) except Exception as e: #raise AnsibleError("failure encoding into utf-8: %s" % str(e)) diff --git a/lib/ansible/vars/__init__.py b/lib/ansible/vars/__init__.py index 040c2244483..4cf10709b93 100644 --- a/lib/ansible/vars/__init__.py +++ b/lib/ansible/vars/__init__.py @@ -212,9 +212,13 @@ class VariableManager: # FIXME: make sure all special vars are here # Finally, we create special vars - if host and self._inventory is not None: - hostvars = HostVars(vars_manager=self, inventory=self._inventory, loader=loader) - all_vars['hostvars'] = hostvars + + if host: + all_vars['groups'] = [group.name for group in host.get_groups()] + + if self._inventory is not None: + hostvars = HostVars(vars_manager=self, inventory=self._inventory, loader=loader) + all_vars['hostvars'] = hostvars if self._inventory is not None: all_vars['inventory_dir'] = self._inventory.basedir()