diff options
-rw-r--r-- | changelogs/fragments/34722-ssh-sshpass-prompt-variable.yml | 2 | ||||
-rw-r--r-- | lib/ansible/plugins/connection/ssh.py | 25 | ||||
-rwxr-xr-x | test/integration/targets/connection_ssh/runme.sh | 44 |
3 files changed, 68 insertions, 3 deletions
diff --git a/changelogs/fragments/34722-ssh-sshpass-prompt-variable.yml b/changelogs/fragments/34722-ssh-sshpass-prompt-variable.yml new file mode 100644 index 0000000000..b9f386bea6 --- /dev/null +++ b/changelogs/fragments/34722-ssh-sshpass-prompt-variable.yml @@ -0,0 +1,2 @@ +minor_changes: +- ssh - connection plugin now supports a new variable ``sshpass_prompt`` which gets passed to ``sshpass`` allowing the user to set a custom substring to search for a password prompt (requires sshpass 1.06+) diff --git a/lib/ansible/plugins/connection/ssh.py b/lib/ansible/plugins/connection/ssh.py index 479e1919ce..5a51ee3ba8 100644 --- a/lib/ansible/plugins/connection/ssh.py +++ b/lib/ansible/plugins/connection/ssh.py @@ -48,6 +48,17 @@ DOCUMENTATION = ''' - name: ansible_password - name: ansible_ssh_pass - name: ansible_ssh_password + sshpass_prompt: + description: Password prompt that sshpass should search for. Supported by sshpass 1.06 and up. + default: '' + ini: + - section: 'ssh_connection' + key: 'sshpass_prompt' + env: + - name: ANSIBLE_SSHPASS_PROMPT + vars: + - name: ansible_sshpass_prompt + version_added: '2.10' ssh_args: description: Arguments to pass to all ssh cli tools default: '-C -o ControlMaster=auto -o ControlPersist=60s' @@ -331,13 +342,19 @@ def _handle_error(remaining_retries, command, return_tuple, no_log, host, displa raise AnsibleAuthenticationFailure(msg) # sshpass returns codes are 1-6. We handle 5 previously, so this catches other scenarios. - # No exception is raised, so the connection is retried. + # No exception is raised, so the connection is retried - except when attempting to use + # sshpass_prompt with an sshpass that won't let us pass -P, in which case we fail loudly. elif return_tuple[0] in [1, 2, 3, 4, 6]: msg = 'sshpass error:' if no_log: msg = '{0} <error censored due to no log>'.format(msg) else: - msg = '{0} {1}'.format(msg, to_native(return_tuple[2]).rstrip()) + details = to_native(return_tuple[2]).rstrip() + if "sshpass: invalid option -- 'P'" in details: + details = 'Installed sshpass version does not support customized password prompts. ' \ + 'Upgrade sshpass to use sshpass_prompt, or otherwise switch to ssh keys.' + raise AnsibleError('{0} {1}'.format(msg, details)) + msg = '{0} {1}'.format(msg, details) if return_tuple[0] == 255: SSH_ERROR = True @@ -562,6 +579,10 @@ class Connection(ConnectionBase): self.sshpass_pipe = os.pipe() b_command += [b'sshpass', b'-d' + to_bytes(self.sshpass_pipe[0], nonstring='simplerepr', errors='surrogate_or_strict')] + password_prompt = self.get_option('sshpass_prompt') + if password_prompt: + b_command += [b'-P', to_bytes(password_prompt, errors='surrogate_or_strict')] + if binary == 'ssh': b_command += [to_bytes(self._play_context.ssh_executable, errors='surrogate_or_strict')] else: diff --git a/test/integration/targets/connection_ssh/runme.sh b/test/integration/targets/connection_ssh/runme.sh index a24ff048c5..e7b2b21f0b 100755 --- a/test/integration/targets/connection_ssh/runme.sh +++ b/test/integration/targets/connection_ssh/runme.sh @@ -1,6 +1,48 @@ #!/usr/bin/env bash -set -eux +set -ux + +# We skip this whole section if the test node doesn't have sshpass on it. +if command -v sshpass > /dev/null; then + # Check if our sshpass supports -P + sshpass -P foo > /dev/null + sshpass_supports_prompt=$? + if [[ $sshpass_supports_prompt -eq 0 ]]; then + # If the prompt is wrong, we'll end up hanging (due to sshpass hanging). + # We should probably do something better here, like timing out in Ansible, + # but this has been the behavior for a long time, before we supported custom + # password prompts. + # + # So we search for a custom password prompt that is clearly wrong and call + # ansible with timeout. If we time out, our custom prompt was successfully + # searched for. It's a weird way of doing things, but it does ensure + # that the flag gets passed to sshpass. + timeout 5 ansible -m ping \ + -e ansible_connection=ssh \ + -e ansible_sshpass_prompt=notThis: \ + -e ansible_password=foo \ + -e ansible_user=definitelynotroot \ + -i test_connection.inventory \ + ssh-pipelining + ret=$? + if [[ $ret -ne 124 ]]; then + echo "Expected to time out and we did not. Exiting with failure." + exit 1 + fi + else + ansible -m ping \ + -e ansible_connection=ssh \ + -e ansible_sshpass_prompt=notThis: \ + -e ansible_password=foo \ + -e ansible_user=definitelynotroot \ + -i test_connection.inventory \ + ssh-pipelining | grep 'customized password prompts' + ret=$? + [[ $ret -eq 0 ]] || exit $ret + fi +fi + +set -e # temporary work-around for issues due to new scp filename checking # https://github.com/ansible/ansible/issues/52640 |