xbot.plugins.ssh 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- demo/lib/__init__.py +0 -0
- demo/lib/testbed.py +48 -0
- demo/lib/testcase.py +15 -0
- demo/testcases/__init__.py +0 -0
- demo/testcases/tc_sftp.py +99 -0
- demo/testcases/tc_ssh.py +73 -0
- tests/__init__.py +0 -0
- tests/run.py +39 -0
- tests/test_sftp.py +88 -0
- tests/test_ssh.py +99 -0
- tests/test_utils.py +18 -0
- venv/bin/jp.py +54 -0
- venv/bin/rst2html.py +23 -0
- venv/bin/rst2html4.py +26 -0
- venv/bin/rst2html5.py +34 -0
- venv/bin/rst2latex.py +26 -0
- venv/bin/rst2man.py +26 -0
- venv/bin/rst2odt.py +30 -0
- venv/bin/rst2odt_prepstyles.py +67 -0
- venv/bin/rst2pseudoxml.py +23 -0
- venv/bin/rst2s5.py +24 -0
- venv/bin/rst2xetex.py +27 -0
- venv/bin/rst2xml.py +23 -0
- venv/bin/rstpep2html.py +25 -0
- xbot/plugins/ssh/__init__.py +0 -0
- xbot/plugins/ssh/errors.py +19 -0
- xbot/plugins/ssh/sftp.py +211 -0
- xbot/plugins/ssh/ssh.py +309 -0
- xbot/plugins/ssh/utils.py +30 -0
- xbot/plugins/ssh/version.py +3 -0
- xbot.plugins.ssh-0.1.0.dist-info/LICENSE +24 -0
- xbot.plugins.ssh-0.1.0.dist-info/METADATA +276 -0
- xbot.plugins.ssh-0.1.0.dist-info/RECORD +35 -0
- xbot.plugins.ssh-0.1.0.dist-info/WHEEL +5 -0
- xbot.plugins.ssh-0.1.0.dist-info/top_level.txt +4 -0
xbot/plugins/ssh/ssh.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Copyright (c) 2023-2024, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
SSH module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import textwrap
|
|
8
|
+
import threading
|
|
9
|
+
import socket
|
|
10
|
+
|
|
11
|
+
from typing import Generator, Optional, Union
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from select import select
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
|
|
16
|
+
from paramiko import SSHClient, AutoAddPolicy
|
|
17
|
+
from paramiko.ssh_exception import (AuthenticationException,
|
|
18
|
+
NoValidConnectionsError,
|
|
19
|
+
SSHException)
|
|
20
|
+
|
|
21
|
+
from xbot.framework.logger import getlogger, ExtraAdapter
|
|
22
|
+
from xbot.plugins.ssh.errors import SSHConnectError, SSHCommandError
|
|
23
|
+
from xbot.plugins.ssh.utils import (remove_ansi_escape_chars,
|
|
24
|
+
remove_unprintable_chars)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = getlogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SSHCommandResult(str):
|
|
31
|
+
"""
|
|
32
|
+
Result of SSH command.
|
|
33
|
+
"""
|
|
34
|
+
def __new__(cls, out: str, rc: int = 0, cmd: str = '') -> str:
|
|
35
|
+
"""
|
|
36
|
+
:param out: output.
|
|
37
|
+
:param rc: return code.
|
|
38
|
+
:param cmd: command.
|
|
39
|
+
"""
|
|
40
|
+
out = remove_ansi_escape_chars(out)
|
|
41
|
+
out = remove_unprintable_chars(out)
|
|
42
|
+
out = '\n'.join(out.splitlines())
|
|
43
|
+
o = str.__new__(cls, out.strip())
|
|
44
|
+
o.__rc = rc
|
|
45
|
+
o.__cmd = cmd
|
|
46
|
+
return o
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def rc(self) -> int:
|
|
50
|
+
"""
|
|
51
|
+
Return code.
|
|
52
|
+
"""
|
|
53
|
+
return self.__rc
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def cmd(self) -> str:
|
|
57
|
+
"""
|
|
58
|
+
Command.
|
|
59
|
+
"""
|
|
60
|
+
return self.__cmd
|
|
61
|
+
|
|
62
|
+
def getfield(
|
|
63
|
+
self,
|
|
64
|
+
key: str,
|
|
65
|
+
col: int,
|
|
66
|
+
sep: str = None
|
|
67
|
+
) -> Optional[str]:
|
|
68
|
+
"""
|
|
69
|
+
Get a specified field from the output.
|
|
70
|
+
|
|
71
|
+
:param key: string to filter a line.
|
|
72
|
+
:param col: column number in the filtered line.
|
|
73
|
+
:param sep: char to split the filtered line.
|
|
74
|
+
|
|
75
|
+
>>> r = SSHCommandResult('''\\
|
|
76
|
+
... UID PID CMD
|
|
77
|
+
... postgres 45 /opt/pgsql/bin/postgres
|
|
78
|
+
... postgres 51 postgres: checkpointer process
|
|
79
|
+
... postgres 52 postgres: writer process
|
|
80
|
+
... postgres 53 postgres: wal writer process''', 0, '')
|
|
81
|
+
>>> r.getfield('/opt/pgsql', 2)
|
|
82
|
+
'45'
|
|
83
|
+
>>> r.getfield('checkpointer', 1, sep=':')
|
|
84
|
+
'postgres 51 postgres'
|
|
85
|
+
"""
|
|
86
|
+
matchline = ''
|
|
87
|
+
lines = self.splitlines()
|
|
88
|
+
if isinstance(key, str):
|
|
89
|
+
for line in self.splitlines():
|
|
90
|
+
if key in line:
|
|
91
|
+
matchline = line
|
|
92
|
+
elif isinstance(key, int):
|
|
93
|
+
matchline = lines[key-1]
|
|
94
|
+
if matchline:
|
|
95
|
+
fields = matchline.split(sep)
|
|
96
|
+
return fields[col-1].strip()
|
|
97
|
+
|
|
98
|
+
def getcol(
|
|
99
|
+
self,
|
|
100
|
+
col: int,
|
|
101
|
+
sep: str = None
|
|
102
|
+
) -> list:
|
|
103
|
+
"""
|
|
104
|
+
Get a specified column from the output.
|
|
105
|
+
|
|
106
|
+
:param col: column number.
|
|
107
|
+
:param sep: char to split lines.
|
|
108
|
+
|
|
109
|
+
>>> r = SSHCommandResult('''\\
|
|
110
|
+
... UID PID CMD
|
|
111
|
+
... postgres 45 /opt/pgsql/bin/postgres
|
|
112
|
+
... postgres 51 postgres: checkpointer process
|
|
113
|
+
... postgres 52 postgres: writer process
|
|
114
|
+
... postgres 53 postgres: wal writer process''', 0, '')
|
|
115
|
+
>>> r.getcol(2)
|
|
116
|
+
['PID', '45', '51', '52', '53']
|
|
117
|
+
"""
|
|
118
|
+
fields = []
|
|
119
|
+
for line in self.splitlines():
|
|
120
|
+
segs = line.split(sep)
|
|
121
|
+
if col <= len(segs):
|
|
122
|
+
fields.append(segs[col-1])
|
|
123
|
+
return fields
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SSHConnection(object):
|
|
127
|
+
"""
|
|
128
|
+
SSH connection.
|
|
129
|
+
"""
|
|
130
|
+
def __init__(self, shenvs: dict = {}):
|
|
131
|
+
"""
|
|
132
|
+
:param shenvs: shell environment variables for method `exec`.
|
|
133
|
+
`LANG` defaults to `en_US.UTF-8`.
|
|
134
|
+
`LANGUAGE` defaults to `en_US.UTF-8`.
|
|
135
|
+
"""
|
|
136
|
+
self._logger = ExtraAdapter(logger, {})
|
|
137
|
+
self._sshclient = SSHClient()
|
|
138
|
+
self._sshclient.set_missing_host_key_policy(AutoAddPolicy())
|
|
139
|
+
self._shenvs = shenvs
|
|
140
|
+
for k, v in {'LANG': 'en_US.UTF-8',
|
|
141
|
+
'LANGUAGE': 'en_US.UTF-8'}.items():
|
|
142
|
+
self._shenvs[k] = self._shenvs.get(k, v)
|
|
143
|
+
self._password = None
|
|
144
|
+
self._cdlock = threading.Lock()
|
|
145
|
+
self._cwd = ''
|
|
146
|
+
|
|
147
|
+
def connect(
|
|
148
|
+
self,
|
|
149
|
+
host: str,
|
|
150
|
+
user: str,
|
|
151
|
+
password: str,
|
|
152
|
+
port: int = 22,
|
|
153
|
+
timeout: int = 5
|
|
154
|
+
) -> None:
|
|
155
|
+
"""
|
|
156
|
+
Open the connection.
|
|
157
|
+
|
|
158
|
+
:param host: hostname or ip.
|
|
159
|
+
:param user: user name.
|
|
160
|
+
:param password: user password.
|
|
161
|
+
:param port: SSH port.
|
|
162
|
+
:param timeout: connect timeout(s).
|
|
163
|
+
"""
|
|
164
|
+
transport = self._sshclient.get_transport()
|
|
165
|
+
if transport and transport.active:
|
|
166
|
+
return
|
|
167
|
+
self._logger.extra['prefix'] = f'ssh://{user}@{host}:{port}'
|
|
168
|
+
self._logger.info('Connecting...')
|
|
169
|
+
try:
|
|
170
|
+
self._sshclient.connect(host, port=port, username=user,
|
|
171
|
+
password=password, timeout=timeout)
|
|
172
|
+
self._password = password
|
|
173
|
+
except AuthenticationException:
|
|
174
|
+
raise SSHConnectError(
|
|
175
|
+
f'Authentication failed when SSH connect to {host} with user `{user}`, '
|
|
176
|
+
f'please check whether the username and password are correct.'
|
|
177
|
+
) from None
|
|
178
|
+
except socket.timeout:
|
|
179
|
+
raise SSHConnectError(
|
|
180
|
+
f'Timed out when SSH connect to {host}({timeout}s), '
|
|
181
|
+
'please check whether the network is normal.'
|
|
182
|
+
) from None
|
|
183
|
+
except NoValidConnectionsError:
|
|
184
|
+
raise SSHConnectError(
|
|
185
|
+
f'Could not connect to port {port} on {host}, '
|
|
186
|
+
'please check whether the port is opened.'
|
|
187
|
+
) from None
|
|
188
|
+
except SSHException as e:
|
|
189
|
+
msg = str(e)
|
|
190
|
+
if 'Error reading SSH protocol banner' in msg:
|
|
191
|
+
raise SSHConnectError(
|
|
192
|
+
f'Read SSH protocol banner failed when connect to port {port} '
|
|
193
|
+
f'on {host}, please check whether the port is correct.'
|
|
194
|
+
) from None
|
|
195
|
+
raise e from None
|
|
196
|
+
|
|
197
|
+
def disconnect(self) -> None:
|
|
198
|
+
"""
|
|
199
|
+
Close the connection.
|
|
200
|
+
"""
|
|
201
|
+
self._sshclient.close()
|
|
202
|
+
|
|
203
|
+
def exec(
|
|
204
|
+
self,
|
|
205
|
+
cmd: str,
|
|
206
|
+
expect: Union[int, str, None] = 0,
|
|
207
|
+
timeout: int = 15,
|
|
208
|
+
prompts: dict = {},
|
|
209
|
+
shenvs: dict = {}
|
|
210
|
+
) -> SSHCommandResult:
|
|
211
|
+
"""
|
|
212
|
+
Execute a command on the SSH server.
|
|
213
|
+
|
|
214
|
+
:param cmd: the command to be executed.
|
|
215
|
+
:param expect: expected result of command execution.
|
|
216
|
+
0: expect the return code of command is 0.
|
|
217
|
+
'hello': expect the str 'hello' to appear in the output.
|
|
218
|
+
None : do not check the result of command execution.
|
|
219
|
+
:param timeout: command timeout (seconds).
|
|
220
|
+
:param prompts: prompts and answers for interactive command.
|
|
221
|
+
:param shenvs: shell environment variables for command.
|
|
222
|
+
:return: output(stdout and stderr) of command.
|
|
223
|
+
|
|
224
|
+
:raises:
|
|
225
|
+
`.SSHCommandError` -- if the result is not as expected.
|
|
226
|
+
`TimeoutError` -- if the command execution is timedout.
|
|
227
|
+
|
|
228
|
+
>>> exec('cd /home') # successful # doctest: +SKIP
|
|
229
|
+
>>> exec('cd /errpath') # SSHCommandError # doctest: +SKIP
|
|
230
|
+
>>> exec('cd /errpath', expect=1) # no error # doctest: +SKIP
|
|
231
|
+
>>> exec('cd /errpath', expect=None) # no error # doctest: +SKIP
|
|
232
|
+
>>> exec('echo hello', expect='hello') # successful # doctest: +SKIP
|
|
233
|
+
>>> exec('echo hello', expect='world') # SSHCommandError # doctest: +SKIP
|
|
234
|
+
>>> exec('sudo whoami', prompts={'password:': 'mypwd'}) # doctest: +SKIP
|
|
235
|
+
"""
|
|
236
|
+
if self._cwd:
|
|
237
|
+
cmd = f'cd {self._cwd} && {cmd}'
|
|
238
|
+
extra = {'hook': {}}
|
|
239
|
+
self._logger.info(f"Command: '{cmd}', Expect: '{expect}'", extra=extra)
|
|
240
|
+
envs = self._shenvs.copy()
|
|
241
|
+
envs.update(shenvs)
|
|
242
|
+
stdin, stdout, _ = self._sshclient.exec_command(cmd, get_pty=True, environment=envs)
|
|
243
|
+
start = datetime.now()
|
|
244
|
+
output = ''
|
|
245
|
+
encoding = envs['LANG'].split('.')[-1]
|
|
246
|
+
while (datetime.now() - start).seconds <= timeout:
|
|
247
|
+
rlist, _, _ = select([stdout.channel], [], [], 0.1)
|
|
248
|
+
if stdout.channel in rlist:
|
|
249
|
+
data = stdout.channel.recv(1024).decode(encoding=encoding, errors='ignore')
|
|
250
|
+
output += data
|
|
251
|
+
if data == '':
|
|
252
|
+
break
|
|
253
|
+
if prompts and output:
|
|
254
|
+
lastline = output.splitlines()[-1]
|
|
255
|
+
written = None
|
|
256
|
+
for k, v in prompts.items():
|
|
257
|
+
if k in lastline:
|
|
258
|
+
stdin.write(v + '\n')
|
|
259
|
+
stdin.flush()
|
|
260
|
+
written = k
|
|
261
|
+
break
|
|
262
|
+
if written:
|
|
263
|
+
prompts.pop(written)
|
|
264
|
+
else:
|
|
265
|
+
result = SSHCommandResult(output, rc=-1)
|
|
266
|
+
extra['hook']['more'] = result
|
|
267
|
+
raise TimeoutError(f"Command '{cmd}' timedout({timeout}s):\n{result}")
|
|
268
|
+
result = SSHCommandResult(output, rc=stdout.channel.recv_exit_status(), cmd=cmd)
|
|
269
|
+
extra['hook']['more'] = result
|
|
270
|
+
if expect != None:
|
|
271
|
+
if (isinstance(expect, int) and expect != result.rc) or \
|
|
272
|
+
(isinstance(expect, str) and expect not in result):
|
|
273
|
+
msg = textwrap.dedent(f"""\
|
|
274
|
+
Expections not met:
|
|
275
|
+
Command: {cmd}
|
|
276
|
+
Excpect: {expect}
|
|
277
|
+
ReturnCode: {result.rc}
|
|
278
|
+
Output:
|
|
279
|
+
{output}
|
|
280
|
+
""")
|
|
281
|
+
raise SSHCommandError(msg)
|
|
282
|
+
return result
|
|
283
|
+
|
|
284
|
+
def sudo(self, cmd, *args, **kwargs) -> SSHCommandResult:
|
|
285
|
+
"""
|
|
286
|
+
Execute a command with sudo, arguments are same to `exec`.
|
|
287
|
+
"""
|
|
288
|
+
kwargs['prompts'] = {'[sudo] password': self._password}
|
|
289
|
+
return self.exec(f'sudo {cmd}', *args, **kwargs)
|
|
290
|
+
|
|
291
|
+
@contextmanager
|
|
292
|
+
def cd(self, path) -> Generator[None, str, None]:
|
|
293
|
+
"""
|
|
294
|
+
change current directory.
|
|
295
|
+
|
|
296
|
+
>>> with cd('/my/workdir'): # doctest: +SKIP
|
|
297
|
+
... d = exec('pwd') # doctest: +SKIP
|
|
298
|
+
... # doctest: +SKIP
|
|
299
|
+
>>> d # doctest: +SKIP
|
|
300
|
+
'/my/workdir' # doctest: +SKIP
|
|
301
|
+
"""
|
|
302
|
+
self._cdlock.acquire()
|
|
303
|
+
try:
|
|
304
|
+
self._cwd = path
|
|
305
|
+
yield
|
|
306
|
+
finally:
|
|
307
|
+
self._cwd = ''
|
|
308
|
+
self._cdlock.release()
|
|
309
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright (c) 2023-2024, zhaowcheng <zhaowcheng@163.com>
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Utility functions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import string
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def remove_ansi_escape_chars(s: str) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Remove ansi esacpe characters from `s`.
|
|
14
|
+
|
|
15
|
+
>>> remove_ansi_escape_chars('\x1b[31mhello\x1b[0m')
|
|
16
|
+
'hello'
|
|
17
|
+
"""
|
|
18
|
+
escapes = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
19
|
+
return escapes.sub('', s)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def remove_unprintable_chars(s: str) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Remove unprintable characters from `s`.
|
|
25
|
+
|
|
26
|
+
>>> remove_unprintable_chars('hello\xe9')
|
|
27
|
+
'hello'
|
|
28
|
+
"""
|
|
29
|
+
return ''.join(c for c in s if c in string.printable)
|
|
30
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2023, zhaowcheng <zhaowcheng@163.com>
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: xbot.plugins.ssh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SSH library for xbot.framework
|
|
5
|
+
Home-page: https://github.com/zhaowcheng/xbot.plugins.ssh
|
|
6
|
+
Author: zhaowcheng
|
|
7
|
+
Author-email: zhaowcheng@163.com
|
|
8
|
+
License: UNKNOWN
|
|
9
|
+
Project-URL: Homepage, https://github.com/zhaowcheng/xbot.plugins.ssh
|
|
10
|
+
Project-URL: Issues, https://github.com/zhaowcheng/xbot.plugins.ssh/issues
|
|
11
|
+
Platform: UNKNOWN
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Requires-Python: >=3.6
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: xbot.framework (>=0.4.0) ; python_version >= "3.6"
|
|
26
|
+
Requires-Dist: paramiko ; python_version >= "3.6"
|
|
27
|
+
|
|
28
|
+
## Introduction
|
|
29
|
+
|
|
30
|
+
`xbot.plugins.ssh` provides ssh and sftp support for [xbot.framework](https://github.com/zhaowcheng/xbot.framework)
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Install via pip:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install xbot.plugins.ssh
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Getting Started
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import os
|
|
44
|
+
import shutil
|
|
45
|
+
|
|
46
|
+
from xbot.plugins.ssh.ssh import SSHConnection
|
|
47
|
+
from xbot.plugins.ssh.sftp import SFTPConnection
|
|
48
|
+
|
|
49
|
+
# Modify the following information to your own before trying to run.
|
|
50
|
+
host = '192.168.8.8'
|
|
51
|
+
user = 'xbot'
|
|
52
|
+
pwd = 'xbot'
|
|
53
|
+
|
|
54
|
+
# Create ssh connection.
|
|
55
|
+
sshconn = SSHConnection()
|
|
56
|
+
sshconn.connect(host, user, pwd)
|
|
57
|
+
|
|
58
|
+
# Expect the return code of command is 0.
|
|
59
|
+
sshconn.exec('ls -l', expect=0)
|
|
60
|
+
|
|
61
|
+
# Expect the return code of command is 2.
|
|
62
|
+
sshconn.exec('ls /errpath', expect=2)
|
|
63
|
+
|
|
64
|
+
# Expect the output of command contains `username`.
|
|
65
|
+
sshconn.exec('whoami', expect=user)
|
|
66
|
+
|
|
67
|
+
# Don't check the result of command.
|
|
68
|
+
sshconn.exec('ls -l', expect=None)
|
|
69
|
+
sshconn.exec('ls /errpath', expect=None)
|
|
70
|
+
|
|
71
|
+
# Execute command with sudo.
|
|
72
|
+
sshconn.sudo('whoami', expect='root')
|
|
73
|
+
|
|
74
|
+
# Interactive command.
|
|
75
|
+
sshconn.exec("read -p 'input: '", prompts={'input:': 'hello'})
|
|
76
|
+
|
|
77
|
+
# Attributes and methods of SSHCommandResult.
|
|
78
|
+
cmd = 'echo -e "jack 20\ntom 30"'
|
|
79
|
+
result = sshconn.exec(cmd)
|
|
80
|
+
assert str(result) == 'jack 20\ntom 30'
|
|
81
|
+
assert result.rc == 0
|
|
82
|
+
assert result.cmd == cmd
|
|
83
|
+
assert result.getfield('tom', 2) == '30'
|
|
84
|
+
assert result.getcol(2) == ['20', '30']
|
|
85
|
+
|
|
86
|
+
# Create sftp connection.
|
|
87
|
+
sftpconn = SFTPConnection()
|
|
88
|
+
sftpconn.connect(host, user, pwd)
|
|
89
|
+
|
|
90
|
+
# Preparing for sftp testing.
|
|
91
|
+
lhome = os.environ.get('HOME') or os.environ['HOMEPATH']
|
|
92
|
+
lputdir = os.path.join(lhome, 'lputdir')
|
|
93
|
+
lgetdir = os.path.join(lhome, 'lgetdir')
|
|
94
|
+
rputdir = '/tmp/rputgetdir'
|
|
95
|
+
rgetdir = rputdir
|
|
96
|
+
if os.path.exists(lputdir):
|
|
97
|
+
shutil.rmtree(lputdir)
|
|
98
|
+
if sftpconn.exists(rputdir):
|
|
99
|
+
sshconn.exec(f'rm -rf {rputdir}')
|
|
100
|
+
sftpconn.makedirs(rputdir)
|
|
101
|
+
os.makedirs(os.path.join(lputdir, 'dir', 'subdir'))
|
|
102
|
+
os.system('whoami > %s' % os.path.join(lputdir, 'file'))
|
|
103
|
+
os.system('whoami > %s' % os.path.join(lputdir, 'dir', 'subfile'))
|
|
104
|
+
|
|
105
|
+
# Put directory.
|
|
106
|
+
l = os.path.join(lputdir, 'dir')
|
|
107
|
+
sftpconn.putdir(l, rputdir)
|
|
108
|
+
p1 = sftpconn.join(rputdir, 'dir', 'subdir')
|
|
109
|
+
assert sftpconn.exists(p1) == True
|
|
110
|
+
p2 = sftpconn.join(rputdir, 'dir', 'subfile')
|
|
111
|
+
assert sftpconn.exists(p2) == True
|
|
112
|
+
|
|
113
|
+
# Put file.
|
|
114
|
+
l = os.path.join(lputdir, 'file')
|
|
115
|
+
sftpconn.putfile(l, rputdir)
|
|
116
|
+
p1 = sftpconn.join(rputdir, 'file')
|
|
117
|
+
assert sftpconn.exists(p1) == True
|
|
118
|
+
sftpconn.putfile(l, rputdir, 'newfile') # rename
|
|
119
|
+
p2 = sftpconn.join(rputdir, 'newfile')
|
|
120
|
+
assert sftpconn.exists(p2) == True
|
|
121
|
+
|
|
122
|
+
# Get directory.
|
|
123
|
+
r = sftpconn.join(rgetdir, 'dir')
|
|
124
|
+
sftpconn.getdir(r, lgetdir)
|
|
125
|
+
p1 = os.path.join(lgetdir, 'dir', 'subdir')
|
|
126
|
+
assert os.path.exists(p1) == True
|
|
127
|
+
p2 = os.path.join(lgetdir, 'dir', 'subfile')
|
|
128
|
+
assert os.path.exists(p2) == True
|
|
129
|
+
|
|
130
|
+
# Get file.
|
|
131
|
+
r = sftpconn.join(rgetdir, 'file')
|
|
132
|
+
sftpconn.getfile(r, lgetdir)
|
|
133
|
+
p1 = os.path.join(lgetdir, 'file')
|
|
134
|
+
assert os.path.exists(p1) == True
|
|
135
|
+
sftpconn.getfile(r, lgetdir, 'newfile') # rename
|
|
136
|
+
p2 = os.path.join(lgetdir, 'newfile')
|
|
137
|
+
assert os.path.exists(p2) == True
|
|
138
|
+
|
|
139
|
+
# Open file.
|
|
140
|
+
p = sftpconn.join(rputdir, 'file')
|
|
141
|
+
with sftpconn.open(p, 'w') as fp:
|
|
142
|
+
fp.write('xbot')
|
|
143
|
+
with sftpconn.open(p, 'r') as fp:
|
|
144
|
+
assert fp.read().decode('utf-8') == 'xbot'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Demo Project
|
|
148
|
+
|
|
149
|
+
Here is a [demo](https://github.com/zhaowcheng/xbot.plugins.ssh/tree/v0.1.0/demo) project showing how to use `xbot.plugins.ssh` in a test project.
|
|
150
|
+
|
|
151
|
+
***
|
|
152
|
+
|
|
153
|
+
## 简介
|
|
154
|
+
|
|
155
|
+
`xbot.plugins.ssh` 是为 [xbot.framework](https://github.com/zhaowcheng/xbot.framework) 提供 ssh 和 sftp 支持的插件。
|
|
156
|
+
|
|
157
|
+
## 安装
|
|
158
|
+
|
|
159
|
+
使用 pip 安装:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
pip install xbot.plugins.ssh
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## 入门
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
import os
|
|
169
|
+
import shutil
|
|
170
|
+
|
|
171
|
+
from xbot.plugins.ssh.ssh import SSHConnection
|
|
172
|
+
from xbot.plugins.ssh.sftp import SFTPConnection
|
|
173
|
+
|
|
174
|
+
# Modify the following information to your own before trying to run.
|
|
175
|
+
host = '192.168.8.8'
|
|
176
|
+
user = 'xbot'
|
|
177
|
+
pwd = 'xbot'
|
|
178
|
+
|
|
179
|
+
# Create ssh connection.
|
|
180
|
+
sshconn = SSHConnection()
|
|
181
|
+
sshconn.connect(host, user, pwd)
|
|
182
|
+
|
|
183
|
+
# Expect the return code of command is 0.
|
|
184
|
+
sshconn.exec('ls -l', expect=0)
|
|
185
|
+
|
|
186
|
+
# Expect the return code of command is 2.
|
|
187
|
+
sshconn.exec('ls /errpath', expect=2)
|
|
188
|
+
|
|
189
|
+
# Expect the output of command contains `username`.
|
|
190
|
+
sshconn.exec('whoami', expect=user)
|
|
191
|
+
|
|
192
|
+
# Don't check the result of command.
|
|
193
|
+
sshconn.exec('ls -l', expect=None)
|
|
194
|
+
sshconn.exec('ls /errpath', expect=None)
|
|
195
|
+
|
|
196
|
+
# Execute command with sudo.
|
|
197
|
+
sshconn.sudo('whoami', expect='root')
|
|
198
|
+
|
|
199
|
+
# Interactive command.
|
|
200
|
+
sshconn.exec("read -p 'input: '", prompts={'input:': 'hello'})
|
|
201
|
+
|
|
202
|
+
# Attributes and methods of SSHCommandResult.
|
|
203
|
+
cmd = 'echo -e "jack 20\ntom 30"'
|
|
204
|
+
result = sshconn.exec(cmd)
|
|
205
|
+
assert str(result) == 'jack 20\ntom 30'
|
|
206
|
+
assert result.rc == 0
|
|
207
|
+
assert result.cmd == cmd
|
|
208
|
+
assert result.getfield('tom', 2) == '30'
|
|
209
|
+
assert result.getcol(2) == ['20', '30']
|
|
210
|
+
|
|
211
|
+
# Create sftp connection.
|
|
212
|
+
sftpconn = SFTPConnection()
|
|
213
|
+
sftpconn.connect(host, user, pwd)
|
|
214
|
+
|
|
215
|
+
# Preparing for sftp testing.
|
|
216
|
+
lhome = os.environ.get('HOME') or os.environ['HOMEPATH']
|
|
217
|
+
lputdir = os.path.join(lhome, 'lputdir')
|
|
218
|
+
lgetdir = os.path.join(lhome, 'lgetdir')
|
|
219
|
+
rputdir = '/tmp/rputgetdir'
|
|
220
|
+
rgetdir = rputdir
|
|
221
|
+
if os.path.exists(lputdir):
|
|
222
|
+
shutil.rmtree(lputdir)
|
|
223
|
+
if sftpconn.exists(rputdir):
|
|
224
|
+
sshconn.exec(f'rm -rf {rputdir}')
|
|
225
|
+
sftpconn.makedirs(rputdir)
|
|
226
|
+
os.makedirs(os.path.join(lputdir, 'dir', 'subdir'))
|
|
227
|
+
os.system('whoami > %s' % os.path.join(lputdir, 'file'))
|
|
228
|
+
os.system('whoami > %s' % os.path.join(lputdir, 'dir', 'subfile'))
|
|
229
|
+
|
|
230
|
+
# Put directory.
|
|
231
|
+
l = os.path.join(lputdir, 'dir')
|
|
232
|
+
sftpconn.putdir(l, rputdir)
|
|
233
|
+
p1 = sftpconn.join(rputdir, 'dir', 'subdir')
|
|
234
|
+
assert sftpconn.exists(p1) == True
|
|
235
|
+
p2 = sftpconn.join(rputdir, 'dir', 'subfile')
|
|
236
|
+
assert sftpconn.exists(p2) == True
|
|
237
|
+
|
|
238
|
+
# Put file.
|
|
239
|
+
l = os.path.join(lputdir, 'file')
|
|
240
|
+
sftpconn.putfile(l, rputdir)
|
|
241
|
+
p1 = sftpconn.join(rputdir, 'file')
|
|
242
|
+
assert sftpconn.exists(p1) == True
|
|
243
|
+
sftpconn.putfile(l, rputdir, 'newfile') # rename
|
|
244
|
+
p2 = sftpconn.join(rputdir, 'newfile')
|
|
245
|
+
assert sftpconn.exists(p2) == True
|
|
246
|
+
|
|
247
|
+
# Get directory.
|
|
248
|
+
r = sftpconn.join(rgetdir, 'dir')
|
|
249
|
+
sftpconn.getdir(r, lgetdir)
|
|
250
|
+
p1 = os.path.join(lgetdir, 'dir', 'subdir')
|
|
251
|
+
assert os.path.exists(p1) == True
|
|
252
|
+
p2 = os.path.join(lgetdir, 'dir', 'subfile')
|
|
253
|
+
assert os.path.exists(p2) == True
|
|
254
|
+
|
|
255
|
+
# Get file.
|
|
256
|
+
r = sftpconn.join(rgetdir, 'file')
|
|
257
|
+
sftpconn.getfile(r, lgetdir)
|
|
258
|
+
p1 = os.path.join(lgetdir, 'file')
|
|
259
|
+
assert os.path.exists(p1) == True
|
|
260
|
+
sftpconn.getfile(r, lgetdir, 'newfile') # rename
|
|
261
|
+
p2 = os.path.join(lgetdir, 'newfile')
|
|
262
|
+
assert os.path.exists(p2) == True
|
|
263
|
+
|
|
264
|
+
# Open file.
|
|
265
|
+
p = sftpconn.join(rputdir, 'file')
|
|
266
|
+
with sftpconn.open(p, 'w') as fp:
|
|
267
|
+
fp.write('xbot')
|
|
268
|
+
with sftpconn.open(p, 'r') as fp:
|
|
269
|
+
assert fp.read().decode('utf-8') == 'xbot'
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## 示例项目
|
|
273
|
+
|
|
274
|
+
展示如何在一个测试项目中使用本插件的示例:[demo](https://github.com/zhaowcheng/xbot.plugins.ssh/tree/v0.1.0/demo)
|
|
275
|
+
|
|
276
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
demo/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
demo/lib/testbed.py,sha256=lfE37K0J-nYh-NjY-hfmrgg83x9XfiM58vdgTkx6GaI,1473
|
|
3
|
+
demo/lib/testcase.py,sha256=Vfk7Hu-D115mwqrF8FEzmYKd1HmZGYVa4jD-8G9OTzU,274
|
|
4
|
+
demo/testcases/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
demo/testcases/tc_sftp.py,sha256=lZH7bEtJFNnTsuqaRxVLniAFsHIa0kCGdx-TzZK9DoM,3107
|
|
6
|
+
demo/testcases/tc_ssh.py,sha256=w31_oDCYB60kFgqisDDcxI2ShN27CppBV1NRLud7S10,1759
|
|
7
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
tests/run.py,sha256=euqwJS2p2Xrd42rLpWh1yZ7zq8hMW98E8MJgYe15Tvo,1248
|
|
9
|
+
tests/test_sftp.py,sha256=FAX032RdJn5LUPQb0nBFnI1ZQJizaEbPBpgvwvnskJY,2876
|
|
10
|
+
tests/test_ssh.py,sha256=keBb3qil_CWQ7dBDeP7xH-nMOQ0gvInSM1U5OYSvfjA,3066
|
|
11
|
+
tests/test_utils.py,sha256=ct7SCz4d_o46Bz-MxnTvWqHAa5Qb_WEghUrv6fAP350,368
|
|
12
|
+
venv/bin/jp.py,sha256=0ySfHy8KDu-3cJiE8eqI8Ieb7cGYmKoBpLtZ0Fi6CTc,1738
|
|
13
|
+
venv/bin/rst2html.py,sha256=9Yxlnx112zpvDiBqKSMoY7PRYzW6COILaeVbhNdRXKo,637
|
|
14
|
+
venv/bin/rst2html4.py,sha256=AhGF-BwIqXbDD1Es2LzqlelSz1UQRoKn1TlPt64EVmc,757
|
|
15
|
+
venv/bin/rst2html5.py,sha256=YTLK_83dLObvwgW37wAjGnuf5IjJSc-DRKe1aSN8yL8,1125
|
|
16
|
+
venv/bin/rst2latex.py,sha256=5WTHI5Kv4-60iO_P1rOh-3MQZSYSafNZkQgOfOB1JB4,834
|
|
17
|
+
venv/bin/rst2man.py,sha256=jY20oTs_SeqvIEy40igerxoG9XNpd0u4iMfZ9zA2yiE,642
|
|
18
|
+
venv/bin/rst2odt.py,sha256=nCMzoDDWEatWoUXFvuVfRDGPCv2pqV6eFu1tO0p7S4s,807
|
|
19
|
+
venv/bin/rst2odt_prepstyles.py,sha256=OZMmH98BVoe45pZv_c40_eQ98cIUXQ8UEQ3DY7Ph0n8,1769
|
|
20
|
+
venv/bin/rst2pseudoxml.py,sha256=ikGQyHTvqEODzbm_Cqe6kYfzPO4d431iM0i4iS0MndU,644
|
|
21
|
+
venv/bin/rst2s5.py,sha256=IWF7CubeKTdk542rHp5hTeK4kzQQqIeWcBl3afMAQ84,680
|
|
22
|
+
venv/bin/rst2xetex.py,sha256=uwhPZpFiJiBI2_CywfsyFSTngpHXrLrDrGPA4mkQE9w,914
|
|
23
|
+
venv/bin/rst2xml.py,sha256=tu-9THNer6T_BETrVtv_PXo34i8nfMmb_-Hm4VZXYT4,645
|
|
24
|
+
venv/bin/rstpep2html.py,sha256=9tvD-vgLl3-ejyxpUA0xDUSerXd8TMkBpmO2lFc5ndg,713
|
|
25
|
+
xbot/plugins/ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
xbot/plugins/ssh/errors.py,sha256=_0WfsQEpatW268bBcHpfZg5S_Zk-F37tfz5l60Q6Ebg,251
|
|
27
|
+
xbot/plugins/ssh/sftp.py,sha256=_3T_7GUtX6binCdTuN3RUQq2yOIznTDDsaanNEQs7rY,6402
|
|
28
|
+
xbot/plugins/ssh/ssh.py,sha256=yM5GV_t0D1yn10eKKuGEa2TEfc6XjCZiTOBMOYQ0xVQ,10637
|
|
29
|
+
xbot/plugins/ssh/utils.py,sha256=azGmqwZ3Cc-chMWa1B5MIl6E1f1ojRg_3eAvH7xcBaU,611
|
|
30
|
+
xbot/plugins/ssh/version.py,sha256=FVLP2TwCOKt2azqm3NX6S7fWkrGdfnlsVif-3YM5Xxo,82
|
|
31
|
+
xbot.plugins.ssh-0.1.0.dist-info/LICENSE,sha256=QQgf5JlcyKhtVWQcDwDfv7aU_8RZ5qZKxLnpp3NEXos,1325
|
|
32
|
+
xbot.plugins.ssh-0.1.0.dist-info/METADATA,sha256=5Pm_z91_FzbSr7ru1Aq7XxDrploI-qxvTCj3TdDn2ms,7782
|
|
33
|
+
xbot.plugins.ssh-0.1.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
34
|
+
xbot.plugins.ssh-0.1.0.dist-info/top_level.txt,sha256=BxlEQC_aYmPtUrAk0MdzoR10KHMdLDM4Bv93jWrebuQ,21
|
|
35
|
+
xbot.plugins.ssh-0.1.0.dist-info/RECORD,,
|