Skip to content

Commit 5f107c0

Browse files
espositocloudAntonio Esposito
authored andcommitted
Fix #8 Add basic stdin support to Subprocess and Paramiko
1 parent 2882885 commit 5f107c0

File tree

9 files changed

+247
-13
lines changed

9 files changed

+247
-13
lines changed

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ Execution result object has a set of useful properties:
181181

182182
* `cmd` - Command
183183
* `exit_code` - Command return code. If possible to decode using enumerators for Linux -> it used.
184+
* `stdin` -> `str`. Text representation of stdin.
184185
* `stdout` -> `typing.Tuple[bytes]`. Raw stdout output.
185186
* `stderr` -> `typing.Tuple[bytes]`. Raw stderr output.
186187
* `stdout_bin` -> `bytearray`. Binary stdout output.

doc/source/ExecResult.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ API: ExecResult
1010
1111
Command execution result.
1212

13-
.. py:method:: __init__(cmd, stdout=None, stderr=None, exit_code=ExitCodes.EX_INVALID)
13+
.. py:method:: __init__(cmd, stdin=None, stdout=None, stderr=None, exit_code=ExitCodes.EX_INVALID)
1414
1515
:param cmd: command
1616
:type cmd: ``str``
17+
:param stdin: STDIN
18+
:type stdin: ``typing.Optional[str]``
1719
:param stdout: binary STDOUT
1820
:type stdout: ``typing.Optional[typing.Iterable[bytes]]``
1921
:param stderr: binary STDERR
@@ -36,6 +38,11 @@ API: ExecResult
3638
``str``
3739
Command
3840

41+
.. py:attribute:: stdin
42+
43+
``str``
44+
Stdin input as string.
45+
3946
.. py:attribute:: stdout
4047
4148
``typing.Tuple[bytes]``

doc/source/SSHClient.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,24 @@ API: SSHClient and SSHAuth.
101101
:param enforce: Enforce sudo enabled or disabled. By default: None
102102
:type enforce: ``typing.Optional[bool]``
103103

104-
.. py:method:: execute_async(command, get_pty=False, open_stdout=True, open_stderr=True, **kwargs)
104+
.. py:method:: execute_async(command, get_pty=False, open_stdout=True, open_stderr=True, stdin=None, **kwargs)
105105
106106
Execute command in async mode and return channel with IO objects.
107107

108108
:param command: Command for execution
109109
:type command: ``str``
110110
:param get_pty: open PTY on remote machine
111111
:type get_pty: ``bool``
112+
:param stdin: pass STDIN text to the process
113+
:type stdin: ``typing.Union[six.text_type, six.binary_type, None]``
112114
:param open_stdout: open STDOUT stream for read
113115
:type open_stdout: bool
114116
:param open_stderr: open STDERR stream for read
115117
:type open_stderr: bool
116118
:rtype: ``typing.Tuple[paramiko.Channel, paramiko.ChannelFile, paramiko.ChannelFile, paramiko.ChannelFile]``
117119

118120
.. versionchanged:: 1.2.0 open_stdout and open_stderr flags
121+
.. versionchanged:: 1.2.0 stdin data
119122

120123
.. py:method:: execute(command, verbose=False, timeout=1*60*60, **kwargs)
121124

doc/source/Subprocess.rst

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,20 @@ API: Subprocess
4545

4646
:param command: Command for execution
4747
:type command: ``str``
48+
:param stdin: STDIN passed to execution
49+
:type stdin: ``typing.Union[six.text_type, six.binary_type, None]``
4850
:param verbose: Produce log.info records for command call and output
4951
:type verbose: ``bool``
5052
:param timeout: Timeout for command execution.
5153
:type timeout: ``typing.Optional[int]``
5254
:rtype: ExecResult
5355
:raises ExecHelperTimeoutError: Timeout exceeded
5456

57+
.. note:: stdin channel is closed after the input processing
5558
.. versionchanged:: 1.1.0 make method
56-
.. versionchanged:: 1.2.0
57-
58-
open_stdout and open_stderr flags
59-
default timeout 1 hour
59+
.. versionchanged:: 1.2.0 open_stdout and open_stderr flags
60+
.. versionchanged:: 1.2.0 default timeout 1 hour
61+
.. versionchanged:: 1.2.0 stdin data
6062

6163
.. py:method:: check_call(command, verbose=False, timeout=1*60*60, error_info=None, expected=None, raise_on_err=True, **kwargs)
6264

exec_helpers/_ssh_client_base.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ def execute_async(
489489
self,
490490
command, # type: str
491491
get_pty=False, # type: bool
492+
stdin=None, # type: typing.Union[six.text_type, six.binary_type, None]
492493
open_stdout=True, # type: bool
493494
open_stderr=True, # type: bool
494495
**kwargs
@@ -499,6 +500,8 @@ def execute_async(
499500
:type command: str
500501
:param get_pty: open PTY on remote machine
501502
:type get_pty: bool
503+
:param stdin: pass STDIN text to the process
504+
:type stdin: typing.Union[six.text_type, six.binary_type, None]
502505
:param open_stdout: open STDOUT stream for read
503506
:type open_stdout: bool
504507
:param open_stderr: open STDERR stream for read
@@ -511,6 +514,7 @@ def execute_async(
511514
]
512515
513516
.. versionchanged:: 1.2.0 open_stdout and open_stderr flags
517+
.. versionchanged:: 1.2.0 stdin data
514518
"""
515519
cmd_for_log = self._mask_command(
516520
cmd=command,
@@ -532,7 +536,7 @@ def execute_async(
532536
width_pixels=0, height_pixels=0
533537
)
534538

535-
stdin = chan.makefile('wb')
539+
_stdin = chan.makefile('wb')
536540
stdout = chan.makefile('rb') if open_stdout else None
537541
stderr = chan.makefile_stderr('rb') if open_stderr else None
538542
cmd = "{command}\n".format(command=command)
@@ -545,11 +549,17 @@ def execute_async(
545549
)
546550
chan.exec_command(cmd) # nosec # Sanitize on caller side
547551
if stdout.channel.closed is False:
548-
self.auth.enter_password(stdin)
549-
stdin.flush()
552+
self.auth.enter_password(_stdin)
553+
_stdin.flush()
550554
else:
551555
chan.exec_command(cmd) # nosec # Sanitize on caller side
552-
return chan, stdin, stderr, stdout
556+
if stdin is not None:
557+
if not isinstance(stdin, six.binary_type):
558+
stdin = stdin.encode(encoding='utf-8')
559+
_stdin.write('{}\n'.format(stdin))
560+
_stdin.flush()
561+
562+
return chan, _stdin, stderr, stdout
553563

554564
def __exec_command(
555565
self,

exec_helpers/exec_result.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class ExecResult(object):
4242
"""Execution result."""
4343

4444
__slots__ = [
45-
'__cmd', '__stdout', '__stderr', '__exit_code',
45+
'__cmd', '__stdin', '__stdout', '__stderr', '__exit_code',
4646
'__timestamp',
4747
'__stdout_str', '__stderr_str', '__stdout_brief', '__stderr_brief',
4848
'__lock'
@@ -51,6 +51,7 @@ class ExecResult(object):
5151
def __init__(
5252
self,
5353
cmd, # type: str
54+
stdin=None, # type: typing.Union[six.text_type, six.binary_type, None]
5455
stdout=None, # type: typing.Optional[typing.Iterable[bytes]]
5556
stderr=None, # type: typing.Optional[typing.Iterable[bytes]]
5657
exit_code=proc_enums.ExitCodes.EX_INVALID # type: _type_exit_codes
@@ -59,6 +60,8 @@ def __init__(
5960
6061
:param cmd: command
6162
:type cmd: str
63+
:param stdin: string STDIN
64+
:type stdin: typing.Union[six.text_type, six.binary_type, None]
6265
:param stdout: binary STDOUT
6366
:type stdout: typing.Optional[typing.Iterable[bytes]]
6467
:param stderr: binary STDERR
@@ -69,6 +72,9 @@ def __init__(
6972
self.__lock = threading.RLock()
7073

7174
self.__cmd = cmd
75+
if stdin is not None and not isinstance(stdin, six.text_type):
76+
stdin = self._get_str_from_bin(stdin)
77+
self.__stdin = stdin
7278
self.__stdout = tuple(stdout) if stdout is not None else ()
7379
self.__stderr = tuple(stderr) if stderr is not None else ()
7480

@@ -141,6 +147,14 @@ def cmd(self): # type: () -> str
141147
"""
142148
return self.__cmd
143149

150+
@property
151+
def stdin(self): # type: () -> str
152+
"""Stdin input as string.
153+
154+
:rtype: str
155+
"""
156+
return self.__stdin
157+
144158
@property
145159
def stdout(self): # type: () -> typing.Tuple[bytes]
146160
"""Stdout output as list of binaries.

exec_helpers/subprocess_runner.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def __exec_command(
169169
timeout=constants.DEFAULT_TIMEOUT, # type: typing.Optional[int]
170170
verbose=False, # type: bool
171171
log_mask_re=None, # type: typing.Optional[str]
172+
stdin=None, # type: typing.Union[six.text_type, six.binary_type, None]
172173
open_stdout=True, # type: bool
173174
open_stderr=True, # type: bool
174175
):
@@ -183,6 +184,7 @@ def __exec_command(
183184
:param log_mask_re: regex lookup rule to mask command for logger.
184185
all MATCHED groups will be replaced by '<*masked*>'
185186
:type log_mask_re: typing.Optional[str]
187+
:type stdin: typing.Union[six.text_type, six.binary_type, None]
186188
:param open_stdout: open STDOUT stream for read
187189
:type open_stdout: bool
188190
:param open_stderr: open STDERR stream for read
@@ -277,12 +279,19 @@ def poll_pipes(
277279
# Run
278280
self.__process = subprocess.Popen(
279281
args=[command],
280-
stdin=subprocess.PIPE,
281282
stdout=subprocess.PIPE if open_stdout else devnull,
282283
stderr=subprocess.PIPE if open_stderr else devnull,
283-
shell=True, cwd=cwd, env=env,
284+
stdin=subprocess.PIPE,
285+
shell=True,
286+
cwd=cwd,
287+
env=env,
284288
universal_newlines=False,
285289
)
290+
if stdin is not None:
291+
if not isinstance(stdin, six.binary_type):
292+
stdin = stdin.encode(encoding='utf-8')
293+
self.__process.stdin.write(stdin)
294+
self.__process.stdin.close()
286295

287296
# Poll output
288297

test/test_ssh_client.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def __iter__(self):
6161
encoded_cmd = base64.b64encode(
6262
"{}\n".format(command).encode('utf-8')
6363
).decode('utf-8')
64+
print_stdin = 'read line; echo "$line"'
6465

6566

6667
@mock.patch('exec_helpers._ssh_client_base.logger', autospec=True)
@@ -1046,6 +1047,99 @@ def test_check_stderr(self, check_call, client, policy, logger):
10461047
command, verbose, timeout=None,
10471048
error_info=None, raise_on_err=raise_on_err)
10481049

1050+
@mock.patch('exec_helpers.ssh_client.SSHClient.check_call')
1051+
def test_check_stdin_str(self, check_call, client, policy, logger):
1052+
stdin = u'this is a line'
1053+
1054+
return_value = exec_result.ExecResult(
1055+
cmd=print_stdin,
1056+
stdin=stdin,
1057+
stdout=[stdin],
1058+
stderr=[],
1059+
exit_code=0
1060+
)
1061+
check_call.return_value = return_value
1062+
1063+
verbose = False
1064+
raise_on_err = True
1065+
1066+
# noinspection PyTypeChecker
1067+
result = self.get_ssh().check_call(
1068+
command=print_stdin,
1069+
stdin=stdin,
1070+
verbose=verbose,
1071+
timeout=None,
1072+
raise_on_err=raise_on_err)
1073+
check_call.assert_called_once_with(
1074+
command=print_stdin,
1075+
stdin=stdin,
1076+
verbose=verbose,
1077+
timeout=None,
1078+
raise_on_err=raise_on_err)
1079+
self.assertEqual(result, return_value)
1080+
1081+
@mock.patch('exec_helpers.ssh_client.SSHClient.check_call')
1082+
def test_check_stdin_bytes(self, check_call, client, policy, logger):
1083+
stdin = b'this is a line'
1084+
1085+
return_value = exec_result.ExecResult(
1086+
cmd=print_stdin,
1087+
stdin=stdin,
1088+
stdout=[stdin],
1089+
stderr=[],
1090+
exit_code=0
1091+
)
1092+
check_call.return_value = return_value
1093+
1094+
verbose = False
1095+
raise_on_err = True
1096+
1097+
# noinspection PyTypeChecker
1098+
result = self.get_ssh().check_call(
1099+
command=print_stdin,
1100+
stdin=stdin,
1101+
verbose=verbose,
1102+
timeout=None,
1103+
raise_on_err=raise_on_err)
1104+
check_call.assert_called_once_with(
1105+
command=print_stdin,
1106+
stdin=stdin,
1107+
verbose=verbose,
1108+
timeout=None,
1109+
raise_on_err=raise_on_err)
1110+
self.assertEqual(result, return_value)
1111+
1112+
@mock.patch('exec_helpers.ssh_client.SSHClient.check_call')
1113+
def test_check_stdin_bytearray(self, check_call, client, policy, logger):
1114+
stdin = bytearray(b'this is a line')
1115+
1116+
return_value = exec_result.ExecResult(
1117+
cmd=print_stdin,
1118+
stdin=stdin,
1119+
stdout=[stdin],
1120+
stderr=[],
1121+
exit_code=0
1122+
)
1123+
check_call.return_value = return_value
1124+
1125+
verbose = False
1126+
raise_on_err = True
1127+
1128+
# noinspection PyTypeChecker
1129+
result = self.get_ssh().check_call(
1130+
command=print_stdin,
1131+
stdin=stdin,
1132+
verbose=verbose,
1133+
timeout=None,
1134+
raise_on_err=raise_on_err)
1135+
check_call.assert_called_once_with(
1136+
command=print_stdin,
1137+
stdin=stdin,
1138+
verbose=verbose,
1139+
timeout=None,
1140+
raise_on_err=raise_on_err)
1141+
self.assertEqual(result, return_value)
1142+
10491143

10501144
@mock.patch('exec_helpers._ssh_client_base.logger', autospec=True)
10511145
@mock.patch(

0 commit comments

Comments
 (0)