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.
@@ -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,3 @@
1
+ # Copyright (c) 2023-2024, zhaowcheng <zhaowcheng@163.com>
2
+
3
+ __version__ = '0.1.0'
@@ -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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.37.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ demo
2
+ tests
3
+ venv
4
+ xbot