diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index 5837ecae809..08d522fcb60 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -237,7 +237,7 @@ DEFAULT_NULL_REPRESENTATION = get_config(p, DEFAULTS, 'null_representation', # CONNECTION RELATED ANSIBLE_SSH_ARGS = get_config(p, 'ssh_connection', 'ssh_args', 'ANSIBLE_SSH_ARGS', '-o ControlMaster=auto -o ControlPersist=60s') ANSIBLE_SSH_CONTROL_PATH = get_config(p, 'ssh_connection', 'control_path', 'ANSIBLE_SSH_CONTROL_PATH', "%(directory)s/ansible-ssh-%%h-%%p-%%r") -ANSIBLE_SSH_PIPELINING = get_config(p, 'ssh_connection', 'pipelining', 'ANSIBLE_SSH_PIPELINING', True, boolean=True) +ANSIBLE_SSH_PIPELINING = get_config(p, 'ssh_connection', 'pipelining', 'ANSIBLE_SSH_PIPELINING', False, boolean=True) ANSIBLE_SSH_RETRIES = get_config(p, 'ssh_connection', 'retries', 'ANSIBLE_SSH_RETRIES', 0, integer=True) PARAMIKO_RECORD_HOST_KEYS = get_config(p, 'paramiko_connection', 'record_host_keys', 'ANSIBLE_PARAMIKO_RECORD_HOST_KEYS', True, boolean=True) diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index 73eb5e4346f..64a3b51e5d3 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -177,7 +177,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): if tmp and "tmp" in tmp: # tmp has already been created return False - if not self._connection.has_pipelining or not self._play_context.pipelining or C.DEFAULT_KEEP_REMOTE_FILES: + if not self._connection.has_pipelining or not self._play_context.pipelining or C.DEFAULT_KEEP_REMOTE_FILES or self._play_context.become_method == 'su': # tmp is necessary to store the module source code # or we want to keep the files on the target system return True @@ -439,9 +439,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): # not sudoing or sudoing to root, so can cleanup files in the same step rm_tmp = tmp - python_interp = task_vars.get('ansible_python_interpreter', 'python') - - cmd = self._connection._shell.build_module_command(environment_string, shebang, cmd, arg_path=args_file_path, rm_tmp=rm_tmp, python_interpreter=python_interp) + cmd = self._connection._shell.build_module_command(environment_string, shebang, cmd, arg_path=args_file_path, rm_tmp=rm_tmp) cmd = cmd.strip() sudoable = True diff --git a/lib/ansible/plugins/connection/ssh.py b/lib/ansible/plugins/connection/ssh.py index 607dcd667fe..debe36bd320 100644 --- a/lib/ansible/plugins/connection/ssh.py +++ b/lib/ansible/plugins/connection/ssh.py @@ -241,7 +241,7 @@ class Connection(ConnectionBase): return self._command - def _send_initial_data(self, fh, in_data, tty=False): + def _send_initial_data(self, fh, in_data): ''' Writes initial data to the stdin filehandle of the subprocess and closes it. (The handle must be closed; otherwise, for example, "sftp -b -" will @@ -252,8 +252,6 @@ class Connection(ConnectionBase): try: fh.write(in_data) - if tty: - fh.write("__EOF__942d747a0772c3284ffb5920e234bd57__\n") fh.close() except (OSError, IOError): raise AnsibleConnectionFailure('SSH Error: data could not be sent to the remote host. Make sure this host can be reached over ssh') @@ -316,7 +314,7 @@ class Connection(ConnectionBase): return ''.join(output), remainder - def _run(self, cmd, in_data, sudoable=True, tty=False): + def _run(self, cmd, in_data, sudoable=True): ''' Starts the command and communicates with it until it ends. ''' @@ -324,10 +322,25 @@ class Connection(ConnectionBase): display_cmd = map(pipes.quote, cmd[:-1]) + [cmd[-1]] display.vvv('SSH: EXEC {0}'.format(' '.join(display_cmd)), host=self.host) - # Start the given command. + # Start the given command. If we don't need to pipeline data, we can try + # to use a pseudo-tty (ssh will have been invoked with -tt). If we are + # pipelining data, or can't create a pty, we fall back to using plain + # old pipes. - p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdin = p.stdin + p = None + if not in_data: + try: + # Make sure stdin is a proper pty to avoid tcgetattr errors + master, slave = pty.openpty() + p = subprocess.Popen(cmd, stdin=slave, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdin = os.fdopen(master, 'w', 0) + os.close(slave) + except (OSError, IOError): + p = None + + if not p: + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdin = p.stdin # If we are using SSH password authentication, write the password into # the pipe we opened in _build_command. @@ -390,7 +403,7 @@ class Connection(ConnectionBase): # before we call select. if states[state] == 'ready_to_send' and in_data: - self._send_initial_data(stdin, in_data, tty) + self._send_initial_data(stdin, in_data) state += 1 while True: @@ -488,7 +501,7 @@ class Connection(ConnectionBase): if states[state] == 'ready_to_send': if in_data: - self._send_initial_data(stdin, in_data, tty) + self._send_initial_data(stdin, in_data) state += 1 # Now we're awaiting_exit: has the child process exited? If it has, @@ -544,9 +557,17 @@ class Connection(ConnectionBase): display.vvv("ESTABLISH SSH CONNECTION FOR USER: {0}".format(self._play_context.remote_user), host=self._play_context.remote_addr) - cmd = self._build_command('ssh', '-tt', self.host, cmd) + # we can only use tty when we are not pipelining the modules. piping + # data into /usr/bin/python inside a tty automatically invokes the + # python interactive-mode but the modules are not compatible with the + # interactive-mode ("unexpected indent" mainly because of empty lines) - (returncode, stdout, stderr) = self._run(cmd, in_data, sudoable=sudoable, tty=True) + if in_data: + cmd = self._build_command('ssh', self.host, cmd) + else: + cmd = self._build_command('ssh', '-tt', self.host, cmd) + + (returncode, stdout, stderr) = self._run(cmd, in_data, sudoable=sudoable) return (returncode, stdout, stderr) diff --git a/lib/ansible/plugins/shell/powershell.py b/lib/ansible/plugins/shell/powershell.py index 9fd1541b63e..096a0cf95d6 100644 --- a/lib/ansible/plugins/shell/powershell.py +++ b/lib/ansible/plugins/shell/powershell.py @@ -110,7 +110,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, python_interpreter=None): + def build_module_command(self, env_string, shebang, cmd, arg_path=None, rm_tmp=None): cmd_parts = shlex.split(to_bytes(cmd), posix=False) cmd_parts = map(to_unicode, cmd_parts) if shebang and shebang.lower() == '#!powershell': diff --git a/lib/ansible/plugins/shell/sh.py b/lib/ansible/plugins/shell/sh.py index 719ac83ffb0..f1fa3565b76 100644 --- a/lib/ansible/plugins/shell/sh.py +++ b/lib/ansible/plugins/shell/sh.py @@ -138,17 +138,12 @@ class ShellModule(object): cmd = "%s; %s || (echo \'0 \'%s)" % (test, cmd, shell_escaped_path) return cmd - def build_module_command(self, env_string, shebang, cmd, arg_path=None, rm_tmp=None, python_interpreter='python'): + def build_module_command(self, env_string, shebang, cmd, arg_path=None, rm_tmp=None): # don't quote the cmd if it's an empty string, because this will # break pipelining mode - env = env_string.strip() - exe = shebang.replace("#!", "").strip() - if cmd.strip() == '': - reader = "%s -uc 'import sys; [sys.stdout.write(s) for s in iter(sys.stdin.readline, \"__EOF__942d747a0772c3284ffb5920e234bd57__\\n\")]'|" % python_interpreter - cmd_parts = [env, reader, env, exe] - else: + if cmd.strip() != '': cmd = pipes.quote(cmd) - cmd_parts = [env, exe, cmd] + cmd_parts = [env_string.strip(), shebang.replace("#!", "").strip(), cmd] if arg_path is not None: cmd_parts.append(arg_path) new_cmd = " ".join(cmd_parts)