diff options
Diffstat (limited to 'lib/ansible/plugins/connection/ssh.py')
-rw-r--r-- | lib/ansible/plugins/connection/ssh.py | 131 |
1 files changed, 86 insertions, 45 deletions
diff --git a/lib/ansible/plugins/connection/ssh.py b/lib/ansible/plugins/connection/ssh.py index e4d96289..49b2ed22 100644 --- a/lib/ansible/plugins/connection/ssh.py +++ b/lib/ansible/plugins/connection/ssh.py @@ -4,7 +4,7 @@ # 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) +from __future__ import (annotations, absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' @@ -20,7 +20,7 @@ DOCUMENTATION = ''' - connection_pipelining version_added: historical notes: - - Many options default to C(None) here but that only means we do not override the SSH tool's defaults and/or configuration. + - Many options default to V(None) here but that only means we do not override the SSH tool's defaults and/or configuration. For example, if you specify the port in this plugin it will override any C(Port) entry in your C(.ssh/config). - The ssh CLI tool uses return code 255 as a 'connection error', this can conflict with commands/tools that also return 255 as an error code and will look like an 'unreachable' condition or 'connection error' to this plugin. @@ -28,6 +28,7 @@ DOCUMENTATION = ''' host: description: Hostname/IP to connect to. default: inventory_hostname + type: string vars: - name: inventory_hostname - name: ansible_host @@ -54,7 +55,8 @@ DOCUMENTATION = ''' - name: ansible_ssh_host_key_checking version_added: '2.5' password: - description: Authentication password for the C(remote_user). Can be supplied as CLI option. + description: Authentication password for the O(remote_user). Can be supplied as CLI option. + type: string vars: - name: ansible_password - name: ansible_ssh_pass @@ -64,6 +66,7 @@ DOCUMENTATION = ''' - Password prompt that sshpass should search for. Supported by sshpass 1.06 and up. - Defaults to C(Enter PIN for) when pkcs11_provider is set. default: '' + type: string ini: - section: 'ssh_connection' key: 'sshpass_prompt' @@ -75,6 +78,7 @@ DOCUMENTATION = ''' ssh_args: description: Arguments to pass to all SSH CLI tools. default: '-C -o ControlMaster=auto -o ControlPersist=60s' + type: string ini: - section: 'ssh_connection' key: 'ssh_args' @@ -85,6 +89,7 @@ DOCUMENTATION = ''' version_added: '2.7' ssh_common_args: description: Common extra args for all SSH CLI tools. + type: string ini: - section: 'ssh_connection' key: 'ssh_common_args' @@ -100,9 +105,10 @@ DOCUMENTATION = ''' ssh_executable: default: ssh description: - - This defines the location of the SSH binary. It defaults to C(ssh) which will use the first SSH binary available in $PATH. + - This defines the location of the SSH binary. It defaults to V(ssh) which will use the first SSH binary available in $PATH. - This option is usually not required, it might be useful when access to system SSH is restricted, or when using SSH wrappers to connect to remote hosts. + type: string env: [{name: ANSIBLE_SSH_EXECUTABLE}] ini: - {key: ssh_executable, section: ssh_connection} @@ -114,7 +120,8 @@ DOCUMENTATION = ''' sftp_executable: default: sftp description: - - This defines the location of the sftp binary. It defaults to C(sftp) which will use the first binary available in $PATH. + - This defines the location of the sftp binary. It defaults to V(sftp) which will use the first binary available in $PATH. + type: string env: [{name: ANSIBLE_SFTP_EXECUTABLE}] ini: - {key: sftp_executable, section: ssh_connection} @@ -125,7 +132,8 @@ DOCUMENTATION = ''' scp_executable: default: scp description: - - This defines the location of the scp binary. It defaults to C(scp) which will use the first binary available in $PATH. + - This defines the location of the scp binary. It defaults to V(scp) which will use the first binary available in $PATH. + type: string env: [{name: ANSIBLE_SCP_EXECUTABLE}] ini: - {key: scp_executable, section: ssh_connection} @@ -135,6 +143,7 @@ DOCUMENTATION = ''' version_added: '2.7' scp_extra_args: description: Extra exclusive to the C(scp) CLI + type: string vars: - name: ansible_scp_extra_args env: @@ -149,6 +158,7 @@ DOCUMENTATION = ''' default: '' sftp_extra_args: description: Extra exclusive to the C(sftp) CLI + type: string vars: - name: ansible_sftp_extra_args env: @@ -163,6 +173,7 @@ DOCUMENTATION = ''' default: '' ssh_extra_args: description: Extra exclusive to the SSH CLI. + type: string vars: - name: ansible_ssh_extra_args env: @@ -209,6 +220,7 @@ DOCUMENTATION = ''' description: - User name with which to login to the remote server, normally set by the remote_user keyword. - If no user is supplied, Ansible will let the SSH client binary choose the user as it normally. + type: string ini: - section: defaults key: remote_user @@ -239,6 +251,7 @@ DOCUMENTATION = ''' private_key_file: description: - Path to private key file to use for authentication. + type: string ini: - section: defaults key: private_key_file @@ -257,6 +270,7 @@ DOCUMENTATION = ''' - Since 2.3, if null (default), ansible will generate a unique hash. Use ``%(directory)s`` to indicate where to use the control dir path setting. - Before 2.3 it defaulted to ``control_path=%(directory)s/ansible-ssh-%%h-%%p-%%r``. - Be aware that this setting is ignored if C(-o ControlPath) is set in ssh args. + type: string env: - name: ANSIBLE_SSH_CONTROL_PATH ini: @@ -270,6 +284,7 @@ DOCUMENTATION = ''' description: - This sets the directory to use for ssh control path if the control path setting is null. - Also, provides the ``%(directory)s`` variable for the control path setting. + type: string env: - name: ANSIBLE_SSH_CONTROL_PATH_DIR ini: @@ -279,7 +294,7 @@ DOCUMENTATION = ''' - name: ansible_control_path_dir version_added: '2.7' sftp_batch_mode: - default: 'yes' + default: true description: 'TODO: write it' env: [{name: ANSIBLE_SFTP_BATCH_MODE}] ini: @@ -295,6 +310,7 @@ DOCUMENTATION = ''' - For OpenSSH >=9.0 you must add an additional option to enable scp (scp_extra_args="-O") - Using 'piped' creates an ssh pipe with C(dd) on either side to copy the data choices: ['sftp', 'scp', 'piped', 'smart'] + type: string env: [{name: ANSIBLE_SSH_TRANSFER_METHOD}] ini: - {key: transfer_method, section: ssh_connection} @@ -303,16 +319,16 @@ DOCUMENTATION = ''' version_added: '2.12' scp_if_ssh: deprecated: - why: In favor of the "ssh_transfer_method" option. + why: In favor of the O(ssh_transfer_method) option. version: "2.17" - alternatives: ssh_transfer_method + alternatives: O(ssh_transfer_method) default: smart description: - "Preferred method to use when transferring files over SSH." - - When set to I(smart), Ansible will try them until one succeeds or they all fail. - - If set to I(True), it will force 'scp', if I(False) it will use 'sftp'. - - For OpenSSH >=9.0 you must add an additional option to enable scp (scp_extra_args="-O") - - This setting will overridden by ssh_transfer_method if set. + - When set to V(smart), Ansible will try them until one succeeds or they all fail. + - If set to V(True), it will force 'scp', if V(False) it will use 'sftp'. + - For OpenSSH >=9.0 you must add an additional option to enable scp (C(scp_extra_args="-O")) + - This setting will overridden by O(ssh_transfer_method) if set. env: [{name: ANSIBLE_SCP_IF_SSH}] ini: - {key: scp_if_ssh, section: ssh_connection} @@ -321,7 +337,7 @@ DOCUMENTATION = ''' version_added: '2.7' use_tty: version_added: '2.5' - default: 'yes' + default: true description: add -tt to ssh commands to force tty allocation. env: [{name: ANSIBLE_SSH_USETTY}] ini: @@ -354,6 +370,7 @@ DOCUMENTATION = ''' pkcs11_provider: version_added: '2.12' default: "" + 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. @@ -364,15 +381,18 @@ DOCUMENTATION = ''' - name: ansible_ssh_pkcs11_provider ''' +import collections.abc as c import errno import fcntl import hashlib +import io import os import pty import re import shlex import subprocess import time +import typing as t from functools import wraps from ansible.errors import ( @@ -384,7 +404,7 @@ from ansible.errors import ( from ansible.errors import AnsibleOptionsError from ansible.module_utils.compat import selectors from ansible.module_utils.six import PY3, text_type, binary_type -from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.module_utils.parsing.convert_bool import BOOLEANS, boolean from ansible.plugins.connection import ConnectionBase, BUFSIZE from ansible.plugins.shell.powershell import _parse_clixml @@ -393,6 +413,8 @@ from ansible.utils.path import unfrackpath, makedirs_safe display = Display() +P = t.ParamSpec('P') + # error messages that indicate 255 return code is not from ssh itself. b_NOT_SSH_ERRORS = (b'Traceback (most recent call last):', # Python-2.6 when there's an exception # while invoking a script via -m @@ -410,7 +432,14 @@ class AnsibleControlPersistBrokenPipeError(AnsibleError): pass -def _handle_error(remaining_retries, command, return_tuple, no_log, host, display=display): +def _handle_error( + remaining_retries: int, + command: bytes, + return_tuple: tuple[int, bytes, bytes], + no_log: bool, + host: str, + display: Display = display, +) -> None: # sshpass errors if command == b'sshpass': @@ -466,7 +495,9 @@ def _handle_error(remaining_retries, command, return_tuple, no_log, host, displa display.vvv(msg, host=host) -def _ssh_retry(func): +def _ssh_retry( + func: c.Callable[t.Concatenate[Connection, P], tuple[int, bytes, bytes]], +) -> c.Callable[t.Concatenate[Connection, P], tuple[int, bytes, bytes]]: """ Decorator to retry ssh/scp/sftp in the case of a connection failure @@ -479,12 +510,12 @@ def _ssh_retry(func): * retries limit reached """ @wraps(func) - def wrapped(self, *args, **kwargs): + def wrapped(self: Connection, *args: P.args, **kwargs: P.kwargs) -> tuple[int, bytes, bytes]: 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 for attempt in range(remaining_tries): - cmd = args[0] + cmd = t.cast(list[bytes], args[0]) if attempt != 0 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() @@ -497,13 +528,13 @@ def _ssh_retry(func): if self._play_context.no_log: display.vvv(u'rc=%s, stdout and stderr censored due to no log' % return_tuple[0], host=self.host) else: - display.vvv(return_tuple, host=self.host) + display.vvv(str(return_tuple), host=self.host) # 0 = success # 1-254 = remote command return code # 255 could be a failure from the ssh command itself except (AnsibleControlPersistBrokenPipeError): # Retry one more time because of the ControlPersist broken pipe (see #16731) - cmd = args[0] + cmd = t.cast(list[bytes], args[0]) if 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() @@ -551,15 +582,15 @@ class Connection(ConnectionBase): transport = 'ssh' has_pipelining = True - def __init__(self, *args, **kwargs): + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: super(Connection, self).__init__(*args, **kwargs) # TODO: all should come from get_option(), but not might be set at this point yet self.host = self._play_context.remote_addr self.port = self._play_context.port self.user = self._play_context.remote_user - self.control_path = None - self.control_path_dir = None + self.control_path: str | None = None + self.control_path_dir: str | None = None # Windows operates differently from a POSIX connection/shell plugin, # we need to set various properties to ensure SSH on Windows continues @@ -574,11 +605,17 @@ class Connection(ConnectionBase): # put_file, and fetch_file methods, so we don't need to do any connection # management here. - def _connect(self): + def _connect(self) -> Connection: return self @staticmethod - def _create_control_path(host, port, user, connection=None, pid=None): + def _create_control_path( + host: str | None, + port: int | None, + user: str | None, + connection: ConnectionBase | None = None, + pid: int | None = None, + ) -> str: '''Make a hash for the controlpath based on con attributes''' pstring = '%s-%s-%s' % (host, port, user) if connection: @@ -592,7 +629,7 @@ class Connection(ConnectionBase): return cpath @staticmethod - def _sshpass_available(): + def _sshpass_available() -> bool: global SSHPASS_AVAILABLE # We test once if sshpass is available, and remember the result. It @@ -610,7 +647,7 @@ class Connection(ConnectionBase): return SSHPASS_AVAILABLE @staticmethod - def _persistence_controls(b_command): + def _persistence_controls(b_command: list[bytes]) -> tuple[bool, bool]: ''' Takes a command array and scans it for ControlPersist and ControlPath settings and returns two booleans indicating whether either was found. @@ -629,7 +666,7 @@ class Connection(ConnectionBase): return controlpersist, controlpath - def _add_args(self, b_command, b_args, explanation): + def _add_args(self, b_command: list[bytes], b_args: t.Iterable[bytes], explanation: str) -> None: """ Adds arguments to the ssh command and displays a caller-supplied explanation of why. @@ -645,7 +682,7 @@ class Connection(ConnectionBase): display.vvvvv(u'SSH: %s: (%s)' % (explanation, ')('.join(to_text(a) for a in b_args)), host=self.host) b_command += b_args - def _build_command(self, binary, subsystem, *other_args): + def _build_command(self, binary: str, subsystem: str, *other_args: bytes | str) -> list[bytes]: ''' Takes a executable (ssh, scp, sftp or wrapper) and optional extra arguments and returns the remote command wrapped in local ssh shell commands and ready for execution. @@ -702,6 +739,7 @@ class Connection(ConnectionBase): # 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 + 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'] @@ -801,7 +839,7 @@ class Connection(ConnectionBase): return b_command - def _send_initial_data(self, fh, in_data, ssh_process): + def _send_initial_data(self, fh: io.IOBase, in_data: bytes, ssh_process: subprocess.Popen) -> None: ''' Writes initial data to the stdin filehandle of the subprocess and closes it. (The handle must be closed; otherwise, for example, "sftp -b -" will @@ -828,7 +866,7 @@ class Connection(ConnectionBase): # Used by _run() to kill processes on failures @staticmethod - def _terminate_process(p): + def _terminate_process(p: subprocess.Popen) -> None: """ Terminate a process, ignoring errors """ try: p.terminate() @@ -837,7 +875,7 @@ class Connection(ConnectionBase): # This is separate from _run() because we need to do the same thing for stdout # and stderr. - def _examine_output(self, source, state, b_chunk, sudoable): + def _examine_output(self, source: str, state: str, b_chunk: bytes, sudoable: bool) -> tuple[bytes, bytes]: ''' Takes a string, extracts complete lines from it, tests to see if they are a prompt, error message, etc., and sets appropriate flags in self. @@ -886,7 +924,7 @@ class Connection(ConnectionBase): return b''.join(output), remainder - def _bare_run(self, cmd, in_data, sudoable=True, checkrc=True): + 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. ''' @@ -932,7 +970,7 @@ class Connection(ConnectionBase): else: p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdin = p.stdin + 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)) @@ -1182,13 +1220,13 @@ class Connection(ConnectionBase): return (p.returncode, b_stdout, b_stderr) @_ssh_retry - def _run(self, cmd, in_data, sudoable=True, checkrc=True): + def _run(self, cmd: list[bytes], in_data: bytes | None, sudoable: bool = True, checkrc: bool = True) -> tuple[int, bytes, bytes]: """Wrapper around _bare_run that retries the connection """ return self._bare_run(cmd, in_data, sudoable=sudoable, checkrc=checkrc) @_ssh_retry - def _file_transport_command(self, in_path, out_path, sftp_action): + def _file_transport_command(self, in_path: str, out_path: str, sftp_action: str) -> tuple[int, bytes, bytes]: # scp and sftp require square brackets for IPv6 addresses, but # accept them for hostnames and IPv4 addresses too. host = '[%s]' % self.host @@ -1276,7 +1314,7 @@ class Connection(ConnectionBase): raise AnsibleError("failed to transfer file to %s %s:\n%s\n%s" % (to_native(in_path), to_native(out_path), to_native(stdout), to_native(stderr))) - def _escape_win_path(self, path): + def _escape_win_path(self, path: str) -> str: """ converts a Windows path to one that's supported by SFTP and SCP """ # If using a root path then we need to start with / prefix = "" @@ -1289,7 +1327,7 @@ class Connection(ConnectionBase): # # Main public methods # - def exec_command(self, cmd, in_data=None, sudoable=True): + def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool = True) -> tuple[int, bytes, bytes]: ''' run a command on the remote host ''' super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) @@ -1306,8 +1344,10 @@ class Connection(ConnectionBase): # Make sure our first command is to set the console encoding to # utf-8, this must be done via chcp to get utf-8 (65001) - cmd_parts = ["chcp.com", "65001", self._shell._SHELL_REDIRECT_ALLNULL, self._shell._SHELL_AND] - cmd_parts.extend(self._shell._encode_script(cmd, as_list=True, strict_mode=False, preserve_rc=False)) + # union-attr ignores rely on internal powershell shell plugin details, + # this should be fixed at a future point in time. + cmd_parts = ["chcp.com", "65001", self._shell._SHELL_REDIRECT_ALLNULL, self._shell._SHELL_AND] # type: ignore[union-attr] + cmd_parts.extend(self._shell._encode_script(cmd, as_list=True, strict_mode=False, preserve_rc=False)) # type: ignore[union-attr] cmd = ' '.join(cmd_parts) # we can only use tty when we are not pipelining the modules. piping @@ -1321,6 +1361,7 @@ class Connection(ConnectionBase): # to disable it as a troubleshooting method. use_tty = self.get_option('use_tty') + args: tuple[str, ...] if not in_data and sudoable and use_tty: args = ('-tt', self.host, cmd) else: @@ -1335,7 +1376,7 @@ class Connection(ConnectionBase): return (returncode, stdout, stderr) - def put_file(self, in_path, out_path): + def put_file(self, in_path: str, out_path: str) -> tuple[int, bytes, bytes]: # type: ignore[override] # Used by tests and would break API ''' transfer a file from local to remote ''' super(Connection, self).put_file(in_path, out_path) @@ -1351,7 +1392,7 @@ class Connection(ConnectionBase): return self._file_transport_command(in_path, out_path, 'put') - def fetch_file(self, in_path, out_path): + def fetch_file(self, in_path: str, out_path: str) -> tuple[int, bytes, bytes]: # type: ignore[override] # Used by tests and would break API ''' fetch a file from remote to local ''' super(Connection, self).fetch_file(in_path, out_path) @@ -1366,7 +1407,7 @@ class Connection(ConnectionBase): return self._file_transport_command(in_path, out_path, 'get') - def reset(self): + def reset(self) -> None: run_reset = False self.host = self.get_option('host') or self._play_context.remote_addr @@ -1395,5 +1436,5 @@ class Connection(ConnectionBase): self.close() - def close(self): + def close(self) -> None: self._connected = False |