Skip to content

Commit

Permalink
Merge 1eef3d9 into b3dc162
Browse files Browse the repository at this point in the history
  • Loading branch information
penguinolog committed Dec 13, 2018
2 parents b3dc162 + 1eef3d9 commit 0f29024
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 50 deletions.
10 changes: 7 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Pros:
* Open Source: https://github.com/python-useful-helpers/exec-helpers
* PyPI packaged: https://pypi.python.org/pypi/exec-helpers
* Self-documented code: docstrings with types in comments
* Tested: see bages on top
* Tested: see badges on top
* Support multiple Python versions:

::
Expand All @@ -51,7 +51,7 @@ Pros:
Python 3.7
PyPy3 3.5+

.. note:: For Python 2.7 and PyPy please use versions 1.x.x. For python 3.4 use versions 2.x.x
.. note:: Pythons: For Python 2.7 and PyPy use versions 1.x.x, python 3.4 use versions 2.x.x, python 3.6+ use versions 4+

This package includes:

Expand Down Expand Up @@ -105,7 +105,7 @@ Creation from scratch:
)
Key is a main connection key (always tried first) and keys are alternate keys.
Key filename is afilename or list of filenames with keys, which should be loaded.
Key filename is a filename or list of filenames with keys, which should be loaded.
Passphrase is an alternate password for keys, if it differs from main password.
If main key now correct for username - alternate keys tried, if correct key found - it became main.
If no working key - password is used and None is set as main key.
Expand Down Expand Up @@ -234,6 +234,8 @@ Possible to call commands in parallel on multiple hosts if it's not produce huge
expected=(0,), # type: typing.Iterable[typing.Union[int, ExitCodes]]
raise_on_err=True, # type: bool
# Keyword only:
stdin=None, # type: typing.Union[bytes, str, bytearray, None]
log_mask_re=None, # type: typing.Optional[str]
exception_class=ParallelCallProcessError # typing.Type[ParallelCallProcessError]
)
results # type: typing.Dict[typing.Tuple[str, int], exec_result.ExecResult]
Expand All @@ -253,6 +255,8 @@ For execute through SSH host can be used `execute_through_host` method:
timeout=1 * 60 * 60, # type: type: typing.Union[int, float, None]
verbose=False, # type: bool
# Keyword only:
stdin=None, # type: typing.Union[bytes, str, bytearray, None]
log_mask_re=None, # type: typing.Optional[str]
get_pty=False, # type: bool
width=80, # type: int
height=24 # type: int
Expand Down
12 changes: 2 additions & 10 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,11 @@ environment:

matrix:
- PYTHON: "C:\\Python35"
PYTHON_VERSION: "3.5.x" # currently 3.5.1
PYTHON_VERSION: "3.5.x"
PYTHON_ARCH: "32"

- PYTHON: "C:\\Python35-x64"
PYTHON_VERSION: "3.5.x" # currently 3.5.1
PYTHON_ARCH: "64"

- PYTHON: "C:\\Python36"
PYTHON_VERSION: "3.6.x" # currently 3.6.0
PYTHON_ARCH: "32"

- PYTHON: "C:\\Python36-x64"
PYTHON_VERSION: "3.6.x" # currently 3.6.0
PYTHON_VERSION: "3.5.x"
PYTHON_ARCH: "64"

install:
Expand Down
12 changes: 0 additions & 12 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,6 @@ jobs:
Python35_x86:
python.version: '3.5'
python.architecture: 'x86'
Python36_x64:
python.version: '3.6'
python.architecture: 'x64'
Python36_x86:
python.version: '3.6'
python.architecture: 'x86'
Python37_x64:
python.version: '3.7'
python.architecture: 'x64'
Python37_x86:
python.version: '3.7'
python.architecture: 'x86'

steps:
- task: UsePythonVersion@0
Expand Down
17 changes: 14 additions & 3 deletions doc/source/SSHClient.rst
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ API: SSHClient and SSHAuth.
.. versionchanged:: 1.2.0 default timeout 1 hour
.. versionchanged:: 3.2.0 Exception class can be substituted

.. py:method:: execute_through_host(hostname, command, auth=None, target_port=22, verbose=False, timeout=1*60*60, *, get_pty=False, width=80, height=24, **kwargs)
.. py:method:: execute_through_host(hostname, command, auth=None, target_port=22, verbose=False, timeout=1*60*60, *, stdin=None, log_mask_re="", get_pty=False, width=80, height=24, **kwargs)
Execute command on remote host through currently connected host.

Expand All @@ -249,6 +249,11 @@ API: SSHClient and SSHAuth.
:type verbose: ``bool``
:param timeout: Timeout for command execution.
:type timeout: ``typing.Union[int, float, None]``
:param stdin: pass STDIN text to the process
:type stdin: typing.Union[bytes, str, bytearray, None]
:param log_mask_re: regex lookup rule to mask command for logger.
all MATCHED groups will be replaced by '<*masked*>'
:type log_mask_re: typing.Optional[str]
:param get_pty: open PTY on target machine
:type get_pty: ``bool``
:param width: PTY width
Expand All @@ -261,9 +266,9 @@ API: SSHClient and SSHAuth.
.. versionchanged:: 1.2.0 default timeout 1 hour
.. versionchanged:: 3.2.0 Expose pty options as optional keyword-only arguments
.. versionchanged:: 3.2.0 Exception class can be substituted
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
.. versionchanged:: 3.5.0 Expose stdin and log_mask_re as optional keyword-only arguments

.. py:classmethod:: execute_together(remotes, command, timeout=1*60*60, expected=(0,), raise_on_err=True, *, exception_class=ParallelCallProcessError, **kwargs)
.. py:classmethod:: execute_together(remotes, command, timeout=1*60*60, expected=(0,), raise_on_err=True, *, stdin=None, log_mask_re="", exception_class=ParallelCallProcessError, **kwargs)
Execute command on multiple remotes in async mode.

Expand All @@ -277,6 +282,11 @@ API: SSHClient and SSHAuth.
:type expected: typing.Iterable[typing.Union[int, ExitCodes]]
:param raise_on_err: Raise exception on unexpected return code
:type raise_on_err: ``bool``
:param stdin: pass STDIN text to the process
:type stdin: typing.Union[bytes, str, bytearray, None]
:param log_mask_re: regex lookup rule to mask command for logger.
all MATCHED groups will be replaced by '<*masked*>'
:type log_mask_re: typing.Optional[str]
:param exception_class: Exception to raise on error. Mandatory subclass of ParallelCallProcessError
:type exception_class: typing.Type[ParallelCallProcessError]
:return: dictionary {(hostname, port): result}
Expand All @@ -287,6 +297,7 @@ API: SSHClient and SSHAuth.
.. versionchanged:: 1.2.0 default timeout 1 hour
.. versionchanged:: 3.2.0 Exception class can be substituted
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
.. versionchanged:: 3.5.0 Expose stdin and log_mask_re as optional keyword-only arguments

.. py:method:: open(path, mode='r')
Expand Down
47 changes: 31 additions & 16 deletions exec_helpers/_ssh_client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ def execute_async( # pylint: disable=arguments-differ
chan.get_pty(term="vt100", width=width, height=height, width_pixels=0, height_pixels=0)

_stdin = chan.makefile("wb") # type: paramiko.ChannelFile
stdout = chan.makefile("rb") if open_stdout else None
stdout = chan.makefile("rb") # type: paramiko.ChannelFile
stderr = chan.makefile_stderr("rb") if open_stderr else None

cmd = "{command}\n".format(command=command)
Expand All @@ -634,7 +634,13 @@ def execute_async( # pylint: disable=arguments-differ
else:
self.logger.warning("STDIN Send failed: closed channel")

return SshExecuteAsyncResult(chan, _stdin, stderr, stdout, started)
if open_stdout:
res_stdout = stdout
else:
stdout.close()
res_stdout = None

return SshExecuteAsyncResult(interface=chan, stdin=_stdin, stderr=stderr, stdout=res_stdout, started=started)

def _exec_command( # type: ignore
self,
Expand Down Expand Up @@ -725,10 +731,11 @@ def execute_through_host(
verbose: bool = False,
timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT,
*,
stdin: typing.Union[bytes, str, bytearray, None] = None,
log_mask_re: typing.Optional[str] = None,
get_pty: bool = False,
width: int = 80,
height: int = 24,
**kwargs: typing.Any
height: int = 24
) -> exec_result.ExecResult:
"""Execute command on remote host through currently connected host.
Expand All @@ -744,23 +751,27 @@ def execute_through_host(
:type verbose: bool
:param timeout: Timeout for command execution.
:type timeout: typing.Union[int, float, None]
:param stdin: pass STDIN text to the process
:type stdin: typing.Union[bytes, str, bytearray, None]
:param log_mask_re: regex lookup rule to mask command for logger.
all MATCHED groups will be replaced by '<*masked*>'
:type log_mask_re: typing.Optional[str]
:param get_pty: open PTY on target machine
:type get_pty: bool
:param width: PTY width
:type width: int
:param height: PTY height
:type height: int
:param kwargs: additional parameters for call.
:type kwargs: typing.Any
:return: Execution result
:rtype: ExecResult
:raises ExecHelperTimeoutError: Timeout exceeded
.. versionchanged:: 1.2.0 default timeout 1 hour
.. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd
.. versionchanged:: 3.2.0 Expose pty options as optional keyword-only arguments
.. versionchanged:: 3.5.0 Expose stdin and log_mask_re as optional keyword-only arguments
"""
cmd_for_log = self._mask_command(cmd=command, log_mask_re=kwargs.get("log_mask_re", None))
cmd_for_log = self._mask_command(cmd=command, log_mask_re=log_mask_re)
self.logger.log( # type: ignore
level=logging.INFO if verbose else logging.DEBUG, msg=_log_templates.CMD_EXEC.format(cmd=cmd_for_log)
)
Expand Down Expand Up @@ -791,7 +802,6 @@ def execute_through_host(

channel.exec_command(command) # nosec # Sanitize on caller side

stdin = kwargs.get("stdin", None)
if stdin is not None:
if not _stdin.channel.closed:
stdin_str = self._string_bytes_bytearray_as_bytes(stdin)
Expand All @@ -806,12 +816,7 @@ def execute_through_host(

# noinspection PyDictCreation
result = self._exec_command(
command,
async_result=async_result,
timeout=timeout,
verbose=verbose,
log_mask_re=kwargs.get("log_mask_re", None),
stdin=stdin,
command, async_result=async_result, timeout=timeout, verbose=verbose, log_mask_re=log_mask_re, stdin=stdin
)

intermediate_channel.close()
Expand All @@ -827,6 +832,8 @@ def execute_together(
expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]] = (proc_enums.EXPECTED,),
raise_on_err: bool = True,
*,
stdin: typing.Union[bytes, str, bytearray, None] = None,
log_mask_re: typing.Optional[str] = None,
exception_class: "typing.Type[exceptions.ParallelCallProcessError]" = exceptions.ParallelCallProcessError,
**kwargs: typing.Any
) -> typing.Dict[typing.Tuple[str, int], exec_result.ExecResult]:
Expand All @@ -842,6 +849,11 @@ def execute_together(
:type expected: typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]
:param raise_on_err: Raise exception on unexpected return code
:type raise_on_err: bool
:param stdin: pass STDIN text to the process
:type stdin: typing.Union[bytes, str, bytearray, None]
:param log_mask_re: regex lookup rule to mask command for logger.
all MATCHED groups will be replaced by '<*masked*>'
:type log_mask_re: typing.Optional[str]
:param exception_class: Exception to raise on error. Mandatory subclass of exceptions.ParallelCallProcessError
:type exception_class: typing.Type[exceptions.ParallelCallProcessError]
:param kwargs: additional parameters for execute_async call.
Expand All @@ -855,12 +867,15 @@ def execute_together(
.. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd
.. versionchanged:: 3.2.0 Exception class can be substituted
.. versionchanged:: 3.4.0 Expected is not optional, defaults os dependent
.. versionchanged:: 3.5.0 Expose stdin and log_mask_re as optional keyword-only arguments
"""

@threaded.threadpooled
def get_result(remote: "SSHClientBase") -> exec_result.ExecResult:
"""Get result from remote call."""
async_result = remote.execute_async(command, **kwargs) # type: SshExecuteAsyncResult
async_result = remote.execute_async(
command, stdin=stdin, log_mask_re=log_mask_re, **kwargs
)

async_result.interface.status_event.wait(timeout)
exit_code = async_result.interface.recv_exit_status()
Expand All @@ -869,7 +884,7 @@ def get_result(remote: "SSHClientBase") -> exec_result.ExecResult:
cmd_for_log = remote._mask_command(cmd=command, log_mask_re=kwargs.get("log_mask_re", None))
# pylint: enable=protected-access

res = exec_result.ExecResult(cmd=cmd_for_log, stdin=kwargs.get("stdin", None))
res = exec_result.ExecResult(cmd=cmd_for_log, stdin=stdin)
res.read_stdout(src=async_result.stdout)
res.read_stderr(src=async_result.stderr)
res.exit_code = exit_code
Expand Down
13 changes: 8 additions & 5 deletions test/test_ssh_client_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,10 @@ def test_001_execute_async(ssh, paramiko_ssh_client, ssh_transport_channel, chan
assert isinstance(res, exec_helpers.SshExecuteAsyncResult)
assert res.interface is ssh_transport_channel
assert res.stdin is chan_makefile.stdin
assert res.stdout is chan_makefile.stdout
if open_stdout:
assert res.stdout is chan_makefile.stdout
else:
assert res.stdout is None

paramiko_ssh_client.assert_has_calls(
(
Expand Down Expand Up @@ -481,8 +484,8 @@ def test_009_execute_together(ssh, ssh2, execute_async, exec_result, run_paramet
)
execute_async.assert_has_calls(
(
mock.call(command, stdin=run_parameters.get("stdin", None)),
mock.call(command, stdin=run_parameters.get("stdin", None)),
mock.call(command, stdin=run_parameters.get("stdin", None), log_mask_re=None),
mock.call(command, stdin=run_parameters.get("stdin", None), log_mask_re=None),
)
)
assert results == {(host, port): exec_result, (host2, port): exec_result}
Expand All @@ -503,8 +506,8 @@ def test_010_execute_together_expected(ssh, ssh2, execute_async, exec_result, ru
)
execute_async.assert_has_calls(
(
mock.call(command, stdin=run_parameters.get("stdin", None)),
mock.call(command, stdin=run_parameters.get("stdin", None)),
mock.call(command, stdin=run_parameters.get("stdin", None), log_mask_re=None),
mock.call(command, stdin=run_parameters.get("stdin", None), log_mask_re=None),
)
)
assert results == {(host, port): exec_result, (host2, port): exec_result}
Expand Down
2 changes: 1 addition & 1 deletion tools/build-wheels.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash
PYTHON_VERSIONS="cp35-cp35m cp36-cp36m cp37-cp37m"
PYTHON_VERSIONS="cp35-cp35m"

# Avoid creation of __pycache__/*.py[c|o]
export PYTHONDONTWRITEBYTECODE=1
Expand Down

0 comments on commit 0f29024

Please sign in to comment.