diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 891ef877..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -repos: - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 - hooks: - - id: flake8 - exclude: ^[tests,doc,versioneer] diff --git a/Changelog.rst b/Changelog.rst index a73ab068..1eb4cca4 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -1,6 +1,22 @@ Change Log ========== +2.15.0 +++++++ + +Changes +------- + +* Added ``HostOutput.cmd`` which has the fully qualified command executed on the host if a command was run + via ``run_command``. + + +Fixes +------ + +* Forwarder thread would be joined at interpreter shutdown even if it was not ever started. + + 2.14.0 ++++++ @@ -25,7 +41,7 @@ Fixes * Forwarder threads used for proxies would not exit gracefully at interpreter shutdown, sometimes causing segfaults. * Client, both parallel and single, going out of scope would cause reading output from existing output objects - to break. + to break - #274. * Explicitly calling ``SSHClient.disconnect`` would sometimes cause segfaults at interpreter shutdown. * Keepalives being configured on native client would keep client in scope forever. diff --git a/ci/integration_tests/libssh2_clients/test_parallel_client.py b/ci/integration_tests/libssh2_clients/test_parallel_client.py index 5702bc2c..db9eb6db 100644 --- a/ci/integration_tests/libssh2_clients/test_parallel_client.py +++ b/ci/integration_tests/libssh2_clients/test_parallel_client.py @@ -1934,5 +1934,17 @@ def test_no_ipv6(self): self.assertEqual(self.host, host_out.host) self.assertIsInstance(host_out.exception, NoIPv6AddressFoundError) + def test_host_cmd_output_per_host_args(self): + client = ParallelSSHClient([self.host, self.host], port=self.port, pkey=self.user_key, num_retries=1) + host_args = ('cmd1', 'cmd2') + cmd = 'echo %s' + output = client.run_command(cmd, host_args=host_args) + expected_cmd_host1 = f"echo {host_args[0]}" + expected_cmd_host2 = f"echo {host_args[1]}" + host1_out = output[0] + host2_out = output[1] + self.assertEqual(host1_out.cmd, expected_cmd_host1) + self.assertEqual(host2_out.cmd, expected_cmd_host2) + # TODO: # * password auth diff --git a/ci/integration_tests/libssh2_clients/test_single_client.py b/ci/integration_tests/libssh2_clients/test_single_client.py index 9b3173a7..6d51252e 100644 --- a/ci/integration_tests/libssh2_clients/test_single_client.py +++ b/ci/integration_tests/libssh2_clients/test_single_client.py @@ -1169,3 +1169,10 @@ def make_client_run(): stdout = list(output.stdout) self.assertListEqual(stdout, [self.resp]) self.assertEqual(output.exit_code, 1) + + def test_output_cmd(self): + output = self.client.run_command(self.cmd, user='my_user', shell='my_shell -c') + self.assertIsInstance(output.cmd, str) + self.assertTrue("sudo -u my_user" in output.cmd) + self.assertTrue("-S my_shell -c" in output.cmd) + self.assertTrue(output.cmd.endswith(f"'{self.cmd}'")) diff --git a/pssh/clients/base/single.py b/pssh/clients/base/single.py index 05140aec..fbcb2ed0 100644 --- a/pssh/clients/base/single.py +++ b/pssh/clients/base/single.py @@ -465,7 +465,7 @@ def _open_session(self): def open_session(self): raise NotImplementedError - def _make_host_output(self, channel, encoding, read_timeout): + def _make_host_output(self, channel, encoding, read_timeout, command): _stdout_buffer = ConcurrentRWBuffer() _stderr_buffer = ConcurrentRWBuffer() _stdout_reader, _stderr_reader = self._make_output_readers( @@ -479,6 +479,7 @@ def _make_host_output(self, channel, encoding, read_timeout): host=self.host, alias=self.alias, channel=channel, stdin=Stdin(channel, self), client=self, encoding=encoding, read_timeout=read_timeout, buffers=_buffers, + cmd=command, ) return host_out @@ -625,11 +626,12 @@ def run_command(self, command, sudo=False, user=None, _command = 'sudo -u %s -S ' % (user,) _shell = shell if shell else '$SHELL -c' _command += "%s '%s'" % (_shell, command,) + _command_str = _command _command = _command.encode(encoding) with GTimeout(seconds=self.timeout): channel = self._execute(_command, use_pty=use_pty) _timeout = read_timeout if read_timeout else timeout - host_out = self._make_host_output(channel, encoding, _timeout) + host_out = self._make_host_output(channel, encoding, _timeout, _command_str) return host_out def _make_sftp(self): diff --git a/pssh/clients/native/tunnel.py b/pssh/clients/native/tunnel.py index 02c41506..906468e2 100644 --- a/pssh/clients/native/tunnel.py +++ b/pssh/clients/native/tunnel.py @@ -96,7 +96,7 @@ def shutdown(self): for client, server in self._servers.items(): server.stop() self._servers = {} - if self.started: + if self.started.is_set(): self.shutdown_triggered.set() try: self.join() diff --git a/pssh/output.py b/pssh/output.py index cd9b043c..8c7f2e66 100644 --- a/pssh/output.py +++ b/pssh/output.py @@ -57,10 +57,16 @@ class HostOutput(object): __slots__ = ('host', 'channel', 'stdin', 'client', 'alias', 'exception', 'encoding', 'read_timeout', 'buffers', + 'cmd', ) def __init__(self, host, channel, stdin, - client, alias=None, exception=None, encoding='utf-8', read_timeout=None, + client, + cmd=None, + alias=None, + exception=None, + encoding='utf-8', + read_timeout=None, buffers=None): """ :param host: Host name output is for @@ -71,6 +77,13 @@ def __init__(self, host, channel, stdin, :type stdin: :py:func:`file`-like object :param client: `SSHClient` output is coming from. :type client: :py:class:`pssh.clients.base.single.BaseSSHClient` or `None`. + :param cmd: The fully qualified command that was run on this host. + This is the generated command run on the host taking into account things like sudo, shell, user and + any per-host command arguments if any were specified. + For example a `run_command(<..>, user='my_user', shell='my_shell -c')` will have a + "sudo -u my_user -S my_shell -c '<..>'" cmd generated. + Only populated if a command was run via `run_command`. + :type cmd: str :param alias: Host alias. :type alias: str :param exception: Exception from host if any @@ -84,6 +97,7 @@ def __init__(self, host, channel, stdin, self.channel = channel self.stdin = stdin self.client = client + self.cmd = cmd self.alias = alias self.exception = exception self.encoding = encoding @@ -125,10 +139,12 @@ def __repr__(self): "\tchannel={channel}{linesep}" \ "\texception={exception}{linesep}" \ "\tencoding={encoding}{linesep}" \ - "\tread_timeout={read_timeout}".format( + "\tread_timeout={read_timeout}{linesep}" \ + "\tcmd={cmd}".format( host=self.host, alias=self.alias, channel=self.channel, exception=self.exception, linesep=linesep, exit_code=self.exit_code, encoding=self.encoding, read_timeout=self.read_timeout, + cmd=self.cmd, ) def __str__(self): diff --git a/requirements_dev.txt b/requirements_dev.txt index 93b64d79..4dbd8b86 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -5,7 +5,6 @@ pytest pytest-cov pytest-rerunfailures jinja2 -pre-commit -r doc/requirements.txt -r requirements.txt -e . diff --git a/tests/test_output.py b/tests/test_output.py index cd083c08..2169017e 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -37,6 +37,7 @@ def setUp(self): def test_print(self): self.assertTrue(str(self.output)) + self.assertTrue("cmd=" in str(self.output)) def test_bad_exit_status(self): self.assertIsNone(self.output.exit_code) @@ -57,7 +58,7 @@ def get_exit_status(self, channel): def test_none_output_client(self): host_out = HostOutput( 'host', None, None, client=None) - exit_code = host_out.exit_code - self.assertEqual(exit_code, None) + self.assertIsNone(host_out.exit_code) self.assertIsNone(host_out.stdout) self.assertIsNone(host_out.stderr) + self.assertIsNone(host_out.cmd)