From f9cc5ea89e0336f5aac75e0726793dfa9393229d Mon Sep 17 00:00:00 2001 From: Panos Date: Fri, 1 Apr 2022 15:01:35 +0100 Subject: [PATCH] Host config (#342) * Updated clients for getting all configuration entries from host config * Updated changelog, documentation * Updated tests * Added bytes support to proxy_pkey, added test - resolves #338 * Updated RTD configuration * Updated setup.cfg * Updated CircleCI cfg * Updated copyright notices --- .circleci/config.yml | 8 +- .environment.yml | 8 -- .readthedocs.yml | 11 ++- Changelog.rst | 11 ++- doc/advanced.rst | 2 +- pssh/__init__.py | 2 +- pssh/clients/__init__.py | 2 +- pssh/clients/base/parallel.py | 110 +++++++++++++++++++-------- pssh/clients/base/single.py | 2 +- pssh/clients/common.py | 2 +- pssh/clients/native/__init__.py | 2 +- pssh/clients/native/parallel.py | 72 +++++++----------- pssh/clients/native/single.py | 6 +- pssh/clients/native/tunnel.py | 2 +- pssh/clients/reader.py | 2 +- pssh/clients/ssh/__init__.py | 2 +- pssh/clients/ssh/parallel.py | 69 +++++------------ pssh/clients/ssh/single.py | 6 +- pssh/config.py | 73 +++++++++++++++--- pssh/constants.py | 2 +- pssh/exceptions.py | 6 +- pssh/output.py | 2 +- pssh/utils.py | 2 +- setup.cfg | 4 +- tests/embedded_server/openssh.py | 32 ++++---- tests/native/base_ssh2_case.py | 14 ++-- tests/native/test_agent.py | 77 ------------------- tests/native/test_parallel_client.py | 108 ++++++++------------------ tests/native/test_single_client.py | 21 +++-- tests/native/test_tunnel.py | 50 ++++++++---- tests/ssh/base_ssh_case.py | 12 +-- tests/ssh/test_parallel_client.py | 28 ++++--- tests/ssh/test_single_client.py | 38 ++------- tests/test_exceptions.py | 2 +- tests/test_host_config.py | 31 +++++++- tests/test_output.py | 2 +- tests/test_reader.py | 2 +- tests/test_utils.py | 2 +- 38 files changed, 393 insertions(+), 434 deletions(-) delete mode 100644 .environment.yml delete mode 100644 tests/native/test_agent.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 0fcc71c6..363a8621 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,10 +39,7 @@ jobs: command: | set -x eval "$(ssh-agent -s)" - pytest --cov-append --cov=pssh tests/test_output.py tests/test_utils.py tests/test_host_config.py - pytest --reruns 10 --cov-append --cov=pssh tests/native/test_tunnel.py tests/native/test_agent.py - pytest --reruns 5 --cov-append --cov=pssh tests/native/test_*_client.py - pytest --reruns 5 --cov-append --cov=pssh tests/ssh + pytest name: Integration tests - run: command: | @@ -97,8 +94,9 @@ workflows: parameters: python_ver: - "3.6" - - "3.7" - "3.8" + - "3.9" + - "3.10" filters: tags: ignore: /.*/ diff --git a/.environment.yml b/.environment.yml deleted file mode 100644 index 72a0f20f..00000000 --- a/.environment.yml +++ /dev/null @@ -1,8 +0,0 @@ -channels: - - conda-forge -dependencies: - - python - - setuptools - - pip - - toolchain3 - - cython diff --git a/.readthedocs.yml b/.readthedocs.yml index dd4afb67..e5226632 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,4 +1,9 @@ -conda: - file: .environment.yml +version: 2 +build: + apt_packages: + - cmake + - openssl python: - pip_install: true + install: + - method: pip + path: . diff --git a/Changelog.rst b/Changelog.rst index 9dd2592b..aecbe7ff 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -1,6 +1,16 @@ Change Log ============ +2.10.0 +++++++ + +Changes +------- + +* All client configuration can now be provided via ``HostConfig`` on parallel clients. +* ``proxy_pkey`` can now also be provided as bytes for proxy authentication from in-memory private key data - #338 +* Removed deprecated since ``2.0.0`` dictionary support for ``host_config`` entries. + 2.9.1 +++++ @@ -18,7 +28,6 @@ Changes * ``pssh.exceptions.ConnectionError`` is now the same as built-in ``ConnectionError`` and deprecated - to be removed. * Clients now attempt to connect with all addresses in DNS list. In the case where an address refuses connection, other available addresses are attempted without delay. - For example where a host resolves to both IPv4 and v6 addresses while only one address is accepting connections, or multiple v4/v6 addresses where only some are accepting connections. * Connection actively refused error is no longer subject to retries. diff --git a/doc/advanced.rst b/doc/advanced.rst index a70c390f..e9c8e2fa 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -352,7 +352,7 @@ In the above example, the client is configured to connect to hostname ``localhos When using ``host_config``, the number of ``HostConfig`` entries must match the number of hosts in ``client.hosts``. An exception is raised on client initialisation if not. -As of `2.2.0`, proxy configuration can also be provided in ``HostConfig``. +As of `2.10.0`, all client configuration can be provided in ``HostConfig``. .. _per-host-cmds: diff --git a/pssh/__init__.py b/pssh/__init__.py index 18f5aa21..bce2c66a 100644 --- a/pssh/__init__.py +++ b/pssh/__init__.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pssh/clients/__init__.py b/pssh/clients/__init__.py index f6c2cfe2..eb9b9d1a 100644 --- a/pssh/clients/__init__.py +++ b/pssh/clients/__init__.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pssh/clients/base/parallel.py b/pssh/clients/base/parallel.py index 5abb7de8..43f05db1 100644 --- a/pssh/clients/base/parallel.py +++ b/pssh/clients/base/parallel.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,15 +20,15 @@ import logging import gevent.pool - from gevent import joinall, spawn, Timeout as GTimeout from gevent.hub import Hub +from ..common import _validate_pkey_path +from ...config import HostConfig from ...constants import DEFAULT_RETRIES, RETRY_DELAY -from ...exceptions import HostArgumentError, Timeout, ShellError +from ...exceptions import HostArgumentError, Timeout, ShellError, HostConfigError from ...output import HostOutput - Hub.NOT_ERROR = (Exception,) logger = logging.getLogger(__name__) @@ -43,6 +43,19 @@ def __init__(self, hosts, user=None, password=None, port=None, pkey=None, host_config=None, retry_delay=RETRY_DELAY, identity_auth=True, ipv6_only=False, + proxy_host=None, + proxy_port=None, + proxy_user=None, + proxy_password=None, + proxy_pkey=None, + keepalive_seconds=None, + cert_file=None, + gssapi_auth=False, + gssapi_server_identity=None, + gssapi_client_identity=None, + gssapi_delegate_credentials=False, + forward_ssh_agent=False, + _auth_thread_pool=True, ): self.allow_agent = allow_agent self.pool_size = pool_size @@ -60,6 +73,19 @@ def __init__(self, hosts, user=None, password=None, port=None, pkey=None, self.cmds = None self.identity_auth = identity_auth self.ipv6_only = ipv6_only + self.proxy_host = proxy_host + self.proxy_port = proxy_port + self.proxy_user = proxy_user + self.proxy_password = proxy_password + self.proxy_pkey = proxy_pkey + self.keepalive_seconds = keepalive_seconds + self.cert_file = cert_file + self.forward_ssh_agent = forward_ssh_agent + self.gssapi_auth = gssapi_auth + self.gssapi_server_identity = gssapi_server_identity + self.gssapi_client_identity = gssapi_client_identity + self.gssapi_delegate_credentials = gssapi_delegate_credentials + self._auth_thread_pool = _auth_thread_pool self._check_host_config() def _validate_hosts(self, _hosts): @@ -100,7 +126,7 @@ def _check_host_config(self): def _open_shell(self, host_i, host, encoding='utf-8', read_timeout=None): try: - _client = self._make_ssh_client(host_i, host) + _client = self._get_ssh_client(host_i, host) shell = _client.open_shell( encoding=encoding, read_timeout=read_timeout) return shell @@ -230,28 +256,29 @@ def get_last_output(self, cmds=None): return self._get_output_from_cmds( cmds, raise_error=False) - def _get_host_config_values(self, host_i, host): + def _get_host_config(self, host_i, host): if self.host_config is None: - return self.user, self.port, self.password, self.pkey, \ - getattr(self, 'proxy_host', None), \ - getattr(self, 'proxy_port', None), getattr(self, 'proxy_user', None), \ - getattr(self, 'proxy_password', None), getattr(self, 'proxy_pkey', None) - elif isinstance(self.host_config, list): - config = self.host_config[host_i] - return config.user or self.user, config.port or self.port, \ - config.password or self.password, config.private_key or self.pkey, \ - config.proxy_host or getattr(self, 'proxy_host', None), \ - config.proxy_port or getattr(self, 'proxy_port', None), \ - config.proxy_user or getattr(self, 'proxy_user', None), \ - config.proxy_password or getattr(self, 'proxy_password', None), \ - config.proxy_pkey or getattr(self, 'proxy_pkey', None) - elif isinstance(self.host_config, dict): - _user = self.host_config.get(host, {}).get('user', self.user) - _port = self.host_config.get(host, {}).get('port', self.port) - _password = self.host_config.get(host, {}).get( - 'password', self.password) - _pkey = self.host_config.get(host, {}).get('private_key', self.pkey) - return _user, _port, _password, _pkey, None, None, None, None, None + config = HostConfig( + user=self.user, port=self.port, password=self.password, private_key=self.pkey, + allow_agent=self.allow_agent, num_retries=self.num_retries, retry_delay=self.retry_delay, + timeout=self.timeout, identity_auth=self.identity_auth, proxy_host=self.proxy_host, + proxy_port=self.proxy_port, proxy_user=self.proxy_user, proxy_password=self.proxy_password, + proxy_pkey=self.proxy_pkey, + keepalive_seconds=self.keepalive_seconds, + ipv6_only=self.ipv6_only, + cert_file=self.cert_file, + forward_ssh_agent=self.forward_ssh_agent, + gssapi_auth=self.gssapi_auth, + gssapi_server_identity=self.gssapi_server_identity, + gssapi_client_identity=self.gssapi_client_identity, + gssapi_delegate_credentials=self.gssapi_delegate_credentials, + ) + return config + elif not isinstance(self.host_config, list): + raise HostConfigError("Host configuration of type %s is invalid - valid types are list[HostConfig]", + type(self.host_config)) + config = self.host_config[host_i] + return config def _run_command(self, host_i, host, command, sudo=False, user=None, shell=None, use_pty=False, @@ -259,7 +286,7 @@ def _run_command(self, host_i, host, command, sudo=False, user=None, """Make SSHClient if needed, run command on host""" logger.debug("_run_command with read timeout %s", read_timeout) try: - _client = self._make_ssh_client(host_i, host) + _client = self._get_ssh_client(host_i, host) host_out = _client.run_command( command, sudo=sudo, user=user, shell=shell, use_pty=use_pty, encoding=encoding, read_timeout=read_timeout) @@ -283,7 +310,7 @@ def connect_auth(self): :returns: list of greenlets to ``joinall`` with. :rtype: list(:py:mod:`gevent.greenlet.Greenlet`) """ - cmds = [spawn(self._make_ssh_client, i, host) for i, host in enumerate(self.hosts)] + cmds = [spawn(self._get_ssh_client, i, host) for i, host in enumerate(self.hosts)] return cmds def _consume_output(self, stdout, stderr): @@ -429,7 +456,7 @@ def copy_file(self, local_file, remote_file, recurse=False, copy_args=None): def _copy_file(self, host_i, host, local_file, remote_file, recurse=False): """Make sftp client, copy file""" - client = self._make_ssh_client(host_i, host) + client = self._get_ssh_client(host_i, host) return client.copy_file( local_file, remote_file, recurse=recurse) @@ -512,7 +539,7 @@ def copy_remote_file(self, remote_file, local_file, recurse=False, def _copy_remote_file(self, host_i, host, remote_file, local_file, recurse, **kwargs): """Make sftp client, copy file to local""" - client = self._make_ssh_client(host_i, host) + client = self._get_ssh_client(host_i, host) return client.copy_remote_file( remote_file, local_file, recurse=recurse, **kwargs) @@ -522,5 +549,26 @@ def _handle_greenlet_exc(self, func, host, *args, **kwargs): except Exception as ex: raise ex - def _make_ssh_client(self, host_i, host): + def _get_ssh_client(self, host_i, host): + logger.debug("Make client request for host %s, (host_i, host) in clients: %s", + host, (host_i, host) in self._host_clients) + _client = self._host_clients.get((host_i, host)) + if _client is not None: + return _client + cfg = self._get_host_config(host_i, host) + _pkey = self.pkey if cfg.private_key is None else cfg.private_key + _pkey_data = self._load_pkey_data(_pkey) + _client = self._make_ssh_client(host, cfg, _pkey_data) + self._host_clients[(host_i, host)] = _client + return _client + + def _load_pkey_data(self, _pkey): + if isinstance(_pkey, str): + _validate_pkey_path(_pkey) + with open(_pkey, 'rb') as fh: + _pkey_data = fh.read() + return _pkey_data + return _pkey + + def _make_ssh_client(self, host, cfg, _pkey_data): raise NotImplementedError diff --git a/pssh/clients/base/single.py b/pssh/clients/base/single.py index 343c1f99..56bc2c16 100644 --- a/pssh/clients/base/single.py +++ b/pssh/clients/base/single.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pssh/clients/common.py b/pssh/clients/common.py index 15677c9b..585d3875 100644 --- a/pssh/clients/common.py +++ b/pssh/clients/common.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pssh/clients/native/__init__.py b/pssh/clients/native/__init__.py index 0220afdc..255e9901 100644 --- a/pssh/clients/native/__init__.py +++ b/pssh/clients/native/__init__.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pssh/clients/native/parallel.py b/pssh/clients/native/parallel.py index c56e6991..afcaa3f5 100644 --- a/pssh/clients/native/parallel.py +++ b/pssh/clients/native/parallel.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -18,12 +18,11 @@ import logging from .single import SSHClient -from ..common import _validate_pkey from ..base.parallel import BaseParallelSSHClient +from ..common import _validate_pkey from ...constants import DEFAULT_RETRIES, RETRY_DELAY from ...exceptions import HostArgumentError - logger = logging.getLogger(__name__) @@ -60,7 +59,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None, :type num_retries: int :param retry_delay: Number of seconds to wait between retries. Defaults to :py:class:`pssh.constants.RETRY_DELAY` - :type retry_delay: int + :type retry_delay: int or float :param timeout: (Optional) Global timeout setting in seconds for all remote operations including all SSH client operations DNS, opening connections, reading output from remote servers, et al. @@ -75,7 +74,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None, Host output read timeout can also be set separately via ``run_command(<..>, read_timeout=)`` Defaults to OS default - usually 60 seconds. - :type timeout: float + :type timeout: int or float :param pool_size: (Optional) Greenlet pool size. Controls concurrency, on how many hosts to execute tasks in parallel. Defaults to 100. Overhead in event @@ -107,11 +106,10 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None, :param proxy_pkey: (Optional) Private key file to be used for authentication with ``proxy_host``. Defaults to available keys from SSHAgent and user's SSH identities. - :type proxy_pkey: str - :param forward_ssh_agent: (Optional) Turn on SSH agent forwarding - - equivalent to `ssh -A` from the `ssh` command line utility. - Defaults to False if not set. - Requires agent forwarding implementation in libssh2 version used. + Bytes type input is used as private key data for authentication. + :type proxy_pkey: str or bytes + :param forward_ssh_agent: (Optional) Turn on SSH agent forwarding. + Currently unused. :type forward_ssh_agent: bool :param ipv6_only: Choose IPv6 addresses only if multiple are available for the host(s) or raise NoIPv6AddressFoundError otherwise. Note this will @@ -229,38 +227,24 @@ def __del__(self): pass del s_client - def _make_ssh_client(self, host_i, host): - auth_thread_pool = True - logger.debug("Make client request for host %s, (host_i, host) in clients: %s", - host, (host_i, host) in self._host_clients) - if (host_i, host) not in self._host_clients \ - or self._host_clients[(host_i, host)] is None: - _user, _port, _password, _pkey, proxy_host, proxy_port, proxy_user, \ - proxy_password, proxy_pkey = self._get_host_config_values(host_i, host) - if isinstance(self.pkey, str): - with open(_pkey, 'rb') as fh: - _pkey_data = fh.read() - else: - _pkey_data = _pkey - _client = SSHClient( - host, user=_user, password=_password, port=_port, - pkey=_pkey_data, num_retries=self.num_retries, - timeout=self.timeout, - allow_agent=self.allow_agent, retry_delay=self.retry_delay, - proxy_host=proxy_host, - proxy_port=proxy_port, - proxy_user=proxy_user, - proxy_password=proxy_password, - proxy_pkey=proxy_pkey, - _auth_thread_pool=auth_thread_pool, - forward_ssh_agent=self.forward_ssh_agent, - keepalive_seconds=self.keepalive_seconds, - identity_auth=self.identity_auth, - ipv6_only=self.ipv6_only, - ) - self._host_clients[(host_i, host)] = _client - return _client - return self._host_clients[(host_i, host)] + def _make_ssh_client(self, host, cfg, _pkey_data): + _client = SSHClient( + host, user=cfg.user or self.user, password=cfg.password or self.password, port=cfg.port or self.port, + pkey=_pkey_data, num_retries=cfg.num_retries or self.num_retries, + timeout=cfg.timeout or self.timeout, + allow_agent=cfg.allow_agent or self.allow_agent, retry_delay=cfg.retry_delay or self.retry_delay, + proxy_host=cfg.proxy_host or self.proxy_host, + proxy_port=cfg.proxy_port or self.proxy_port, + proxy_user=cfg.proxy_user or self.proxy_user, + proxy_password=cfg.proxy_password or self.proxy_password, + proxy_pkey=cfg.proxy_pkey or self.proxy_pkey, + _auth_thread_pool=cfg.auth_thread_pool or self._auth_thread_pool, + forward_ssh_agent=cfg.forward_ssh_agent or self.forward_ssh_agent, + keepalive_seconds=cfg.keepalive_seconds or self.keepalive_seconds, + identity_auth=cfg.identity_auth or self.identity_auth, + ipv6_only=cfg.ipv6_only or self.ipv6_only, + ) + return _client def copy_file(self, local_file, remote_file, recurse=False, copy_args=None): """Copy local file to remote file in parallel via SFTP. @@ -386,13 +370,13 @@ def copy_remote_file(self, remote_file, local_file, recurse=False, encoding=encoding) def _scp_send(self, host_i, host, local_file, remote_file, recurse=False): - self._make_ssh_client(host_i, host) + self._get_ssh_client(host_i, host) return self._handle_greenlet_exc( self._host_clients[(host_i, host)].scp_send, host, local_file, remote_file, recurse=recurse) def _scp_recv(self, host_i, host, remote_file, local_file, recurse=False): - self._make_ssh_client(host_i, host) + self._get_ssh_client(host_i, host) return self._handle_greenlet_exc( self._host_clients[(host_i, host)].scp_recv, host, remote_file, local_file, recurse=recurse) diff --git a/pssh/clients/native/single.py b/pssh/clients/native/single.py index 672a3353..423f06c8 100644 --- a/pssh/clients/native/single.py +++ b/pssh/clients/native/single.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -82,10 +82,10 @@ def __init__(self, host, :type num_retries: int :param retry_delay: Number of seconds to wait between retries. Defaults to :py:class:`pssh.constants.RETRY_DELAY` - :type retry_delay: int + :type retry_delay: int or float :param timeout: SSH session timeout setting in seconds. This controls timeout setting of authenticated SSH sessions. - :type timeout: int + :type timeout: int or float :param allow_agent: (Optional) set to False to disable connecting to the system's SSH agent :type allow_agent: bool diff --git a/pssh/clients/native/tunnel.py b/pssh/clients/native/tunnel.py index 378555a5..5748a3c2 100644 --- a/pssh/clients/native/tunnel.py +++ b/pssh/clients/native/tunnel.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pssh/clients/reader.py b/pssh/clients/reader.py index c3f2d818..f9ab1e9f 100644 --- a/pssh/clients/reader.py +++ b/pssh/clients/reader.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pssh/clients/ssh/__init__.py b/pssh/clients/ssh/__init__.py index 74eadffc..c4881707 100644 --- a/pssh/clients/ssh/__init__.py +++ b/pssh/clients/ssh/__init__.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pssh/clients/ssh/parallel.py b/pssh/clients/ssh/parallel.py index ea73af53..bd7a11a8 100644 --- a/pssh/clients/ssh/parallel.py +++ b/pssh/clients/ssh/parallel.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -68,7 +68,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None, :type num_retries: int :param retry_delay: Number of seconds to wait between retries. Defaults to :py:class:`pssh.constants.RETRY_DELAY` - :type retry_delay: int + :type retry_delay: int or float :param timeout: (Optional) Individual SSH client timeout setting in seconds passed on to each SSH client spawned by `ParallelSSHClient`. @@ -81,7 +81,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None, Parallel functions like `run_command` and `join` have a cummulative timeout setting that is separate to and not affected by `self.timeout`. - :type timeout: float + :type timeout: int or float :param pool_size: (Optional) Greenlet pool size. Controls concurrency, on how many hosts to execute tasks in parallel. Defaults to 100. Overhead in event @@ -98,22 +98,6 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None, authenticate with default identity files from `pssh.clients.base_ssh_client.BaseSSHClient.IDENTITIES` :type identity_auth: bool - :param proxy_host: (Optional) SSH host to tunnel connection through - so that SSH clients connect to host via client -> proxy_host -> host - :type proxy_host: str - :param proxy_port: (Optional) SSH port to use to login to proxy host if - set. Defaults to 22. - :type proxy_port: int - :param proxy_user: (Optional) User to login to ``proxy_host`` as. - Defaults to logged in user. - :type proxy_user: str - :param proxy_password: (Optional) Password to login to ``proxy_host`` - with. Defaults to no password. - :type proxy_password: str - :param proxy_pkey: (Optional) Private key file to be used for - authentication with ``proxy_host``. Defaults to available keys from - SSHAgent and user's SSH identities. - :type proxy_pkey: Private key file path to use. :param forward_ssh_agent: (Optional) Turn on SSH agent forwarding - equivalent to `ssh -A` from the `ssh` command line utility. Defaults to False if not set. @@ -229,34 +213,19 @@ def run_command(self, command, sudo=False, user=None, stop_on_errors=True, read_timeout=read_timeout, ) - def _make_ssh_client(self, host_i, host): - logger.debug("Make client request for host %s, (host_i, host) in clients: %s", - host, (host_i, host) in self._host_clients) - if (host_i, host) not in self._host_clients \ - or self._host_clients[(host_i, host)] is None: - _user, _port, _password, _pkey, _, _, _, _, _ = \ - self._get_host_config_values(host_i, host) - if isinstance(self.pkey, str): - with open(_pkey, 'rb') as fh: - _pkey_data = fh.read() - else: - _pkey_data = _pkey - _client = SSHClient( - host, user=_user, password=_password, port=_port, - pkey=_pkey_data, - cert_file=self.cert_file, - num_retries=self.num_retries, - timeout=self.timeout, - allow_agent=self.allow_agent, retry_delay=self.retry_delay, - gssapi_auth=self.gssapi_auth, - gssapi_server_identity=self.gssapi_server_identity, - gssapi_client_identity=self.gssapi_client_identity, - gssapi_delegate_credentials=self.gssapi_delegate_credentials, - identity_auth=self.identity_auth, - ipv6_only=self.ipv6_only, - ) - self._host_clients[(host_i, host)] = _client - # TODO - Add forward agent functionality - # forward_ssh_agent=self.forward_ssh_agent) - return _client - return self._host_clients[(host_i, host)] + def _make_ssh_client(self, host, cfg, _pkey_data): + _client = SSHClient( + host, user=cfg.user or self.user, password=cfg.password or self.password, port=cfg.port or self.port, + pkey=_pkey_data, num_retries=cfg.num_retries or self.num_retries, + timeout=cfg.timeout or self.timeout, + allow_agent=cfg.allow_agent or self.allow_agent, retry_delay=cfg.retry_delay or self.retry_delay, + _auth_thread_pool=cfg.auth_thread_pool or self._auth_thread_pool, + identity_auth=cfg.identity_auth or self.identity_auth, + ipv6_only=cfg.ipv6_only or self.ipv6_only, + gssapi_auth=self.gssapi_auth, + gssapi_server_identity=self.gssapi_server_identity, + gssapi_client_identity=self.gssapi_client_identity, + gssapi_delegate_credentials=self.gssapi_delegate_credentials, + cert_file=cfg.cert_file, + ) + return _client diff --git a/pssh/clients/ssh/single.py b/pssh/clients/ssh/single.py index f06eccc4..6a11c2ab 100644 --- a/pssh/clients/ssh/single.py +++ b/pssh/clients/ssh/single.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -77,10 +77,10 @@ def __init__(self, host, :type num_retries: int :param retry_delay: Number of seconds to wait between retries. Defaults to :py:class:`pssh.constants.RETRY_DELAY` - :type retry_delay: int + :type retry_delay: int or float :param timeout: (Optional) If provided, all commands will timeout after number of seconds. - :type timeout: int + :type timeout: int or float :param allow_agent: (Optional) set to False to disable connecting to the system's SSH agent. Currently unused. :type allow_agent: bool diff --git a/pssh/config.py b/pssh/config.py index f73cbe31..5c1cc949 100644 --- a/pssh/config.py +++ b/pssh/config.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -27,7 +27,9 @@ class HostConfig(object): __slots__ = ('user', 'port', 'password', 'private_key', 'allow_agent', 'num_retries', 'retry_delay', 'timeout', 'identity_auth', 'proxy_host', 'proxy_port', 'proxy_user', 'proxy_password', 'proxy_pkey', - 'keepalive_seconds', 'ipv6_only', + 'keepalive_seconds', 'ipv6_only', 'cert_file', 'auth_thread_pool', 'gssapi_auth', + 'gssapi_server_identity', 'gssapi_client_identity', 'gssapi_delegate_credentials', + 'forward_ssh_agent', ) def __init__(self, user=None, port=None, password=None, private_key=None, @@ -37,6 +39,13 @@ def __init__(self, user=None, port=None, password=None, private_key=None, proxy_pkey=None, keepalive_seconds=None, ipv6_only=None, + cert_file=None, + auth_thread_pool=True, + gssapi_auth=False, + gssapi_server_identity=None, + gssapi_client_identity=None, + gssapi_delegate_credentials=False, + forward_ssh_agent=False, ): """ :param user: Username to login as. @@ -53,9 +62,9 @@ def __init__(self, user=None, port=None, password=None, private_key=None, and SSH operations. :type num_retries: int :param retry_delay: Delay in seconds between retry attempts. - :type retry_delay: int + :type retry_delay: int or float :param timeout: Timeout value for connection and SSH sessions in seconds. - :type timeout: int + :type timeout: int or float :param identity_auth: Enable/disable identity file authentication under user's home directory (~/.ssh). :type identity_auth: bool @@ -73,8 +82,21 @@ def __init__(self, user=None, port=None, password=None, private_key=None, :param keepalive_seconds: Seconds between keepalive packets being sent. 0 to disable. :type keepalive_seconds: int - :param ipv6_only: Use IPv6 addresses only. Currently unused. + :param ipv6_only: Use IPv6 addresses only. :type ipv6_only: bool + :param cert_file: Certificate file for authentication (pssh.clients.ssh only) + :type cert_file: str + :param auth_thread_pool: Enable/Disable use of thread pool for authentication. + :type auth_thread_pool: bool + :param forward_ssh_agent: Currently unused. + :type forward_ssh_agent: bool + :param gssapi_server_identity: Set GSSAPI server identity. (pssh.clients.ssh only) + :type gssapi_server_identity: str + :param gssapi_server_identity: Set GSSAPI client identity. (pssh.clients.ssh only) + :type gssapi_server_identity: str + :param gssapi_delegate_credentials: Enable/disable server credentials + delegation. (pssh.clients.ssh only) + :type gssapi_delegate_credentials: bool """ self.user = user self.port = port @@ -92,6 +114,13 @@ def __init__(self, user=None, port=None, password=None, private_key=None, self.proxy_pkey = proxy_pkey self.keepalive_seconds = keepalive_seconds self.ipv6_only = ipv6_only + self.cert_file = cert_file + self.auth_thread_pool = auth_thread_pool + self.forward_ssh_agent = forward_ssh_agent + self.gssapi_auth = gssapi_auth + self.gssapi_server_identity = gssapi_server_identity + self.gssapi_client_identity = gssapi_client_identity + self.gssapi_delegate_credentials = gssapi_delegate_credentials self._sanity_checks() def _sanity_checks(self): @@ -101,16 +130,20 @@ def _sanity_checks(self): raise ValueError("Port %s is not an integer" % (self.port,)) if self.password is not None and not isinstance(self.password, str): raise ValueError("Password %s is not a string" % (self.password,)) - if self.private_key is not None and not isinstance(self.private_key, str): - raise ValueError("Private key %s is not a string" % (self.private_key,)) + if self.private_key is not None and not ( + isinstance(self.private_key, str) or isinstance(self.private_key, bytes) + ): + raise ValueError("Private key %s is not a string or bytes" % (self.private_key,)) if self.allow_agent is not None and not isinstance(self.allow_agent, bool): raise ValueError("Allow agent %s is not a boolean" % (self.allow_agent,)) if self.num_retries is not None and not isinstance(self.num_retries, int): raise ValueError("Num retries %s is not an integer" % (self.num_retries,)) - if self.timeout is not None and not isinstance(self.timeout, int): + if self.timeout is not None and not \ + (isinstance(self.timeout, int) or isinstance(self.timeout, float)): raise ValueError("Timeout %s is not an integer" % (self.timeout,)) - if self.retry_delay is not None and not isinstance(self.retry_delay, int): - raise ValueError("Retry delay %s is not an integer" % (self.retry_delay,)) + if self.retry_delay is not None and not \ + (isinstance(self.retry_delay, int) or isinstance(self.retry_delay, float)): + raise ValueError("Retry delay %s is not a number" % (self.retry_delay,)) if self.identity_auth is not None and not isinstance(self.identity_auth, bool): raise ValueError("Identity auth %s is not a boolean" % (self.identity_auth,)) if self.proxy_host is not None and not isinstance(self.proxy_host, str): @@ -121,9 +154,25 @@ def _sanity_checks(self): raise ValueError("Proxy user %s is not a string" % (self.proxy_user,)) if self.proxy_password is not None and not isinstance(self.proxy_password, str): raise ValueError("Proxy password %s is not a string" % (self.proxy_password,)) - if self.proxy_pkey is not None and not isinstance(self.proxy_pkey, str): - raise ValueError("Proxy pkey %s is not a string" % (self.proxy_pkey,)) + if self.proxy_pkey is not None and not ( + isinstance(self.proxy_pkey, str) or isinstance(self.proxy_pkey, bytes) + ): + raise ValueError("Proxy pkey %s is not a string or bytes" % (self.proxy_pkey,)) if self.keepalive_seconds is not None and not isinstance(self.keepalive_seconds, int): raise ValueError("Keepalive seconds %s is not an integer" % (self.keepalive_seconds,)) if self.ipv6_only is not None and not isinstance(self.ipv6_only, bool): raise ValueError("IPv6 only %s is not a boolean value" % (self.ipv6_only,)) + if self.cert_file is not None and not ( + isinstance(self.cert_file, str) or isinstance(self.cert_file, bytes) + ): + raise ValueError("Cert file %s is not a string or bytes", self.cert_file) + if self.forward_ssh_agent is not None and not isinstance(self.forward_ssh_agent, bool): + raise ValueError("Forward SSH agent %s is not a bool", self.forward_ssh_agent) + if self.gssapi_auth is not None and not isinstance(self.gssapi_auth, bool): + raise ValueError("GSSAPI auth %s is not a bool", self.gssapi_auth) + if self.gssapi_server_identity is not None and not isinstance(self.gssapi_server_identity, str): + raise ValueError("GSSAPI server identity %s is not a string", self.gssapi_server_identity) + if self.gssapi_client_identity is not None and not isinstance(self.gssapi_client_identity, str): + raise ValueError("GSSAPI client identity %s is not a string", self.gssapi_client_identity) + if self.gssapi_delegate_credentials is not None and not isinstance(self.gssapi_delegate_credentials, bool): + raise ValueError("GSSAPI delegate credentials %s is not a bool", self.gssapi_delegate_credentials) diff --git a/pssh/constants.py b/pssh/constants.py index 69f15a5b..5b58aa4c 100644 --- a/pssh/constants.py +++ b/pssh/constants.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pssh/exceptions.py b/pssh/exceptions.py index 3e6a3602..14fdf35e 100644 --- a/pssh/exceptions.py +++ b/pssh/exceptions.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -96,3 +96,7 @@ class PKeyFileError(Exception): class ShellError(Exception): """Raised on errors running command on interactive shell""" + + +class HostConfigError(Exception): + """Raised on invalid host configuration""" diff --git a/pssh/output.py b/pssh/output.py index 1d4223b6..c7e9375e 100644 --- a/pssh/output.py +++ b/pssh/output.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/pssh/utils.py b/pssh/utils.py index 51747b61..9eca4d23 100644 --- a/pssh/utils.py +++ b/pssh/utils.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis. +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/setup.cfg b/setup.cfg index 12c0bdb8..46e264d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,9 +5,9 @@ versionfile_source = pssh/_version.py tag_prefix = '' [flake8] -max-line-length = 100 +max-line-length = 120 [tool:pytest] -addopts=-v --cov=pssh --cov-append --cov-report=term --cov-report=term-missing --durations=10 +addopts=-v --cov=pssh --cov-append --cov-report=term --cov-report=term-missing --durations=10 --reruns=5 testpaths = tests diff --git a/tests/embedded_server/openssh.py b/tests/embedded_server/openssh.py index 67cf0a10..cd347d76 100644 --- a/tests/embedded_server/openssh.py +++ b/tests/embedded_server/openssh.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -15,17 +15,16 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import logging import os import random import string -import logging -import pwd +from getpass import getuser from subprocess import Popen, TimeoutExpired -from sys import version_info +from gevent import Timeout from jinja2 import Template - logger = logging.getLogger('pssh.test.openssh_server') logger.setLevel(logging.DEBUG) @@ -42,6 +41,10 @@ PRINCIPALS = os.path.abspath(os.path.sep.join([DIR_NAME, 'principals'])) +class OpenSSHServerError(Exception): + pass + + class OpenSSHServer(object): def __init__(self, listen_ip='127.0.0.1', port=2222): @@ -55,15 +58,15 @@ def __init__(self, listen_ip='127.0.0.1', port=2222): self.make_config() def _fix_masks(self): - _mask = int('0600') if version_info <= (2,) else 0o600 - dir_mask = int('0755') if version_info <= (2,) else 0o755 + _mask = 0o600 + dir_mask = 0o755 for _file in [SERVER_KEY, CA_HOST_KEY]: os.chmod(_file, _mask) for _dir in [DIR_NAME, PDIR_NAME, PPDIR_NAME]: os.chmod(_dir, dir_mask) def make_config(self): - user = pwd.getpwuid(os.geteuid()).pw_name + user = getuser() with open(SSHD_CONFIG_TMPL) as fh: tmpl = fh.read() template = Template(tmpl) @@ -86,15 +89,18 @@ def start_server(self): logger.debug("Starting server with %s" % (" ".join(cmd),)) self.server_proc = Popen(cmd) try: - self.server_proc.wait(.3) - except TimeoutExpired: - pass - else: + with Timeout(seconds=5, exception=TimeoutError): + while True: + try: + self.server_proc.wait(.1) + except TimeoutExpired: + break + except TimeoutError: if self.server_proc.stdout is not None: logger.error(self.server_proc.stdout.read()) if self.server_proc.stderr is not None: logger.error(self.server_proc.stderr.read()) - raise Exception("Server could not start") + raise OpenSSHServerError("Server could not start") def stop(self): if self.server_proc is not None and self.server_proc.returncode is None: diff --git a/tests/native/base_ssh2_case.py b/tests/native/base_ssh2_case.py index e001f976..9fdfff5e 100644 --- a/tests/native/base_ssh2_case.py +++ b/tests/native/base_ssh2_case.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -15,13 +15,13 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import unittest -import pwd -import os import logging +import os +import unittest +from getpass import getuser from sys import version_info -from pssh.clients.native import SSHClient, logger as ssh_logger +from pssh.clients.native import SSHClient from ..embedded_server.openssh import OpenSSHServer @@ -42,6 +42,8 @@ def setup_root_logger(): class SSH2TestCase(unittest.TestCase): + client = None + server = None @classmethod def setUpClass(cls): @@ -55,7 +57,7 @@ def setUpClass(cls): cls.resp = u'me' cls.user_key = PKEY_FILENAME cls.user_pub_key = PUB_FILE - cls.user = pwd.getpwuid(os.geteuid()).pw_name + cls.user = getuser() cls.client = SSHClient(cls.host, port=cls.port, pkey=PKEY_FILENAME, num_retries=1, diff --git a/tests/native/test_agent.py b/tests/native/test_agent.py deleted file mode 100644 index f7d1ae20..00000000 --- a/tests/native/test_agent.py +++ /dev/null @@ -1,77 +0,0 @@ -# This file is part of parallel-ssh. -# -# Copyright (C) 2014-2020 Panos Kittenis -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation, version 2.1. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import unittest -import pwd -import logging -import os -from sys import version_info -from subprocess import call - -from ssh2.channel import Channel -from pssh import logger as pssh_logger -from pssh.clients import ParallelSSHClient - -from .base_ssh2_case import PKEY_FILENAME, PUB_FILE -from ..embedded_server.openssh import OpenSSHServer - - -pssh_logger.setLevel(logging.DEBUG) -logging.basicConfig() - - -class ForwardTestCase(unittest.TestCase): - - @classmethod - def setUpClass(cls): - _mask = int('0600') if version_info <= (2,) else 0o600 - os.chmod(PKEY_FILENAME, _mask) - if not hasattr(Channel, 'request_auth_agent'): - raise unittest.SkipTest( - "Agent forwarding implementation not available in libssh2") - if call('ssh-add %s' % PKEY_FILENAME, shell=True) != 0: - raise unittest.SkipTest("No agent available.") - cls.server = OpenSSHServer() - cls.server.start_server() - cls.host = '127.0.0.1' - cls.port = 2222 - cls.cmd = 'echo me' - cls.resp = u'me' - cls.user_key = PKEY_FILENAME - cls.user_pub_key = PUB_FILE - cls.user = pwd.getpwuid(os.geteuid()).pw_name - # Single client for all tests ensures that the client does not do - # anything that causes server to disconnect the session and - # affect all subsequent uses of the same session. - cls.client = ParallelSSHClient([cls.host], - pkey=PKEY_FILENAME, - port=cls.port, - num_retries=1) - - @classmethod - def tearDownClass(cls): - call('ssh-add -d %s' % PKEY_FILENAME, shell=True) - cls.server.stop() - del cls.server - - def test_agent_forwarding(self): - client = ParallelSSHClient(['localhost'], forward_ssh_agent=True, - port=self.port) - output = client.run_command(self.cmd) - stdout = [list(output['localhost'].stdout) for k in output] - expected_stdout = [[self.resp]] - self.assertListEqual(stdout, expected_stdout) diff --git a/tests/native/test_parallel_client.py b/tests/native/test_parallel_client.py index db25153f..5b760985 100644 --- a/tests/native/test_parallel_client.py +++ b/tests/native/test_parallel_client.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # This file is part of parallel-ssh. # -# Copyright (C) 2014-2021 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -20,7 +20,7 @@ """Unittests for :mod:`pssh.ParallelSSHClient` class""" import unittest -import pwd +from getpass import getuser import os import shutil import string @@ -57,7 +57,7 @@ def setUpClass(cls): cls.resp = u'me' cls.user_key = PKEY_FILENAME cls.user_pub_key = PUB_FILE - cls.user = pwd.getpwuid(os.geteuid()).pw_name + cls.user = getuser() # Single client for all tests ensures that the client does not do # anything that causes server to disconnect the session and # affect all subsequent uses of the same session. @@ -116,7 +116,7 @@ def test_client_shells(self): def test_client_shells_read_timeout(self): shells = self.client.open_shell(read_timeout=.1) self.client.run_shell_commands(shells, self.cmd) - self.client.run_shell_commands(shells, [self.cmd, 'sleep .25', 'exit 1']) + self.client.run_shell_commands(shells, [self.cmd, 'sleep .5', 'exit 1']) stdout = [] for shell in shells: try: @@ -133,8 +133,8 @@ def test_client_shells_read_timeout(self): def test_client_shells_timeout(self): client = ParallelSSHClient([self.host], pkey=self.user_key, port=self.port, timeout=0.01, num_retries=1) - client._make_ssh_client = MagicMock() - client._make_ssh_client.side_effect = Timeout + client._get_ssh_client = MagicMock() + client._get_ssh_client.side_effect = Timeout self.assertRaises(Timeout, client.open_shell) def test_client_shells_join_timeout(self): @@ -220,7 +220,7 @@ def test_pssh_client_no_stdout_non_zero_exit_code_immediate_exit_no_join(self): output = self.client.run_command('exit 1') expected_exit_code = 1 for host_out in output: - for line in host_out.stdout: + for _ in host_out.stdout: pass exit_code = output[0].exit_code self.assertEqual(expected_exit_code, exit_code) @@ -299,7 +299,8 @@ def test_timeout_on_open_session(self): pkey=self.user_key, timeout=timeout, num_retries=1) - def _session(timeout=None): + + def _session(_=None): sleep(.2) joinall(client.connect_auth()) sleep(.01) @@ -531,7 +532,6 @@ def test_pssh_client_directory_abs_path(self): def test_pssh_client_copy_file_failure(self): """Test failure scenarios of file copy""" - test_file_data = 'test' local_test_path = 'directory_test' remote_test_path = 'directory_test_copied' dir_name = os.path.dirname(__file__) @@ -551,7 +551,6 @@ def test_pssh_client_copy_file_failure(self): os.mkdir(local_test_path) os.mkdir(remote_test_path_abs) local_file_path = os.path.join(local_test_path, 'test_file') - remote_file_path = os.path.join(remote_test_path, 'test_file') remote_test_path_abs = os.sep.join((dir_name, remote_test_path)) test_file = open(local_file_path, 'w') test_file.write('testing\n') @@ -568,7 +567,6 @@ def test_pssh_client_copy_file_failure(self): self.assertFalse(os.path.isfile(remote_test_path_abs)) # Create directory tree failure test local_file_path = os.path.join(local_test_path, 'test_file') - remote_file_path = os.path.join(remote_test_path, 'test_dir', 'test_file') remote_test_path_abs = os.sep.join((dir_name, remote_test_path)) cmds = self.client.copy_file(local_file_path, remote_test_path_abs, recurse=True) try: @@ -779,11 +777,11 @@ def test_pssh_hosts_more_than_pool_size(self): stdout = [list(host_out.stdout) for host_out in output] expected_stdout = [[self.resp] for _ in hosts] self.assertEqual(len(hosts), len(output), - msg="Did not get output from all hosts. Got output for " \ - "%s/%s hosts" % (len(output), len(hosts),)) + msg="Did not get output from all hosts. Got output for " + "%s/%s hosts" % (len(output), len(hosts),)) self.assertEqual(expected_stdout, stdout, - msg="Did not get expected output from all hosts. \ - Got %s - expected %s" % (stdout, expected_stdout,)) + msg="Did not get expected output from all hosts. " + "Got %s - expected %s" % (stdout, expected_stdout,)) del client server2.stop() @@ -801,11 +799,12 @@ def test_pssh_hosts_iterator_hosts_modification(self): pool_size=1, ) output = client.run_command(self.cmd) - stdout = [host_out.stdout for host_out in output] + stdout = [list(host_out.stdout) for host_out in output] expected_stdout = [[self.resp], [self.resp]] + self.assertListEqual(stdout, expected_stdout) self.assertEqual(len(hosts), len(output), - msg="Did not get output from all hosts. Got output for " \ - "%s/%s hosts" % (len(output), len(hosts),)) + msg="Did not get output from all hosts. Got output for " + "%s/%s hosts" % (len(output), len(hosts),)) # Run again without re-assigning host list, should run the same output = client.run_command(self.cmd) self.assertEqual(len(output), len(hosts)) @@ -814,8 +813,8 @@ def test_pssh_hosts_iterator_hosts_modification(self): client.hosts = iter(hosts) output = client.run_command(self.cmd) self.assertEqual(len(hosts), len(output), - msg="Did not get output from all hosts. Got output for " \ - "%s/%s hosts" % (len(output), len(hosts),)) + msg="Did not get output from all hosts. Got output for " + "%s/%s hosts" % (len(output), len(hosts),)) self.assertEqual(output[1].host, hosts[1], msg="Did not get output for new host %s" % (hosts[1],)) server2.stop() @@ -826,7 +825,7 @@ def test_bash_variable_substitution(self): command = """for i in 1 2 3; do echo $i; done""" host_output = self.client.run_command(command)[0] output = list(host_output.stdout) - expected = ['1','2','3'] + expected = ['1', '2', '3'] self.assertListEqual(output, expected) def test_identical_host_output(self): @@ -890,7 +889,7 @@ def test_multiple_single_quotes_in_cmd(self): output = self.client.run_command("echo 'me' 'and me'") stdout = list(output[0].stdout) expected = 'me and me' - self.assertTrue(len(stdout)==1, + self.assertTrue(len(stdout) == 1, msg="Got incorrect number of lines in output - %s" % (stdout,)) self.assertEqual(output[0].exit_code, 0) self.assertEqual(expected, stdout[0], @@ -926,46 +925,7 @@ def test_escaped_quotes(self): def test_host_config(self): """Test per-host configuration functionality of ParallelSSHClient""" hosts = [('127.0.0.%01d' % n, self.make_random_port()) - for n in range(1,3)] - host_config = dict.fromkeys([h for h,_ in hosts]) - servers = [] - password = 'overriden_pass' - fake_key = 'FAKE KEY' - for host, port in hosts: - server = OpenSSHServer(listen_ip=host, port=port) - server.start_server() - host_config[host] = {} - host_config[host]['port'] = port - host_config[host]['user'] = self.user - host_config[host]['password'] = password - host_config[host]['private_key'] = self.user_key - servers.append(server) - host_config[hosts[1][0]]['private_key'] = fake_key - client = ParallelSSHClient([h for h, _ in hosts], - host_config=host_config, - num_retries=1) - output = client.run_command(self.cmd, stop_on_errors=False) - client.join(output) - self.assertEqual(len(hosts), len(output)) - try: - raise output[1].exception - except PKeyFileError as ex: - self.assertEqual(output[1].host, hosts[1][0]) - else: - raise AssertionError("Expected ValueError on host %s", - hosts[0][0]) - self.assertTrue(output[1].exit_code is None, - msg="Execution failed on host %s" % (hosts[1][0],)) - self.assertEqual(client._host_clients[0, hosts[0][0]].user, self.user) - self.assertEqual(client._host_clients[0, hosts[0][0]].password, password) - self.assertEqual(client._host_clients[0, hosts[0][0]].pkey, os.path.abspath(self.user_key)) - for server in servers: - server.stop() - - def test_host_config_list_type(self): - """Test per-host configuration functionality of ParallelSSHClient""" - hosts = [('127.0.0.%01d' % n, self.make_random_port()) - for n in range(1,3)] + for n in range(1, 3)] host_config = [HostConfig() for _ in hosts] servers = [] password = 'overriden_pass' @@ -987,16 +947,13 @@ def test_host_config_list_type(self): self.assertEqual(len(hosts), len(output)) try: raise output[1].exception - except PKeyFileError as ex: + except PKeyFileError: self.assertEqual(output[1].host, hosts[1][0]) - else: - raise AssertionError("Expected ValueError on host %s", - hosts[0][0]) self.assertTrue(output[1].exit_code is None, msg="Execution failed on host %s" % (hosts[1][0],)) self.assertEqual(client._host_clients[0, hosts[0][0]].user, self.user) self.assertEqual(client._host_clients[0, hosts[0][0]].password, password) - self.assertEqual(client._host_clients[0, hosts[0][0]].pkey, os.path.abspath(self.user_key)) + self.assertEqual(client._host_clients[0, hosts[0][0]].pkey, open(os.path.abspath(self.user_key), 'rb').read()) for server in servers: server.stop() @@ -1033,7 +990,7 @@ def test_pssh_client_override_allow_agent_authentication(self): expected_stderr,)) def test_per_host_tuple_args(self): - host2, host3 = '127.0.0.4', '127.0.0.5' + host2, host3 = '127.0.0.2', '127.0.0.3' server2 = OpenSSHServer(host2, port=self.port) server3 = OpenSSHServer(host3, port=self.port) servers = [server2, server3] @@ -1044,7 +1001,9 @@ def test_per_host_tuple_args(self): cmd = 'echo %s' client = ParallelSSHClient(hosts, port=self.port, pkey=self.user_key, - num_retries=2) + num_retries=2, + retry_delay=.2, + ) output = client.run_command(cmd, host_args=host_args) client.join() for i, host in enumerate(hosts): @@ -1162,14 +1121,7 @@ def test_pty(self): def test_output_attributes(self): output = self.client.run_command(self.cmd) - expected_exit_code = 0 - expected_stdout = [self.resp] - expected_stderr = [] self.client.join(output) - exit_code = output[0].exit_code - stdout = list(output[0].stdout) - stderr = list(output[0].stderr) - host_output = output[0] self.assertTrue(hasattr(output[0], 'host')) self.assertTrue(hasattr(output[0], 'channel')) self.assertTrue(hasattr(output[0], 'stdout')) @@ -1538,7 +1490,6 @@ def _scp_larger_files(self, hosts): file_h.write(data) sha.update(data) source_file_sha = sha.hexdigest() - sha = sha256() cmds = client.scp_send('%(local_file)s', '%(remote_file)s', copy_args=copy_args) try: joinall(cmds, raise_error=True) @@ -1547,6 +1498,7 @@ def _scp_larger_files(self, hosts): else: del client for remote_file_name in remote_file_names: + sha = sha256() remote_file_abspath = os.path.expanduser('~/' + remote_file_name) self.assertTrue(os.path.isfile(remote_file_abspath)) with open(remote_file_abspath, 'rb') as remote_fh: @@ -1554,8 +1506,8 @@ def _scp_larger_files(self, hosts): while data: sha.update(data) data = remote_fh.read(10240) + sha.update(data) remote_file_sha = sha.hexdigest() - sha = sha256() self.assertEqual(source_file_sha, remote_file_sha) finally: try: diff --git a/tests/native/test_single_client.py b/tests/native/test_single_client.py index 33594b74..b99a4065 100644 --- a/tests/native/test_single_client.py +++ b/tests/native/test_single_client.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,33 +16,30 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import os -import subprocess import shutil +import subprocess import tempfile +from datetime import datetime +from hashlib import sha256 from tempfile import NamedTemporaryFile - -import pytest -from pytest import raises from unittest.mock import MagicMock, call, patch -from hashlib import sha256 -from datetime import datetime +import pytest from gevent import sleep, spawn, Timeout as GTimeout, socket - -from pssh.clients.native import SSHClient -from ssh2.session import Session +from pytest import raises from ssh2.exceptions import (SocketDisconnectError, BannerRecvError, SocketRecvError, AgentConnectionError, AgentListIdentitiesError, AgentAuthenticationError, AgentGetIdentityError, SFTPProtocolError, AuthenticationError as SSH2AuthenticationError, ) +from ssh2.session import Session + +from pssh.clients.native import SSHClient from pssh.exceptions import (AuthenticationException, ConnectionErrorException, SessionError, SFTPIOError, SFTPError, SCPError, PKeyFileError, Timeout, AuthenticationError, NoIPv6AddressFoundError, ConnectionError ) - from .base_ssh2_case import SSH2TestCase -from ..embedded_server.openssh import OpenSSHServer class SSH2ClientTest(SSH2TestCase): diff --git a/tests/native/test_tunnel.py b/tests/native/test_tunnel.py index 82797147..8137ddd9 100644 --- a/tests/native/test_tunnel.py +++ b/tests/native/test_tunnel.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2021 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -15,27 +15,27 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import unittest -import pwd +import gc import os import time -import gc - +import unittest from datetime import datetime +from getpass import getuser from sys import version_info + from gevent import sleep, spawn, Timeout as GTimeout +from ssh2.exceptions import SocketSendError, SocketRecvError -from pssh.config import HostConfig from pssh.clients.native import SSHClient, ParallelSSHClient from pssh.clients.native.tunnel import LocalForwarder, TunnelServer, FORWARDER +from pssh.config import HostConfig from pssh.exceptions import ProxyError -from ssh2.exceptions import SocketSendError, SocketRecvError - from .base_ssh2_case import PKEY_FILENAME, PUB_FILE from ..embedded_server.openssh import OpenSSHServer class TunnelTest(unittest.TestCase): + server = None @classmethod def setUpClass(cls): @@ -46,7 +46,7 @@ def setUpClass(cls): cls.resp = u'me' cls.user_key = PKEY_FILENAME cls.user_pub_key = PUB_FILE - cls.user = pwd.getpwuid(os.geteuid()).pw_name + cls.user = getuser() cls.proxy_host = '127.0.0.9' cls.proxy_port = cls.port + 1 cls.server = OpenSSHServer(listen_ip=cls.proxy_host, port=cls.proxy_port) @@ -87,7 +87,29 @@ def test_tunnel_server(self): self.assertEqual(self.port, client.port) finally: remote_server.stop() - + + def test_proxy_pkey_bytes_data(self): + remote_host = '127.0.0.8' + remote_server = OpenSSHServer(listen_ip=remote_host, port=self.port) + remote_server.start_server() + with open(self.user_key, 'rb') as fh: + pkey_data = fh.read() + try: + client = ParallelSSHClient( + [remote_host], port=self.port, pkey=pkey_data, + num_retries=1, + proxy_host=self.proxy_host, + proxy_pkey=pkey_data, + proxy_port=self.proxy_port, + ) + output = client.run_command(self.cmd) + _stdout = list(output[0].stdout) + self.assertListEqual(_stdout, [self.resp]) + self.assertEqual(remote_host, output[0].host) + self.assertEqual(self.port, client.port) + finally: + remote_server.stop() + # The purpose of this test is to exercise # https://github.com/ParallelSSH/parallel-ssh/issues/304 def test_tunnel_server_reconn(self): @@ -107,8 +129,7 @@ def test_tunnel_server_reconn(self): proxy_port=self.proxy_port, ) output = client.run_command(self.cmd) - _stdout = list(output.stdout) - self.assertListEqual(_stdout, [self.resp]) + client.wait_finished(output) self.assertEqual(remote_host, client.host) self.assertEqual(self.port, client.port) client.disconnect() @@ -138,7 +159,7 @@ def test_tunnel_server_same_port(self): remote_server.stop() def test_tunnel_parallel_client(self): - hosts = ['127.0.0.1%s' % (d,) for d in range(10)] + hosts = ['127.0.0.1%s' % (d,) for d in range(5)] servers = [OpenSSHServer(listen_ip=_host, port=self.port) for _host in hosts] for server in servers: server.start_server() @@ -245,11 +266,12 @@ def test_tunnel_host_config(self): output = client.run_command(self.cmd, stop_on_errors=False) client.join(output) self.assertIsInstance(output[1].exception, ProxyError) + self.assertTrue(output[0].exception is None) stdout = list(output[0].stdout) self.assertListEqual(stdout, [self.resp]) def test_proxy_error(self): - client = ParallelSSHClient([self.proxy_host], self.port, pkey=self.user_key, + client = ParallelSSHClient([self.proxy_host], port=self.port, pkey=self.user_key, proxy_host='127.0.0.155', proxy_port=123, num_retries=1) diff --git a/tests/ssh/base_ssh_case.py b/tests/ssh/base_ssh_case.py index 6a86c4c9..d14924f2 100644 --- a/tests/ssh/base_ssh_case.py +++ b/tests/ssh/base_ssh_case.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -15,15 +15,15 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import unittest -import pwd -import os import logging +import os import subprocess +import unittest +from getpass import getuser from sys import version_info +from pssh.clients.ssh.single import SSHClient from ..embedded_server.openssh import OpenSSHServer -from pssh.clients.ssh.single import SSHClient, logger as ssh_logger def setup_root_logger(): @@ -44,7 +44,7 @@ def setup_root_logger(): USER_CERT_PUB_KEY = "%s.pub" % (USER_CERT_PRIV_KEY,) USER_CERT_FILE = "%s-cert.pub" % (USER_CERT_PRIV_KEY,) CA_USER_KEY = os.path.sep.join([os.path.dirname(__file__), '..', 'embedded_server', 'ca_user_key']) -USER = pwd.getpwuid(os.geteuid()).pw_name +USER = getuser() def sign_cert(): diff --git a/tests/ssh/test_parallel_client.py b/tests/ssh/test_parallel_client.py index 3a9305e3..401fd791 100644 --- a/tests/ssh/test_parallel_client.py +++ b/tests/ssh/test_parallel_client.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -15,24 +15,20 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import unittest import os -import subprocess +import unittest from datetime import datetime from sys import version_info from unittest.mock import patch, MagicMock -from gevent import joinall, spawn, socket, Greenlet, sleep +from gevent import joinall, spawn, socket, sleep + from pssh import logger as pssh_logger -from pssh.output import HostOutput -from pssh.exceptions import UnknownHostException, \ - AuthenticationException, ConnectionErrorException, SessionError, \ - HostArgumentException, SFTPError, SFTPIOError, Timeout, SCPError, \ - ProxyError, PKeyFileError, NoIPv6AddressFoundError from pssh.clients.ssh.parallel import ParallelSSHClient - +from pssh.exceptions import AuthenticationException, ConnectionErrorException, Timeout, PKeyFileError +from pssh.output import HostOutput from .base_ssh_case import PKEY_FILENAME, PUB_FILE, USER_CERT_PRIV_KEY, \ - USER_CERT_PUB_KEY, USER_CERT_FILE, CA_USER_KEY, USER, sign_cert + USER_CERT_FILE, CA_USER_KEY, USER, sign_cert from ..embedded_server.openssh import OpenSSHServer @@ -83,13 +79,14 @@ def make_random_port(self): return listen_port def test_timeout_on_open_session(self): - timeout = 1 + timeout = .1 client = ParallelSSHClient([self.host], port=self.port, pkey=self.user_key, timeout=timeout, num_retries=1) - def _session(timeout=1): - sleep(timeout+1) + + def _session(_=None): + sleep(.2) joinall(client.connect_auth()) sleep(.01) client._host_clients[(0, self.host)].open_session = _session @@ -267,7 +264,8 @@ def test_connection_timeout(self): client = ParallelSSHClient([host], port=self.port, pkey=self.user_key, timeout=client_timeout, - num_retries=1) + num_retries=1, + retry_delay=.1) output = client.run_command('sleep 1', stop_on_errors=False) self.assertIsInstance(output[0].exception, ConnectionErrorException) diff --git a/tests/ssh/test_single_client.py b/tests/ssh/test_single_client.py index 27738a4b..b109ba1f 100644 --- a/tests/ssh/test_single_client.py +++ b/tests/ssh/test_single_client.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -15,21 +15,17 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -import unittest import logging - from datetime import datetime from gevent import sleep, Timeout as GTimeout, spawn -# from ssh.session import Session from ssh.exceptions import AuthenticationDenied + +from pssh.clients.ssh.single import SSHClient, logger as ssh_logger from pssh.exceptions import AuthenticationException, ConnectionErrorException, \ - SessionError, SFTPIOError, SFTPError, SCPError, PKeyFileError, Timeout, \ + SessionError, Timeout, \ AuthenticationError -from pssh.clients.ssh.single import SSHClient, logger as ssh_logger - from .base_ssh_case import SSHTestCase -from ..embedded_server.openssh import OpenSSHServer ssh_logger.setLevel(logging.DEBUG) logging.basicConfig() @@ -43,16 +39,6 @@ def test_context_manager(self): num_retries=1) as client: self.assertIsInstance(client, SSHClient) - def test_open_session_timeout(self): - client = SSHClient(self.host, port=self.port, - pkey=self.user_key, - num_retries=1, - timeout=1) - def _session(timeout=2): - sleep(2) - client.open_session = _session - self.assertRaises(GTimeout, client.run_command, self.cmd) - def test_pkey_from_memory(self): with open(self.user_key, 'rb') as fh: key_data = fh.read() @@ -159,13 +145,6 @@ def test_client_bad_sock(self): client.sock = None self.assertIsNone(client.poll()) - def test_client_read_timeout(self): - client = SSHClient(self.host, port=self.port, - pkey=self.user_key, - num_retries=1) - host_out = client.run_command('sleep 2; echo me', timeout=0.2) - self.assertRaises(Timeout, list, host_out.stdout) - def test_multiple_clients_exec_terminates_channels(self): # See #200 - Multiple clients should not interfere with # each other. session.disconnect can leave state in library @@ -249,19 +228,12 @@ def test_open_session_timeout(self): pkey=self.user_key, num_retries=2, timeout=.1) + def _session(timeout=None): sleep(.2) client.open_session = _session self.assertRaises(GTimeout, client.run_command, self.cmd) - def test_connection_timeout(self): - cmd = spawn(SSHClient, 'fakehost.com', port=12345, - retry_delay=.1, - num_retries=2, timeout=.2, _auth_thread_pool=False) - # Should fail within greenlet timeout, otherwise greenlet will - # raise timeout which will fail the test - self.assertRaises(ConnectionErrorException, cmd.get, timeout=5) - def test_client_read_timeout(self): client = SSHClient(self.host, port=self.port, pkey=self.user_key, diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 7562cd6e..3b95c7ed 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/tests/test_host_config.py b/tests/test_host_config.py index 0b3731f6..0bdf863c 100644 --- a/tests/test_host_config.py +++ b/tests/test_host_config.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -35,11 +35,26 @@ def test_host_config_entries(self): proxy_host = 'proxy_host' keepalive_seconds = 1 ipv6_only = True + cert_file = 'file' + auth_thread_pool = True + forward_ssh_agent = False + gssapi_auth = True + gssapi_server_identity = 'some_id' + gssapi_client_identity = 'some_id' + gssapi_delegate_credentials = True cfg = HostConfig( user=user, port=port, password=password, private_key=private_key, allow_agent=allow_agent, num_retries=num_retries, retry_delay=retry_delay, timeout=timeout, identity_auth=identity_auth, proxy_host=proxy_host, ipv6_only=ipv6_only, + keepalive_seconds=keepalive_seconds, + cert_file=cert_file, + auth_thread_pool=auth_thread_pool, + forward_ssh_agent=forward_ssh_agent, + gssapi_auth=gssapi_auth, + gssapi_server_identity=gssapi_server_identity, + gssapi_client_identity=gssapi_client_identity, + gssapi_delegate_credentials=gssapi_delegate_credentials, ) self.assertEqual(cfg.user, user) self.assertEqual(cfg.port, port) @@ -52,6 +67,13 @@ def test_host_config_entries(self): self.assertEqual(cfg.identity_auth, identity_auth) self.assertEqual(cfg.proxy_host, proxy_host) self.assertEqual(cfg.ipv6_only, ipv6_only) + self.assertEqual(cfg.keepalive_seconds, keepalive_seconds) + self.assertEqual(cfg.cert_file, cert_file) + self.assertEqual(cfg.forward_ssh_agent, forward_ssh_agent) + self.assertEqual(cfg.gssapi_auth, gssapi_auth) + self.assertEqual(cfg.gssapi_server_identity, gssapi_server_identity) + self.assertEqual(cfg.gssapi_client_identity, gssapi_client_identity) + self.assertEqual(cfg.gssapi_delegate_credentials, gssapi_delegate_credentials) def test_host_config_bad_entries(self): self.assertRaises(ValueError, HostConfig, user=22) @@ -70,3 +92,10 @@ def test_host_config_bad_entries(self): self.assertRaises(ValueError, HostConfig, proxy_pkey=1) self.assertRaises(ValueError, HostConfig, keepalive_seconds='') self.assertRaises(ValueError, HostConfig, ipv6_only='') + self.assertRaises(ValueError, HostConfig, keepalive_seconds='') + self.assertRaises(ValueError, HostConfig, cert_file=1) + self.assertRaises(ValueError, HostConfig, forward_ssh_agent='') + self.assertRaises(ValueError, HostConfig, gssapi_auth='') + self.assertRaises(ValueError, HostConfig, gssapi_server_identity=1) + self.assertRaises(ValueError, HostConfig, gssapi_client_identity=1) + self.assertRaises(ValueError, HostConfig, gssapi_delegate_credentials='') diff --git a/tests/test_output.py b/tests/test_output.py index acd00875..15ad3dd6 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/tests/test_reader.py b/tests/test_reader.py index 4bf9230c..b353f80c 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public diff --git a/tests/test_utils.py b/tests/test_utils.py index f7ee9bc7..1346e28b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ # This file is part of parallel-ssh. # -# Copyright (C) 2014-2020 Panos Kittenis +# Copyright (C) 2014-2022 Panos Kittenis and contributors. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public