diff options
Diffstat (limited to 'lib/ansible/plugins/connection/paramiko_ssh.py')
-rw-r--r-- | lib/ansible/plugins/connection/paramiko_ssh.py | 227 |
1 files changed, 155 insertions, 72 deletions
diff --git a/lib/ansible/plugins/connection/paramiko_ssh.py b/lib/ansible/plugins/connection/paramiko_ssh.py index b9fd8980..172dbda2 100644 --- a/lib/ansible/plugins/connection/paramiko_ssh.py +++ b/lib/ansible/plugins/connection/paramiko_ssh.py @@ -1,15 +1,15 @@ # (c) 2012, Michael DeHaan <michael.dehaan@gmail.com> # (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 = """ author: Ansible Core Team name: paramiko - short_description: Run tasks via python ssh (paramiko) + short_description: Run tasks via Python SSH (paramiko) description: - - Use the python ssh implementation (Paramiko) to connect to targets + - Use the Python SSH implementation (Paramiko) to connect to targets - The paramiko transport is provided because many distributions, in particular EL6 and before do not support ControlPersist in their SSH implementations. - This is needed on the Ansible control machine to be reasonably efficient with connections. @@ -22,15 +22,38 @@ DOCUMENTATION = """ description: - Address of the remote target default: inventory_hostname + type: string vars: - name: inventory_hostname - name: ansible_host - name: ansible_ssh_host - name: ansible_paramiko_host + port: + description: Remote port to connect to. + type: int + default: 22 + ini: + - section: defaults + key: remote_port + - section: paramiko_connection + key: remote_port + version_added: '2.15' + env: + - name: ANSIBLE_REMOTE_PORT + - name: ANSIBLE_REMOTE_PARAMIKO_PORT + version_added: '2.15' + vars: + - name: ansible_port + - name: ansible_ssh_port + - name: ansible_paramiko_port + version_added: '2.15' + keyword: + - name: port remote_user: description: - User to login/authenticate as - Can be set from the CLI via the C(--user) or C(-u) options. + type: string vars: - name: ansible_user - name: ansible_ssh_user @@ -51,6 +74,7 @@ DOCUMENTATION = """ description: - Secret used to either login the ssh server or as a passphrase for ssh keys that require it - Can be set from the CLI via the C(--ask-pass) option. + type: string vars: - name: ansible_password - name: ansible_ssh_pass @@ -62,7 +86,7 @@ DOCUMENTATION = """ description: - Whether or not to enable RSA SHA2 algorithms for pubkeys and hostkeys - On paramiko versions older than 2.9, this only affects hostkeys - - For behavior matching paramiko<2.9 set this to C(False) + - For behavior matching paramiko<2.9 set this to V(False) vars: - name: ansible_paramiko_use_rsa_sha2_algorithms ini: @@ -90,12 +114,17 @@ DOCUMENTATION = """ description: - Proxy information for running the connection via a jumphost - Also this plugin will scan 'ssh_args', 'ssh_extra_args' and 'ssh_common_args' from the 'ssh' plugin settings for proxy information if set. + type: string env: [{name: ANSIBLE_PARAMIKO_PROXY_COMMAND}] ini: - {key: proxy_command, section: paramiko_connection} + vars: + - name: ansible_paramiko_proxy_command + version_added: '2.15' ssh_args: description: Only used in parsing ProxyCommand for use in this plugin. default: '' + type: string ini: - section: 'ssh_connection' key: 'ssh_args' @@ -104,8 +133,13 @@ DOCUMENTATION = """ vars: - name: ansible_ssh_args version_added: '2.7' + deprecated: + why: In favor of the "proxy_command" option. + version: "2.18" + alternatives: proxy_command ssh_common_args: description: Only used in parsing ProxyCommand for use in this plugin. + type: string ini: - section: 'ssh_connection' key: 'ssh_common_args' @@ -118,8 +152,13 @@ DOCUMENTATION = """ cli: - name: ssh_common_args default: '' + deprecated: + why: In favor of the "proxy_command" option. + version: "2.18" + alternatives: proxy_command ssh_extra_args: description: Only used in parsing ProxyCommand for use in this plugin. + type: string vars: - name: ansible_ssh_extra_args env: @@ -132,6 +171,10 @@ DOCUMENTATION = """ cli: - name: ssh_extra_args default: '' + deprecated: + why: In favor of the "proxy_command" option. + version: "2.18" + alternatives: proxy_command pty: default: True description: 'SUDO usually requires a PTY, True to give a PTY and False to not give a PTY.' @@ -194,8 +237,54 @@ DOCUMENTATION = """ key: banner_timeout env: - name: ANSIBLE_PARAMIKO_BANNER_TIMEOUT -# TODO: -#timeout=self._play_context.timeout, + timeout: + type: int + default: 10 + description: Number of seconds until the plugin gives up on failing to establish a TCP connection. + ini: + - section: defaults + key: timeout + - section: ssh_connection + key: timeout + version_added: '2.11' + - section: paramiko_connection + key: timeout + version_added: '2.15' + env: + - name: ANSIBLE_TIMEOUT + - name: ANSIBLE_SSH_TIMEOUT + version_added: '2.11' + - name: ANSIBLE_PARAMIKO_TIMEOUT + version_added: '2.15' + vars: + - name: ansible_ssh_timeout + version_added: '2.11' + - name: ansible_paramiko_timeout + version_added: '2.15' + cli: + - name: timeout + private_key_file: + description: + - Path to private key file to use for authentication. + type: string + ini: + - section: defaults + key: private_key_file + - section: paramiko_connection + key: private_key_file + version_added: '2.15' + env: + - name: ANSIBLE_PRIVATE_KEY_FILE + - name: ANSIBLE_PARAMIKO_PRIVATE_KEY_FILE + version_added: '2.15' + vars: + - name: ansible_private_key_file + - name: ansible_ssh_private_key_file + - name: ansible_paramiko_private_key_file + version_added: '2.15' + cli: + - name: private_key_file + option: '--private-key' """ import os @@ -203,10 +292,9 @@ import socket import tempfile import traceback import fcntl -import sys import re +import typing as t -from termios import tcflush, TCIFLUSH from ansible.module_utils.compat.version import LooseVersion from binascii import hexlify @@ -220,7 +308,7 @@ from ansible.module_utils.compat.paramiko import PARAMIKO_IMPORT_ERR, paramiko from ansible.plugins.connection import ConnectionBase from ansible.utils.display import Display from ansible.utils.path import makedirs_safe -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 display = Display() @@ -234,8 +322,12 @@ Are you sure you want to continue connecting (yes/no)? # SSH Options Regex SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)') +MissingHostKeyPolicy: type = object +if paramiko: + MissingHostKeyPolicy = paramiko.MissingHostKeyPolicy + -class MyAddPolicy(object): +class MyAddPolicy(MissingHostKeyPolicy): """ Based on AutoAddPolicy in paramiko so we can determine when keys are added @@ -245,14 +337,13 @@ class MyAddPolicy(object): local L{HostKeys} object, and saving it. This is used by L{SSHClient}. """ - def __init__(self, new_stdin, connection): - self._new_stdin = new_stdin + def __init__(self, connection: Connection) -> None: self.connection = connection self._options = connection._options - def missing_host_key(self, client, hostname, key): + def missing_host_key(self, client, hostname, key) -> None: - if all((self._options['host_key_checking'], not self._options['host_key_auto_add'])): + if all((self.connection.get_option('host_key_checking'), not self.connection.get_option('host_key_auto_add'))): fingerprint = hexlify(key.get_fingerprint()) ktype = key.get_name() @@ -262,18 +353,10 @@ class MyAddPolicy(object): # to the question anyway raise AnsibleError(AUTHENTICITY_MSG[1:92] % (hostname, ktype, fingerprint)) - self.connection.connection_lock() - - old_stdin = sys.stdin - sys.stdin = self._new_stdin - - # clear out any premature input on sys.stdin - tcflush(sys.stdin, TCIFLUSH) - - inp = input(AUTHENTICITY_MSG % (hostname, ktype, fingerprint)) - sys.stdin = old_stdin - - self.connection.connection_unlock() + inp = to_text( + display.prompt_until(AUTHENTICITY_MSG % (hostname, ktype, fingerprint), private=False), + errors='surrogate_or_strict' + ) if inp not in ['yes', 'y', '']: raise AnsibleError("host connection rejected by user") @@ -289,20 +372,20 @@ class MyAddPolicy(object): # keep connection objects on a per host basis to avoid repeated attempts to reconnect -SSH_CONNECTION_CACHE = {} # type: dict[str, paramiko.client.SSHClient] -SFTP_CONNECTION_CACHE = {} # type: dict[str, paramiko.sftp_client.SFTPClient] +SSH_CONNECTION_CACHE: dict[str, paramiko.client.SSHClient] = {} +SFTP_CONNECTION_CACHE: dict[str, paramiko.sftp_client.SFTPClient] = {} class Connection(ConnectionBase): ''' SSH based connections with Paramiko ''' transport = 'paramiko' - _log_channel = None + _log_channel: str | None = None - def _cache_key(self): - return "%s__%s__" % (self._play_context.remote_addr, self._play_context.remote_user) + def _cache_key(self) -> str: + return "%s__%s__" % (self.get_option('remote_addr'), self.get_option('remote_user')) - def _connect(self): + def _connect(self) -> Connection: cache_key = self._cache_key() if cache_key in SSH_CONNECTION_CACHE: self.ssh = SSH_CONNECTION_CACHE[cache_key] @@ -312,11 +395,11 @@ class Connection(ConnectionBase): self._connected = True return self - def _set_log_channel(self, name): + def _set_log_channel(self, name: str) -> None: '''Mimic paramiko.SSHClient.set_log_channel''' self._log_channel = name - def _parse_proxy_command(self, port=22): + def _parse_proxy_command(self, port: int = 22) -> dict[str, t.Any]: proxy_command = None # Parse ansible_ssh_common_args, specifically looking for ProxyCommand ssh_args = [ @@ -345,15 +428,15 @@ class Connection(ConnectionBase): sock_kwarg = {} if proxy_command: replacers = { - '%h': self._play_context.remote_addr, + '%h': self.get_option('remote_addr'), '%p': port, - '%r': self._play_context.remote_user + '%r': self.get_option('remote_user') } for find, replace in replacers.items(): proxy_command = proxy_command.replace(find, str(replace)) try: sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)} - display.vvv("CONFIGURE PROXY COMMAND FOR CONNECTION: %s" % proxy_command, host=self._play_context.remote_addr) + display.vvv("CONFIGURE PROXY COMMAND FOR CONNECTION: %s" % proxy_command, host=self.get_option('remote_addr')) except AttributeError: display.warning('Paramiko ProxyCommand support unavailable. ' 'Please upgrade to Paramiko 1.9.0 or newer. ' @@ -361,24 +444,25 @@ class Connection(ConnectionBase): return sock_kwarg - def _connect_uncached(self): + def _connect_uncached(self) -> paramiko.SSHClient: ''' activates the connection object ''' if paramiko is None: raise AnsibleError("paramiko is not installed: %s" % to_native(PARAMIKO_IMPORT_ERR)) - port = self._play_context.port or 22 - display.vvv("ESTABLISH PARAMIKO SSH CONNECTION FOR USER: %s on PORT %s TO %s" % (self._play_context.remote_user, port, self._play_context.remote_addr), - host=self._play_context.remote_addr) + port = self.get_option('port') + display.vvv("ESTABLISH PARAMIKO SSH CONNECTION FOR USER: %s on PORT %s TO %s" % (self.get_option('remote_user'), port, self.get_option('remote_addr')), + host=self.get_option('remote_addr')) ssh = paramiko.SSHClient() # Set pubkey and hostkey algorithms to disable, the only manipulation allowed currently # is keeping or omitting rsa-sha2 algorithms + # default_keys: t.Tuple[str] = () paramiko_preferred_pubkeys = getattr(paramiko.Transport, '_preferred_pubkeys', ()) paramiko_preferred_hostkeys = getattr(paramiko.Transport, '_preferred_keys', ()) use_rsa_sha2_algorithms = self.get_option('use_rsa_sha2_algorithms') - disabled_algorithms = {} + disabled_algorithms: t.Dict[str, t.Iterable[str]] = {} if not use_rsa_sha2_algorithms: if paramiko_preferred_pubkeys: disabled_algorithms['pubkeys'] = tuple(a for a in paramiko_preferred_pubkeys if 'rsa-sha2' in a) @@ -403,9 +487,9 @@ class Connection(ConnectionBase): ssh_connect_kwargs = self._parse_proxy_command(port) - ssh.set_missing_host_key_policy(MyAddPolicy(self._new_stdin, self)) + ssh.set_missing_host_key_policy(MyAddPolicy(self)) - conn_password = self.get_option('password') or self._play_context.password + conn_password = self.get_option('password') allow_agent = True @@ -414,25 +498,25 @@ class Connection(ConnectionBase): try: key_filename = None - if self._play_context.private_key_file: - key_filename = os.path.expanduser(self._play_context.private_key_file) + if self.get_option('private_key_file'): + key_filename = os.path.expanduser(self.get_option('private_key_file')) # paramiko 2.2 introduced auth_timeout parameter if LooseVersion(paramiko.__version__) >= LooseVersion('2.2.0'): - ssh_connect_kwargs['auth_timeout'] = self._play_context.timeout + ssh_connect_kwargs['auth_timeout'] = self.get_option('timeout') # paramiko 1.15 introduced banner timeout parameter if LooseVersion(paramiko.__version__) >= LooseVersion('1.15.0'): ssh_connect_kwargs['banner_timeout'] = self.get_option('banner_timeout') ssh.connect( - self._play_context.remote_addr.lower(), - username=self._play_context.remote_user, + self.get_option('remote_addr').lower(), + username=self.get_option('remote_user'), allow_agent=allow_agent, look_for_keys=self.get_option('look_for_keys'), key_filename=key_filename, password=conn_password, - timeout=self._play_context.timeout, + timeout=self.get_option('timeout'), port=port, disabled_algorithms=disabled_algorithms, **ssh_connect_kwargs, @@ -448,14 +532,14 @@ class Connection(ConnectionBase): raise AnsibleError("paramiko version issue, please upgrade paramiko on the machine running ansible") elif u"Private key file is encrypted" in msg: msg = 'ssh %s@%s:%s : %s\nTo connect as a different user, use -u <username>.' % ( - self._play_context.remote_user, self._play_context.remote_addr, port, msg) + self.get_option('remote_user'), self.get_options('remote_addr'), port, msg) raise AnsibleConnectionFailure(msg) else: raise AnsibleConnectionFailure(msg) return ssh - 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) @@ -481,7 +565,7 @@ class Connection(ConnectionBase): if self.get_option('pty') and sudoable: chan.get_pty(term=os.getenv('TERM', 'vt100'), width=int(os.getenv('COLUMNS', 0)), height=int(os.getenv('LINES', 0))) - display.vvv("EXEC %s" % cmd, host=self._play_context.remote_addr) + display.vvv("EXEC %s" % cmd, host=self.get_option('remote_addr')) cmd = to_bytes(cmd, errors='surrogate_or_strict') @@ -498,11 +582,10 @@ class Connection(ConnectionBase): display.debug('Waiting for Privilege Escalation input') chunk = chan.recv(bufsize) - display.debug("chunk is: %s" % chunk) + display.debug("chunk is: %r" % chunk) if not chunk: if b'unknown user' in become_output: - n_become_user = to_native(self.become.get_option('become_user', - playcontext=self._play_context)) + n_become_user = to_native(self.become.get_option('become_user')) raise AnsibleError('user %s does not exist' % n_become_user) else: break @@ -511,17 +594,17 @@ class Connection(ConnectionBase): # need to check every line because we might get lectured # and we might get the middle of a line in a chunk - for l in become_output.splitlines(True): - if self.become.check_success(l): + for line in become_output.splitlines(True): + if self.become.check_success(line): become_sucess = True break - elif self.become.check_password_prompt(l): + elif self.become.check_password_prompt(line): passprompt = True break if passprompt: if self.become: - become_pass = self.become.get_option('become_pass', playcontext=self._play_context) + become_pass = self.become.get_option('become_pass') chan.sendall(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n') else: raise AnsibleError("A password is required but none was supplied") @@ -529,19 +612,19 @@ class Connection(ConnectionBase): no_prompt_out += become_output no_prompt_err += become_output except socket.timeout: - raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + become_output) + raise AnsibleError('ssh timed out waiting for privilege escalation.\n' + to_text(become_output)) stdout = b''.join(chan.makefile('rb', bufsize)) stderr = b''.join(chan.makefile_stderr('rb', bufsize)) return (chan.recv_exit_status(), no_prompt_out + stdout, no_prompt_out + stderr) - def put_file(self, in_path, out_path): + def put_file(self, in_path: str, out_path: str) -> None: ''' transfer a file from local to remote ''' super(Connection, self).put_file(in_path, out_path) - display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr) + display.vvv("PUT %s TO %s" % (in_path, out_path), host=self.get_option('remote_addr')) if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')): raise AnsibleFileNotFound("file or module does not exist: %s" % in_path) @@ -556,21 +639,21 @@ class Connection(ConnectionBase): except IOError: raise AnsibleError("failed to transfer file to %s" % out_path) - def _connect_sftp(self): + def _connect_sftp(self) -> paramiko.sftp_client.SFTPClient: - cache_key = "%s__%s__" % (self._play_context.remote_addr, self._play_context.remote_user) + cache_key = "%s__%s__" % (self.get_option('remote_addr'), self.get_option('remote_user')) if cache_key in SFTP_CONNECTION_CACHE: return SFTP_CONNECTION_CACHE[cache_key] else: result = SFTP_CONNECTION_CACHE[cache_key] = self._connect().ssh.open_sftp() return result - def fetch_file(self, in_path, out_path): + def fetch_file(self, in_path: str, out_path: str) -> None: ''' save a remote file to the specified path ''' super(Connection, self).fetch_file(in_path, out_path) - display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr) + display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self.get_option('remote_addr')) try: self.sftp = self._connect_sftp() @@ -582,7 +665,7 @@ class Connection(ConnectionBase): except IOError: raise AnsibleError("failed to transfer file from %s" % in_path) - def _any_keys_added(self): + def _any_keys_added(self) -> bool: for hostname, keys in self.ssh._host_keys.items(): for keytype, key in keys.items(): @@ -591,14 +674,14 @@ class Connection(ConnectionBase): return True return False - def _save_ssh_host_keys(self, filename): + def _save_ssh_host_keys(self, filename: str) -> None: ''' not using the paramiko save_ssh_host_keys function as we want to add new SSH keys at the bottom so folks don't complain about it :) ''' if not self._any_keys_added(): - return False + return path = os.path.expanduser("~/.ssh") makedirs_safe(path) @@ -621,13 +704,13 @@ class Connection(ConnectionBase): if added_this_time: f.write("%s %s %s\n" % (hostname, keytype, key.get_base64())) - def reset(self): + def reset(self) -> None: if not self._connected: return self.close() self._connect() - def close(self): + def close(self) -> None: ''' terminate the connection ''' cache_key = self._cache_key() |