From bbd6b8bb42cc8fa48157ce5db0efb0a836c94917 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 16 Jan 2018 00:15:04 -0500 Subject: [PATCH] Temporary (#31677) * allow shells to have per host options, remote_tmp added language to shell removed module lang setting from general as plugins have it now use get to avoid bad powershell plugin more resilient tmp discovery, fall back to `pwd` add shell to docs fixed options for when frags are only options added shell set ops in t_e and fixed option frags normalize tmp dir usag4e - pass tmpdir/tmp/temp options as env var to commands, making it default for tempfile - adjusted ansiballz tmpdir - default local tempfile usage to the configured local tmp - set env temp in action add options to powershell shift temporary to internal envvar/params ensure tempdir is set if we pass var ensure basic and url use expected tempdir ensure localhost uses local tmp give /var/tmp priority, less perms issues more consistent tempfile mgmt for ansiballz made async_dir configurable better action handling, allow for finally rm tmp fixed tmp issue and no more tempdir in ballz hostvarize world readable and admin users always set shell tempdir added comment to discourage use of exception/flow control * Mostly revert expand_user as it's not quite working. This was an additional feature anyhow. Kept the use of pwd as a fallback but moved it to a second ssh connection. This is not optimal but getting that to work in a single ssh connection was part of the problem holding this up. (cherry picked from commit 395b714120522f15e4c90a346f5e8e8d79213aca) * fixed script and other action plugins ensure tmpdir deletion allow for connections that don't support new options (legacy, 3rd party) fixed tests --- docs/bin/plugin_formatter.py | 3 +- .../developing_modules_best_practices.rst | 5 +- lib/ansible/cli/console.py | 6 +- lib/ansible/cli/doc.py | 16 +- lib/ansible/config/base.yml | 42 --- lib/ansible/constants.py | 3 +- lib/ansible/errors/__init__.py | 33 +- lib/ansible/executor/task_executor.py | 10 + lib/ansible/inventory/data.py | 1 + lib/ansible/module_utils/basic.py | 78 ++--- lib/ansible/module_utils/urls.py | 3 + lib/ansible/modules/files/copy.py | 42 +-- .../modules/utilities/logic/async_status.py | 7 +- .../modules/utilities/logic/async_wrapper.py | 4 +- lib/ansible/playbook/play_context.py | 2 - lib/ansible/plugins/action/__init__.py | 125 ++++---- lib/ansible/plugins/action/assemble.py | 112 ++++--- lib/ansible/plugins/action/command.py | 4 + lib/ansible/plugins/action/copy.py | 68 ++-- lib/ansible/plugins/action/fetch.py | 298 +++++++++--------- lib/ansible/plugins/action/normal.py | 22 +- lib/ansible/plugins/action/package.py | 43 +-- lib/ansible/plugins/action/patch.py | 56 ++-- lib/ansible/plugins/action/script.py | 153 +++++---- lib/ansible/plugins/action/service.py | 57 ++-- lib/ansible/plugins/action/shell.py | 7 +- lib/ansible/plugins/action/template.py | 199 ++++++------ lib/ansible/plugins/action/unarchive.py | 155 ++++----- lib/ansible/plugins/action/win_copy.py | 3 +- lib/ansible/plugins/connection/__init__.py | 6 +- lib/ansible/plugins/loader.py | 31 +- lib/ansible/plugins/shell/__init__.py | 81 ++--- lib/ansible/plugins/shell/csh.py | 30 +- lib/ansible/plugins/shell/fish.py | 34 +- lib/ansible/plugins/shell/powershell.py | 40 +-- lib/ansible/plugins/shell/sh.py | 45 ++- .../module_docs_fragments/constructed.py | 19 +- .../module_docs_fragments/shell_common.py | 92 ++++++ lib/ansible/utils/plugin_docs.py | 11 +- test/integration/targets/copy/tasks/main.yml | 5 +- .../integration/targets/script/tasks/main.yml | 4 +- test/sanity/validate-modules/main.py | 5 +- test/units/plugins/action/test_action.py | 17 +- test/units/plugins/action/test_synchronize.py | 5 + 44 files changed, 1010 insertions(+), 972 deletions(-) create mode 100644 lib/ansible/utils/module_docs_fragments/shell_common.py diff --git a/docs/bin/plugin_formatter.py b/docs/bin/plugin_formatter.py index 2bd7267e461..603290e5077 100755 --- a/docs/bin/plugin_formatter.py +++ b/docs/bin/plugin_formatter.py @@ -49,6 +49,7 @@ from six import iteritems, string_types from ansible.errors import AnsibleError from ansible.module_utils._text import to_bytes +from ansible.plugins.loader import fragment_loader from ansible.utils import plugin_docs from ansible.utils.display import Display @@ -235,7 +236,7 @@ def get_plugin_info(module_dir, limit_to=None, verbose=False): primary_category = module_categories[0] # use ansible core library to parse out doc metadata YAML and plaintext examples - doc, examples, returndocs, metadata = plugin_docs.get_docstring(module_path, verbose=verbose) + doc, examples, returndocs, metadata = plugin_docs.get_docstring(module_path, fragment_loader, verbose=verbose) # save all the information module_info[module] = {'path': module_path, diff --git a/docs/docsite/rst/dev_guide/developing_modules_best_practices.rst b/docs/docsite/rst/dev_guide/developing_modules_best_practices.rst index 74ac9bd8439..8ab766c7db5 100644 --- a/docs/docsite/rst/dev_guide/developing_modules_best_practices.rst +++ b/docs/docsite/rst/dev_guide/developing_modules_best_practices.rst @@ -22,7 +22,7 @@ and guidelines: * In the event of failure, a key of 'failed' should be included, along with a string explanation in 'msg'. Modules that raise tracebacks (stacktraces) are generally considered 'poor' modules, though Ansible can deal with these returns and will automatically convert anything unparseable into a failed result. If you are using the AnsibleModule common Python code, the 'failed' element will be included for you automatically when you call 'fail_json'. -* Return codes from modules are actually not significant, but continue on with 0=success and non-zero=failure for reasons of future proofing. +* Return codes from modules are used if 'failed' is missing, 0=success and non-zero=failure. * As results from many hosts will be aggregated at once, modules should return only relevant output. Returning the entire contents of a log file is generally bad form. @@ -194,5 +194,4 @@ Avoid creating a module that does the work of other modules; this leads to code Avoid creating 'caches'. Ansible is designed without a central server or authority, so you cannot guarantee it will not run with different permissions, options or locations. If you need a central authority, have it on top of Ansible (for example, using bastion/cm/ci server or tower); do not try to build it into modules. -Always use the hacking/test-module script when developing modules and it will warn -you about these kind of things. +Always use the hacking/test-module script when developing modules and it will warn you about these kind of things. diff --git a/lib/ansible/cli/console.py b/lib/ansible/cli/console.py index c26ddea0e72..afe767bec5f 100644 --- a/lib/ansible/cli/console.py +++ b/lib/ansible/cli/console.py @@ -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.parsing.splitter import parse_kv from ansible.playbook.play import Play -from ansible.plugins.loader import module_loader +from ansible.plugins.loader import module_loader, fragment_loader from ansible.utils import plugin_docs from ansible.utils.color import stringc @@ -356,7 +356,7 @@ class ConsoleCLI(CLI, cmd.Cmd): if module_name in self.modules: in_path = module_loader.find_plugin(module_name) if in_path: - oc, a, _, _ = plugin_docs.get_docstring(in_path) + oc, a, _, _ = plugin_docs.get_docstring(in_path, fragment_loader) if oc: display.display(oc['short_description']) display.display('Parameters:') @@ -388,7 +388,7 @@ class ConsoleCLI(CLI, cmd.Cmd): def module_args(self, module_name): in_path = module_loader.find_plugin(module_name) - oc, a, _, _ = plugin_docs.get_docstring(in_path) + oc, a, _, _ = plugin_docs.get_docstring(in_path, fragment_loader) return list(oc['options'].keys()) def run(self): diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py index dba5efa262b..e9f3c2d5a36 100644 --- a/lib/ansible/cli/doc.py +++ b/lib/ansible/cli/doc.py @@ -29,8 +29,9 @@ from ansible.module_utils._text import to_native from ansible.module_utils.six import string_types from ansible.parsing.yaml.dumper import AnsibleDumper from ansible.plugins.loader import module_loader, action_loader, lookup_loader, callback_loader, cache_loader, \ - vars_loader, connection_loader, strategy_loader, inventory_loader -from ansible.utils import plugin_docs + vars_loader, connection_loader, strategy_loader, inventory_loader, shell_loader, fragment_loader +from ansible.utils.plugin_docs import BLACKLIST, get_docstring + try: from __main__ import display except ImportError: @@ -71,7 +72,7 @@ class DocCLI(CLI): help='**For internal testing only** Show documentation for all plugins.') self.parser.add_option("-t", "--type", action="store", default='module', dest='type', type='choice', help='Choose which plugin type (defaults to "module")', - choices=['cache', 'callback', 'connection', 'inventory', 'lookup', 'module', 'strategy', 'vars']) + choices=['cache', 'callback', 'connection', 'inventory', 'lookup', 'module', 'shell', 'strategy', 'vars']) super(DocCLI, self).parse() @@ -101,6 +102,8 @@ class DocCLI(CLI): loader = vars_loader elif plugin_type == 'inventory': loader = inventory_loader + elif plugin_type == 'shell': + loader = shell_loader else: loader = module_loader @@ -146,7 +149,6 @@ class DocCLI(CLI): # process command line list text = '' for plugin in self.args: - try: # 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, check_aliases=True) @@ -158,7 +160,7 @@ class DocCLI(CLI): continue try: - doc, plainexamples, returndocs, metadata = plugin_docs.get_docstring(filename, verbose=(self.options.verbosity > 0)) + doc, plainexamples, returndocs, metadata = get_docstring(filename, fragment_loader, verbose=(self.options.verbosity > 0)) except: display.vvv(traceback.format_exc()) display.error("%s %s has a documentation error formatting or is missing documentation." % (plugin_type, plugin)) @@ -229,7 +231,7 @@ class DocCLI(CLI): plugin = os.path.splitext(plugin)[0] # removes the extension plugin = plugin.lstrip('_') # remove underscore from deprecated plugins - if plugin not in plugin_docs.BLACKLIST.get(bkey, ()): + if plugin not in BLACKLIST.get(bkey, ()): self.plugin_list.add(plugin) display.vvvv("Added %s" % plugin) @@ -254,7 +256,7 @@ class DocCLI(CLI): doc = None try: - doc, plainexamples, returndocs, metadata = plugin_docs.get_docstring(filename) + doc, plainexamples, returndocs, metadata = get_docstring(filename, fragment_loader) except: display.warning("%s has a documentation formatting error" % plugin) diff --git a/lib/ansible/config/base.yml b/lib/ansible/config/base.yml index 7cc21a61aae..a9f38658e32 100644 --- a/lib/ansible/config/base.yml +++ b/lib/ansible/config/base.yml @@ -1,18 +1,6 @@ # Copyright (c) 2017 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) --- -ALLOW_WORLD_READABLE_TMPFILES: - name: Allow world readable temporary files - default: False - description: - - This makes the temporary files created on the machine to be world readable and will issue a warning instead of failing the task. - - It is useful when becoming an unprivileged user. - env: [] - ini: - - {key: allow_world_readable_tmpfiles, section: defaults} - type: boolean - yaml: {key: defaults.allow_world_readable_tmpfiles} - version_added: "2.1" ANSIBLE_COW_SELECTION: name: Cowsay filter selection default: default @@ -744,15 +732,6 @@ DEFAULT_MODULE_COMPRESSION: - {key: module_compression, section: defaults} # vars: # - name: ansible_module_compression -DEFAULT_MODULE_LANG: - name: Target language environment - default: "{{CONTROLER_LANG}}" - description: "Language locale setting to use for modules when they execute on the target, if empty it defaults to 'en_US.UTF-8'" - env: [{name: ANSIBLE_MODULE_LANG}] - ini: - - {key: module_lang, section: defaults} -# vars: -# - name: ansible_module_lang DEFAULT_MODULE_NAME: name: Default adhoc module default: command @@ -768,16 +747,6 @@ DEFAULT_MODULE_PATH: ini: - {key: library, section: defaults} type: pathspec -DEFAULT_MODULE_SET_LOCALE: - name: Target locale - default: False - description: Controls if we set locale for modules when executing on the target. - env: [{name: ANSIBLE_MODULE_SET_LOCALE}] - ini: - - {key: module_set_locale, section: defaults} - type: boolean -# vars: -# - name: ansible_module_locale DEFAULT_MODULE_UTILS_PATH: name: Module Utils Path description: Colon separated paths in which Ansible will search for Module utils files, which are shared by modules. @@ -851,17 +820,6 @@ DEFAULT_REMOTE_PORT: - {key: remote_port, section: defaults} type: integer yaml: {key: defaults.remote_port} -DEFAULT_REMOTE_TMP: - name: Target temporary directory - default: ~/.ansible/tmp - description: - - Temporary directory to use on targets when executing tasks. - - In some cases Ansible may still choose to use a system temporary dir to avoid permission issues. - env: [{name: ANSIBLE_REMOTE_TEMP}] - ini: - - {key: remote_tmp, section: defaults} - vars: - - name: ansible_remote_tmp DEFAULT_REMOTE_USER: name: Login/Remote User default: diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index 48f57d6068b..ccb819fdac4 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -5,7 +5,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -import os # used to set lang and for backwards compat get_config +import os from ast import literal_eval from jinja2 import Template @@ -114,7 +114,6 @@ MAGIC_VARIABLE_MAPPING = dict( 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'), diff --git a/lib/ansible/errors/__init__.py b/lib/ansible/errors/__init__.py index 1a971f89003..73dd4d0c3ef 100644 --- a/lib/ansible/errors/__init__.py +++ b/lib/ansible/errors/__init__.py @@ -252,11 +252,38 @@ class AnsibleFileNotFound(AnsibleRuntimeError): suppress_extended_error=suppress_extended_error, orig_exc=orig_exc) -class AnsibleActionSkip(AnsibleRuntimeError): +# These Exceptions are temporary, using them as flow control until we can get a better solution. +# DO NOT USE as they will probably be removed soon. +class AnsibleAction(AnsibleRuntimeError): + ''' Base Exception for Action plugin flow control ''' + + def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): + + super(AnsibleAction, self).__init__(message=message, obj=obj, show_content=show_content, + suppress_extended_error=suppress_extended_error, orig_exc=orig_exc) + if result is None: + self.result = {} + else: + self.result = result + + +class AnsibleActionSkip(AnsibleAction): ''' an action runtime skip''' - pass + + def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): + super(AnsibleActionSkip, self).__init__(message=message, obj=obj, show_content=show_content, + suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result) + self.result.update({'skipped': True, 'msg': message}) -class AnsibleActionFail(AnsibleRuntimeError): +class AnsibleActionFail(AnsibleAction): ''' an action runtime failure''' + def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): + super(AnsibleActionFail, self).__init__(message=message, obj=obj, show_content=show_content, + suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result) + self.result.update({'failed': True, 'msg': message}) + + +class AnsibleActionDone(AnsibleAction): + ''' an action runtime early exit''' pass diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py index 9adc7e13523..8efc61cdea3 100644 --- a/lib/ansible/executor/task_executor.py +++ b/lib/ansible/executor/task_executor.py @@ -486,6 +486,7 @@ class TaskExecutor: self._connection._play_context = self._play_context self._set_connection_options(variables, templar) + self._set_shell_options(variables, templar) # get handler self._handler = self._get_action_handler(connection=self._connection, templar=templar) @@ -774,6 +775,15 @@ class TaskExecutor: # set options with 'templated vars' specific to this plugin self._connection.set_options(var_options=options) + self._set_shell_options(final_vars, templar) + + def _set_shell_options(self, variables, templar): + option_vars = C.config.get_plugin_vars('shell', self._connection._shell._load_name) + options = {} + for k in option_vars: + if k in variables: + options[k] = templar.template(variables[k]) + self._connection._shell.set_options(var_options=options) def _get_action_handler(self, connection, templar): ''' diff --git a/lib/ansible/inventory/data.py b/lib/ansible/inventory/data.py index 04308a0ed91..e7674eb7dbb 100644 --- a/lib/ansible/inventory/data.py +++ b/lib/ansible/inventory/data.py @@ -97,6 +97,7 @@ class InventoryData(object): 'You can correct this by setting ansible_python_interpreter for localhost') new_host.set_variable("ansible_python_interpreter", py_interp) new_host.set_variable("ansible_connection", 'local') + new_host.set_variable("ansible_remote_tmp", C.DEFAULT_LOCAL_TMP) self.localhost = new_host diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py index c42c753b106..20fcdc9b1d5 100644 --- a/lib/ansible/module_utils/basic.py +++ b/lib/ansible/module_utils/basic.py @@ -37,9 +37,25 @@ FILE_ATTRIBUTES = { 'Z': 'compresseddirty', } -# ansible modules can be written in any language. To simplify -# development of Python modules, the functions available here can -# be used to do many common tasks +PASS_VARS = { + 'check_mode': 'check_mode', + 'debug': '_debug', + 'diff': '_diff', + 'module_name': '_name', + 'no_log': 'no_log', + 'selinux_special_fs': '_selinux_special_fs', + 'shell_executable': '_shell', + 'socket': '_socket_path', + 'syslog_facility': '_syslog_facility', + 'verbosity': '_verbosity', + 'version': 'ansible_version', +} + +PASS_BOOLS = ('no_log', 'debug', 'diff') + +# Ansible modules can be written in any language. +# The functions available here can be used to do many common tasks, +# to simplify development of Python modules. import locale import os @@ -90,7 +106,7 @@ NoneType = type(None) try: from collections.abc import KeysView SEQUENCETYPE = (Sequence, frozenset, KeysView) -except: +except ImportError: SEQUENCETYPE = (Sequence, frozenset) try: @@ -826,11 +842,12 @@ class AnsibleModule(object): self._clean = {} self.aliases = {} - self._legal_inputs = ['_ansible_check_mode', '_ansible_no_log', '_ansible_debug', '_ansible_diff', '_ansible_verbosity', - '_ansible_selinux_special_fs', '_ansible_module_name', '_ansible_version', '_ansible_syslog_facility', - '_ansible_socket', '_ansible_shell_executable'] + self._legal_inputs = ['_ansible_%s' % k for k in PASS_VARS] self._options_context = list() + # set tempdir to remote tmp + self.tempdir = os.environ.get('ANSIBLE_REMOTE_TEMP', None) + if add_file_common_args: for k, v in FILE_COMMON_ARGUMENTS.items(): if k not in self.argument_spec: @@ -1634,44 +1651,17 @@ class AnsibleModule(object): for (k, v) in list(param.items()): - if k == '_ansible_check_mode' and v: - self.check_mode = True - - elif k == '_ansible_no_log': - self.no_log = self.boolean(v) - - elif k == '_ansible_debug': - self._debug = self.boolean(v) - - elif k == '_ansible_diff': - self._diff = self.boolean(v) - - elif k == '_ansible_verbosity': - self._verbosity = v - - elif k == '_ansible_selinux_special_fs': - self._selinux_special_fs = v - - elif k == '_ansible_syslog_facility': - self._syslog_facility = v - - elif k == '_ansible_version': - self.ansible_version = v - - elif k == '_ansible_module_name': - self._name = v - - elif k == '_ansible_socket': - self._socket_path = v - - elif k == '_ansible_shell_executable' and v: - self._shell = v - - elif check_invalid_arguments and k not in legal_inputs: + if check_invalid_arguments and k not in legal_inputs: unsupported_parameters.add(k) + elif k.startswith('_ansible_'): + # handle setting internal properties from internal ansible vars + key = k.replace('_ansible_', '') + if key in PASS_BOOLS: + setattr(self, PASS_VARS[key], self.boolean(v)) + else: + setattr(self, PASS_VARS[key], v) - # clean up internal params: - if k.startswith('_ansible_'): + # clean up internal params: del self.params[k] if unsupported_parameters: @@ -2202,7 +2192,7 @@ class AnsibleModule(object): except: # we don't have access to the cwd, probably because of sudo. # Try and move to a neutral location to prevent errors - for cwd in [os.path.expandvars('$HOME'), tempfile.gettempdir()]: + for cwd in [self.tempdir, os.path.expandvars('$HOME'), tempfile.gettempdir()]: try: if os.access(cwd, os.F_OK | os.R_OK): os.chdir(cwd) diff --git a/lib/ansible/module_utils/urls.py b/lib/ansible/module_utils/urls.py index 46b79a85b22..c7e03f9c9ca 100644 --- a/lib/ansible/module_utils/urls.py +++ b/lib/ansible/module_utils/urls.py @@ -973,6 +973,9 @@ def fetch_url(module, url, data=None, headers=None, method=None, if not HAS_URLPARSE: module.fail_json(msg='urlparse is not installed') + # ensure we use proper tempdir + tempfile.tempdir = module.tempdir + # Get validate_certs from the module params validate_certs = module.params.get('validate_certs', True) diff --git a/lib/ansible/modules/files/copy.py b/lib/ansible/modules/files/copy.py index b1230f2fe29..b9df3933741 100644 --- a/lib/ansible/modules/files/copy.py +++ b/lib/ansible/modules/files/copy.py @@ -97,32 +97,32 @@ notes: ''' EXAMPLES = r''' -# Example from Ansible Playbooks -- copy: +- name: example copying file with owner and permissions + copy: src: /srv/myfiles/foo.conf dest: /etc/foo.conf owner: foo group: foo mode: 0644 -# The same example as above, but using a symbolic mode equivalent to 0644 -- copy: +- name: The same example as above, but using a symbolic mode equivalent to 0644 + copy: src: /srv/myfiles/foo.conf dest: /etc/foo.conf owner: foo group: foo mode: u=rw,g=r,o=r -# Another symbolic mode example, adding some permissions and removing others -- copy: +- name: Another symbolic mode example, adding some permissions and removing others + copy: src: /srv/myfiles/foo.conf dest: /etc/foo.conf owner: foo group: foo mode: u+rw,g-wx,o-rwx -# Copy a new "ntp.conf file into place, backing up the original if it differs from the copied version -- copy: +- name: Copy a new "ntp.conf file into place, backing up the original if it differs from the copied version + copy: src: /mine/ntp.conf dest: /etc/ntp.conf owner: root @@ -130,33 +130,23 @@ EXAMPLES = r''' mode: 0644 backup: yes -# Copy a new "sudoers" file into place, after passing validation with visudo -- copy: +- name: Copy a new "sudoers" file into place, after passing validation with visudo + copy: src: /mine/sudoers dest: /etc/sudoers validate: /usr/sbin/visudo -cf %s -# Copy a "sudoers" file on the remote machine for editing -- copy: +- name: Copy a "sudoers" file on the remote machine for editing + copy: src: /etc/sudoers dest: /etc/sudoers.edit remote_src: yes validate: /usr/sbin/visudo -cf %s -# Create a CSV file from your complete inventory using an inline template -- hosts: all - tasks: - - copy: - content: | - HOSTNAME;IPADDRESS;FQDN;OSNAME;OSVERSION;PROCESSOR;ARCHITECTURE;MEMORY; - {% for host in hostvars %} - {% set vars = hostvars[host|string] %} - {{ vars.ansible_hostname }};{{ vars.remote_host }};{{ vars.ansible_fqdn }};{{ vars.ansible_distribution }};{{ vars.ansible_distribution_version }};{{ vars.ansible_processor[1] }};{{ vars.ansible_architecture }};{{ (vars.ansible_memtotal_mb/1024)|round|int }}; # NOQA - {% endfor %} - dest: /some/path/systems.csv - backup: yes - run_once: yes - delegate_to: localhost +- name: Copy using the 'content' for inline data + copy: + content: '# This file was moved to /etc/other.conf' + dest: /etc/mine.conf' ''' RETURN = r''' diff --git a/lib/ansible/modules/utilities/logic/async_status.py b/lib/ansible/modules/utilities/logic/async_status.py index 70ea812228f..8f76e680e88 100644 --- a/lib/ansible/modules/utilities/logic/async_status.py +++ b/lib/ansible/modules/utilities/logic/async_status.py @@ -28,8 +28,7 @@ options: required: true mode: description: - - if C(status), obtain the status; if C(cleanup), clean up the async job cache - located in C(~/.ansible_async/) for the specified job I(jid). + - if C(status), obtain the status; if C(cleanup), clean up the async job cache (by default in C(~/.ansible_async/)) for the specified job I(jid). choices: [ "status", "cleanup" ] default: "status" notes: @@ -57,8 +56,10 @@ def main(): mode = module.params['mode'] jid = module.params['jid'] + async_dir = os.environ.get('ANSIBLE_ASYNC_DIR', '~/.ansible_async') + # setup logging directory - logdir = os.path.expanduser("~/.ansible_async") + logdir = os.path.expanduser(async_dir) log_path = os.path.join(logdir, jid) if not os.path.exists(log_path): diff --git a/lib/ansible/modules/utilities/logic/async_wrapper.py b/lib/ansible/modules/utilities/logic/async_wrapper.py index fb3a7aca8cb..bbe4c28f63a 100644 --- a/lib/ansible/modules/utilities/logic/async_wrapper.py +++ b/lib/ansible/modules/utilities/logic/async_wrapper.py @@ -216,8 +216,10 @@ if __name__ == '__main__': cmd = wrapped_module step = 5 + async_dir = os.environ.get('ANSIBLE_ASYNC_DIR', '~/.ansible_async') + # setup job output directory - jobdir = os.path.expanduser("~/.ansible_async") + jobdir = os.path.expanduser(async_dir) job_path = os.path.join(jobdir, jid) if not os.path.exists(jobdir): diff --git a/lib/ansible/playbook/play_context.py b/lib/ansible/playbook/play_context.py index 636abe656f0..f6793b41e66 100644 --- a/lib/ansible/playbook/play_context.py +++ b/lib/ansible/playbook/play_context.py @@ -49,7 +49,6 @@ except ImportError: __all__ = ['PlayContext'] - # TODO: needs to be configurable b_SU_PROMPT_LOCALIZATIONS = [ to_bytes('Password'), @@ -136,7 +135,6 @@ class PlayContext(Base): # connection fields, some are inherited from Base: # (connection, port, remote_user, environment, no_log) _remote_addr = FieldAttribute(isa='string') - _remote_tmp_dir = FieldAttribute(isa='string', default=C.DEFAULT_REMOTE_TMP) _password = FieldAttribute(isa='string') _timeout = FieldAttribute(isa='int', default=C.DEFAULT_TIMEOUT) _connection_user = FieldAttribute(isa='string') diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index e4cad73945d..8160e13ce09 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -1,19 +1,6 @@ -# (c) 2012-2014, Michael DeHaan -# -# 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 . +# Copyright: (c) 2012-2014, Michael DeHaan +# Copyright: (c) 2018, 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) @@ -65,13 +52,14 @@ class ActionBase(with_metaclass(ABCMeta, object)): self._loader = loader self._templar = templar self._shared_loader_obj = shared_loader_obj - # Backwards compat: self._display isn't really needed, just import the global display and use that. - self._display = display self._cleanup_remote_tmp = False self._supports_check_mode = True self._supports_async = False + # Backwards compat: self._display isn't really needed, just import the global display and use that. + self._display = display + @abstractmethod def run(self, tmp=None, task_vars=None): """ Action Plugins should implement this method to perform their @@ -99,6 +87,11 @@ class ActionBase(with_metaclass(ABCMeta, object)): elif self._task.async_val and self._play_context.check_mode: raise AnsibleActionFail('check mode and async cannot be used on same task.') + if not tmp and self._early_needs_tmp_path(): + self._make_tmp_path() + else: + self._connection._shell.tempdir = tmp + return result def _remote_file_exists(self, path): @@ -236,16 +229,20 @@ class ActionBase(with_metaclass(ABCMeta, object)): if remote_user is None: remote_user = self._play_context.remote_user + try: + admin_users = self._connection._shell.get_option('admin_users') + [remote_user] + except KeyError: + admin_users = ['root', remote_user] # plugin does not support admin_users + try: + remote_tmp = self._connection._shell.get_option('remote_temp') + except KeyError: + remote_tmp = '~/ansible' + + # deal with tmpdir creation basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48)) - use_system_tmp = False - - if self._play_context.become and self._play_context.become_user not in ('root', remote_user): - use_system_tmp = True - - tmp_mode = 0o700 - tmpdir = self._remote_expand_user(self._play_context.remote_tmp_dir, sudoable=False) - - cmd = self._connection._shell.mkdtemp(basefile, use_system_tmp, tmp_mode, tmpdir) + use_system_tmp = bool(self._play_context.become and self._play_context.become_user not in admin_users) + tmpdir = self._remote_expand_user(remote_tmp, sudoable=False) + cmd = self._connection._shell.mkdtemp(basefile=basefile, system=use_system_tmp, tmpdir=tmpdir) result = self._low_level_execute_command(cmd, sudoable=False) # error handling on this seems a little aggressive? @@ -287,11 +284,14 @@ class ActionBase(with_metaclass(ABCMeta, object)): if rc == '/': raise AnsibleError('failed to resolve remote temporary directory from %s: `%s` returned empty string' % (basefile, cmd)) + self._connection._shell.tempdir = rc + + if not use_system_tmp: + self._connection._shell.env.update({'ANSIBLE_REMOTE_TEMP': self._connection._shell.tempdir}) return rc def _should_remove_tmp_path(self, tmp_path): '''Determine if temporary path should be deleted or kept by user request/config''' - return tmp_path and self._cleanup_remote_tmp and not C.DEFAULT_KEEP_REMOTE_FILES and "-tmp-" in tmp_path def _remove_tmp_path(self, tmp_path): @@ -320,7 +320,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): if isinstance(data, dict): data = jsonify(data) - afd, afile = tempfile.mkstemp() + afd, afile = tempfile.mkstemp(dir=C.DEFAULT_LOCAL_TMP) afo = os.fdopen(afd, 'wb') try: data = to_bytes(data, errors='surrogate_or_strict') @@ -393,7 +393,12 @@ class ActionBase(with_metaclass(ABCMeta, object)): # we have a need for it, at which point we'll have to do something different. return remote_paths - if self._play_context.become and self._play_context.become_user and self._play_context.become_user not in ('root', remote_user): + try: + admin_users = self._connection._shell.get_option('admin_users') + except KeyError: + admin_users = ['root'] # plugin does not support admin users + + if self._play_context.become and self._play_context.become_user and self._play_context.become_user not in admin_users + [remote_user]: # Unprivileged user that's different than the ssh user. Let's get # to work! @@ -420,12 +425,12 @@ class ActionBase(with_metaclass(ABCMeta, object)): raise AnsibleError('Failed to set file mode on remote temporary files (rc: {0}, err: {1})'.format(res['rc'], to_native(res['stderr']))) res = self._remote_chown(remote_paths, self._play_context.become_user) - if res['rc'] != 0 and remote_user == 'root': + if res['rc'] != 0 and remote_user in admin_users: # chown failed even if remove_user is root - raise AnsibleError('Failed to change ownership of the temporary files Ansible needs to create despite connecting as root. ' + raise AnsibleError('Failed to change ownership of the temporary files Ansible needs to create despite connecting as a privileged user. ' 'Unprivileged become user would be unable to read the file.') elif res['rc'] != 0: - if C.ALLOW_WORLD_READABLE_TMPFILES: + if self._connection._shell('allow_world_readable_temp'): # chown and fs acls failed -- do things this insecure # way only if the user opted in in the config file display.warning('Using world-readable permissions for temporary files Ansible needs to create when becoming an unprivileged user. ' @@ -534,33 +539,46 @@ class ActionBase(with_metaclass(ABCMeta, object)): finally: return x # pylint: disable=lost-exception - def _remote_expand_user(self, path, sudoable=True): - ''' takes a remote path and performs tilde expansion on the remote host ''' - if not path.startswith('~'): # FIXME: Windows paths may start with "~ instead of just ~ + def _remote_expand_user(self, path, sudoable=True, pathsep=None): + ''' takes a remote path and performs tilde/$HOME expansion on the remote host ''' + + # We only expand ~/path and ~username/path + if not path.startswith('~'): return path - # FIXME: Can't use os.path.sep for Windows paths. + # Per Jborean, we don't have to worry about Windows as we don't have a notion of user's home + # dir there. split_path = path.split(os.path.sep, 1) expand_path = split_path[0] + if sudoable and expand_path == '~' and self._play_context.become and self._play_context.become_user: expand_path = '~%s' % self._play_context.become_user + # use shell to construct appropriate command and execute cmd = self._connection._shell.expand_user(expand_path) data = self._low_level_execute_command(cmd, sudoable=False) + try: initial_fragment = data['stdout'].strip().splitlines()[-1] except IndexError: initial_fragment = None if not initial_fragment: - # Something went wrong trying to expand the path remotely. Return + # Something went wrong trying to expand the path remotely. Try using pwd, if not, return # the original string - return path + cmd = self._connection._shell.pwd() + pwd = self._low_level_execute_command(cmd, sudoable=False).get('stdout', '').strip() + if pwd: + expanded = pwd + else: + expanded = path - if len(split_path) > 1: - return self._connection._shell.join_path(initial_fragment, *split_path[1:]) + elif len(split_path) > 1: + expanded = self._connection._shell.join_path(initial_fragment, *split_path[1:]) else: - return initial_fragment + expanded = initial_fragment + + return expanded def _strip_success_message(self, data): ''' @@ -655,8 +673,11 @@ class ActionBase(with_metaclass(ABCMeta, object)): if not self._is_pipelining_enabled(module_style, wrap_async): # we might need remote tmp dir - if not tmp or 'tmp' not in tmp: - tmp = self._make_tmp_path() + if not tmp: + if not self._connection._shell.tempdir or tmp is None or 'tmp' not in tmp: + tmp = self._make_tmp_path() + else: + tmp = self._connection._shell.tempdir remote_module_filename = self._connection._shell.get_remote_filename(module_path) remote_module_path = self._connection._shell.join_path(tmp, remote_module_filename) @@ -733,14 +754,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): else: cmd = remote_module_path - rm_tmp = None - - if self._should_remove_tmp_path(tmp) and not persist_files and delete_remote_tmp: - if not self._play_context.become or self._play_context.become_user == 'root': - # not sudoing or sudoing to root, so can cleanup files in the same step - rm_tmp = tmp - - cmd = self._connection._shell.build_module_command(environment_string, shebang, cmd, arg_path=args_file_path, rm_tmp=rm_tmp).strip() + cmd = self._connection._shell.build_module_command(environment_string, shebang, cmd, arg_path=args_file_path).strip() # Fix permissions of the tmp path and tmp files. This should be called after all files have been transferred. if remote_files: @@ -756,15 +770,12 @@ class ActionBase(with_metaclass(ABCMeta, object)): # NOTE: INTERNAL KEYS ONLY ACCESSIBLE HERE # get internal info before cleaning - tmpdir_delete = (not data.pop("_ansible_suppress_tmpdir_delete", False) and wrap_async) + if data.pop("_ansible_suppress_tmpdir_delete", False): + self._cleanup_remote_tmp = False # remove internal keys remove_internal_keys(data) - # 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: - self._remove_tmp_path(tmp) - # FIXME: for backwards compat, figure out if still makes sense if wrap_async: data['changed'] = True diff --git a/lib/ansible/plugins/action/assemble.py b/lib/ansible/plugins/action/assemble.py index 398b1a3a9ff..41cd32fa8a1 100644 --- a/lib/ansible/plugins/action/assemble.py +++ b/lib/ansible/plugins/action/assemble.py @@ -25,7 +25,8 @@ import os.path import re import tempfile -from ansible.errors import AnsibleError +from ansible import constants as C +from ansible.errors import AnsibleError, AnsibleAction, AnsibleActionDone, AnsibleActionFail from ansible.module_utils._text import to_native, to_text from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase @@ -39,7 +40,7 @@ class ActionModule(ActionBase): def _assemble_from_fragments(self, src_path, delimiter=None, compiled_regexp=None, ignore_hidden=False, decrypt=True): ''' assemble a file from a directory of fragments ''' - tmpfd, temp_path = tempfile.mkstemp() + tmpfd, temp_path = tempfile.mkstemp(dir=C.DEFAULT_LOCAL_TMP) tmp = os.fdopen(tmpfd, 'wb') delimit_me = False add_newline = False @@ -96,78 +97,73 @@ class ActionModule(ActionBase): ignore_hidden = self._task.args.get('ignore_hidden', False) decrypt = self._task.args.get('decrypt', True) - if src is None or dest is None: - result['failed'] = True - result['msg'] = "src and dest are required" - return result + try: + if src is None or dest is None: + raise AnsibleActionFail("src and dest are required") - if boolean(remote_src, strict=False): - result.update(self._execute_module(tmp=tmp, task_vars=task_vars)) - return result - else: - try: - src = self._find_needle('files', src) - except AnsibleError as e: - result['failed'] = True - result['msg'] = to_native(e) - return result + if boolean(remote_src, strict=False): + result.update(self._execute_module(tmp=tmp, task_vars=task_vars)) + raise AnsibleActionDone() + else: + try: + src = self._find_needle('files', src) + except AnsibleError as e: + raise AnsibleActionFail(to_native(e)) - if not tmp: - tmp = self._make_tmp_path() + if not os.path.isdir(src): + raise AnsibleActionFail(u"Source (%s) is not a directory" % src) - if not os.path.isdir(src): - result['failed'] = True - result['msg'] = u"Source (%s) is not a directory" % src - return result + _re = None + if regexp is not None: + _re = re.compile(regexp) - _re = None - if regexp is not None: - _re = re.compile(regexp) + # Does all work assembling the file + path = self._assemble_from_fragments(src, delimiter, _re, ignore_hidden, decrypt) - # Does all work assembling the file - path = self._assemble_from_fragments(src, delimiter, _re, ignore_hidden, decrypt) + path_checksum = checksum_s(path) + dest = self._remote_expand_user(dest) + dest_stat = self._execute_remote_stat(dest, all_vars=task_vars, follow=follow, tmp=tmp) - path_checksum = checksum_s(path) - dest = self._remote_expand_user(dest) - dest_stat = self._execute_remote_stat(dest, all_vars=task_vars, follow=follow, tmp=tmp) + diff = {} - diff = {} + # setup args for running modules + new_module_args = self._task.args.copy() - # setup args for running modules - new_module_args = self._task.args.copy() + # clean assemble specific options + for opt in ['remote_src', 'regexp', 'delimiter', 'ignore_hidden', 'decrypt']: + if opt in new_module_args: + del new_module_args[opt] - # clean assemble specific options - for opt in ['remote_src', 'regexp', 'delimiter', 'ignore_hidden', 'decrypt']: - if opt in new_module_args: - del new_module_args[opt] - - new_module_args.update( - dict( - dest=dest, - original_basename=os.path.basename(src), + new_module_args.update( + dict( + dest=dest, + original_basename=os.path.basename(src), + ) ) - ) - if path_checksum != dest_stat['checksum']: + if path_checksum != dest_stat['checksum']: - if self._play_context.diff: - diff = self._get_diff_data(dest, path, task_vars) + if self._play_context.diff: + diff = self._get_diff_data(dest, path, task_vars) - remote_path = self._connection._shell.join_path(tmp, 'src') - xfered = self._transfer_file(path, remote_path) + remote_path = self._connection._shell.join_path(self._connection._shell.tempdir, 'src') + xfered = self._transfer_file(path, remote_path) - # fix file permissions when the copy is done as a different user - self._fixup_perms2((tmp, remote_path)) + # fix file permissions when the copy is done as a different user + self._fixup_perms2((self._connection._shell.tempdir, remote_path)) - new_module_args.update(dict(src=xfered,)) + new_module_args.update(dict(src=xfered,)) - res = self._execute_module(module_name='copy', module_args=new_module_args, task_vars=task_vars, tmp=tmp, delete_remote_tmp=False) - if diff: - res['diff'] = diff - result.update(res) - else: - result.update(self._execute_module(module_name='file', module_args=new_module_args, task_vars=task_vars, tmp=tmp, delete_remote_tmp=False)) + res = self._execute_module(module_name='copy', module_args=new_module_args, task_vars=task_vars, tmp=tmp) + if diff: + res['diff'] = diff + result.update(res) + else: + result.update(self._execute_module(module_name='file', module_args=new_module_args, task_vars=task_vars, tmp=tmp)) - self._remove_tmp_path(tmp) + except AnsibleAction as e: + result.update(e.result) + finally: + self._remove_tmp_path(self._connection._shell.tempdir) return result diff --git a/lib/ansible/plugins/action/command.py b/lib/ansible/plugins/action/command.py index 87775f3ca91..455480a4dd8 100644 --- a/lib/ansible/plugins/action/command.py +++ b/lib/ansible/plugins/action/command.py @@ -22,4 +22,8 @@ class ActionModule(ActionBase): wrap_async = self._task.async_val and not self._connection.has_native_async results = merge_hash(results, self._execute_module(tmp=tmp, task_vars=task_vars, wrap_async=wrap_async)) + if not wrap_async: + # remove a temporary path we created + self._remove_tmp_path(self._connection._shell.tempdir) + return results diff --git a/lib/ansible/plugins/action/copy.py b/lib/ansible/plugins/action/copy.py index c78f3ada26d..0960b0972db 100644 --- a/lib/ansible/plugins/action/copy.py +++ b/lib/ansible/plugins/action/copy.py @@ -26,8 +26,8 @@ import os.path import stat import tempfile import traceback -from itertools import chain +from ansible import constants as C from ansible.errors import AnsibleError, AnsibleFileNotFound from ansible.module_utils._text import to_bytes, to_native, to_text from ansible.module_utils.parsing.convert_bool import boolean @@ -186,12 +186,13 @@ def _walk_dirs(topdir, base_path=None, local_follow=False, trailing_slash_detect class ActionModule(ActionBase): + TRANSFERS_FILES = True + def _create_remote_file_args(self, module_args): # remove action plugin only keys return dict((k, v) for k, v in module_args.items() if k not in ('content', 'decrypt')) - def _copy_file(self, source_full, source_rel, content, content_tempfile, - dest, task_vars, tmp, delete_remote_tmp): + def _copy_file(self, source_full, source_rel, content, content_tempfile, dest, task_vars, tmp): decrypt = boolean(self._task.args.get('decrypt', True), strict=False) follow = boolean(self._task.args.get('follow', False), strict=False) force = boolean(self._task.args.get('force', 'yes'), strict=False) @@ -206,7 +207,6 @@ class ActionModule(ActionBase): except AnsibleFileNotFound as e: result['failed'] = True result['msg'] = "could not find src=%s, %s" % (source_full, to_text(e)) - self._remove_tmp_path(tmp) return result # Get the local mode and set if user wanted it preserved @@ -221,13 +221,7 @@ class ActionModule(ActionBase): if self._connection._shell.path_has_trailing_slash(dest): dest_file = self._connection._shell.join_path(dest, source_rel) else: - dest_file = self._connection._shell.join_path(dest) - - # Create a tmp path if missing only if this is not recursive. - # If this is recursive we already have a tmp path. - if delete_remote_tmp: - if tmp is None or "-tmp-" not in tmp: - tmp = self._make_tmp_path() + dest_file = dest # Attempt to get remote file info dest_status = self._execute_remote_stat(dest_file, all_vars=task_vars, follow=follow, tmp=tmp, checksum=force) @@ -237,7 +231,6 @@ class ActionModule(ActionBase): if content is not None: # If source was defined as content remove the temporary file and fail out. self._remove_tempfile_if_content_defined(content, content_tempfile) - self._remove_tmp_path(tmp) result['failed'] = True result['msg'] = "can not use content with a dir as dest" return result @@ -265,7 +258,7 @@ class ActionModule(ActionBase): return result # Define a remote directory that we will copy the file to. - tmp_src = self._connection._shell.join_path(tmp, 'source') + tmp_src = self._connection._shell.join_path(self._connection._shell.tempdir, 'source') remote_path = None @@ -280,7 +273,7 @@ class ActionModule(ActionBase): # fix file permissions when the copy is done as a different user if remote_path: - self._fixup_perms2((tmp, remote_path)) + self._fixup_perms2((self._connection._shell.tempdir, remote_path)) if raw: # Continue to next iteration if raw is defined. @@ -301,9 +294,7 @@ class ActionModule(ActionBase): if lmode: new_module_args['mode'] = lmode - module_return = self._execute_module(module_name='copy', - module_args=new_module_args, task_vars=task_vars, - tmp=tmp, delete_remote_tmp=delete_remote_tmp) + module_return = self._execute_module(module_name='copy', module_args=new_module_args, task_vars=task_vars, tmp=tmp) else: # no need to transfer the file, already correct hash, but still need to call @@ -312,8 +303,6 @@ class ActionModule(ActionBase): self._loader.cleanup_tmp_file(source_full) if raw: - # Continue to next iteration if raw is defined. - self._remove_tmp_path(tmp) return None # Fix for https://github.com/ansible/ansible-modules-core/issues/1568. @@ -339,9 +328,7 @@ class ActionModule(ActionBase): new_module_args['mode'] = lmode # Execute the file module. - module_return = self._execute_module(module_name='file', - module_args=new_module_args, task_vars=task_vars, - tmp=tmp, delete_remote_tmp=delete_remote_tmp) + module_return = self._execute_module(module_name='file', module_args=new_module_args, task_vars=task_vars, tmp=tmp) if not module_return.get('checksum'): module_return['checksum'] = local_checksum @@ -379,7 +366,7 @@ class ActionModule(ActionBase): def _create_content_tempfile(self, content): ''' Create a tempfile containing defined content ''' - fd, content_tempfile = tempfile.mkstemp() + fd, content_tempfile = tempfile.mkstemp(dir=C.DEFAULT_LOCAL_TMP) f = os.fdopen(fd, 'wb') content = to_bytes(content) try: @@ -402,6 +389,9 @@ class ActionModule(ActionBase): result = super(ActionModule, self).run(tmp, task_vars) + if tmp is None: + tmp = self._connection._shell.tempdir + source = self._task.args.get('src', None) content = self._task.args.get('content', None) dest = self._task.args.get('dest', None) @@ -493,19 +483,6 @@ class ActionModule(ActionBase): # Used to cut down on command calls when not recursive. module_executed = False - # Optimization: Can delete remote_tmp on the first call if we're only - # copying a single file. Otherwise we keep the remote_tmp until it - # is no longer needed. - delete_remote_tmp = False - if sum(len(f) for f in chain(source_files.values())) == 1: - # Tell _execute_module to delete the file if there is one file. - delete_remote_tmp = True - - # If this is a recursive action create a tmp path that we can share as the _exec_module create is too late. - if not delete_remote_tmp: - if tmp is None or "-tmp-" not in tmp: - tmp = self._make_tmp_path() - # expand any user home dir specifier dest = self._remote_expand_user(dest) @@ -513,7 +490,7 @@ class ActionModule(ActionBase): for source_full, source_rel in source_files['files']: # copy files over. This happens first as directories that have # a file do not need to be created later - module_return = self._copy_file(source_full, source_rel, content, content_tempfile, dest, task_vars, tmp, delete_remote_tmp) + module_return = self._copy_file(source_full, source_rel, content, content_tempfile, dest, task_vars, tmp) if module_return is None: continue @@ -539,9 +516,7 @@ class ActionModule(ActionBase): new_module_args['state'] = 'directory' new_module_args['mode'] = self._task.args.get('directory_mode', None) - module_return = self._execute_module(module_name='file', - module_args=new_module_args, task_vars=task_vars, - tmp=tmp, delete_remote_tmp=delete_remote_tmp) + module_return = self._execute_module(module_name='file', module_args=new_module_args, task_vars=task_vars, tmp=tmp) module_executed = True changed = changed or module_return.get('changed', False) @@ -553,15 +528,11 @@ class ActionModule(ActionBase): new_module_args['state'] = 'link' new_module_args['force'] = True - module_return = self._execute_module(module_name='file', - module_args=new_module_args, task_vars=task_vars, - tmp=tmp, delete_remote_tmp=delete_remote_tmp) + module_return = self._execute_module(module_name='file', module_args=new_module_args, task_vars=task_vars, tmp=tmp) module_executed = True if module_return.get('failed'): result.update(module_return) - if not delete_remote_tmp: - self._remove_tmp_path(tmp) return result changed = changed or module_return.get('changed', False) @@ -571,13 +542,12 @@ class ActionModule(ActionBase): if 'path' in module_return and 'dest' not in module_return: module_return['dest'] = module_return['path'] - # Delete tmp path if we were recursive or if we did not execute a module. - if not delete_remote_tmp or (delete_remote_tmp and not module_executed): - self._remove_tmp_path(tmp) - if module_executed and len(source_files['files']) == 1: result.update(module_return) else: result.update(dict(dest=dest, src=source, changed=changed)) + # Delete tmp path + self._remove_tmp_path(self._connection._shell.tempdir) + return result diff --git a/lib/ansible/plugins/action/fetch.py b/lib/ansible/plugins/action/fetch.py index 62d1ac51f52..24881eb0aa2 100644 --- a/lib/ansible/plugins/action/fetch.py +++ b/lib/ansible/plugins/action/fetch.py @@ -44,170 +44,174 @@ class ActionModule(ActionBase): result = super(ActionModule, self).run(tmp, task_vars) - if self._play_context.check_mode: - result['skipped'] = True - result['msg'] = 'check mode not (yet) supported for this module' - return result + try: + if self._play_context.check_mode: + result['skipped'] = True + result['msg'] = 'check mode not (yet) supported for this module' + return result - source = self._task.args.get('src', None) - dest = self._task.args.get('dest', None) - flat = boolean(self._task.args.get('flat'), strict=False) - fail_on_missing = boolean(self._task.args.get('fail_on_missing'), strict=False) - validate_checksum = boolean(self._task.args.get('validate_checksum', - self._task.args.get('validate_md5', True)), - strict=False) + source = self._task.args.get('src', None) + dest = self._task.args.get('dest', None) + flat = boolean(self._task.args.get('flat'), strict=False) + fail_on_missing = boolean(self._task.args.get('fail_on_missing'), strict=False) + validate_checksum = boolean(self._task.args.get('validate_checksum', + self._task.args.get('validate_md5', True)), + strict=False) - # validate source and dest are strings FIXME: use basic.py and module specs - if not isinstance(source, string_types): - result['msg'] = "Invalid type supplied for source option, it must be a string" + # validate source and dest are strings FIXME: use basic.py and module specs + if not isinstance(source, string_types): + result['msg'] = "Invalid type supplied for source option, it must be a string" - if not isinstance(dest, string_types): - result['msg'] = "Invalid type supplied for dest option, it must be a string" + if not isinstance(dest, string_types): + result['msg'] = "Invalid type supplied for dest option, it must be a string" - # validate_md5 is the deprecated way to specify validate_checksum - if 'validate_md5' in self._task.args and 'validate_checksum' in self._task.args: - result['msg'] = "validate_checksum and validate_md5 cannot both be specified" + # validate_md5 is the deprecated way to specify validate_checksum + if 'validate_md5' in self._task.args and 'validate_checksum' in self._task.args: + result['msg'] = "validate_checksum and validate_md5 cannot both be specified" - if 'validate_md5' in self._task.args: - display.deprecated('Use validate_checksum instead of validate_md5', version='2.8') + if 'validate_md5' in self._task.args: + display.deprecated('Use validate_checksum instead of validate_md5', version='2.8') - if source is None or dest is None: - result['msg'] = "src and dest are required" + if source is None or dest is None: + result['msg'] = "src and dest are required" - if result.get('msg'): - result['failed'] = True - return result + if result.get('msg'): + result['failed'] = True + return result - source = self._connection._shell.join_path(source) - source = self._remote_expand_user(source) + source = self._connection._shell.join_path(source) + source = self._remote_expand_user(source) - remote_checksum = None - if not self._play_context.become: - # calculate checksum for the remote file, don't bother if using become as slurp will be used - # Force remote_checksum to follow symlinks because fetch always follows symlinks - remote_checksum = self._remote_checksum(source, all_vars=task_vars, follow=True) + remote_checksum = None + if not self._play_context.become: + # calculate checksum for the remote file, don't bother if using become as slurp will be used + # Force remote_checksum to follow symlinks because fetch always follows symlinks + remote_checksum = self._remote_checksum(source, all_vars=task_vars, follow=True) - # use slurp if permissions are lacking or privilege escalation is needed - remote_data = None - if remote_checksum in ('1', '2', None): - slurpres = self._execute_module(module_name='slurp', module_args=dict(src=source), task_vars=task_vars, tmp=tmp) - if slurpres.get('failed'): - if not fail_on_missing and (slurpres.get('msg').startswith('file not found') or remote_checksum == '1'): - result['msg'] = "the remote file does not exist, not transferring, ignored" - result['file'] = source - result['changed'] = False + # use slurp if permissions are lacking or privilege escalation is needed + remote_data = None + if remote_checksum in ('1', '2', None): + slurpres = self._execute_module(module_name='slurp', module_args=dict(src=source), task_vars=task_vars, tmp=tmp) + if slurpres.get('failed'): + if not fail_on_missing and (slurpres.get('msg').startswith('file not found') or remote_checksum == '1'): + result['msg'] = "the remote file does not exist, not transferring, ignored" + result['file'] = source + result['changed'] = False + else: + result.update(slurpres) + return result else: - result.update(slurpres) + if slurpres['encoding'] == 'base64': + remote_data = base64.b64decode(slurpres['content']) + if remote_data is not None: + remote_checksum = checksum_s(remote_data) + # the source path may have been expanded on the + # target system, so we compare it here and use the + # expanded version if it's different + remote_source = slurpres.get('source') + if remote_source and remote_source != source: + source = remote_source + + # calculate the destination name + if os.path.sep not in self._connection._shell.join_path('a', ''): + source = self._connection._shell._unquote(source) + source_local = source.replace('\\', '/') + else: + source_local = source + + dest = os.path.expanduser(dest) + if flat: + if os.path.isdir(to_bytes(dest, errors='surrogate_or_strict')) and not dest.endswith(os.sep): + result['msg'] = "dest is an existing directory, use a trailing slash if you want to fetch src into that directory" + result['file'] = dest + result['failed'] = True + return result + if dest.endswith(os.sep): + # if the path ends with "/", we'll use the source filename as the + # destination filename + base = os.path.basename(source_local) + dest = os.path.join(dest, base) + if not dest.startswith("/"): + # if dest does not start with "/", we'll assume a relative path + dest = self._loader.path_dwim(dest) + else: + # files are saved in dest dir, with a subdir for each host, then the filename + if 'inventory_hostname' in task_vars: + target_name = task_vars['inventory_hostname'] + else: + target_name = self._play_context.remote_addr + dest = "%s/%s/%s" % (self._loader.path_dwim(dest), target_name, source_local) + + dest = dest.replace("//", "/") + + if remote_checksum in ('0', '1', '2', '3', '4', '5'): + result['changed'] = False + result['file'] = source + if remote_checksum == '0': + result['msg'] = "unable to calculate the checksum of the remote file" + elif remote_checksum == '1': + result['msg'] = "the remote file does not exist" + elif remote_checksum == '2': + result['msg'] = "no read permission on remote file" + elif remote_checksum == '3': + result['msg'] = "remote file is a directory, fetch cannot work on directories" + elif remote_checksum == '4': + result['msg'] = "python isn't present on the system. Unable to compute checksum" + elif remote_checksum == '5': + result['msg'] = "stdlib json or simplejson was not found on the remote machine. Only the raw module can work without those installed" + # Historically, these don't fail because you may want to transfer + # a log file that possibly MAY exist but keep going to fetch other + # log files. Today, this is better achieved by adding + # ignore_errors or failed_when to the task. Control the behaviour + # via fail_when_missing + if fail_on_missing: + result['failed'] = True + del result['changed'] + else: + result['msg'] += ", not transferring, ignored" return result - else: - if slurpres['encoding'] == 'base64': - remote_data = base64.b64decode(slurpres['content']) - if remote_data is not None: - remote_checksum = checksum_s(remote_data) - # the source path may have been expanded on the - # target system, so we compare it here and use the - # expanded version if it's different - remote_source = slurpres.get('source') - if remote_source and remote_source != source: - source = remote_source - # calculate the destination name - if os.path.sep not in self._connection._shell.join_path('a', ''): - source = self._connection._shell._unquote(source) - source_local = source.replace('\\', '/') - else: - source_local = source + # calculate checksum for the local file + local_checksum = checksum(dest) - dest = os.path.expanduser(dest) - if flat: - if os.path.isdir(to_bytes(dest, errors='surrogate_or_strict')) and not dest.endswith(os.sep): - result['msg'] = "dest is an existing directory, use a trailing slash if you want to fetch src into that directory" - result['file'] = dest - result['failed'] = True - return result - if dest.endswith(os.sep): - # if the path ends with "/", we'll use the source filename as the - # destination filename - base = os.path.basename(source_local) - dest = os.path.join(dest, base) - if not dest.startswith("/"): - # if dest does not start with "/", we'll assume a relative path - dest = self._loader.path_dwim(dest) - else: - # files are saved in dest dir, with a subdir for each host, then the filename - if 'inventory_hostname' in task_vars: - target_name = task_vars['inventory_hostname'] - else: - target_name = self._play_context.remote_addr - dest = "%s/%s/%s" % (self._loader.path_dwim(dest), target_name, source_local) + if remote_checksum != local_checksum: + # create the containing directories, if needed + makedirs_safe(os.path.dirname(dest)) - dest = dest.replace("//", "/") - - if remote_checksum in ('0', '1', '2', '3', '4', '5'): - result['changed'] = False - result['file'] = source - if remote_checksum == '0': - result['msg'] = "unable to calculate the checksum of the remote file" - elif remote_checksum == '1': - result['msg'] = "the remote file does not exist" - elif remote_checksum == '2': - result['msg'] = "no read permission on remote file" - elif remote_checksum == '3': - result['msg'] = "remote file is a directory, fetch cannot work on directories" - elif remote_checksum == '4': - result['msg'] = "python isn't present on the system. Unable to compute checksum" - elif remote_checksum == '5': - result['msg'] = "stdlib json or simplejson was not found on the remote machine. Only the raw module can work without those installed" - # Historically, these don't fail because you may want to transfer - # a log file that possibly MAY exist but keep going to fetch other - # log files. Today, this is better achieved by adding - # ignore_errors or failed_when to the task. Control the behaviour - # via fail_when_missing - if fail_on_missing: - result['failed'] = True - del result['changed'] - else: - result['msg'] += ", not transferring, ignored" - return result - - # calculate checksum for the local file - local_checksum = checksum(dest) - - if remote_checksum != local_checksum: - # create the containing directories, if needed - makedirs_safe(os.path.dirname(dest)) - - # fetch the file and check for changes - if remote_data is None: - self._connection.fetch_file(source, dest) - else: + # fetch the file and check for changes + if remote_data is None: + self._connection.fetch_file(source, dest) + else: + try: + f = open(to_bytes(dest, errors='surrogate_or_strict'), 'wb') + f.write(remote_data) + f.close() + except (IOError, OSError) as e: + raise AnsibleError("Failed to fetch the file: %s" % e) + new_checksum = secure_hash(dest) + # For backwards compatibility. We'll return None on FIPS enabled systems try: - f = open(to_bytes(dest, errors='surrogate_or_strict'), 'wb') - f.write(remote_data) - f.close() - except (IOError, OSError) as e: - raise AnsibleError("Failed to fetch the file: %s" % e) - new_checksum = secure_hash(dest) - # For backwards compatibility. We'll return None on FIPS enabled systems - try: - new_md5 = md5(dest) - except ValueError: - new_md5 = None + new_md5 = md5(dest) + except ValueError: + new_md5 = None - if validate_checksum and new_checksum != remote_checksum: - result.update(dict(failed=True, md5sum=new_md5, - msg="checksum mismatch", file=source, dest=dest, remote_md5sum=None, - checksum=new_checksum, remote_checksum=remote_checksum)) + if validate_checksum and new_checksum != remote_checksum: + result.update(dict(failed=True, md5sum=new_md5, + msg="checksum mismatch", file=source, dest=dest, remote_md5sum=None, + checksum=new_checksum, remote_checksum=remote_checksum)) + else: + result.update({'changed': True, 'md5sum': new_md5, 'dest': dest, + 'remote_md5sum': None, 'checksum': new_checksum, + 'remote_checksum': remote_checksum}) else: - result.update({'changed': True, 'md5sum': new_md5, 'dest': dest, - 'remote_md5sum': None, 'checksum': new_checksum, - 'remote_checksum': remote_checksum}) - else: - # For backwards compatibility. We'll return None on FIPS enabled systems - try: - local_md5 = md5(dest) - except ValueError: - local_md5 = None - result.update(dict(changed=False, md5sum=local_md5, file=source, dest=dest, checksum=local_checksum)) + # For backwards compatibility. We'll return None on FIPS enabled systems + try: + local_md5 = md5(dest) + except ValueError: + local_md5 = None + result.update(dict(changed=False, md5sum=local_md5, file=source, dest=dest, checksum=local_checksum)) + + finally: + self._remove_tmp_path(self._connection._shell.tempdir) return result diff --git a/lib/ansible/plugins/action/normal.py b/lib/ansible/plugins/action/normal.py index f04888c25e1..221c2982438 100644 --- a/lib/ansible/plugins/action/normal.py +++ b/lib/ansible/plugins/action/normal.py @@ -29,24 +29,28 @@ class ActionModule(ActionBase): self._supports_check_mode = True self._supports_async = True - results = super(ActionModule, self).run(tmp, task_vars) + result = super(ActionModule, self).run(tmp, task_vars) - if not results.get('skipped'): + if not result.get('skipped'): - if results.get('invocation', {}).get('module_args'): + if result.get('invocation', {}).get('module_args'): # avoid passing to modules in case of no_log # should not be set anymore but here for backwards compatibility - del results['invocation']['module_args'] + del result['invocation']['module_args'] # FUTURE: better to let _execute_module calculate this internally? wrap_async = self._task.async_val and not self._connection.has_native_async # do work! - results = merge_hash(results, self._execute_module(tmp=tmp, task_vars=task_vars, wrap_async=wrap_async)) + result = merge_hash(result, self._execute_module(tmp=tmp, task_vars=task_vars, wrap_async=wrap_async)) - # hack to keep --verbose from showing all the setup module results - # moved from setup module as now we filter out all _ansible_ from results + # hack to keep --verbose from showing all the setup module result + # moved from setup module as now we filter out all _ansible_ from result if self._task.action == 'setup': - results['_ansible_verbose_override'] = True + result['_ansible_verbose_override'] = True - return results + if not wrap_async: + # remove a temporary path we created + self._remove_tmp_path(self._connection._shell.tempdir) + + return result diff --git a/lib/ansible/plugins/action/package.py b/lib/ansible/plugins/action/package.py index f675f3fd8b9..f01176f5dd7 100644 --- a/lib/ansible/plugins/action/package.py +++ b/lib/ansible/plugins/action/package.py @@ -17,6 +17,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +from ansible.errors import AnsibleAction, AnsibleActionFail from ansible.plugins.action import ActionBase try: @@ -46,29 +47,35 @@ class ActionModule(ActionBase): module = self._templar.template("{{hostvars['%s']['ansible_facts']['pkg_mgr']}}" % self._task.delegate_to) else: module = self._templar.template('{{ansible_facts.pkg_mgr}}') - except: + except Exception: pass # could not get it from template! - if module == 'auto': - facts = self._execute_module(module_name='setup', module_args=dict(filter='ansible_pkg_mgr', gather_subset='!all'), task_vars=task_vars) - display.debug("Facts %s" % facts) - module = facts.get('ansible_facts', {}).get('ansible_pkg_mgr', 'auto') + try: + if module == 'auto': + facts = self._execute_module(module_name='setup', module_args=dict(filter='ansible_pkg_mgr', gather_subset='!all'), task_vars=task_vars) + display.debug("Facts %s" % facts) + module = facts.get('ansible_facts', {}).get('ansible_pkg_mgr', 'auto') - if module != 'auto': + if module != 'auto': - if module not in self._shared_loader_obj.module_loader: - result['failed'] = True - result['msg'] = 'Could not find a module for %s.' % module + if module not in self._shared_loader_obj.module_loader: + raise AnsibleActionFail('Could not find a module for %s.' % module) + else: + # run the 'package' module + new_module_args = self._task.args.copy() + if 'use' in new_module_args: + del new_module_args['use'] + + display.vvvv("Running %s" % module) + result.update(self._execute_module(module_name=module, module_args=new_module_args, task_vars=task_vars, wrap_async=self._task.async_val)) else: - # run the 'package' module - new_module_args = self._task.args.copy() - if 'use' in new_module_args: - del new_module_args['use'] + raise AnsibleActionFail('Could not detect which package manager to use. Try gathering facts or setting the "use" option.') - display.vvvv("Running %s" % module) - result.update(self._execute_module(module_name=module, module_args=new_module_args, task_vars=task_vars, wrap_async=self._task.async_val)) - else: - result['failed'] = True - result['msg'] = 'Could not detect which package manager to use. Try gathering facts or setting the "use" option.' + except AnsibleAction as e: + result.update(e.result) + finally: + if not self._task.async_val: + # remove a temporary path we created + self._remove_tmp_path(self._connection._shell.tempdir) return result diff --git a/lib/ansible/plugins/action/patch.py b/lib/ansible/plugins/action/patch.py index e3aea3923a7..a54548c9a64 100644 --- a/lib/ansible/plugins/action/patch.py +++ b/lib/ansible/plugins/action/patch.py @@ -20,7 +20,7 @@ __metaclass__ = type import os -from ansible.errors import AnsibleError +from ansible.errors import AnsibleError, AnsibleAction, AnsibleActionDone, AnsibleActionFail from ansible.module_utils._text import to_native from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase @@ -28,6 +28,8 @@ from ansible.plugins.action import ActionBase class ActionModule(ActionBase): + TRANSFERS_FILES = True + def run(self, tmp=None, task_vars=None): if task_vars is None: task_vars = dict() @@ -37,39 +39,33 @@ class ActionModule(ActionBase): src = self._task.args.get('src', None) remote_src = boolean(self._task.args.get('remote_src', 'no'), strict=False) - if src is None: - result['failed'] = True - result['msg'] = "src is required" - return result - elif remote_src: - # everything is remote, so we just execute the module - # without changing any of the module arguments - result.update(self._execute_module(task_vars=task_vars)) - return result - try: - src = self._find_needle('files', src) - except AnsibleError as e: - result['failed'] = True - result['msg'] = to_native(e) - return result + if src is None: + raise AnsibleActionFail("src is required") + elif remote_src: + # everything is remote, so we just execute the module + # without changing any of the module arguments + raise AnsibleActionDone(result=self._execute_module(task_vars=task_vars)) - # create the remote tmp dir if needed, and put the source file there - if tmp is None or "-tmp-" not in tmp: - tmp = self._make_tmp_path() + try: + src = self._find_needle('files', src) + except AnsibleError as e: + raise AnsibleActionFail(to_native(e)) - tmp_src = self._connection._shell.join_path(tmp, os.path.basename(src)) - self._transfer_file(src, tmp_src) + tmp_src = self._connection._shell.join_path(self._connection._shell.tempdir, os.path.basename(src)) + self._transfer_file(src, tmp_src) + self._fixup_perms2((tmp_src,)) - self._fixup_perms2((tmp, tmp_src)) - - new_module_args = self._task.args.copy() - new_module_args.update( - dict( - src=tmp_src, + new_module_args = self._task.args.copy() + new_module_args.update( + dict( + src=tmp_src, + ) ) - ) - result.update(self._execute_module('patch', module_args=new_module_args, task_vars=task_vars)) - self._remove_tmp_path(tmp) + result.update(self._execute_module('patch', module_args=new_module_args, task_vars=task_vars)) + except AnsibleAction as e: + result.update(e.result) + finally: + self._remove_tmp_path(self._connection._shell.tempdir) return result diff --git a/lib/ansible/plugins/action/script.py b/lib/ansible/plugins/action/script.py index 3549e233212..62f35cf8976 100644 --- a/lib/ansible/plugins/action/script.py +++ b/lib/ansible/plugins/action/script.py @@ -21,12 +21,13 @@ import os import re import shlex -from ansible.errors import AnsibleError +from ansible.errors import AnsibleError, AnsibleAction, AnsibleActionDone, AnsibleActionFail, AnsibleActionSkip from ansible.module_utils._text import to_native, to_text from ansible.plugins.action import ActionBase class ActionModule(ActionBase): + TRANSFERS_FILES = True # On Windows platform, absolute paths begin with a (back)slash @@ -40,95 +41,91 @@ class ActionModule(ActionBase): result = super(ActionModule, self).run(tmp, task_vars) - if not tmp: - tmp = self._make_tmp_path() - - creates = self._task.args.get('creates') - if creates: - # do not run the command if the line contains creates=filename - # and the filename already exists. This allows idempotence - # of command executions. - if self._remote_file_exists(creates): - self._remove_tmp_path(tmp) - return dict(skipped=True, msg=("skipped, since %s exists" % creates)) - - removes = self._task.args.get('removes') - if removes: - # do not run the command if the line contains removes=filename - # and the filename does not exist. This allows idempotence - # of command executions. - if not self._remote_file_exists(removes): - self._remove_tmp_path(tmp) - return dict(skipped=True, msg=("skipped, since %s does not exist" % removes)) - - # The chdir must be absolute, because a relative path would rely on - # remote node behaviour & user config. - chdir = self._task.args.get('chdir') - if chdir: - # Powershell is the only Windows-path aware shell - if self._connection._shell.SHELL_FAMILY == 'powershell' and \ - not self.windows_absolute_path_detection.matches(chdir): - return dict(failed=True, msg='chdir %s must be an absolute path for a Windows remote node' % chdir) - # Every other shell is unix-path-aware. - if self._connection._shell.SHELL_FAMILY != 'powershell' and not chdir.startswith('/'): - return dict(failed=True, msg='chdir %s must be an absolute path for a Unix-aware remote node' % chdir) - - # Split out the script as the first item in raw_params using - # shlex.split() in order to support paths and files with spaces in the name. - # Any arguments passed to the script will be added back later. - raw_params = to_native(self._task.args.get('_raw_params', ''), errors='surrogate_or_strict') - parts = [to_text(s, errors='surrogate_or_strict') for s in shlex.split(raw_params.strip())] - source = parts[0] - try: - source = self._loader.get_real_file(self._find_needle('files', source), decrypt=self._task.args.get('decrypt', True)) - except AnsibleError as e: - return dict(failed=True, msg=to_native(e)) + creates = self._task.args.get('creates') + if creates: + # do not run the command if the line contains creates=filename + # and the filename already exists. This allows idempotence + # of command executions. + if self._remote_file_exists(creates): + raise AnsibleActionSkip("%s exists, matching creates option" % creates) - if not self._play_context.check_mode: - # transfer the file to a remote tmp location - tmp_src = self._connection._shell.join_path(tmp, os.path.basename(source)) + removes = self._task.args.get('removes') + if removes: + # do not run the command if the line contains removes=filename + # and the filename does not exist. This allows idempotence + # of command executions. + if not self._remote_file_exists(removes): + raise AnsibleActionSkip("%s does not exist, matching removes option" % removes) - # Convert raw_params to text for the purpose of replacing the script since - # parts and tmp_src are both unicode strings and raw_params will be different - # depending on Python version. - # - # Once everything is encoded consistently, replace the script path on the remote - # system with the remainder of the raw_params. This preserves quoting in parameters - # that would have been removed by shlex.split(). - target_command = to_text(raw_params).strip().replace(parts[0], tmp_src) + # The chdir must be absolute, because a relative path would rely on + # remote node behaviour & user config. + chdir = self._task.args.get('chdir') + if chdir: + # Powershell is the only Windows-path aware shell + if self._connection._shell.SHELL_FAMILY == 'powershell' and \ + not self.windows_absolute_path_detection.matches(chdir): + raise AnsibleActionFail('chdir %s must be an absolute path for a Windows remote node' % chdir) + # Every other shell is unix-path-aware. + if self._connection._shell.SHELL_FAMILY != 'powershell' and not chdir.startswith('/'): + raise AnsibleActionFail('chdir %s must be an absolute path for a Unix-aware remote node' % chdir) - self._transfer_file(source, tmp_src) + # Split out the script as the first item in raw_params using + # shlex.split() in order to support paths and files with spaces in the name. + # Any arguments passed to the script will be added back later. + raw_params = to_native(self._task.args.get('_raw_params', ''), errors='surrogate_or_strict') + parts = [to_text(s, errors='surrogate_or_strict') for s in shlex.split(raw_params.strip())] + source = parts[0] - # set file permissions, more permissive when the copy is done as a different user - self._fixup_perms2((tmp, tmp_src), execute=True) + try: + source = self._loader.get_real_file(self._find_needle('files', source), decrypt=self._task.args.get('decrypt', True)) + except AnsibleError as e: + raise AnsibleActionFail(to_native(e)) - # add preparation steps to one ssh roundtrip executing the script - env_dict = dict() - env_string = self._compute_environment_string(env_dict) - script_cmd = ' '.join([env_string, target_command]) - - if self._play_context.check_mode: + # now we execute script, always assume changed. result['changed'] = True - self._remove_tmp_path(tmp) - return result - script_cmd = self._connection._shell.wrap_for_exec(script_cmd) + if not self._play_context.check_mode: + # transfer the file to a remote tmp location + tmp_src = self._connection._shell.join_path(self._connection._shell.tempdir, os.path.basename(source)) - exec_data = None - # HACK: come up with a sane way to pass around env outside the command - if self._connection.transport == "winrm": - exec_data = self._connection._create_raw_wrapper_payload(script_cmd, env_dict) + # Convert raw_params to text for the purpose of replacing the script since + # parts and tmp_src are both unicode strings and raw_params will be different + # depending on Python version. + # + # Once everything is encoded consistently, replace the script path on the remote + # system with the remainder of the raw_params. This preserves quoting in parameters + # that would have been removed by shlex.split(). + target_command = to_text(raw_params).strip().replace(parts[0], tmp_src) - result.update(self._low_level_execute_command(cmd=script_cmd, in_data=exec_data, sudoable=True, chdir=chdir)) + self._transfer_file(source, tmp_src) - # clean up after - self._remove_tmp_path(tmp) + # set file permissions, more permissive when the copy is done as a different user + self._fixup_perms2((tmp_src,), execute=True) - result['changed'] = True + # add preparation steps to one ssh roundtrip executing the script + env_dict = dict() + env_string = self._compute_environment_string(env_dict) + script_cmd = ' '.join([env_string, target_command]) - if 'rc' in result and result['rc'] != 0: - result['failed'] = True - result['msg'] = 'non-zero return code' + if self._play_context.check_mode: + raise AnsibleActionDone() + + script_cmd = self._connection._shell.wrap_for_exec(script_cmd) + + exec_data = None + # HACK: come up with a sane way to pass around env outside the command + if self._connection.transport == "winrm": + exec_data = self._connection._create_raw_wrapper_payload(script_cmd, env_dict) + + result.update(self._low_level_execute_command(cmd=script_cmd, in_data=exec_data, sudoable=True, chdir=chdir)) + + if 'rc' in result and result['rc'] != 0: + raise AnsibleActionFail('non-zero return code') + + except AnsibleAction as e: + result.update(e.result) + finally: + self._remove_tmp_path(self._connection._shell.tempdir) return result diff --git a/lib/ansible/plugins/action/service.py b/lib/ansible/plugins/action/service.py index b3f3f5fd9db..5291a889b3a 100644 --- a/lib/ansible/plugins/action/service.py +++ b/lib/ansible/plugins/action/service.py @@ -18,6 +18,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +from ansible.errors import AnsibleAction, AnsibleActionFail from ansible.plugins.action import ActionBase @@ -48,35 +49,41 @@ class ActionModule(ActionBase): except: pass # could not get it from template! - if module == 'auto': - facts = self._execute_module(module_name='setup', module_args=dict(gather_subset='!all', filter='ansible_service_mgr'), task_vars=task_vars) - self._display.debug("Facts %s" % facts) - module = facts.get('ansible_facts', {}).get('ansible_service_mgr', 'auto') + try: + if module == 'auto': + facts = self._execute_module(module_name='setup', module_args=dict(gather_subset='!all', filter='ansible_service_mgr'), task_vars=task_vars) + self._display.debug("Facts %s" % facts) + module = facts.get('ansible_facts', {}).get('ansible_service_mgr', 'auto') - if not module or module == 'auto' or module not in self._shared_loader_obj.module_loader: - module = 'service' + if not module or module == 'auto' or module not in self._shared_loader_obj.module_loader: + module = 'service' - if module != 'auto': - # run the 'service' module - new_module_args = self._task.args.copy() - if 'use' in new_module_args: - del new_module_args['use'] + if module != 'auto': + # run the 'service' module + new_module_args = self._task.args.copy() + if 'use' in new_module_args: + del new_module_args['use'] - # for backwards compatibility - if 'state' in new_module_args and new_module_args['state'] == 'running': - self._display.deprecated(msg="state=running is deprecated. Please use state=started", version="2.7") - new_module_args['state'] = 'started' + # for backwards compatibility + if 'state' in new_module_args and new_module_args['state'] == 'running': + self._display.deprecated(msg="state=running is deprecated. Please use state=started", version="2.7") + new_module_args['state'] = 'started' - if module in self.UNUSED_PARAMS: - for unused in self.UNUSED_PARAMS[module]: - if unused in new_module_args: - del new_module_args[unused] - self._display.warning('Ignoring "%s" as it is not used in "%s"' % (unused, module)) + if module in self.UNUSED_PARAMS: + for unused in self.UNUSED_PARAMS[module]: + if unused in new_module_args: + del new_module_args[unused] + self._display.warning('Ignoring "%s" as it is not used in "%s"' % (unused, module)) - self._display.vvvv("Running %s" % module) - result.update(self._execute_module(module_name=module, module_args=new_module_args, task_vars=task_vars, wrap_async=self._task.async_val)) - else: - result['failed'] = True - result['msg'] = 'Could not detect which service manager to use. Try gathering facts or setting the "use" option.' + self._display.vvvv("Running %s" % module) + result.update(self._execute_module(module_name=module, module_args=new_module_args, task_vars=task_vars, wrap_async=self._task.async_val)) + else: + raise AnsibleActionFail('Could not detect which service manager to use. Try gathering facts or setting the "use" option.') + + except AnsibleAction as e: + result.update(e.result) + finally: + if not self._task.async_val: + self._remove_tmp_path(self._connection._shell.tempdir) return result diff --git a/lib/ansible/plugins/action/shell.py b/lib/ansible/plugins/action/shell.py index 3e464721bf4..8aedd05060d 100644 --- a/lib/ansible/plugins/action/shell.py +++ b/lib/ansible/plugins/action/shell.py @@ -22,4 +22,9 @@ class ActionModule(ActionBase): loader=self._loader, templar=self._templar, shared_loader_obj=self._shared_loader_obj) - return command_action.run(task_vars=task_vars) + result = command_action.run(task_vars=task_vars) + + # remove a temporary path we created + self._remove_tmp_path(self._connection._shell.tempdir) + + return result diff --git a/lib/ansible/plugins/action/template.py b/lib/ansible/plugins/action/template.py index e0674b1569e..fcbe0eae0ba 100644 --- a/lib/ansible/plugins/action/template.py +++ b/lib/ansible/plugins/action/template.py @@ -21,13 +21,11 @@ import os import shutil import tempfile -from ansible import constants as C -from ansible.errors import AnsibleError, AnsibleFileNotFound +from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleAction, AnsibleActionFail from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase from ansible.template import generate_ansible_template_vars -from ansible.utils.hashing import checksum_s class ActionModule(ActionBase): @@ -35,20 +33,6 @@ class ActionModule(ActionBase): TRANSFERS_FILES = True DEFAULT_NEWLINE_SEQUENCE = "\n" - def get_checksum(self, dest, all_vars, try_directory=False, source=None, tmp=None): - try: - dest_stat = self._execute_remote_stat(dest, all_vars=all_vars, follow=False, tmp=tmp) - - if dest_stat['exists'] and dest_stat['isdir'] and try_directory and source: - base = os.path.basename(source) - dest = os.path.join(dest, base) - dest_stat = self._execute_remote_stat(dest, all_vars=all_vars, follow=False, tmp=tmp) - - except AnsibleError as e: - return dict(failed=True, msg=to_text(e)) - - return dest_stat['checksum'] - def run(self, tmp=None, task_vars=None): ''' handler for template operations ''' @@ -76,108 +60,103 @@ class ActionModule(ActionBase): if newline_sequence in wrong_sequences: newline_sequence = allowed_sequences[wrong_sequences.index(newline_sequence)] - if state is not None: - result['failed'] = True - result['msg'] = "'state' cannot be specified on a template" - elif source is None or dest is None: - result['failed'] = True - result['msg'] = "src and dest are required" - elif newline_sequence not in allowed_sequences: - result['failed'] = True - result['msg'] = "newline_sequence needs to be one of: \n, \r or \r\n" - else: + try: + if state is not None: + raise AnsibleActionFail("'state' cannot be specified on a template") + elif source is None or dest is None: + raise AnsibleActionFail("src and dest are required") + elif newline_sequence not in allowed_sequences: + raise AnsibleActionFail("newline_sequence needs to be one of: \n, \r or \r\n") + else: + try: + source = self._find_needle('templates', source) + except AnsibleError as e: + raise AnsibleActionFail(to_text(e)) + + # Get vault decrypted tmp file try: - source = self._find_needle('templates', source) - except AnsibleError as e: - result['failed'] = True - result['msg'] = to_text(e) + tmp_source = self._loader.get_real_file(source) + except AnsibleFileNotFound as e: + raise AnsibleActionFail("could not find src=%s, %s" % (source, to_text(e))) - if 'failed' in result: - return result + # template the source data locally & get ready to transfer + try: + with open(tmp_source, 'r') as f: + template_data = to_text(f.read()) - # Get vault decrypted tmp file - try: - tmp_source = self._loader.get_real_file(source) - except AnsibleFileNotFound as e: - result['failed'] = True - result['msg'] = "could not find src=%s, %s" % (source, e) - self._remove_tmp_path(tmp) - return result + # set jinja2 internal search path for includes + searchpath = task_vars.get('ansible_search_path', []) + searchpath.extend([self._loader._basedir, os.path.dirname(source)]) - # template the source data locally & get ready to transfer - try: - with open(tmp_source, 'r') as f: - template_data = to_text(f.read()) + # We want to search into the 'templates' subdir of each search path in + # addition to our original search paths. + newsearchpath = [] + for p in searchpath: + newsearchpath.append(os.path.join(p, 'templates')) + newsearchpath.append(p) + searchpath = newsearchpath - # set jinja2 internal search path for includes - searchpath = task_vars.get('ansible_search_path', []) - searchpath.extend([self._loader._basedir, os.path.dirname(source)]) + self._templar.environment.loader.searchpath = searchpath + self._templar.environment.newline_sequence = newline_sequence + if block_start_string is not None: + self._templar.environment.block_start_string = block_start_string + if block_end_string is not None: + self._templar.environment.block_end_string = block_end_string + if variable_start_string is not None: + self._templar.environment.variable_start_string = variable_start_string + if variable_end_string is not None: + self._templar.environment.variable_end_string = variable_end_string + if trim_blocks is not None: + self._templar.environment.trim_blocks = bool(trim_blocks) - # We want to search into the 'templates' subdir of each search path in - # addition to our original search paths. - newsearchpath = [] - for p in searchpath: - newsearchpath.append(os.path.join(p, 'templates')) - newsearchpath.append(p) - searchpath = newsearchpath + # add ansible 'template' vars + temp_vars = task_vars.copy() + temp_vars.update(generate_ansible_template_vars(source)) - self._templar.environment.loader.searchpath = searchpath - self._templar.environment.newline_sequence = newline_sequence - if block_start_string is not None: - self._templar.environment.block_start_string = block_start_string - if block_end_string is not None: - self._templar.environment.block_end_string = block_end_string - if variable_start_string is not None: - self._templar.environment.variable_start_string = variable_start_string - if variable_end_string is not None: - self._templar.environment.variable_end_string = variable_end_string - if trim_blocks is not None: - self._templar.environment.trim_blocks = bool(trim_blocks) + old_vars = self._templar._available_variables + self._templar.set_available_variables(temp_vars) + resultant = self._templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False) + self._templar.set_available_variables(old_vars) + except AnsibleAction: + raise + except Exception as e: + raise AnsibleActionFail("%s: %s" % (type(e).__name__, to_text(e))) + finally: + self._loader.cleanup_tmp_file(tmp_source) - # add ansible 'template' vars - temp_vars = task_vars.copy() - temp_vars.update(generate_ansible_template_vars(source)) + new_task = self._task.copy() + new_task.args.pop('newline_sequence', None) + new_task.args.pop('block_start_string', None) + new_task.args.pop('block_end_string', None) + new_task.args.pop('variable_start_string', None) + new_task.args.pop('variable_end_string', None) + new_task.args.pop('trim_blocks', None) + try: + tempdir = tempfile.mkdtemp() + result_file = os.path.join(tempdir, os.path.basename(source)) + with open(result_file, 'wb') as f: + f.write(to_bytes(resultant, errors='surrogate_or_strict')) - old_vars = self._templar._available_variables - self._templar.set_available_variables(temp_vars) - resultant = self._templar.do_template(template_data, preserve_trailing_newlines=True, escape_backslashes=False) - self._templar.set_available_variables(old_vars) - except Exception as e: - result['failed'] = True - result['msg'] = "%s: %s" % (type(e).__name__, to_text(e)) - return result + new_task.args.update( + dict( + src=result_file, + dest=dest, + follow=follow, + ), + ) + copy_action = self._shared_loader_obj.action_loader.get('copy', + task=new_task, + connection=self._connection, + play_context=self._play_context, + loader=self._loader, + templar=self._templar, + shared_loader_obj=self._shared_loader_obj) + result.update(copy_action.run(task_vars=task_vars)) + finally: + shutil.rmtree(tempdir) + except AnsibleAction as e: + result.update(e.result) finally: - self._loader.cleanup_tmp_file(tmp_source) - - new_task = self._task.copy() - new_task.args.pop('newline_sequence', None) - new_task.args.pop('block_start_string', None) - new_task.args.pop('block_end_string', None) - new_task.args.pop('variable_start_string', None) - new_task.args.pop('variable_end_string', None) - new_task.args.pop('trim_blocks', None) - try: - tempdir = tempfile.mkdtemp() - result_file = os.path.join(tempdir, os.path.basename(source)) - with open(result_file, 'wb') as f: - f.write(to_bytes(resultant, errors='surrogate_or_strict')) - - new_task.args.update( - dict( - src=result_file, - dest=dest, - follow=follow, - ), - ) - copy_action = self._shared_loader_obj.action_loader.get('copy', - task=new_task, - connection=self._connection, - play_context=self._play_context, - loader=self._loader, - templar=self._templar, - shared_loader_obj=self._shared_loader_obj) - result.update(copy_action.run(task_vars=task_vars)) - finally: - shutil.rmtree(tempdir) + self._remove_tmp_path(self._connection._shell.tempdir) return result diff --git a/lib/ansible/plugins/action/unarchive.py b/lib/ansible/plugins/action/unarchive.py index 1a1464debe4..7b244c1ec44 100644 --- a/lib/ansible/plugins/action/unarchive.py +++ b/lib/ansible/plugins/action/unarchive.py @@ -20,7 +20,7 @@ __metaclass__ = type import os -from ansible.errors import AnsibleError +from ansible.errors import AnsibleError, AnsibleAction, AnsibleActionFail, AnsibleActionSkip from ansible.module_utils._text import to_text from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase @@ -43,96 +43,81 @@ class ActionModule(ActionBase): creates = self._task.args.get('creates', None) decrypt = self._task.args.get('decrypt', True) - # "copy" is deprecated in favor of "remote_src". - if 'copy' in self._task.args: - # They are mutually exclusive. - if 'remote_src' in self._task.args: - result['failed'] = True - result['msg'] = "parameters are mutually exclusive: ('copy', 'remote_src')" - return result - # We will take the information from copy and store it in - # the remote_src var to use later in this file. - self._task.args['remote_src'] = remote_src = not boolean(self._task.args.pop('copy'), strict=False) - - if source is None or dest is None: - result['failed'] = True - result['msg'] = "src (or content) and dest are required" - return result - - if not tmp: - tmp = self._make_tmp_path() - - if creates: - # do not run the command if the line contains creates=filename - # and the filename already exists. This allows idempotence - # of command executions. - creates = self._remote_expand_user(creates) - if self._remote_file_exists(creates): - result['skipped'] = True - result['msg'] = "skipped, since %s exists" % creates - self._remove_tmp_path(tmp) - return result - - dest = self._remote_expand_user(dest) # CCTODO: Fix path for Windows hosts. - source = os.path.expanduser(source) - - if not remote_src: - try: - source = self._loader.get_real_file(self._find_needle('files', source), decrypt=decrypt) - except AnsibleError as e: - result['failed'] = True - result['msg'] = to_text(e) - self._remove_tmp_path(tmp) - return result - try: - remote_stat = self._execute_remote_stat(dest, all_vars=task_vars, follow=True) - except AnsibleError as e: - result['failed'] = True - result['msg'] = to_text(e) - self._remove_tmp_path(tmp) - return result + # "copy" is deprecated in favor of "remote_src". + if 'copy' in self._task.args: + # They are mutually exclusive. + if 'remote_src' in self._task.args: + raise AnsibleActionFail("parameters are mutually exclusive: ('copy', 'remote_src')") + # We will take the information from copy and store it in + # the remote_src var to use later in this file. + self._task.args['remote_src'] = remote_src = not boolean(self._task.args.pop('copy'), strict=False) - if not remote_stat['exists'] or not remote_stat['isdir']: - result['failed'] = True - result['msg'] = "dest '%s' must be an existing dir" % dest - self._remove_tmp_path(tmp) - return result + if source is None or dest is None: + raise AnsibleActionFail("src (or content) and dest are required") - if not remote_src: - # transfer the file to a remote tmp location - tmp_src = self._connection._shell.join_path(tmp, 'source') - self._transfer_file(source, tmp_src) + if creates: + # do not run the command if the line contains creates=filename + # and the filename already exists. This allows idempotence + # of command executions. + creates = self._remote_expand_user(creates) + if self._remote_file_exists(creates): + raise AnsibleActionSkip("skipped, since %s exists" % creates) - # handle diff mode client side - # handle check mode client side + dest = self._remote_expand_user(dest) # CCTODO: Fix path for Windows hosts. + source = os.path.expanduser(source) - if not remote_src: - # fix file permissions when the copy is done as a different user - self._fixup_perms2((tmp, tmp_src)) - # Build temporary module_args. - new_module_args = self._task.args.copy() - new_module_args.update( - dict( - src=tmp_src, - original_basename=os.path.basename(source), - ), - ) + if not remote_src: + try: + source = self._loader.get_real_file(self._find_needle('files', source), decrypt=decrypt) + except AnsibleError as e: + raise AnsibleActionFail(to_text(e)) - else: - new_module_args = self._task.args.copy() - new_module_args.update( - dict( - original_basename=os.path.basename(source), - ), - ) + try: + remote_stat = self._execute_remote_stat(dest, all_vars=task_vars, follow=True) + except AnsibleError as e: + raise AnsibleActionFail(to_text(e)) - # remove action plugin only key - for key in ('decrypt',): - if key in new_module_args: - del new_module_args[key] + if not remote_stat['exists'] or not remote_stat['isdir']: + raise AnsibleActionFail("dest '%s' must be an existing dir" % dest) - # execute the unarchive module now, with the updated args - result.update(self._execute_module(module_args=new_module_args, task_vars=task_vars)) - self._remove_tmp_path(tmp) + if not remote_src: + # transfer the file to a remote tmp location + tmp_src = self._connection._shell.join_path(self._connection._shell.tempdir, 'source') + self._transfer_file(source, tmp_src) + + # handle diff mode client side + # handle check mode client side + + if not remote_src: + # fix file permissions when the copy is done as a different user + self._fixup_perms2((self._connection._shell.tempdir, tmp_src)) + # Build temporary module_args. + new_module_args = self._task.args.copy() + new_module_args.update( + dict( + src=tmp_src, + original_basename=os.path.basename(source), + ), + ) + + else: + new_module_args = self._task.args.copy() + new_module_args.update( + dict( + original_basename=os.path.basename(source), + ), + ) + + # remove action plugin only key + for key in ('decrypt',): + if key in new_module_args: + del new_module_args[key] + + # execute the unarchive module now, with the updated args + result.update(self._execute_module(module_args=new_module_args, task_vars=task_vars)) + except AnsibleAction as e: + result.update(e.result) + finally: + self._remove_tmp_path(self._connection._shell.tempdir) return result diff --git a/lib/ansible/plugins/action/win_copy.py b/lib/ansible/plugins/action/win_copy.py index b5013812f8a..cdb4f2999d6 100644 --- a/lib/ansible/plugins/action/win_copy.py +++ b/lib/ansible/plugins/action/win_copy.py @@ -15,6 +15,7 @@ import tempfile import traceback import zipfile +from ansible import constants as C from ansible.errors import AnsibleError, AnsibleFileNotFound from ansible.module_utils._text import to_bytes, to_native, to_text from ansible.module_utils.parsing.convert_bool import boolean @@ -218,7 +219,7 @@ class ActionModule(ActionBase): def _create_content_tempfile(self, content): ''' Create a tempfile containing defined content ''' - fd, content_tempfile = tempfile.mkstemp() + fd, content_tempfile = tempfile.mkstemp(dir=C.DEFAULT_LOCAL_TMP) f = os.fdopen(fd, 'wb') content = to_bytes(content) try: diff --git a/lib/ansible/plugins/connection/__init__.py b/lib/ansible/plugins/connection/__init__.py index f34e3d51fe2..f8ae0131c5f 100644 --- a/lib/ansible/plugins/connection/__init__.py +++ b/lib/ansible/plugins/connection/__init__.py @@ -60,7 +60,7 @@ class ConnectionBase(AnsiblePlugin): supports_persistence = False force_persistence = False - def __init__(self, play_context, new_stdin, *args, **kwargs): + def __init__(self, play_context, new_stdin, shell=None, *args, **kwargs): super(ConnectionBase, self).__init__() @@ -78,9 +78,11 @@ class ConnectionBase(AnsiblePlugin): self.success_key = None self.prompt = None self._connected = False - self._socket_path = None + if shell is not None: + self._shell = shell + # load the shell plugin for this action/connection if play_context.shell: shell_type = play_context.shell diff --git a/lib/ansible/plugins/loader.py b/lib/ansible/plugins/loader.py index 4177138b16f..16f66ed0404 100644 --- a/lib/ansible/plugins/loader.py +++ b/lib/ansible/plugins/loader.py @@ -19,7 +19,7 @@ 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 +from ansible.utils.plugin_docs import get_docstring try: from __main__ import display @@ -209,14 +209,14 @@ class PluginLoader: if self.class_name: type_name = get_plugin_class(self.class_name) - # FIXME: expand from just connection and callback - if type_name in ('callback', 'connection', 'inventory', 'lookup'): - dstring = read_docstring(path, verbose=False, ignore_errors=False) + # FIXME: expand to other plugins, but never doc fragments + # if type name != 'module_doc_fragment': + if type_name in ('callback', 'connection', 'inventory', 'lookup', 'shell'): + dstring = get_docstring(path, fragment_loader, verbose=False, ignore_errors=True)[0] - if dstring.get('doc', False): - if 'options' in dstring['doc'] and isinstance(dstring['doc']['options'], dict): - C.config.initialize_plugin_configuration_definitions(type_name, name, dstring['doc']['options']) - display.debug('Loaded config def from plugin (%s/%s)' % (type_name, name)) + if 'options' in dstring and isinstance(dstring['options'], dict): + C.config.initialize_plugin_configuration_definitions(type_name, name, dstring['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 ''' @@ -462,6 +462,14 @@ class PluginLoader: self._update_object(obj, name, path) yield obj +# doc fragments first +fragment_loader = PluginLoader( + 'ModuleDocFragment', + 'ansible.utils.module_docs_fragments', + os.path.join(os.path.dirname(__file__), 'module_docs_fragments'), + '', +) + action_loader = PluginLoader( 'ActionModule', 'ansible.plugins.action', @@ -545,13 +553,6 @@ test_loader = PluginLoader( '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', diff --git a/lib/ansible/plugins/shell/__init__.py b/lib/ansible/plugins/shell/__init__.py index 4b057f23ae5..a1ed09e0f29 100644 --- a/lib/ansible/plugins/shell/__init__.py +++ b/lib/ansible/plugins/shell/__init__.py @@ -18,10 +18,10 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import os -import re -import ansible.constants as C -import time +import os.path import random +import re +import time from ansible.module_utils.six import text_type from ansible.module_utils.six.moves import shlex_quote @@ -31,26 +31,32 @@ _USER_HOME_PATH_RE = re.compile(r'^~[_.A-Za-z0-9][-_.A-Za-z0-9]*$') class ShellBase(AnsiblePlugin): - def __init__(self): super(ShellBase, self).__init__() - self.env = dict() - if C.DEFAULT_MODULE_SET_LOCALE: - module_locale = C.DEFAULT_MODULE_LANG or os.getenv('LANG', 'en_US.UTF-8') + self.env = {} + self.tempdir = None + + def set_options(self, task_keys=None, var_options=None, direct=None): + + super(ShellBase, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) + + # not all shell modules have this option + if self.get_option('set_module_language'): self.env.update( dict( - LANG=module_locale, - LC_ALL=module_locale, - LC_MESSAGES=module_locale, + LANG=self.get_option('module_language'), + LC_ALL=self.get_option('module_language'), + LC_MESSAGES=self.get_option('module_language'), ) ) + # set env + self.env.update(self.get_option('environment')) + def env_prefix(self, **kwargs): - env = self.env.copy() - env.update(kwargs) - return ' '.join(['%s=%s' % (k, shlex_quote(text_type(v))) for k, v in env.items()]) + return ' '.join(['%s=%s' % (k, shlex_quote(text_type(v))) for k, v in kwargs.items()]) def join_path(self, *args): return os.path.join(*args) @@ -96,32 +102,27 @@ class ShellBase(AnsiblePlugin): cmd = ['test', '-e', shlex_quote(path)] return ' '.join(cmd) - def mkdtemp(self, basefile=None, system=False, mode=None, tmpdir=None): + def mkdtemp(self, basefile=None, system=False, mode=0o700, tmpdir=None): if not basefile: basefile = 'ansible-tmp-%s-%s' % (time.time(), random.randint(0, 2**48)) # When system is specified we have to create this in a directory where - # other users can read and access the temp directory. This is because - # we use system to create tmp dirs for unprivileged users who are - # sudo'ing to a second unprivileged user. The only dirctories where - # that is standard are the tmp dirs, /tmp and /var/tmp. So we only - # allow one of those two locations if system=True. However, users - # might want to have some say over which of /tmp or /var/tmp is used - # (because /tmp may be a tmpfs and want to conserve RAM or persist the - # tmp files beyond a reboot. So we check if the user set REMOTE_TMP - # to somewhere in or below /var/tmp and if so use /var/tmp. If - # anything else we use /tmp (because /tmp is specified by POSIX nad - # /var/tmp is not). + # other users can read and access the temp directory. + # This is because we use system to create tmp dirs for unprivileged users who are + # sudo'ing to a second unprivileged user. + # The 'system_temps' setting defines dirctories we can use for this purpose + # the default are, /tmp and /var/tmp. + # So we only allow one of those locations if system=True, using the + # passed in tmpdir if it is valid or the first one from the setting if not. if system: - # FIXME: create 'system tmp dirs' config/var and check tmpdir is in those values to allow for /opt/tmp, etc - if tmpdir.startswith('/var/tmp'): - basetmpdir = '/var/tmp' + if tmpdir.startswith(tuple(self.get_option('system_temps'))): + basetmpdir = tmpdir else: - basetmpdir = '/tmp' + basetmpdir = self.get_option('system_temps')[0] else: if tmpdir is None: - basetmpdir = C.DEFAULT_REMOTE_TMP + basetmpdir = self.get_option('remote_temp') else: basetmpdir = tmpdir @@ -138,13 +139,15 @@ class ShellBase(AnsiblePlugin): return cmd - def expand_user(self, user_home_path): + def expand_user(self, user_home_path, username=''): ''' Return a command to expand tildes in a path - It can be either "~" or "~username". We use the POSIX definition of - a username: + It can be either "~" or "~username". We just ignore $HOME + We use the POSIX definition of a username: http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap03.html#tag_03_426 http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap03.html#tag_03_276 + + Falls back to 'current workind directory' as we assume 'home is where the remote user ends up' ''' # Check that the user_path to expand is safe @@ -152,9 +155,17 @@ class ShellBase(AnsiblePlugin): if not _USER_HOME_PATH_RE.match(user_home_path): # shlex_quote will make the shell return the string verbatim user_home_path = shlex_quote(user_home_path) + elif username: + # if present the user name is appended to resolve "that user's home" + user_home_path += username + return 'echo %s' % user_home_path - def build_module_command(self, env_string, shebang, cmd, arg_path=None, rm_tmp=None): + def pwd(self): + """Return the working directory after connecting""" + return 'echo %spwd%s' % (self._SHELL_SUB_LEFT, self._SHELL_SUB_RIGHT) + + def build_module_command(self, env_string, shebang, cmd, arg_path=None): # don't quote the cmd if it's an empty string, because this will break pipelining mode if cmd.strip() != '': cmd = shlex_quote(cmd) @@ -168,8 +179,6 @@ class ShellBase(AnsiblePlugin): if arg_path is not None: cmd_parts.append(arg_path) new_cmd = " ".join(cmd_parts) - if rm_tmp: - new_cmd = '%s; rm -rf "%s" %s' % (new_cmd, rm_tmp, self._SHELL_REDIRECT_ALLNULL) return new_cmd def append_command(self, cmd, cmd_to_append): diff --git a/lib/ansible/plugins/shell/csh.py b/lib/ansible/plugins/shell/csh.py index 8ed3decf492..11b46f5f134 100644 --- a/lib/ansible/plugins/shell/csh.py +++ b/lib/ansible/plugins/shell/csh.py @@ -1,24 +1,22 @@ -# (c) 2014, Chris Church -# -# 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 . +# Copyright (c) 2014, Chris Church +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible.plugins.shell import ShellBase +DOCUMENTATION = ''' + name: csh + plugin_type: shell + version_added: "" + short_description: C shell (/bin/csh) + description: + - When you have no other option than to use csh + extends_documentation_fragment: + - shell_common +''' + class ShellModule(ShellBase): diff --git a/lib/ansible/plugins/shell/fish.py b/lib/ansible/plugins/shell/fish.py index e70a8c206fc..b9ce67a867b 100644 --- a/lib/ansible/plugins/shell/fish.py +++ b/lib/ansible/plugins/shell/fish.py @@ -1,19 +1,6 @@ -# (c) 2014, Chris Church -# -# 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 . +# Copyright (c) 2014, Chris Church +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type @@ -21,6 +8,17 @@ from ansible.module_utils.six import text_type from ansible.module_utils.six.moves import shlex_quote from ansible.plugins.shell.sh import ShellModule as ShModule +DOCUMENTATION = ''' + name: fish + plugin_type: shell + version_added: "" + short_description: fish shell (/bin/fish) + description: + - This is here because some people are restricted to fish. + extends_documentation_fragment: + - shell_common +''' + class ShellModule(ShModule): @@ -43,7 +41,7 @@ class ShellModule(ShModule): env.update(kwargs) return ' '.join(['set -lx %s %s;' % (k, shlex_quote(text_type(v))) for k, v in env.items()]) - def build_module_command(self, env_string, shebang, cmd, arg_path=None, rm_tmp=None): + def build_module_command(self, env_string, shebang, cmd, arg_path=None): # don't quote the cmd if it's an empty string, because this will break pipelining mode if cmd.strip() != '': cmd = shlex_quote(cmd) @@ -51,8 +49,6 @@ class ShellModule(ShModule): if arg_path is not None: cmd_parts.append(arg_path) new_cmd = " ".join(cmd_parts) - if rm_tmp: - new_cmd = 'begin ; %s; rm -rf "%s" %s ; end' % (new_cmd, rm_tmp, self._SHELL_REDIRECT_ALLNULL) return new_cmd def checksum(self, path, python_interp): diff --git a/lib/ansible/plugins/shell/powershell.py b/lib/ansible/plugins/shell/powershell.py index d131442cd14..dd0de946954 100644 --- a/lib/ansible/plugins/shell/powershell.py +++ b/lib/ansible/plugins/shell/powershell.py @@ -1,22 +1,18 @@ -# (c) 2014, Chris Church -# -# 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 . +# Copyright (c) 2014, Chris Church +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type +DOCUMENTATION = ''' + name: powershell + plugin_type: shell + version_added: "" + short_description: Windows Powershell + description: + - The only option whne using 'winrm' as a connection plugin +''' + import base64 import os import re @@ -1693,8 +1689,10 @@ Function Run($payload) { ''' # end async_watchdog +from ansible.plugins import AnsiblePlugin -class ShellModule(object): + +class ShellModule(AnsiblePlugin): # Common shell filenames that this plugin handles # Powershell is handled differently. It's selected when winrm is the @@ -1773,7 +1771,7 @@ class ShellModule(object): # FIXME: Support system temp path and passed in tmpdir! return self._encode_script('''(New-Item -Type Directory -Path $env:temp -Name "%s").FullName | Write-Host -Separator '';''' % basefile) - def expand_user(self, user_home_path): + def expand_user(self, user_home_path, username=''): # PowerShell only supports "~" (not "~username"). Resolve-Path ~ does # not seem to work remotely, though by default we are always starting # in the user's home directory. @@ -1823,7 +1821,7 @@ class ShellModule(object): ''' % dict(path=path) return self._encode_script(script) - def build_module_command(self, env_string, shebang, cmd, arg_path=None, rm_tmp=None): + def build_module_command(self, env_string, shebang, cmd, arg_path=None): # pipelining bypass if cmd == '': return '-' @@ -1878,10 +1876,6 @@ class ShellModule(object): Exit 1 } ''' % (env_string, ' '.join(cmd_parts)) - if rm_tmp: - rm_tmp = self._escape(self._unquote(rm_tmp)) - rm_cmd = 'Remove-Item "%s" -Force -Recurse -ErrorAction SilentlyContinue' % rm_tmp - script = '%s\nFinally { %s }' % (script, rm_cmd) return self._encode_script(script, preserve_rc=False) def wrap_for_exec(self, cmd): diff --git a/lib/ansible/plugins/shell/sh.py b/lib/ansible/plugins/shell/sh.py index b7acb664951..b4c59f4d467 100644 --- a/lib/ansible/plugins/shell/sh.py +++ b/lib/ansible/plugins/shell/sh.py @@ -1,22 +1,19 @@ -# (c) 2014, Chris Church -# -# 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 . +# Copyright (c) 2014, Chris Church +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type +DOCUMENTATION = ''' +name: sh +plugin_type: shell +short_description: "POSIX shell (/bin/sh)" +version_added: historical +description: + - This shell plugin is the one you want to use on most Unix systems, it is the most compatible and widely installed shell. +extends_documentation_fragment: + - shell_common +''' from ansible.module_utils.six.moves import shlex_quote from ansible.plugins.shell import ShellBase @@ -26,6 +23,8 @@ class ShellModule(ShellBase): # Common shell filenames that this plugin handles. # Note: sh is the default shell plugin so this plugin may also be selected + # This code needs to be SH-compliant. BASH-isms will not work if /bin/sh points to a non-BASH shell. + # if the filename is not listed in any Shell plugin. COMPATIBLE_SHELLS = frozenset(('sh', 'zsh', 'bash', 'dash', 'ksh')) # Family of shells this has. Must match the filename without extension @@ -42,22 +41,16 @@ class ShellModule(ShellBase): _SHELL_GROUP_RIGHT = ')' def checksum(self, path, python_interp): - # The following test needs to be SH-compliant. BASH-isms will - # not work if /bin/sh points to a non-BASH shell. - # # In the following test, each condition is a check and logical # comparison (|| or &&) that sets the rc value. Every check is run so - # the last check in the series to fail will be the rc that is - # returned. + # the last check in the series to fail will be the rc that is returned. # # If a check fails we error before invoking the hash functions because # hash functions may successfully take the hash of a directory on BSDs - # (UFS filesystem?) which is not what the rest of the ansible code - # expects + # (UFS filesystem?) which is not what the rest of the ansible code expects # - # If all of the available hashing methods fail we fail with an rc of - # 0. This logic is added to the end of the cmd at the bottom of this - # function. + # If all of the available hashing methods fail we fail with an rc of 0. + # This logic is added to the end of the cmd at the bottom of this function. # Return codes: # checksum: success! diff --git a/lib/ansible/utils/module_docs_fragments/constructed.py b/lib/ansible/utils/module_docs_fragments/constructed.py index 3b7fcdb8921..4f8036abdc0 100644 --- a/lib/ansible/utils/module_docs_fragments/constructed.py +++ b/lib/ansible/utils/module_docs_fragments/constructed.py @@ -1,20 +1,5 @@ -# -# (c) 2016, Sumit Kumar -# -# 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 . +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) class ModuleDocFragment(object): diff --git a/lib/ansible/utils/module_docs_fragments/shell_common.py b/lib/ansible/utils/module_docs_fragments/shell_common.py new file mode 100644 index 00000000000..d6482e0821a --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/shell_common.py @@ -0,0 +1,92 @@ +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + + # common shelldocumentation fragment + DOCUMENTATION = """ +options: + remote_temp: + description: + - Temporary directory to use on targets when executing tasks. + default: '~/.ansible/tmp' + env: [{name: ANSIBLE_REMOTE_TEMP}] + ini: + - section: defaults + key: remote_tmp + vars: + - name: ansible_remote_tmp + system_temps: + description: + - List of valid system temporary directories for Ansible to choose when it cannot use ``remote_temp``, normally due to permission issues. + default: [ /var/tmp, /tmp ] + type: list + env: [{name: ANSIBLE_SYSTEM_TMPS}] + ini: + - section: defaults + key: system_tmps + vars: + - name: ansible_system_tmps + async_dir: + description: + - Directory in which ansible will keep async job inforamtion + default: '~/.ansible_async' + env: [{name: ANSIBLE_ASYNC_DIR}] + ini: + - section: defaults + key: async_dir + vars: + - name: ansible_async_dir + set_module_language: + default: False + description: Controls if we set locale for modules when executing on the target. + env: + - name: ANSIBLE_MODULE_SET_LOCALE + ini: + - section: defaults + key: module_set_locale + type: boolean + vars: + - name: ansible_module_set_locale + module_language: + description: + - "If 'set_module_language' is true, this is the language language/locale setting to use for modules when they execute on the target." + - "Defaults to match the controller's settings." + default: "{{CONTROLLER_LANG}}" + env: + - name: ANSIBLE_MODULE_LANG + ini: + - section: defaults + key: module_lang + vars: + - name: ansible_module_lang + environment: + type: dict + default: {} + description: + - dictionary of environment variables and their values to use when executing commands. + admin_users: + type: list + default: ['root', 'toor', 'admin'] + description: + - list of users to be expected to have admin privileges, for BSD you might want to add 'toor' for windows 'Administrator'. + env: + - name: ANSIBLE_ADMIN_USERS + ini: + - section: defaults + key: admin_users + vars: + - name: ansible_admin_users + allow_world_readable_temp: + type: boolean + description: + - This makes the temporary files created on the machine to be world readable and will issue a warning instead of failing the task. + - It is useful when becoming an unprivileged user. + ini: + - section: defaults + key: allow_world_readable_tmpfiles + vars: + - name: ansible_world_readable_tmpfiles + version_added: "2.1" +""" diff --git a/lib/ansible/utils/plugin_docs.py b/lib/ansible/utils/plugin_docs.py index 09d6e5f374e..206442a52da 100644 --- a/lib/ansible/utils/plugin_docs.py +++ b/lib/ansible/utils/plugin_docs.py @@ -27,7 +27,6 @@ from ansible.module_utils.six import string_types from ansible.module_utils._text import to_native from ansible.parsing.plugin_docs import read_docstring from ansible.parsing.yaml.loader import AnsibleLoader -from ansible.plugins.loader import fragment_loader try: from __main__ import display @@ -59,7 +58,7 @@ def merge_fragment(target, source): target[key] = value -def add_fragments(doc, filename): +def add_fragments(doc, filename, fragment_loader): fragments = doc.pop('extends_documentation_fragment', []) @@ -99,6 +98,8 @@ def add_fragments(doc, filename): merge_fragment(doc['options'], fragment.pop('options')) except Exception as e: raise AnsibleError("%s options (%s) of unknown type: %s" % (to_native(e), fragment_name, filename)) + else: + doc['options'] = fragment.pop('options') # merge rest of the sections try: @@ -107,15 +108,15 @@ def add_fragments(doc, filename): raise AnsibleError("%s (%s) of unknown type: %s" % (to_native(e), fragment_name, filename)) -def get_docstring(filename, verbose=False): +def get_docstring(filename, fragment_loader, verbose=False, ignore_errors=False): """ DOCUMENTATION can be extended using documentation fragments loaded by the PluginLoader from the module_docs_fragments directory. """ - data = read_docstring(filename, verbose=verbose) + data = read_docstring(filename, verbose=verbose, ignore_errors=ignore_errors) # add fragments to documentation if data.get('doc', False): - add_fragments(data['doc'], filename) + add_fragments(data['doc'], filename, fragment_loader=fragment_loader) return data['doc'], data['plainexamples'], data['returndocs'], data['metadata'] diff --git a/test/integration/targets/copy/tasks/main.yml b/test/integration/targets/copy/tasks/main.yml index 274712a7e79..c76ba37d1c3 100644 --- a/test/integration/targets/copy/tasks/main.yml +++ b/test/integration/targets/copy/tasks/main.yml @@ -1,7 +1,7 @@ - block: - name: Create a local temporary directory - shell: mktemp -d "${TMPDIR:-/tmp}/ansible_test.XXXXXXXXX" + shell: mktemp -d /tmp/ansible_test.XXXXXXXXX register: tempfile_result connection: local @@ -10,6 +10,9 @@ # output_dir is hardcoded in test/runner/lib/executor.py and created there remote_dir: '{{ output_dir }}' + - file: path={{local_temp_dir}} state=directory + name: ensure temp dir exists + - name: Create remote unprivileged remote user user: name: '{{ remote_unprivileged_user }}' diff --git a/test/integration/targets/script/tasks/main.yml b/test/integration/targets/script/tasks/main.yml index 15b0179ac06..37975ca5d11 100644 --- a/test/integration/targets/script/tasks/main.yml +++ b/test/integration/targets/script/tasks/main.yml @@ -198,7 +198,7 @@ assert: that: - _check_mode_test2 is skipped - - '_check_mode_test2.msg == "skipped, since {{ output_dir_test | expanduser }}/afile2.txt exists"' + - '_check_mode_test2.msg == "{{ output_dir_test | expanduser }}/afile2.txt exists, matching creates option"' - name: Remove afile2.txt file: @@ -220,4 +220,4 @@ assert: that: - _check_mode_test3 is skipped - - '_check_mode_test3.msg == "skipped, since {{ output_dir_test | expanduser }}/afile2.txt does not exist"' + - '_check_mode_test3.msg == "{{ output_dir_test | expanduser }}/afile2.txt does not exist, matching removes option"' diff --git a/test/sanity/validate-modules/main.py b/test/sanity/validate-modules/main.py index eb6759ae189..908ea3bd4fe 100755 --- a/test/sanity/validate-modules/main.py +++ b/test/sanity/validate-modules/main.py @@ -38,6 +38,7 @@ from fnmatch import fnmatch from ansible import __version__ as ansible_version from ansible.executor.module_common import REPLACER_WINDOWS +from ansible.plugins.loader import fragment_loader from ansible.utils.plugin_docs import BLACKLIST, get_docstring from module_args import AnsibleModuleImportError, get_argument_spec @@ -829,7 +830,7 @@ class ModuleValidator(Validator): if not errors and not traces: with CaptureStd(): try: - get_docstring(self.path, verbose=True) + get_docstring(self.path, fragment_loader, verbose=True) except AssertionError: fragment = doc['extends_documentation_fragment'] self.reporter.error( @@ -1026,7 +1027,7 @@ class ModuleValidator(Validator): with CaptureStd(): try: - existing_doc, _, _, _ = get_docstring(self.base_module, verbose=True) + existing_doc = get_docstring(self.base_module, fragment_loader, verbose=True)[0] existing_options = existing_doc.get('options', {}) or {} except AssertionError: fragment = doc['extends_documentation_fragment'] diff --git a/test/units/plugins/action/test_action.py b/test/units/plugins/action/test_action.py index 0139732b63c..987af9e360c 100644 --- a/test/units/plugins/action/test_action.py +++ b/test/units/plugins/action/test_action.py @@ -21,6 +21,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import os +import re from ansible import constants as C from ansible.compat.tests import unittest @@ -229,11 +230,23 @@ class TestActionBase(unittest.TestCase): # create our fake task mock_task = MagicMock() + def get_shell_opt(opt): + + ret = None + if opt == 'admin_users': + ret = ['root', 'toor', 'Administrator'] + elif opt == 'remote_temp': + ret = '~/.ansible/tmp' + + return ret + # create a mock connection, so we don't actually try and connect to things mock_connection = MagicMock() mock_connection.transport = 'ssh' mock_connection._shell.mkdtemp.return_value = 'mkdir command' mock_connection._shell.join_path.side_effect = os.path.join + mock_connection._shell.get_option = get_shell_opt + mock_connection._shell.HOMES_RE = re.compile(r'(\'|\")?(~|\$HOME)(.*)') # we're using a real play context here play_context = PlayContext() @@ -395,12 +408,10 @@ class TestActionBase(unittest.TestCase): mock_task.args = dict(a=1, b=2, c=3) # create a mock connection, so we don't actually try and connect to things - def build_module_command(env_string, shebang, cmd, arg_path=None, rm_tmp=None): + def build_module_command(env_string, shebang, cmd, arg_path=None): to_run = [env_string, cmd] if arg_path: to_run.append(arg_path) - if rm_tmp: - to_run.append(rm_tmp) return " ".join(to_run) mock_connection = MagicMock() diff --git a/test/units/plugins/action/test_synchronize.py b/test/units/plugins/action/test_synchronize.py index ad78f9ccaf0..5d5ea26a137 100644 --- a/test/units/plugins/action/test_synchronize.py +++ b/test/units/plugins/action/test_synchronize.py @@ -63,6 +63,11 @@ class ConnectionMock(object): transport = None _new_stdin = StdinMock() + # my shell + _shell = MagicMock() + _shell.mkdtemp.return_value = 'mkdir command' + _shell.join_path.side_effect = os.path.join + class PlayContextMock(object): shell = None