diff --git a/lib/ansible/modules/commands/script.py b/lib/ansible/modules/commands/script.py index 755792f6e56..308424c6985 100644 --- a/lib/ansible/modules/commands/script.py +++ b/lib/ansible/modules/commands/script.py @@ -50,6 +50,12 @@ options: required: no default: null version_added: "1.5" + chdir: + description: + - cd into this directory on the remote node before running the script + version_added: "2.4" + required: false + default: null notes: - It is usually preferable to write Ansible modules than pushing scripts. Convert your script to an Ansible module for bonus points! - The ssh connection plugin will force pseudo-tty allocation via -tt when scripts are executed. pseudo-ttys do not have a stderr channel and all diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index f222f8e11b3..a229a4dba27 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -842,7 +842,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): data['rc'] = res['rc'] return data - def _low_level_execute_command(self, cmd, sudoable=True, in_data=None, executable=None, encoding_errors='surrogate_then_replace'): + def _low_level_execute_command(self, cmd, sudoable=True, in_data=None, executable=None, encoding_errors='surrogate_then_replace', chdir=None): ''' This is the function which executes the low level shell command, which may be commands to create/remove directories for temporary files, or to @@ -855,6 +855,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): used as a key or is going to be written back out to a file verbatim, then this won't work. May have to use some sort of replacement strategy (python3 could use surrogateescape) + :kwarg chdir: cd into this directory before executing the command. ''' display.debug("_low_level_execute_command(): starting") @@ -863,6 +864,10 @@ class ActionBase(with_metaclass(ABCMeta, object)): # display.debug("_low_level_execute_command(): no command, exiting") # return dict(stdout='', stderr='', rc=254) + if chdir: + display.debug("_low_level_execute_command(): changing cwd to %s for this command" % chdir) + cmd = self._connection._shell.append_command('cd %s' % chdir, cmd) + allow_same_user = C.BECOME_ALLOW_SAME_USER same_user = self._play_context.become_user == self._play_context.remote_user if sudoable and self._play_context.become and (allow_same_user or not same_user): diff --git a/lib/ansible/plugins/action/script.py b/lib/ansible/plugins/action/script.py index f369e67ca5c..f37964da3cf 100644 --- a/lib/ansible/plugins/action/script.py +++ b/lib/ansible/plugins/action/script.py @@ -18,6 +18,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import os +import re from ansible.errors import AnsibleError from ansible.module_utils._text import to_native @@ -27,6 +28,10 @@ from ansible.plugins.action import ActionBase class ActionModule(ActionBase): TRANSFERS_FILES = True + # On Windows platform, absolute paths begin with a (back)slash + # after chopping off a potential drive letter. + windows_absolute_path_detection = re.compile(r'^(?:[a-zA-Z]\:)?(\\|\/)') + def run(self, tmp=None, task_vars=None): ''' handler for file transfer operations ''' if task_vars is None: @@ -55,6 +60,18 @@ class ActionModule(ActionBase): 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) + # the script name is the first item in the raw params, so we split it # out now so we know the file name we need to transfer to the remote, # and everything else is an argument to the script which we need later @@ -86,7 +103,7 @@ class ActionModule(ActionBase): 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)) + result.update(self._low_level_execute_command(cmd=script_cmd, in_data=exec_data, sudoable=True, chdir=chdir)) # clean up after self._remove_tmp_path(tmp)