mirror of
https://github.com/ansible/ansible.git
synced 2026-08-03 08:03:05 +02:00
Remove reliance on sshpass and utilize SSH_ASKPASS (#83936)
* Add SSH_ASKPASS as an alternative means to provide ssh with passwords
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
minor_changes:
|
||||
- ssh connection plugin - Support ``SSH_ASKPASS`` mechanism to provide passwords, making it the default, but still offering an explicit choice to use ``sshpass``
|
||||
(https://github.com/ansible/ansible/pull/83936)
|
||||
@@ -9,6 +9,19 @@ import locale
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
# We overload the ``ansible`` adhoc command to provide the functionality for
|
||||
# ``SSH_ASKPASS``. This code is here, and not in ``adhoc.py`` to bypass
|
||||
# unnecessary code. The program provided to ``SSH_ASKPASS`` can only be invoked
|
||||
# as a singular command, ``python -m`` doesn't work for that use case, and we
|
||||
# aren't adding a new entrypoint at this time. Assume that if we are executing
|
||||
# and there is only a single item in argv plus the executable, and the env var
|
||||
# is set we are in ``SSH_ASKPASS`` mode
|
||||
if 1 <= len(sys.argv) <= 2 and os.path.basename(sys.argv[0]) == "ansible" and os.getenv('_ANSIBLE_SSH_ASKPASS_SHM'):
|
||||
from ansible.cli import _ssh_askpass
|
||||
_ssh_askpass.main()
|
||||
|
||||
|
||||
# Used for determining if the system is running a new enough python version
|
||||
# and should only restrict on our documented minimum versions
|
||||
if sys.version_info < (3, 11):
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright: Contributors to the Ansible project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import typing as t
|
||||
from multiprocessing.shared_memory import SharedMemory
|
||||
|
||||
HOST_KEY_RE = re.compile(
|
||||
r'(The authenticity of host |differs from the key for the IP address)',
|
||||
)
|
||||
|
||||
|
||||
def main() -> t.Never:
|
||||
try:
|
||||
if HOST_KEY_RE.search(sys.argv[1]):
|
||||
sys.stdout.buffer.write(b'no')
|
||||
sys.stdout.flush()
|
||||
sys.exit(0)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
kwargs: dict[str, bool] = {}
|
||||
if sys.version_info[:2] >= (3, 13):
|
||||
# deprecated: description='unneeded due to track argument for SharedMemory' python_version='3.12'
|
||||
kwargs['track'] = False
|
||||
try:
|
||||
shm = SharedMemory(name=os.environ['_ANSIBLE_SSH_ASKPASS_SHM'], **kwargs)
|
||||
except FileNotFoundError:
|
||||
# We must be running after the ansible fork is shutting down
|
||||
sys.exit(1)
|
||||
cfg = json.loads(shm.buf.tobytes().rstrip(b'\x00'))
|
||||
sys.stdout.buffer.write(cfg['password'].encode('utf-8'))
|
||||
sys.stdout.flush()
|
||||
shm.buf[:] = b'\x00' * shm.size
|
||||
shm.close()
|
||||
sys.exit(0)
|
||||
@@ -62,6 +62,21 @@ DOCUMENTATION = """
|
||||
- name: ansible_password
|
||||
- name: ansible_ssh_pass
|
||||
- name: ansible_ssh_password
|
||||
password_mechanism:
|
||||
description: Mechanism to use for handling ssh password prompt
|
||||
type: string
|
||||
default: ssh_askpass
|
||||
choices:
|
||||
- ssh_askpass
|
||||
- sshpass
|
||||
- disable
|
||||
version_added: '2.19'
|
||||
env:
|
||||
- name: ANSIBLE_SSH_PASSWORD_MECHANISM
|
||||
ini:
|
||||
- {key: password_mechanism, section: ssh_connection}
|
||||
vars:
|
||||
- name: ansible_ssh_password_mechanism
|
||||
sshpass_prompt:
|
||||
description:
|
||||
- Password prompt that sshpass should search for. Supported by sshpass 1.06 and up.
|
||||
@@ -357,7 +372,6 @@ DOCUMENTATION = """
|
||||
type: string
|
||||
description:
|
||||
- "PKCS11 SmartCard provider such as opensc, example: /usr/local/lib/opensc-pkcs11.so"
|
||||
- Requires sshpass version 1.06+, sshpass must support the -P option.
|
||||
env: [{name: ANSIBLE_PKCS11_PROVIDER}]
|
||||
ini:
|
||||
- {key: pkcs11_provider, section: ssh_connection}
|
||||
@@ -367,26 +381,32 @@ DOCUMENTATION = """
|
||||
|
||||
import collections.abc as c
|
||||
import errno
|
||||
import contextlib
|
||||
import fcntl
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import pty
|
||||
import re
|
||||
import selectors
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import typing as t
|
||||
|
||||
from functools import wraps
|
||||
from multiprocessing.shared_memory import SharedMemory
|
||||
|
||||
from ansible.errors import (
|
||||
AnsibleAuthenticationFailure,
|
||||
AnsibleConnectionFailure,
|
||||
AnsibleError,
|
||||
AnsibleFileNotFound,
|
||||
)
|
||||
from ansible.module_utils.six import PY3, text_type, binary_type
|
||||
from ansible.module_utils.six import text_type, binary_type
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.plugins.connection import ConnectionBase, BUFSIZE
|
||||
from ansible.plugins.shell.powershell import _replace_stderr_clixml
|
||||
@@ -408,6 +428,8 @@ b_NOT_SSH_ERRORS = (b'Traceback (most recent call last):', # Python-2.6 when th
|
||||
SSHPASS_AVAILABLE = None
|
||||
SSH_DEBUG = re.compile(r'^debug\d+: .*')
|
||||
|
||||
_HAS_RESOURCE_TRACK = sys.version_info[:2] >= (3, 13)
|
||||
|
||||
|
||||
class AnsibleControlPersistBrokenPipeError(AnsibleError):
|
||||
""" ControlPersist broken pipe """
|
||||
@@ -497,9 +519,10 @@ def _ssh_retry(
|
||||
remaining_tries = int(self.get_option('reconnection_retries')) + 1
|
||||
cmd_summary = u"%s..." % to_text(args[0])
|
||||
conn_password = self.get_option('password') or self._play_context.password
|
||||
is_sshpass = self.get_option('password_mechanism') == 'sshpass'
|
||||
for attempt in range(remaining_tries):
|
||||
cmd = t.cast(list[bytes], args[0])
|
||||
if attempt != 0 and conn_password and isinstance(cmd, list):
|
||||
if attempt != 0 and is_sshpass and conn_password and isinstance(cmd, list):
|
||||
# If this is a retry, the fd/pipe for sshpass is closed, and we need a new one
|
||||
self.sshpass_pipe = os.pipe()
|
||||
cmd[1] = b'-d' + to_bytes(self.sshpass_pipe[0], nonstring='simplerepr', errors='surrogate_or_strict')
|
||||
@@ -518,7 +541,7 @@ def _ssh_retry(
|
||||
except (AnsibleControlPersistBrokenPipeError):
|
||||
# Retry one more time because of the ControlPersist broken pipe (see #16731)
|
||||
cmd = t.cast(list[bytes], args[0])
|
||||
if conn_password and isinstance(cmd, list):
|
||||
if is_sshpass and conn_password and isinstance(cmd, list):
|
||||
# This is a retry, so the fd/pipe for sshpass is closed, and we need a new one
|
||||
self.sshpass_pipe = os.pipe()
|
||||
cmd[1] = b'-d' + to_bytes(self.sshpass_pipe[0], nonstring='simplerepr', errors='surrogate_or_strict')
|
||||
@@ -559,6 +582,24 @@ def _ssh_retry(
|
||||
return wrapped
|
||||
|
||||
|
||||
def _clean_shm(func):
|
||||
def inner(self, *args, **kwargs):
|
||||
try:
|
||||
ret = func(self, *args, **kwargs)
|
||||
finally:
|
||||
if self.shm:
|
||||
self.shm.close()
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
self.shm.unlink()
|
||||
if not _HAS_RESOURCE_TRACK:
|
||||
# deprecated: description='unneeded due to track argument for SharedMemory' python_version='3.12'
|
||||
# There is a resource tracking issue where the resource is deleted, but tracking still has a record
|
||||
# This will effectively overwrite the record and remove it
|
||||
SharedMemory(name=self.shm.name, create=True, size=1).unlink()
|
||||
return ret
|
||||
return inner
|
||||
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
""" ssh based connections """
|
||||
|
||||
@@ -574,6 +615,8 @@ class Connection(ConnectionBase):
|
||||
self.user = self._play_context.remote_user
|
||||
self.control_path: str | None = None
|
||||
self.control_path_dir: str | None = None
|
||||
self.shm: SharedMemory | None = None
|
||||
self.sshpass_pipe: tuple[int, int] | None = None
|
||||
|
||||
# Windows operates differently from a POSIX connection/shell plugin,
|
||||
# we need to set various properties to ensure SSH on Windows continues
|
||||
@@ -615,17 +658,10 @@ class Connection(ConnectionBase):
|
||||
def _sshpass_available() -> bool:
|
||||
global SSHPASS_AVAILABLE
|
||||
|
||||
# We test once if sshpass is available, and remember the result. It
|
||||
# would be nice to use distutils.spawn.find_executable for this, but
|
||||
# distutils isn't always available; shutils.which() is Python3-only.
|
||||
# We test once if sshpass is available, and remember the result.
|
||||
|
||||
if SSHPASS_AVAILABLE is None:
|
||||
try:
|
||||
p = subprocess.Popen(["sshpass"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p.communicate()
|
||||
SSHPASS_AVAILABLE = True
|
||||
except OSError:
|
||||
SSHPASS_AVAILABLE = False
|
||||
SSHPASS_AVAILABLE = shutil.which('sshpass') is not None
|
||||
|
||||
return SSHPASS_AVAILABLE
|
||||
|
||||
@@ -678,17 +714,18 @@ class Connection(ConnectionBase):
|
||||
|
||||
b_command = []
|
||||
conn_password = self.get_option('password') or self._play_context.password
|
||||
pkcs11_provider = self.get_option("pkcs11_provider")
|
||||
password_mechanism = self.get_option('password_mechanism')
|
||||
|
||||
#
|
||||
# First, the command to invoke
|
||||
#
|
||||
|
||||
# If we want to use password authentication, we have to set up a pipe to
|
||||
# If we want to use sshpass for password authentication, we have to set up a pipe to
|
||||
# write the password to sshpass.
|
||||
pkcs11_provider = self.get_option("pkcs11_provider")
|
||||
if conn_password or pkcs11_provider:
|
||||
if password_mechanism == 'sshpass' and (conn_password or pkcs11_provider):
|
||||
if not self._sshpass_available():
|
||||
raise AnsibleError("to use the 'ssh' connection type with passwords or pkcs11_provider, you must install the sshpass program")
|
||||
raise AnsibleError("to use the password_mechanism=sshpass, you must install the sshpass program")
|
||||
if not conn_password and pkcs11_provider:
|
||||
raise AnsibleError("to use pkcs11_provider you must specify a password/pin")
|
||||
|
||||
@@ -721,12 +758,12 @@ class Connection(ConnectionBase):
|
||||
# sftp batch mode allows us to correctly catch failed transfers, but can
|
||||
# be disabled if the client side doesn't support the option. However,
|
||||
# sftp batch mode does not prompt for passwords so it must be disabled
|
||||
# if not using controlpersist and using sshpass
|
||||
# if not using controlpersist and using password auth
|
||||
b_args: t.Iterable[bytes]
|
||||
if subsystem == 'sftp' and self.get_option('sftp_batch_mode'):
|
||||
if conn_password:
|
||||
b_args = [b'-o', b'BatchMode=no']
|
||||
self._add_args(b_command, b_args, u'disable batch mode for sshpass')
|
||||
self._add_args(b_command, b_args, u'disable batch mode for password auth')
|
||||
b_command += [b'-b', b'-']
|
||||
|
||||
if display.verbosity:
|
||||
@@ -907,6 +944,50 @@ class Connection(ConnectionBase):
|
||||
|
||||
return b''.join(output), remainder
|
||||
|
||||
def _init_shm(self) -> dict[str, t.Any]:
|
||||
env = os.environ.copy()
|
||||
popen_kwargs: dict[str, t.Any] = {}
|
||||
|
||||
if self.get_option('password_mechanism') != 'ssh_askpass':
|
||||
return popen_kwargs
|
||||
|
||||
conn_password = self.get_option('password') or self._play_context.password
|
||||
pkcs11_provider = self.get_option("pkcs11_provider")
|
||||
if not conn_password and pkcs11_provider:
|
||||
raise AnsibleError("to use pkcs11_provider you must specify a password/pin")
|
||||
|
||||
if not conn_password:
|
||||
return popen_kwargs
|
||||
|
||||
kwargs = {}
|
||||
if _HAS_RESOURCE_TRACK:
|
||||
# deprecated: description='track argument for SharedMemory always available' python_version='3.12'
|
||||
kwargs['track'] = False
|
||||
self.shm = shm = SharedMemory(create=True, size=16384, **kwargs) # type: ignore[arg-type]
|
||||
|
||||
data = json.dumps(
|
||||
{'password': conn_password},
|
||||
).encode('utf-8')
|
||||
shm.buf[:len(data)] = bytearray(data)
|
||||
shm.close()
|
||||
|
||||
env['_ANSIBLE_SSH_ASKPASS_SHM'] = str(self.shm.name)
|
||||
adhoc = pathlib.Path(sys.argv[0]).with_name('ansible')
|
||||
env['SSH_ASKPASS'] = str(adhoc) if adhoc.is_file() else 'ansible'
|
||||
|
||||
# SSH_ASKPASS_REQUIRE was added in openssh 8.4, prior to 8.4 there must be no tty, and DISPLAY must be set
|
||||
env['SSH_ASKPASS_REQUIRE'] = 'force'
|
||||
if not env.get('DISPLAY'):
|
||||
# If the user has DISPLAY set, assume it is there for a reason
|
||||
env['DISPLAY'] = '-'
|
||||
|
||||
popen_kwargs['env'] = env
|
||||
# start_new_session runs setsid which detaches the tty to support the use of ASKPASS prior to openssh 8.4
|
||||
popen_kwargs['start_new_session'] = True
|
||||
|
||||
return popen_kwargs
|
||||
|
||||
@_clean_shm
|
||||
def _bare_run(self, cmd: list[bytes], in_data: bytes | None, sudoable: bool = True, checkrc: bool = True) -> tuple[int, bytes, bytes]:
|
||||
"""
|
||||
Starts the command and communicates with it until it ends.
|
||||
@@ -916,6 +997,9 @@ class Connection(ConnectionBase):
|
||||
display_cmd = u' '.join(shlex.quote(to_text(c)) for c in cmd)
|
||||
display.vvv(u'SSH: EXEC {0}'.format(display_cmd), host=self.host)
|
||||
|
||||
conn_password = self.get_option('password') or self._play_context.password
|
||||
password_mechanism = self.get_option('password_mechanism')
|
||||
|
||||
# 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
|
||||
@@ -928,17 +1012,16 @@ class Connection(ConnectionBase):
|
||||
else:
|
||||
cmd = list(map(to_bytes, cmd))
|
||||
|
||||
conn_password = self.get_option('password') or self._play_context.password
|
||||
popen_kwargs = self._init_shm()
|
||||
|
||||
if self.sshpass_pipe:
|
||||
popen_kwargs['pass_fds'] = self.sshpass_pipe
|
||||
|
||||
if not in_data:
|
||||
try:
|
||||
# Make sure stdin is a proper pty to avoid tcgetattr errors
|
||||
master, slave = pty.openpty()
|
||||
if PY3 and conn_password:
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
p = subprocess.Popen(cmd, stdin=slave, stdout=subprocess.PIPE, stderr=subprocess.PIPE, pass_fds=self.sshpass_pipe)
|
||||
else:
|
||||
p = subprocess.Popen(cmd, stdin=slave, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(cmd, stdin=slave, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **popen_kwargs)
|
||||
stdin = os.fdopen(master, 'wb', 0)
|
||||
os.close(slave)
|
||||
except (OSError, IOError):
|
||||
@@ -946,21 +1029,13 @@ class Connection(ConnectionBase):
|
||||
|
||||
if not p:
|
||||
try:
|
||||
if PY3 and conn_password:
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, pass_fds=self.sshpass_pipe)
|
||||
else:
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, **popen_kwargs)
|
||||
stdin = p.stdin # type: ignore[assignment] # stdin will be set and not None due to the calls above
|
||||
except (OSError, IOError) as e:
|
||||
raise AnsibleError('Unable to execute ssh command line on a controller due to: %s' % to_native(e))
|
||||
|
||||
# If we are using SSH password authentication, write the password into
|
||||
# the pipe we opened in _build_command.
|
||||
|
||||
if conn_password:
|
||||
if password_mechanism == 'sshpass' and conn_password:
|
||||
os.close(self.sshpass_pipe[0])
|
||||
try:
|
||||
os.write(self.sshpass_pipe[1], to_bytes(conn_password) + b'\n')
|
||||
@@ -1179,10 +1254,15 @@ class Connection(ConnectionBase):
|
||||
p.stdout.close()
|
||||
p.stderr.close()
|
||||
|
||||
if self.get_option('host_key_checking'):
|
||||
if cmd[0] == b"sshpass" and p.returncode == 6:
|
||||
raise AnsibleError('Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support '
|
||||
'this. Please add this host\'s fingerprint to your known_hosts file to manage this host.')
|
||||
conn_password = self.get_option('password') or self._play_context.password
|
||||
hostkey_fail = any((
|
||||
(cmd[0] == b"sshpass" and p.returncode == 6),
|
||||
b"read_passphrase: can't open /dev/tty" in b_stderr,
|
||||
b"Host key verification failed" in b_stderr,
|
||||
))
|
||||
if password_mechanism and self.get_option('host_key_checking') and conn_password and hostkey_fail:
|
||||
raise AnsibleError('Using a SSH password instead of a key is not possible because Host Key checking is enabled. '
|
||||
'Please add this host\'s fingerprint to your known_hosts file to manage this host.')
|
||||
|
||||
controlpersisterror = b'Bad configuration option: ControlPersist' in b_stderr or b'unknown configuration option: ControlPersist' in b_stderr
|
||||
if p.returncode != 0 and controlpersisterror:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
needs/ssh
|
||||
shippable/posix/group3
|
||||
needs/target/connection
|
||||
needs/target/setup_test_user
|
||||
setup/always/setup_passlib_controller # required for setup_test_user
|
||||
|
||||
@@ -19,6 +19,7 @@ if command -v sshpass > /dev/null; then
|
||||
# that the flag gets passed to sshpass.
|
||||
timeout 5 ansible -m ping \
|
||||
-e ansible_connection=ssh \
|
||||
-e ansible_ssh_password_mechanism=sshpass \
|
||||
-e ansible_sshpass_prompt=notThis: \
|
||||
-e ansible_password=foo \
|
||||
-e ansible_user=definitelynotroot \
|
||||
@@ -34,6 +35,7 @@ if command -v sshpass > /dev/null; then
|
||||
else
|
||||
ansible -m ping \
|
||||
-e ansible_connection=ssh \
|
||||
-e ansible_ssh_password_mechanism=sshpass \
|
||||
-e ansible_sshpass_prompt=notThis: \
|
||||
-e ansible_password=foo \
|
||||
-e ansible_user=definitelynotroot \
|
||||
@@ -82,3 +84,5 @@ ANSIBLE_SSH_CONTROL_PATH='/tmp/ssh cp with spaces' ansible -m ping all -e ansibl
|
||||
|
||||
# Test that timeout on waiting on become is an unreachable error
|
||||
ansible-playbook test_unreachable_become_timeout.yml "$@"
|
||||
|
||||
ANSIBLE_ROLES_PATH=../ ansible-playbook "$@" -i ../../inventory test_ssh_askpass.yml
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
- hosts: all
|
||||
tasks:
|
||||
- import_role:
|
||||
role: setup_test_user
|
||||
|
||||
# macos currently allows password auth, and macos/15.3 prevents restarting sshd
|
||||
- when: ansible_facts.system != 'Darwin'
|
||||
block:
|
||||
- find:
|
||||
paths: /etc/ssh
|
||||
recurse: true
|
||||
contains: 'PasswordAuthentication'
|
||||
register: sshd_confs
|
||||
|
||||
- lineinfile:
|
||||
path: '{{ item }}'
|
||||
regexp: '^PasswordAuthentication'
|
||||
line: PasswordAuthentication yes
|
||||
loop: '{{ sshd_confs.files|default([{"path": "/etc/ssh/sshd_config"}], true)|map(attribute="path") }}'
|
||||
|
||||
- service:
|
||||
name: ssh{{ '' if ansible_facts.os_family == 'Debian' else 'd' }}
|
||||
state: restarted
|
||||
when: ansible_facts.system != 'Darwin'
|
||||
|
||||
- command:
|
||||
argv:
|
||||
- ansible
|
||||
- localhost
|
||||
- -m
|
||||
- command
|
||||
- -a
|
||||
- id
|
||||
- -vvv
|
||||
- -e
|
||||
- ansible_pipelining=yes
|
||||
- -e
|
||||
- ansible_connection=ssh
|
||||
- -e
|
||||
- ansible_ssh_password_mechanism=ssh_askpass
|
||||
- -e
|
||||
- ansible_user={{ test_user_name }}
|
||||
- -e
|
||||
- ansible_password={{ test_user_plaintext_password }}
|
||||
environment:
|
||||
ANSIBLE_NOCOLOR: "1"
|
||||
ANSIBLE_FORCE_COLOR: "0"
|
||||
register: askpass_out
|
||||
|
||||
- debug:
|
||||
var: askpass_out
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- '"EXEC ssh " in askpass_out.stdout'
|
||||
- '"sshpass" not in askpass_out.stdout'
|
||||
- askpass_out.stdout is search('uid=\d+\(' ~ test_user_name ~ '\)')
|
||||
@@ -1,6 +1,8 @@
|
||||
- name: set variables
|
||||
set_fact:
|
||||
test_user_group: staff
|
||||
test_user_groups:
|
||||
- com.apple.access_ssh
|
||||
|
||||
- name: set plaintext password
|
||||
no_log: yes
|
||||
|
||||
@@ -23,7 +23,6 @@ from selectors import SelectorKey, EVENT_READ
|
||||
import pytest
|
||||
|
||||
|
||||
from ansible.errors import AnsibleAuthenticationFailure
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock, PropertyMock
|
||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
|
||||
@@ -54,22 +53,6 @@ class TestConnectionBaseClass(unittest.TestCase):
|
||||
res = conn._connect()
|
||||
self.assertEqual(conn, res)
|
||||
|
||||
ssh.SSHPASS_AVAILABLE = False
|
||||
self.assertFalse(conn._sshpass_available())
|
||||
|
||||
ssh.SSHPASS_AVAILABLE = True
|
||||
self.assertTrue(conn._sshpass_available())
|
||||
|
||||
with patch('subprocess.Popen') as p:
|
||||
ssh.SSHPASS_AVAILABLE = None
|
||||
p.return_value = MagicMock()
|
||||
self.assertTrue(conn._sshpass_available())
|
||||
|
||||
ssh.SSHPASS_AVAILABLE = None
|
||||
p.return_value = None
|
||||
p.side_effect = OSError()
|
||||
self.assertFalse(conn._sshpass_available())
|
||||
|
||||
conn.close()
|
||||
self.assertFalse(conn._connected)
|
||||
|
||||
@@ -412,29 +395,6 @@ class TestSSHConnectionRun(object):
|
||||
assert self.conn._send_initial_data.call_count == 1
|
||||
assert self.conn._send_initial_data.call_args[0][1] == 'this is input data'
|
||||
|
||||
def test_with_password(self):
|
||||
# test with a password set to trigger the sshpass write
|
||||
self.pc.password = '12345'
|
||||
self.mock_popen_res.stdout.read.side_effect = [b"some data", b"", b""]
|
||||
self.mock_popen_res.stderr.read.side_effect = [b""]
|
||||
self.mock_selector.select.side_effect = [
|
||||
[(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
|
||||
[(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
|
||||
[(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
|
||||
[(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
|
||||
[]]
|
||||
self.mock_selector.get_map.side_effect = lambda: True
|
||||
|
||||
return_code, b_stdout, b_stderr = self.conn._run(["ssh", "is", "a", "cmd"], "this is more data")
|
||||
assert return_code == 0
|
||||
assert b_stdout == b'some data'
|
||||
assert b_stderr == b''
|
||||
assert self.mock_selector.register.called is True
|
||||
assert self.mock_selector.register.call_count == 2
|
||||
assert self.conn._send_initial_data.called is True
|
||||
assert self.conn._send_initial_data.call_count == 1
|
||||
assert self.conn._send_initial_data.call_args[0][1] == 'this is more data'
|
||||
|
||||
def _password_with_prompt_examine_output(self, sourice, state, b_chunk, sudoable):
|
||||
if state == 'awaiting_prompt':
|
||||
self.conn._flags['become_prompt'] = True
|
||||
@@ -525,30 +485,6 @@ class TestSSHConnectionRun(object):
|
||||
|
||||
@pytest.mark.usefixtures('mock_run_env')
|
||||
class TestSSHConnectionRetries(object):
|
||||
def test_incorrect_password(self, monkeypatch):
|
||||
self.conn.set_option('host_key_checking', False)
|
||||
self.conn.set_option('reconnection_retries', 5)
|
||||
|
||||
self.mock_popen_res.stdout.read.side_effect = [b'']
|
||||
self.mock_popen_res.stderr.read.side_effect = [b'Permission denied, please try again.\r\n']
|
||||
type(self.mock_popen_res).returncode = PropertyMock(side_effect=[5] * 4)
|
||||
|
||||
self.mock_selector.select.side_effect = [
|
||||
[(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
|
||||
[(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
|
||||
[],
|
||||
]
|
||||
|
||||
self.mock_selector.get_map.side_effect = lambda: True
|
||||
|
||||
self.conn._build_command = MagicMock()
|
||||
self.conn._build_command.return_value = [b'sshpass', b'-d41', b'ssh', b'-C']
|
||||
|
||||
exception_info = pytest.raises(AnsibleAuthenticationFailure, self.conn.exec_command, 'sshpass', 'some data')
|
||||
assert exception_info.value.message == ('Invalid/incorrect username/password. Skipping remaining 5 retries to prevent account lockout: '
|
||||
'Permission denied, please try again.')
|
||||
assert self.mock_popen.call_count == 1
|
||||
|
||||
def test_retry_then_success(self, monkeypatch):
|
||||
self.conn.set_option('host_key_checking', False)
|
||||
self.conn.set_option('reconnection_retries', 3)
|
||||
|
||||
Reference in New Issue
Block a user