xbot.plugins.ssh 0.1.0__tar.gz → 0.2.1__tar.gz
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.
- {xbot.plugins.ssh-0.1.0/xbot.plugins.ssh.egg-info → xbot_plugins_ssh-0.2.1}/PKG-INFO +12 -17
- xbot_plugins_ssh-0.2.1/pyproject.toml +38 -0
- xbot_plugins_ssh-0.2.1/setup.py +26 -0
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/tests/test_sftp.py +15 -4
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/tests/test_ssh.py +2 -1
- xbot_plugins_ssh-0.2.1/xbot/plugins/ssh/py.typed +1 -0
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/xbot/plugins/ssh/sftp.py +41 -20
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/xbot/plugins/ssh/ssh.py +34 -23
- xbot_plugins_ssh-0.2.1/xbot/plugins/ssh/version.py +3 -0
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1/xbot.plugins.ssh.egg-info}/PKG-INFO +12 -17
- xbot_plugins_ssh-0.2.1/xbot.plugins.ssh.egg-info/SOURCES.txt +20 -0
- xbot_plugins_ssh-0.2.1/xbot.plugins.ssh.egg-info/requires.txt +2 -0
- xbot_plugins_ssh-0.2.1/xbot.plugins.ssh.egg-info/top_level.txt +1 -0
- xbot.plugins.ssh-0.1.0/MANIFEST.in +0 -2
- xbot.plugins.ssh-0.1.0/demo/lib/testbed.py +0 -48
- xbot.plugins.ssh-0.1.0/demo/lib/testcase.py +0 -15
- xbot.plugins.ssh-0.1.0/demo/testcases/__init__.py +0 -0
- xbot.plugins.ssh-0.1.0/demo/testcases/tc_sftp.py +0 -99
- xbot.plugins.ssh-0.1.0/demo/testcases/tc_ssh.py +0 -73
- xbot.plugins.ssh-0.1.0/pyproject.toml +0 -3
- xbot.plugins.ssh-0.1.0/requirements.txt +0 -2
- xbot.plugins.ssh-0.1.0/setup.py +0 -64
- xbot.plugins.ssh-0.1.0/tests/__init__.py +0 -0
- xbot.plugins.ssh-0.1.0/tests/run.py +0 -39
- xbot.plugins.ssh-0.1.0/venv/bin/jp.py +0 -54
- xbot.plugins.ssh-0.1.0/venv/bin/rst2html.py +0 -23
- xbot.plugins.ssh-0.1.0/venv/bin/rst2html4.py +0 -26
- xbot.plugins.ssh-0.1.0/venv/bin/rst2html5.py +0 -34
- xbot.plugins.ssh-0.1.0/venv/bin/rst2latex.py +0 -26
- xbot.plugins.ssh-0.1.0/venv/bin/rst2man.py +0 -26
- xbot.plugins.ssh-0.1.0/venv/bin/rst2odt.py +0 -30
- xbot.plugins.ssh-0.1.0/venv/bin/rst2odt_prepstyles.py +0 -67
- xbot.plugins.ssh-0.1.0/venv/bin/rst2pseudoxml.py +0 -23
- xbot.plugins.ssh-0.1.0/venv/bin/rst2s5.py +0 -24
- xbot.plugins.ssh-0.1.0/venv/bin/rst2xetex.py +0 -27
- xbot.plugins.ssh-0.1.0/venv/bin/rst2xml.py +0 -23
- xbot.plugins.ssh-0.1.0/venv/bin/rstpep2html.py +0 -25
- xbot.plugins.ssh-0.1.0/xbot/plugins/ssh/__init__.py +0 -0
- xbot.plugins.ssh-0.1.0/xbot/plugins/ssh/version.py +0 -3
- xbot.plugins.ssh-0.1.0/xbot.plugins.ssh.egg-info/SOURCES.txt +0 -42
- xbot.plugins.ssh-0.1.0/xbot.plugins.ssh.egg-info/requires.txt +0 -4
- xbot.plugins.ssh-0.1.0/xbot.plugins.ssh.egg-info/top_level.txt +0 -6
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/LICENSE +0 -0
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/README.md +0 -0
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/README.zh.md +0 -0
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/setup.cfg +0 -0
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/tests/test_utils.py +0 -0
- {xbot.plugins.ssh-0.1.0/demo/lib → xbot_plugins_ssh-0.2.1/xbot/plugins/ssh}/__init__.py +0 -0
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/xbot/plugins/ssh/errors.py +0 -0
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/xbot/plugins/ssh/utils.py +0 -0
- {xbot.plugins.ssh-0.1.0 → xbot_plugins_ssh-0.2.1}/xbot.plugins.ssh.egg-info/dependency_links.txt +0 -0
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: xbot.plugins.ssh
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: SSH library for xbot.framework
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Author-email: zhaowcheng@163.com
|
|
8
|
-
License: UNKNOWN
|
|
5
|
+
Author-email: zhaowcheng <zhaowcheng@163.com>
|
|
6
|
+
License-Expression: BSD-2-Clause
|
|
9
7
|
Project-URL: Homepage, https://github.com/zhaowcheng/xbot.plugins.ssh
|
|
10
8
|
Project-URL: Issues, https://github.com/zhaowcheng/xbot.plugins.ssh/issues
|
|
11
|
-
Platform: UNKNOWN
|
|
12
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
13
9
|
Classifier: Operating System :: OS Independent
|
|
14
10
|
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
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
-
Requires-Python: >=3.
|
|
14
|
+
Requires-Python: >=3.10
|
|
23
15
|
Description-Content-Type: text/markdown
|
|
24
16
|
License-File: LICENSE
|
|
17
|
+
Requires-Dist: xbot.framework>=0.5.0
|
|
18
|
+
Requires-Dist: paramiko
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: license-file
|
|
25
22
|
|
|
26
23
|
## Introduction
|
|
27
24
|
|
|
@@ -144,7 +141,7 @@ with sftpconn.open(p, 'r') as fp:
|
|
|
144
141
|
|
|
145
142
|
## Demo Project
|
|
146
143
|
|
|
147
|
-
Here is a [demo](https://github.com/zhaowcheng/xbot.plugins.ssh/tree/
|
|
144
|
+
Here is a [demo](https://github.com/zhaowcheng/xbot.plugins.ssh/tree/v/demo) project showing how to use `xbot.plugins.ssh` in a test project.
|
|
148
145
|
|
|
149
146
|
***
|
|
150
147
|
|
|
@@ -269,6 +266,4 @@ with sftpconn.open(p, 'r') as fp:
|
|
|
269
266
|
|
|
270
267
|
## 示例项目
|
|
271
268
|
|
|
272
|
-
展示如何在一个测试项目中使用本插件的示例:[demo](https://github.com/zhaowcheng/xbot.plugins.ssh/tree/
|
|
273
|
-
|
|
274
|
-
|
|
269
|
+
展示如何在一个测试项目中使用本插件的示例:[demo](https://github.com/zhaowcheng/xbot.plugins.ssh/tree/v/demo)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xbot.plugins.ssh"
|
|
7
|
+
description = "SSH library for xbot.framework"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "zhaowcheng", email = "zhaowcheng@163.com" },
|
|
10
|
+
]
|
|
11
|
+
license = "BSD-2-Clause"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"xbot.framework>=0.5.0",
|
|
16
|
+
"paramiko",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
]
|
|
25
|
+
dynamic = ["version", "readme"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/zhaowcheng/xbot.plugins.ssh"
|
|
29
|
+
Issues = "https://github.com/zhaowcheng/xbot.plugins.ssh/issues"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.dynamic]
|
|
32
|
+
version = { attr = "xbot.plugins.ssh.version.__version__" }
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
include = ["xbot.plugins.ssh*"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
"xbot.plugins.ssh" = ["py.typed"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Build hook: generate long_description from README files,
|
|
2
|
+
# stripping language-switcher headers.
|
|
3
|
+
# All other metadata lives in pyproject.toml.
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from setuptools import setup
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _build_long_description() -> str:
|
|
10
|
+
desc = ""
|
|
11
|
+
for readme in ("README.md", "README.zh.md"):
|
|
12
|
+
if desc:
|
|
13
|
+
desc += "\n***\n\n"
|
|
14
|
+
with open(readme, encoding="utf8") as f:
|
|
15
|
+
desc += "".join(f.readlines()[6:])
|
|
16
|
+
with open("pyproject.toml", encoding="utf8") as f:
|
|
17
|
+
m = re.search(r'''^version\s*=\s*["']([^"']+)''', f.read(), re.M)
|
|
18
|
+
version = m.group(1) if m else ""
|
|
19
|
+
desc = desc.replace("/tree/master/", f"/tree/v{version}/")
|
|
20
|
+
return desc
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
setup(
|
|
24
|
+
long_description=_build_long_description(),
|
|
25
|
+
long_description_content_type="text/markdown",
|
|
26
|
+
)
|
|
@@ -35,10 +35,21 @@ class TestSFTPConnection(unittest.TestCase):
|
|
|
35
35
|
os.system('whoami > %s' % os.path.join(cls.LPUTDIR, 'dir', 'subfile'))
|
|
36
36
|
|
|
37
37
|
@classmethod
|
|
38
|
-
def tearDownClass(cls):
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
def tearDownClass(cls) -> None:
|
|
39
|
+
try:
|
|
40
|
+
if cls.sftp.exists(cls.RPUTDIR):
|
|
41
|
+
for top, _, files in reversed(
|
|
42
|
+
list(cls.sftp.walk(cls.RPUTDIR))
|
|
43
|
+
):
|
|
44
|
+
for filename in files:
|
|
45
|
+
cls.sftp._client.remove(
|
|
46
|
+
cls.sftp.join(top, filename)
|
|
47
|
+
)
|
|
48
|
+
cls.sftp._client.rmdir(top)
|
|
49
|
+
finally:
|
|
50
|
+
shutil.rmtree(cls.LPUTDIR, ignore_errors=True)
|
|
51
|
+
shutil.rmtree(cls.LGETDIR, ignore_errors=True)
|
|
52
|
+
cls.sftp.disconnect()
|
|
42
53
|
|
|
43
54
|
def test_01_putdir(self):
|
|
44
55
|
l = os.path.join(self.LPUTDIR, 'dir')
|
|
@@ -56,7 +56,8 @@ class TestSSHConnection(unittest.TestCase):
|
|
|
56
56
|
def test_connect_error_ip(self):
|
|
57
57
|
conn = SSHConnection()
|
|
58
58
|
with self.assertRaises(SSHConnectError) as cm:
|
|
59
|
-
conn.connect('
|
|
59
|
+
conn.connect('255.255.255.255',
|
|
60
|
+
self.USER, self.PWD, self.PORT)
|
|
60
61
|
self.assertIn('please check whether the network is normal',
|
|
61
62
|
str(cm.exception))
|
|
62
63
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -7,7 +7,7 @@ SFTP module
|
|
|
7
7
|
import os
|
|
8
8
|
import stat
|
|
9
9
|
|
|
10
|
-
from typing import
|
|
10
|
+
from typing import Iterator, cast
|
|
11
11
|
from contextlib import contextmanager
|
|
12
12
|
|
|
13
13
|
from paramiko import Transport, SFTPClient, SFTPFile
|
|
@@ -22,10 +22,14 @@ class SFTPConnection(object):
|
|
|
22
22
|
"""
|
|
23
23
|
SFTP connection.
|
|
24
24
|
"""
|
|
25
|
-
def __init__(self):
|
|
26
|
-
self._sftpclient = None
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._sftpclient: SFTPClient | None = None
|
|
27
27
|
self._logger = ExtraAdapter(logger, {})
|
|
28
28
|
|
|
29
|
+
@property
|
|
30
|
+
def _client(self) -> SFTPClient:
|
|
31
|
+
return cast(SFTPClient, self._sftpclient)
|
|
32
|
+
|
|
29
33
|
def connect(
|
|
30
34
|
self,
|
|
31
35
|
host: str,
|
|
@@ -48,9 +52,14 @@ class SFTPConnection(object):
|
|
|
48
52
|
"""
|
|
49
53
|
Close the connection.
|
|
50
54
|
"""
|
|
51
|
-
self.
|
|
55
|
+
self._client.close()
|
|
52
56
|
|
|
53
|
-
def getfile(
|
|
57
|
+
def getfile(
|
|
58
|
+
self,
|
|
59
|
+
rfile: str,
|
|
60
|
+
ldir: str,
|
|
61
|
+
filename: str | None = None
|
|
62
|
+
) -> None:
|
|
54
63
|
"""
|
|
55
64
|
Get `rfile` from SFTP server into `ldir`.
|
|
56
65
|
|
|
@@ -66,9 +75,14 @@ class SFTPConnection(object):
|
|
|
66
75
|
filename = filename or self.basename(rfile)
|
|
67
76
|
lfile = os.path.join(ldir, filename)
|
|
68
77
|
self._logger.info(f'Getting file {lfile} <= {rfile}')
|
|
69
|
-
self.
|
|
78
|
+
self._client.get(rfile, lfile)
|
|
70
79
|
|
|
71
|
-
def putfile(
|
|
80
|
+
def putfile(
|
|
81
|
+
self,
|
|
82
|
+
lfile: str,
|
|
83
|
+
rdir: str,
|
|
84
|
+
filename: str | None = None
|
|
85
|
+
) -> None:
|
|
72
86
|
"""
|
|
73
87
|
Put `lfile` into the `rdir` of SFTP server.
|
|
74
88
|
|
|
@@ -84,7 +98,7 @@ class SFTPConnection(object):
|
|
|
84
98
|
filename = filename or os.path.basename(lfile)
|
|
85
99
|
rfile = self.join(rdir, filename)
|
|
86
100
|
self._logger.info(f'Putting file {lfile} => {rfile}')
|
|
87
|
-
self.
|
|
101
|
+
self._client.put(lfile, rfile)
|
|
88
102
|
|
|
89
103
|
def getdir(self, rdir: str, ldir: str) -> None:
|
|
90
104
|
"""
|
|
@@ -107,7 +121,7 @@ class SFTPConnection(object):
|
|
|
107
121
|
for f in files:
|
|
108
122
|
r = self.join(top, f)
|
|
109
123
|
l = os.path.join(ldir, f)
|
|
110
|
-
self.
|
|
124
|
+
self._client.get(r, l)
|
|
111
125
|
for d in dirs:
|
|
112
126
|
l = os.path.join(ldir, d)
|
|
113
127
|
if not os.path.exists(l):
|
|
@@ -134,7 +148,7 @@ class SFTPConnection(object):
|
|
|
134
148
|
for f in files:
|
|
135
149
|
l = os.path.join(top, f)
|
|
136
150
|
r = self.join(rdir, f)
|
|
137
|
-
self.
|
|
151
|
+
self._client.put(l, r)
|
|
138
152
|
for d in dirs:
|
|
139
153
|
r = self.join(rdir, d)
|
|
140
154
|
if not self.exists(r):
|
|
@@ -144,8 +158,8 @@ class SFTPConnection(object):
|
|
|
144
158
|
"""
|
|
145
159
|
Similar to os.path.join().
|
|
146
160
|
"""
|
|
147
|
-
|
|
148
|
-
return '/'.join(
|
|
161
|
+
normalized_paths = [p.rstrip('/') for p in paths]
|
|
162
|
+
return '/'.join(normalized_paths)
|
|
149
163
|
|
|
150
164
|
def normpath(self, path: str) -> str:
|
|
151
165
|
"""
|
|
@@ -161,22 +175,25 @@ class SFTPConnection(object):
|
|
|
161
175
|
"""
|
|
162
176
|
return path.rsplit('/', 1)[-1]
|
|
163
177
|
|
|
164
|
-
def exists(self, path: str) ->
|
|
178
|
+
def exists(self, path: str) -> bool:
|
|
165
179
|
"""
|
|
166
180
|
Similar to os.path.exists().
|
|
167
181
|
"""
|
|
168
182
|
try:
|
|
169
|
-
self.
|
|
183
|
+
self._client.stat(path)
|
|
170
184
|
return True
|
|
171
185
|
except FileNotFoundError:
|
|
172
186
|
return False
|
|
173
187
|
|
|
174
|
-
def walk(
|
|
188
|
+
def walk(
|
|
189
|
+
self,
|
|
190
|
+
path: str
|
|
191
|
+
) -> Iterator[tuple[str, list[str], list[str]]]:
|
|
175
192
|
"""
|
|
176
193
|
Similar to os.walk().
|
|
177
194
|
"""
|
|
178
195
|
dirs, files = [], []
|
|
179
|
-
for a in self.
|
|
196
|
+
for a in self._client.listdir_attr(path):
|
|
180
197
|
if stat.S_ISDIR(a.st_mode):
|
|
181
198
|
dirs.append(a.filename)
|
|
182
199
|
else:
|
|
@@ -187,7 +204,7 @@ class SFTPConnection(object):
|
|
|
187
204
|
for w in self.walk(self.join(path, d)):
|
|
188
205
|
yield w
|
|
189
206
|
|
|
190
|
-
def makedirs(self, path: str) ->
|
|
207
|
+
def makedirs(self, path: str) -> None:
|
|
191
208
|
"""
|
|
192
209
|
Similar to os.makedirs().
|
|
193
210
|
"""
|
|
@@ -196,15 +213,19 @@ class SFTPConnection(object):
|
|
|
196
213
|
for p in path.split('/'):
|
|
197
214
|
curpath = self.join(curpath, p)
|
|
198
215
|
if not self.exists(curpath):
|
|
199
|
-
self.
|
|
216
|
+
self._client.mkdir(curpath)
|
|
200
217
|
|
|
201
218
|
@contextmanager
|
|
202
|
-
def open(
|
|
219
|
+
def open(
|
|
220
|
+
self,
|
|
221
|
+
filepath: str,
|
|
222
|
+
mode: str = 'r'
|
|
223
|
+
) -> Iterator[SFTPFile]:
|
|
203
224
|
"""
|
|
204
225
|
Similar to builtin open().
|
|
205
226
|
"""
|
|
206
227
|
self._logger.info('Open %s with mode=%s' % (filepath, mode))
|
|
207
|
-
f = self.
|
|
228
|
+
f = self._client.open(filepath, mode)
|
|
208
229
|
try:
|
|
209
230
|
yield f
|
|
210
231
|
finally:
|
|
@@ -8,7 +8,7 @@ import textwrap
|
|
|
8
8
|
import threading
|
|
9
9
|
import socket
|
|
10
10
|
|
|
11
|
-
from typing import
|
|
11
|
+
from typing import Any, Iterator, cast
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
from select import select
|
|
14
14
|
from contextlib import contextmanager
|
|
@@ -31,7 +31,12 @@ class SSHCommandResult(str):
|
|
|
31
31
|
"""
|
|
32
32
|
Result of SSH command.
|
|
33
33
|
"""
|
|
34
|
-
def __new__(
|
|
34
|
+
def __new__(
|
|
35
|
+
cls: type['SSHCommandResult'],
|
|
36
|
+
out: str,
|
|
37
|
+
rc: int = 0,
|
|
38
|
+
cmd: str = ''
|
|
39
|
+
) -> 'SSHCommandResult':
|
|
35
40
|
"""
|
|
36
41
|
:param out: output.
|
|
37
42
|
:param rc: return code.
|
|
@@ -40,7 +45,7 @@ class SSHCommandResult(str):
|
|
|
40
45
|
out = remove_ansi_escape_chars(out)
|
|
41
46
|
out = remove_unprintable_chars(out)
|
|
42
47
|
out = '\n'.join(out.splitlines())
|
|
43
|
-
o = str.__new__(cls, out.strip())
|
|
48
|
+
o = cast(SSHCommandResult, str.__new__(cls, out.strip()))
|
|
44
49
|
o.__rc = rc
|
|
45
50
|
o.__cmd = cmd
|
|
46
51
|
return o
|
|
@@ -61,10 +66,10 @@ class SSHCommandResult(str):
|
|
|
61
66
|
|
|
62
67
|
def getfield(
|
|
63
68
|
self,
|
|
64
|
-
key: str,
|
|
69
|
+
key: str | int,
|
|
65
70
|
col: int,
|
|
66
|
-
sep: str = None
|
|
67
|
-
) ->
|
|
71
|
+
sep: str | None = None
|
|
72
|
+
) -> str | None:
|
|
68
73
|
"""
|
|
69
74
|
Get a specified field from the output.
|
|
70
75
|
|
|
@@ -98,8 +103,8 @@ class SSHCommandResult(str):
|
|
|
98
103
|
def getcol(
|
|
99
104
|
self,
|
|
100
105
|
col: int,
|
|
101
|
-
sep: str = None
|
|
102
|
-
) -> list:
|
|
106
|
+
sep: str | None = None
|
|
107
|
+
) -> list[str]:
|
|
103
108
|
"""
|
|
104
109
|
Get a specified column from the output.
|
|
105
110
|
|
|
@@ -127,7 +132,7 @@ class SSHConnection(object):
|
|
|
127
132
|
"""
|
|
128
133
|
SSH connection.
|
|
129
134
|
"""
|
|
130
|
-
def __init__(self, shenvs: dict = {}):
|
|
135
|
+
def __init__(self, shenvs: dict[str, str] = {}) -> None:
|
|
131
136
|
"""
|
|
132
137
|
:param shenvs: shell environment variables for method `exec`.
|
|
133
138
|
`LANG` defaults to `en_US.UTF-8`.
|
|
@@ -136,11 +141,11 @@ class SSHConnection(object):
|
|
|
136
141
|
self._logger = ExtraAdapter(logger, {})
|
|
137
142
|
self._sshclient = SSHClient()
|
|
138
143
|
self._sshclient.set_missing_host_key_policy(AutoAddPolicy())
|
|
139
|
-
self._shenvs = shenvs
|
|
144
|
+
self._shenvs: dict[str, str] = shenvs
|
|
140
145
|
for k, v in {'LANG': 'en_US.UTF-8',
|
|
141
146
|
'LANGUAGE': 'en_US.UTF-8'}.items():
|
|
142
147
|
self._shenvs[k] = self._shenvs.get(k, v)
|
|
143
|
-
self._password = None
|
|
148
|
+
self._password: str | None = None
|
|
144
149
|
self._cdlock = threading.Lock()
|
|
145
150
|
self._cwd = ''
|
|
146
151
|
|
|
@@ -175,16 +180,16 @@ class SSHConnection(object):
|
|
|
175
180
|
f'Authentication failed when SSH connect to {host} with user `{user}`, '
|
|
176
181
|
f'please check whether the username and password are correct.'
|
|
177
182
|
) 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
183
|
except NoValidConnectionsError:
|
|
184
184
|
raise SSHConnectError(
|
|
185
185
|
f'Could not connect to port {port} on {host}, '
|
|
186
186
|
'please check whether the port is opened.'
|
|
187
187
|
) from None
|
|
188
|
+
except OSError:
|
|
189
|
+
raise SSHConnectError(
|
|
190
|
+
f'Could not connect to {host}, '
|
|
191
|
+
'please check whether the network is normal.'
|
|
192
|
+
) from None
|
|
188
193
|
except SSHException as e:
|
|
189
194
|
msg = str(e)
|
|
190
195
|
if 'Error reading SSH protocol banner' in msg:
|
|
@@ -203,10 +208,10 @@ class SSHConnection(object):
|
|
|
203
208
|
def exec(
|
|
204
209
|
self,
|
|
205
210
|
cmd: str,
|
|
206
|
-
expect:
|
|
211
|
+
expect: int | str | None = 0,
|
|
207
212
|
timeout: int = 15,
|
|
208
|
-
prompts: dict = {},
|
|
209
|
-
shenvs: dict = {}
|
|
213
|
+
prompts: dict[str, str] = {},
|
|
214
|
+
shenvs: dict[str, str] = {}
|
|
210
215
|
) -> SSHCommandResult:
|
|
211
216
|
"""
|
|
212
217
|
Execute a command on the SSH server.
|
|
@@ -281,15 +286,22 @@ class SSHConnection(object):
|
|
|
281
286
|
raise SSHCommandError(msg)
|
|
282
287
|
return result
|
|
283
288
|
|
|
284
|
-
def sudo(
|
|
289
|
+
def sudo(
|
|
290
|
+
self,
|
|
291
|
+
cmd: str,
|
|
292
|
+
*args: Any,
|
|
293
|
+
**kwargs: Any
|
|
294
|
+
) -> SSHCommandResult:
|
|
285
295
|
"""
|
|
286
296
|
Execute a command with sudo, arguments are same to `exec`.
|
|
287
297
|
"""
|
|
288
|
-
kwargs['prompts'] = {
|
|
298
|
+
kwargs['prompts'] = {
|
|
299
|
+
'[sudo] password': cast(str, self._password)
|
|
300
|
+
}
|
|
289
301
|
return self.exec(f'sudo {cmd}', *args, **kwargs)
|
|
290
302
|
|
|
291
303
|
@contextmanager
|
|
292
|
-
def cd(self, path) ->
|
|
304
|
+
def cd(self, path: str) -> Iterator[None]:
|
|
293
305
|
"""
|
|
294
306
|
change current directory.
|
|
295
307
|
|
|
@@ -306,4 +318,3 @@ class SSHConnection(object):
|
|
|
306
318
|
finally:
|
|
307
319
|
self._cwd = ''
|
|
308
320
|
self._cdlock.release()
|
|
309
|
-
|
|
@@ -1,27 +1,24 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: xbot.plugins.ssh
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: SSH library for xbot.framework
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Author-email: zhaowcheng@163.com
|
|
8
|
-
License: UNKNOWN
|
|
5
|
+
Author-email: zhaowcheng <zhaowcheng@163.com>
|
|
6
|
+
License-Expression: BSD-2-Clause
|
|
9
7
|
Project-URL: Homepage, https://github.com/zhaowcheng/xbot.plugins.ssh
|
|
10
8
|
Project-URL: Issues, https://github.com/zhaowcheng/xbot.plugins.ssh/issues
|
|
11
|
-
Platform: UNKNOWN
|
|
12
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
13
9
|
Classifier: Operating System :: OS Independent
|
|
14
10
|
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
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
-
Requires-Python: >=3.
|
|
14
|
+
Requires-Python: >=3.10
|
|
23
15
|
Description-Content-Type: text/markdown
|
|
24
16
|
License-File: LICENSE
|
|
17
|
+
Requires-Dist: xbot.framework>=0.5.0
|
|
18
|
+
Requires-Dist: paramiko
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: license-file
|
|
25
22
|
|
|
26
23
|
## Introduction
|
|
27
24
|
|
|
@@ -144,7 +141,7 @@ with sftpconn.open(p, 'r') as fp:
|
|
|
144
141
|
|
|
145
142
|
## Demo Project
|
|
146
143
|
|
|
147
|
-
Here is a [demo](https://github.com/zhaowcheng/xbot.plugins.ssh/tree/
|
|
144
|
+
Here is a [demo](https://github.com/zhaowcheng/xbot.plugins.ssh/tree/v/demo) project showing how to use `xbot.plugins.ssh` in a test project.
|
|
148
145
|
|
|
149
146
|
***
|
|
150
147
|
|
|
@@ -269,6 +266,4 @@ with sftpconn.open(p, 'r') as fp:
|
|
|
269
266
|
|
|
270
267
|
## 示例项目
|
|
271
268
|
|
|
272
|
-
展示如何在一个测试项目中使用本插件的示例:[demo](https://github.com/zhaowcheng/xbot.plugins.ssh/tree/
|
|
273
|
-
|
|
274
|
-
|
|
269
|
+
展示如何在一个测试项目中使用本插件的示例:[demo](https://github.com/zhaowcheng/xbot.plugins.ssh/tree/v/demo)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
README.zh.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.py
|
|
6
|
+
tests/test_sftp.py
|
|
7
|
+
tests/test_ssh.py
|
|
8
|
+
tests/test_utils.py
|
|
9
|
+
xbot.plugins.ssh.egg-info/PKG-INFO
|
|
10
|
+
xbot.plugins.ssh.egg-info/SOURCES.txt
|
|
11
|
+
xbot.plugins.ssh.egg-info/dependency_links.txt
|
|
12
|
+
xbot.plugins.ssh.egg-info/requires.txt
|
|
13
|
+
xbot.plugins.ssh.egg-info/top_level.txt
|
|
14
|
+
xbot/plugins/ssh/__init__.py
|
|
15
|
+
xbot/plugins/ssh/errors.py
|
|
16
|
+
xbot/plugins/ssh/py.typed
|
|
17
|
+
xbot/plugins/ssh/sftp.py
|
|
18
|
+
xbot/plugins/ssh/ssh.py
|
|
19
|
+
xbot/plugins/ssh/utils.py
|
|
20
|
+
xbot/plugins/ssh/version.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xbot
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
from typing import Union
|
|
2
|
-
|
|
3
|
-
from xbot.framework import testbed
|
|
4
|
-
from xbot.plugins.ssh.ssh import SSHConnection
|
|
5
|
-
from xbot.plugins.ssh.sftp import SFTPConnection
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TestBed(testbed.TestBed):
|
|
9
|
-
"""
|
|
10
|
-
TestBed for ssh demo.
|
|
11
|
-
"""
|
|
12
|
-
def __init__(self, filepath: str):
|
|
13
|
-
"""
|
|
14
|
-
:param filepath: testbed filepath.
|
|
15
|
-
"""
|
|
16
|
-
super().__init__(filepath)
|
|
17
|
-
self._conns = {
|
|
18
|
-
'ssh': {},
|
|
19
|
-
'sftp': {}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
def get_conn(
|
|
23
|
-
self,
|
|
24
|
-
typ: str,
|
|
25
|
-
role: str
|
|
26
|
-
) -> Union[SSHConnection, SFTPConnection]:
|
|
27
|
-
"""
|
|
28
|
-
Get specific connection to the specified role.
|
|
29
|
-
|
|
30
|
-
:param typ: connection type, 'ssh' or 'sftp'.
|
|
31
|
-
:param role: user role.
|
|
32
|
-
"""
|
|
33
|
-
typs = ('ssh', 'sftp')
|
|
34
|
-
if typ not in typs:
|
|
35
|
-
raise ValueError(f'Invalid connection type: {typ}, should be one of {typs}.')
|
|
36
|
-
for user in self.get('host.users'):
|
|
37
|
-
if user['role'] == role:
|
|
38
|
-
if role not in self._conns[typ]:
|
|
39
|
-
conn = SSHConnection() if typ == 'ssh' else SFTPConnection()
|
|
40
|
-
conn.connect(self.get('host.ip'),
|
|
41
|
-
user['name'],
|
|
42
|
-
user['password'],
|
|
43
|
-
port=self.get('host.sshport'))
|
|
44
|
-
self._conns[typ][role] = conn
|
|
45
|
-
return self._conns[typ][role]
|
|
46
|
-
raise ValueError(f'No such user: role={role}')
|
|
47
|
-
|
|
48
|
-
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from xbot.framework import testcase
|
|
2
|
-
|
|
3
|
-
from .testbed import TestBed
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class TestCase(testcase.TestCase):
|
|
7
|
-
"""
|
|
8
|
-
TestCase for ssh demo.
|
|
9
|
-
"""
|
|
10
|
-
@property
|
|
11
|
-
def testbed(self) -> TestBed:
|
|
12
|
-
"""
|
|
13
|
-
TestBed instance.
|
|
14
|
-
"""
|
|
15
|
-
return self.__testbed
|
|
File without changes
|