Skip to content

Commit

Permalink
Host err (#323)
Browse files Browse the repository at this point in the history
* Always use host name in HostOutput when stop errors is False.
* Updated exception host handling, tests
* Updated readme
* Updated documentation, minor updates
* Updated tests, tunnel func
  • Loading branch information
pkittenis authored Oct 31, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent dbf6573 commit a98114e
Showing 12 changed files with 42 additions and 80 deletions.
6 changes: 6 additions & 0 deletions Changelog.rst
Original file line number Diff line number Diff line change
@@ -11,6 +11,12 @@ Changes
* Added ``ipv6_only`` flag to ``ParallelSSHClient`` and ``SSHClient`` for choosing only IPv6 addresses when both v4 and
v6 are available.
* Removed Python 2 from binary wheel compatibility as it is no longer supported and not guaranteed to work.
* Host name is now an argument for all exceptions raised by single clients.

Fixes
-----

* ``HostOutput`` would have empty host on some exceptions when ``stop_on_errors`` is ``False`` - #297

2.6.0
+++++
33 changes: 11 additions & 22 deletions pssh/clients/base/parallel.py
Original file line number Diff line number Diff line change
@@ -105,7 +105,6 @@ def _open_shell(self, host_i, host,
encoding=encoding, read_timeout=read_timeout)
return shell
except (GTimeout, Exception) as ex:
host = ex.host if hasattr(ex, 'host') else None
logger.error("Failed to run on host %s - %s", host, ex)
raise ex

@@ -199,17 +198,17 @@ def run_command(self, command, user=None, stop_on_errors=True,
return self._get_output_from_cmds(cmds, raise_error=stop_on_errors)

def _get_output_from_cmds(self, cmds, raise_error=False):
_cmds = [spawn(self._get_output_from_greenlet, cmd, raise_error=raise_error)
for cmd in cmds]
_cmds = [spawn(self._get_output_from_greenlet, cmd_i, cmd, raise_error=raise_error)
for cmd_i, cmd in enumerate(cmds)]
finished = joinall(_cmds, raise_error=True)
return [f.get() for f in finished]

def _get_output_from_greenlet(self, cmd, raise_error=False):
def _get_output_from_greenlet(self, cmd_i, cmd, raise_error=False):
host = self.hosts[cmd_i]
try:
host_out = cmd.get()
return host_out
except (GTimeout, Exception) as ex:
host = ex.host if hasattr(ex, 'host') else None
if isinstance(ex, GTimeout):
ex = Timeout()
if raise_error:
@@ -266,7 +265,6 @@ def _run_command(self, host_i, host, command, sudo=False, user=None,
use_pty=use_pty, encoding=encoding, read_timeout=read_timeout)
return host_out
except (GTimeout, Exception) as ex:
host = ex.host if hasattr(ex, 'host') else None
logger.error("Failed to run on host %s - %s", host, ex)
raise ex

@@ -312,7 +310,7 @@ def join(self, output=None, consume_output=False, timeout=None):
self.pool.
Since self.timeout is passed onto each individual SSH session it is
**not** used for any parallel functions like `run_command` or `join`.
:type timeout: int
:type timeout: float
:raises: :py:class:`pssh.exceptions.Timeout` on timeout requested and
reached with commands still running.
@@ -431,13 +429,9 @@ 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"""
try:
self._make_ssh_client(host_i, host)
return self._host_clients[(host_i, host)].copy_file(
local_file, remote_file, recurse=recurse)
except Exception as ex:
ex.host = host
raise ex
client = self._make_ssh_client(host_i, host)
return client.copy_file(
local_file, remote_file, recurse=recurse)

def copy_remote_file(self, remote_file, local_file, recurse=False,
suffix_separator='_', copy_args=None, **kwargs):
@@ -518,19 +512,14 @@ 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"""
try:
self._make_ssh_client(host_i, host)
return self._host_clients[(host_i, host)].copy_remote_file(
remote_file, local_file, recurse=recurse, **kwargs)
except Exception as ex:
ex.host = host
raise ex
client = self._make_ssh_client(host_i, host)
return client.copy_remote_file(
remote_file, local_file, recurse=recurse, **kwargs)

def _handle_greenlet_exc(self, func, host, *args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as ex:
ex.host = host
raise ex

def _make_ssh_client(self, host_i, host):
2 changes: 0 additions & 2 deletions pssh/clients/base/single.py
Original file line number Diff line number Diff line change
@@ -299,8 +299,6 @@ def _connect(self, host, port, retries=1):
"Error connecting to host '%s:%s' - %s - retry %s/%s",
host, port, str(error_type), retries,
self.num_retries,)
ex.host = host
ex.port = port
raise ex

def _identity_auth(self):
1 change: 0 additions & 1 deletion pssh/clients/common.py
Original file line number Diff line number Diff line change
@@ -29,6 +29,5 @@ def _validate_pkey_path(pkey, host=None):
"Please use either absolute or relative to user directory " \
"paths like '~/.ssh/my_key' for pkey parameter"
ex = PKeyFileError(msg, pkey)
ex.host = host
raise ex
return pkey
2 changes: 1 addition & 1 deletion pssh/clients/native/parallel.py
Original file line number Diff line number Diff line change
@@ -82,7 +82,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
:type pool_size: int
:param host_config: (Optional) Per-host configuration for cases where
not all hosts use the same configuration.
:type host_config: list
:type host_config: list(:py:class:`pssh.config.HostConfig`)
:param allow_agent: (Optional) set to False to disable connecting to
the system's SSH agent.
:type allow_agent: bool
8 changes: 3 additions & 5 deletions pssh/clients/native/single.py
Original file line number Diff line number Diff line change
@@ -225,8 +225,6 @@ def _init_session(self, retries=1):
logger.error(msg, self.host, self.port, ex)
if isinstance(ex, SSH2Timeout):
raise Timeout(msg, self.host, self.port, ex)
ex.host = self.host
ex.port = self.port
raise

def _keepalive(self):
@@ -483,9 +481,9 @@ def copy_remote_file(self, remote_file, local_file, recurse=False,
try:
self._eagain(sftp.stat, remote_file)
except (SFTPHandleError, SFTPProtocolError):
msg = "Remote file or directory %s does not exist"
logger.error(msg, remote_file)
raise SFTPIOError(msg, remote_file)
msg = "Remote file or directory %s on host %s does not exist"
logger.error(msg, remote_file, self.host)
raise SFTPIOError(msg, remote_file, self.host)
try:
dir_h = self._sftp_openfh(sftp.opendir, remote_file)
except SFTPError:
8 changes: 4 additions & 4 deletions pssh/clients/native/tunnel.py
Original file line number Diff line number Diff line change
@@ -112,8 +112,7 @@ def run(self):
continue
self._start_server()
except Exception:
logger.error("Tunnel thread caught exception and will exit:",
exc_info=1)
logger.exception("Tunnel thread caught exception and will exit:")
self.shutdown()

def cleanup_server(self, client):
@@ -141,6 +140,7 @@ def __init__(self, client, host, port, bind_address='127.0.0.1',
self._client = client
self._retries = num_retries
self.bind_address = bind_address
self.exception = None

@property
def listen_port(self):
@@ -164,9 +164,9 @@ def _read_rw(self, socket, address):
logger.debug("Waiting for read/write greenlets")
self._source_let = source
self._dest_let = dest
self._wait_send_receive_lets(source, dest, channel, socket)
self._wait_send_receive_lets(source, dest, channel)

def _wait_send_receive_lets(self, source, dest, channel, forward_sock):
def _wait_send_receive_lets(self, source, dest, channel):
try:
joinall((source, dest), raise_error=True)
finally:
2 changes: 1 addition & 1 deletion pssh/clients/ssh/parallel.py
Original file line number Diff line number Diff line change
@@ -89,7 +89,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
:type pool_size: int
:param host_config: (Optional) Per-host configuration for cases where
not all hosts use the same configuration.
:type host_config: dict
:type host_config: list(:py:class:`pssh.config.HostConfig`)
:param allow_agent: (Optional) set to False to disable connecting to
the system's SSH agent. Currently unused - always off.
:type allow_agent: bool
2 changes: 0 additions & 2 deletions pssh/clients/ssh/single.py
Original file line number Diff line number Diff line change
@@ -157,8 +157,6 @@ def _init_session(self, retries=1):
return self._connect_init_session_retry(retries=retries+1)
msg = "Error connecting to host %s:%s - %s"
logger.error(msg, self.host, self.port, ex)
ex.host = self.host
ex.port = self.port
raise ex

def _session_connect(self):
31 changes: 9 additions & 22 deletions tests/native/test_parallel_client.py
Original file line number Diff line number Diff line change
@@ -270,7 +270,7 @@ def test_pssh_client_hosts_list_part_failure(self):
self.assertTrue(client.finished(output))
self.assertEqual(len(hosts), len(output))
self.assertIsNotNone(output[1].exception)
self.assertEqual(output[1].exception.host, hosts[1])
self.assertEqual(output[1].exception.args[1], hosts[1])
self.assertIsInstance(output[1].exception, ConnectionErrorException)

def test_pssh_client_timeout(self):
@@ -374,7 +374,7 @@ def test_sftp_exceptions(self):
try:
cmd.get()
except Exception as ex:
self.assertEqual(ex.host, self.host)
self.assertEqual(ex.args[1], self.host)
self.assertIsInstance(ex, ConnectionErrorException)
else:
raise Exception("Expected ConnectionErrorException, got none")
@@ -582,7 +582,7 @@ def test_pssh_copy_remote_file_failure(self):
try:
cmds[0].get()
except Exception as ex:
self.assertEqual(ex.host, self.host)
self.assertEqual(ex.args[2], self.host)
self.assertIsInstance(ex, SFTPIOError)
else:
raise Exception("Expected SFTPIOError, got none")
@@ -871,12 +871,8 @@ def test_connection_error_exception(self):
try:
raise output[0].exception
except ConnectionErrorException as ex:
self.assertEqual(ex.host, host,
msg="Exception host argument is %s, should be %s" % (
ex.host, host,))
self.assertEqual(ex.args[2], port,
msg="Exception port argument is %s, should be %s" % (
ex.args[2], port,))
self.assertEqual(ex.args[1], host)
self.assertEqual(ex.args[2], port)
else:
raise Exception("Expected ConnectionErrorException")

@@ -950,7 +946,7 @@ def test_host_config(self):
try:
raise output[1].exception
except PKeyFileError as ex:
self.assertEqual(ex.host, host)
self.assertEqual(output[1].host, hosts[1][0])
else:
raise AssertionError("Expected ValueError on host %s",
hosts[0][0])
@@ -988,7 +984,7 @@ def test_host_config_list_type(self):
try:
raise output[1].exception
except PKeyFileError as ex:
self.assertEqual(ex.host, host)
self.assertEqual(output[1].host, hosts[1][0])
else:
raise AssertionError("Expected ValueError on host %s",
hosts[0][0])
@@ -1293,15 +1289,6 @@ def test_unknown_host_failure(self):
num_retries=1)
self.assertRaises(UnknownHostException, client.run_command, self.cmd)

def test_open_channel_failure(self):
client = ParallelSSHClient([self.host], port=self.port,
pkey=self.user_key)
output = client.run_command(self.cmd)
client.join(output)
output[0].client.session.disconnect()
self.assertRaises(SessionError, output[0].client.open_session)
self.assertEqual(output[0].exit_code, 0)

def test_invalid_host_out(self):
output = {'blah': None}
self.assertRaises(ValueError, self.client.join, output)
@@ -1621,7 +1608,7 @@ def test_scp_recv_failure(self):
try:
joinall(cmds, raise_error=True)
except Exception as ex:
self.assertEqual(ex.host, self.host)
self.assertEqual(ex.args[2], self.host)
self.assertIsInstance(ex, SCPError)
else:
raise Exception("Expected SCPError, got none")
@@ -1912,7 +1899,7 @@ def test_no_ipv6(self):
client = ParallelSSHClient([self.host], port=self.port, pkey=self.user_key, num_retries=1, ipv6_only=True)
output = client.run_command(self.cmd, stop_on_errors=False)
for host_out in output:
# self.assertEqual(self.host, host_out.host)
self.assertEqual(self.host, host_out.host)
self.assertIsInstance(host_out.exception, NoIPv6AddressFoundError)

# TODO:
17 changes: 4 additions & 13 deletions tests/native/test_tunnel.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is part of parallel-ssh.
#
# Copyright (C) 2014-2020 Panos Kittenis
# Copyright (C) 2014-2021 Panos Kittenis
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -18,27 +18,18 @@
import unittest
import pwd
import os
import shutil
import sys
import string
import random
import time
import gc

from datetime import datetime
from socket import timeout as socket_timeout
from sys import version_info
from collections import deque
from gevent import sleep, spawn, Timeout as GTimeout

from pssh.config import HostConfig
from pssh.clients.native import SSHClient, ParallelSSHClient
from pssh.clients.native.tunnel import LocalForwarder, TunnelServer, FORWARDER
from pssh.exceptions import UnknownHostException, \
AuthenticationException, ConnectionErrorException, SessionError, \
HostArgumentException, SFTPError, SFTPIOError, Timeout, SCPError, \
ProxyError
from ssh2.exceptions import ChannelFailure, SocketSendError, SocketRecvError
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
@@ -350,7 +341,7 @@ def close(self):
source_let = spawn(server._read_forward_sock, _socket, channel)
dest_let = spawn(server._read_channel, _socket, channel)
channel._eof = True
self.assertIsNone(server._wait_send_receive_lets(source_let, dest_let, channel, _socket))
self.assertIsNone(server._wait_send_receive_lets(source_let, dest_let, channel))
let.kill()

def test_server_start(self):
10 changes: 3 additions & 7 deletions tests/ssh/test_parallel_client.py
Original file line number Diff line number Diff line change
@@ -232,7 +232,7 @@ def test_pssh_client_hosts_list_part_failure(self):
self.assertIsNotNone(output[1].exception,
msg="Failed host %s has no exception in output - %s" % (hosts[1], output,))
self.assertTrue(output[1].exception is not None)
self.assertEqual(output[1].exception.host, hosts[1])
self.assertEqual(output[1].exception.args[1], hosts[1])
try:
raise output[1].exception
except ConnectionErrorException:
@@ -316,12 +316,8 @@ def test_connection_error_exception(self):
try:
raise output[0].exception
except ConnectionErrorException as ex:
self.assertEqual(ex.host, host,
msg="Exception host argument is %s, should be %s" % (
ex.host, host,))
self.assertEqual(ex.args[2], port,
msg="Exception port argument is %s, should be %s" % (
ex.args[2], port,))
self.assertEqual(ex.args[1], host)
self.assertEqual(ex.args[2], port)
else:
raise Exception("Expected ConnectionErrorException")

0 comments on commit a98114e

Please sign in to comment.