ginkgo-tools-batchssh 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.
- ginkgo_tools_batchssh/__init__.py +53 -0
- ginkgo_tools_batchssh/moduel_convenient_tools.py +51 -0
- ginkgo_tools_batchssh/module_SSHwrapper.py +832 -0
- ginkgo_tools_batchssh/module_logger.py +57 -0
- ginkgo_tools_batchssh-0.1.0.dist-info/METADATA +218 -0
- ginkgo_tools_batchssh-0.1.0.dist-info/RECORD +8 -0
- ginkgo_tools_batchssh-0.1.0.dist-info/WHEEL +5 -0
- ginkgo_tools_batchssh-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GINKGO-TOOLS-BATCHSSH
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
一个用于批量SSH操作和文件传输的Python工具包。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- SSH连接管理
|
|
9
|
+
- 远程命令执行(支持超时控制)
|
|
10
|
+
- SFTP文件上传下载(支持通配符)
|
|
11
|
+
- 批量主机管理
|
|
12
|
+
- 日志记录系统
|
|
13
|
+
|
|
14
|
+
模块说明:
|
|
15
|
+
- SSHWrapper: 核心SSH连接和操作类
|
|
16
|
+
- module_logger: 日志配置和管理模块
|
|
17
|
+
- moduel_convenient_tools: 便捷工具函数集合
|
|
18
|
+
|
|
19
|
+
作者: 霍城
|
|
20
|
+
邮箱: 495466557@qq.com
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
__author__ = "霍城"
|
|
25
|
+
__email__ = "495466557@qq.com"
|
|
26
|
+
__description__ = "批量SSH操作和文件传输工具包"
|
|
27
|
+
|
|
28
|
+
# 核心类导入
|
|
29
|
+
|
|
30
|
+
from .module_SSHwrapper import SSHWrapper
|
|
31
|
+
from .moduel_convenient_tools import read_hosts_file, parse_and_filter_hosts
|
|
32
|
+
from .module_logger import setup_logging, get_logger
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# 定义公共API
|
|
36
|
+
__all__ = [
|
|
37
|
+
# 核心类
|
|
38
|
+
'SSHWrapper',
|
|
39
|
+
|
|
40
|
+
# 便捷工具函数
|
|
41
|
+
'read_hosts_file',
|
|
42
|
+
'parse_and_filter_hosts',
|
|
43
|
+
|
|
44
|
+
# 日志相关
|
|
45
|
+
'setup_logging',
|
|
46
|
+
'get_logger',
|
|
47
|
+
|
|
48
|
+
# 版本信息
|
|
49
|
+
'__version__',
|
|
50
|
+
'__author__',
|
|
51
|
+
'__email__',
|
|
52
|
+
'__description__'
|
|
53
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
"""读取hosts文件,优先以当前目录下的Hosts为准,如果不存在,则读取/etc/hosts"""
|
|
4
|
+
def read_hosts_file(local_host_dir: str = None) -> str:
|
|
5
|
+
"""读取hosts文件内容"""
|
|
6
|
+
local_host = os.path.join(local_host_dir, "hosts")
|
|
7
|
+
try:
|
|
8
|
+
with open(local_host, "r", encoding="utf-8") as f:
|
|
9
|
+
return f.read()
|
|
10
|
+
except:
|
|
11
|
+
with open("/etc/hosts", "r", encoding="utf-8") as f:
|
|
12
|
+
return f.read()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
"""过滤操作对象的主机地址列表,并执行去重和反向主机名匹配"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_and_filter_hosts(hosts_content: str, filter_list: list) -> dict:
|
|
21
|
+
"""解析并过滤hosts文件内容"""
|
|
22
|
+
"""输入的content可以是read_hosts_file()的值,也可以是手动生成的字符串,保证是ip host1 host2的行格式即可"""
|
|
23
|
+
target_hosts = {}
|
|
24
|
+
|
|
25
|
+
for line_num, line in enumerate(hosts_content.splitlines(), 1):
|
|
26
|
+
# 跳过注释行和空行
|
|
27
|
+
line = line.strip()
|
|
28
|
+
if not line or line.startswith("#"): continue
|
|
29
|
+
|
|
30
|
+
# 解析行内容
|
|
31
|
+
parts = line.split()
|
|
32
|
+
if len(parts) < 2: continue
|
|
33
|
+
|
|
34
|
+
ip_address, *hostnames = parts
|
|
35
|
+
|
|
36
|
+
# 跳过本地回环地址
|
|
37
|
+
if ip_address in ("127.0.0.1", "::1"): continue
|
|
38
|
+
# 应用过滤条件
|
|
39
|
+
if filter_list and not all(filter_word in line for filter_word in filter_list): continue
|
|
40
|
+
# 合并主机名(去重)
|
|
41
|
+
if ip_address in target_hosts:
|
|
42
|
+
target_hosts[ip_address].extend(hostnames)
|
|
43
|
+
else:
|
|
44
|
+
target_hosts[ip_address] = hostnames.copy()
|
|
45
|
+
return target_hosts
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
"""
|
|
2
|
+
作者:霍城 495466557@qq.com
|
|
3
|
+
|
|
4
|
+
SSH 连接封装模块
|
|
5
|
+
提供 SSH 连接功能,支持首次连接确认流程
|
|
6
|
+
"""
|
|
7
|
+
import paramiko
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from . import module_logger
|
|
11
|
+
import os
|
|
12
|
+
import stat
|
|
13
|
+
|
|
14
|
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = module_logger.get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SSHWrapper:
|
|
22
|
+
def __init__(self, host_ip="127.0.0.1", port=22, username="test_user", password="password", host_name="", timeout=10):
|
|
23
|
+
self._ssh_client = None
|
|
24
|
+
self._sftp_client = None
|
|
25
|
+
self.ssh_connected = False
|
|
26
|
+
self.sftp_connected = False
|
|
27
|
+
|
|
28
|
+
self.host_ip = host_ip
|
|
29
|
+
self.host_name = host_name
|
|
30
|
+
self.port = port
|
|
31
|
+
self.password = password
|
|
32
|
+
self.username = username
|
|
33
|
+
self.timeout = timeout
|
|
34
|
+
self.uuid_mark = str(uuid.uuid4()) # 标记主机+本次操作,方便日志追踪
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def ssh_client(self) -> Optional[paramiko.SSHClient]:
|
|
38
|
+
"""SSH客户端实例"""
|
|
39
|
+
if self._ssh_client:
|
|
40
|
+
return self._ssh_client
|
|
41
|
+
else:
|
|
42
|
+
return self.ssh_connect()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def sftp_client(self) -> Optional[paramiko.SFTPClient]:
|
|
46
|
+
"""SFTP客户端实例"""
|
|
47
|
+
if self._sftp_client:
|
|
48
|
+
return self._sftp_client
|
|
49
|
+
else:
|
|
50
|
+
return self.sftp_connect()
|
|
51
|
+
|
|
52
|
+
def _ensure_remote_directory(self, remote_dir: str):
|
|
53
|
+
"""确保远程目录存在,如果不存在则递归创建"""
|
|
54
|
+
# 标准化路径并分割
|
|
55
|
+
path_parts = remote_dir.strip('/').split('/')
|
|
56
|
+
current_path = ""
|
|
57
|
+
|
|
58
|
+
for part in path_parts:
|
|
59
|
+
if not part: # 跳过空部分
|
|
60
|
+
continue
|
|
61
|
+
current_path = f"{current_path}/{part}" if current_path else f"/{part}"
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# 尝试创建当前层级目录
|
|
65
|
+
self.sftp_client.mkdir(current_path)
|
|
66
|
+
logger.debug(f"创建远程目录: {current_path}")
|
|
67
|
+
except OSError:
|
|
68
|
+
# 目录可能已存在,验证是否存在且为目录
|
|
69
|
+
try:
|
|
70
|
+
stat_info = self.sftp_client.stat(current_path)
|
|
71
|
+
# 使用Python标准库的stat模块进行目录类型检查
|
|
72
|
+
|
|
73
|
+
if stat.S_ISDIR(stat_info.st_mode):
|
|
74
|
+
logger.debug(f"确认远程目录已存在: {current_path}")
|
|
75
|
+
else:
|
|
76
|
+
logger.warning(f"路径存在但不是目录: {current_path}")
|
|
77
|
+
return False
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
logger.warning(f"无法创建或确认远程目录: {current_path}")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def _pattern_remote_files(self, path_pattern: str) -> list:
|
|
85
|
+
"""
|
|
86
|
+
扫描远程目录,返回符合通配符模式的所有文件路径
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
path_pattern (str): 包含通配符的路径模式
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
list: 符合条件的文件路径列表
|
|
93
|
+
"""
|
|
94
|
+
import re
|
|
95
|
+
|
|
96
|
+
# 判断是否为绝对路径
|
|
97
|
+
is_absolute = path_pattern.startswith('/')
|
|
98
|
+
|
|
99
|
+
# 标准化路径(保留开头的/)
|
|
100
|
+
if is_absolute:
|
|
101
|
+
normalized_path = path_pattern.lstrip('/')
|
|
102
|
+
base_path = '/'
|
|
103
|
+
else:
|
|
104
|
+
normalized_path = path_pattern.lstrip('/')
|
|
105
|
+
base_path = '..'
|
|
106
|
+
|
|
107
|
+
if not normalized_path:
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
def match_pattern(text: str, pattern: str) -> bool:
|
|
111
|
+
"""通配符匹配"""
|
|
112
|
+
regex_pattern = pattern.replace('*', '.*').replace('?', '.')
|
|
113
|
+
regex_pattern = f"^{regex_pattern}$"
|
|
114
|
+
try:
|
|
115
|
+
return bool(re.match(regex_pattern, text))
|
|
116
|
+
except:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def expand_paths_recursive(segments: list, current_path: str) -> list:
|
|
120
|
+
"""递归展开路径段"""
|
|
121
|
+
if not segments:
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
segment = segments[0]
|
|
125
|
+
remaining_segments = segments[1:]
|
|
126
|
+
results = []
|
|
127
|
+
|
|
128
|
+
if '*' in segment or '?' in segment:
|
|
129
|
+
# 通配符段:列出当前目录并匹配
|
|
130
|
+
try:
|
|
131
|
+
items = self.sftp_client.listdir(current_path)
|
|
132
|
+
|
|
133
|
+
for item in items:
|
|
134
|
+
if match_pattern(item, segment):
|
|
135
|
+
# 构造完整路径
|
|
136
|
+
if current_path == '/' or current_path == '.':
|
|
137
|
+
full_path = f"{current_path}{item}" if current_path == '/' else item
|
|
138
|
+
else:
|
|
139
|
+
full_path = f"{current_path}/{item}"
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
stat_info = self.sftp_client.stat(full_path)
|
|
143
|
+
if stat.S_ISDIR(stat_info.st_mode):
|
|
144
|
+
# 是目录,继续递归
|
|
145
|
+
if remaining_segments:
|
|
146
|
+
results.extend(expand_paths_recursive(remaining_segments, full_path))
|
|
147
|
+
else:
|
|
148
|
+
# 没有更多段了,列出目录中的文件
|
|
149
|
+
dir_files = self.sftp_client.listdir(full_path)
|
|
150
|
+
for filename in dir_files:
|
|
151
|
+
if full_path == '/':
|
|
152
|
+
file_full_path = f"/{filename}"
|
|
153
|
+
else:
|
|
154
|
+
file_full_path = f"{full_path}/{filename}"
|
|
155
|
+
try:
|
|
156
|
+
file_stat = self.sftp_client.stat(file_full_path)
|
|
157
|
+
if stat.S_ISREG(file_stat.st_mode):
|
|
158
|
+
results.append(file_full_path)
|
|
159
|
+
except:
|
|
160
|
+
continue
|
|
161
|
+
elif stat.S_ISREG(stat_info.st_mode) and not remaining_segments:
|
|
162
|
+
# 是文件且是最后一段,直接添加
|
|
163
|
+
results.append(full_path)
|
|
164
|
+
except:
|
|
165
|
+
continue
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.warning(f"处理通配符段 {segment} 在路径 {current_path} 下失败: {e}")
|
|
168
|
+
else:
|
|
169
|
+
# 固定路径段
|
|
170
|
+
if current_path == '/' or current_path == '.':
|
|
171
|
+
next_path = f"{current_path}{segment}" if current_path == '/' else segment
|
|
172
|
+
else:
|
|
173
|
+
next_path = f"{current_path}/{segment}"
|
|
174
|
+
|
|
175
|
+
if remaining_segments:
|
|
176
|
+
results.extend(expand_paths_recursive(remaining_segments, next_path))
|
|
177
|
+
else:
|
|
178
|
+
# 最后一段,如果是目录则列出其中文件,如果是文件则直接返回
|
|
179
|
+
try:
|
|
180
|
+
stat_info = self.sftp_client.stat(next_path)
|
|
181
|
+
if stat.S_ISDIR(stat_info.st_mode):
|
|
182
|
+
# 列出目录中的文件
|
|
183
|
+
dir_files = self.sftp_client.listdir(next_path)
|
|
184
|
+
for filename in dir_files:
|
|
185
|
+
if next_path == '/':
|
|
186
|
+
file_full_path = f"/{filename}"
|
|
187
|
+
else:
|
|
188
|
+
file_full_path = f"{next_path}/{filename}"
|
|
189
|
+
try:
|
|
190
|
+
file_stat = self.sftp_client.stat(file_full_path)
|
|
191
|
+
if stat.S_ISREG(file_stat.st_mode):
|
|
192
|
+
results.append(file_full_path)
|
|
193
|
+
except:
|
|
194
|
+
continue
|
|
195
|
+
elif stat.S_ISREG(stat_info.st_mode):
|
|
196
|
+
results.append(next_path)
|
|
197
|
+
except:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
return results
|
|
201
|
+
|
|
202
|
+
# 处理完整路径
|
|
203
|
+
path_segments = normalized_path.split('/')
|
|
204
|
+
return expand_paths_recursive(path_segments, base_path)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def ssh_connect(self):
|
|
208
|
+
"""
|
|
209
|
+
连接到远程主机SSH
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
paramiko.SSHClient: SSH客户端实例
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
self._ssh_client = paramiko.SSHClient()
|
|
216
|
+
# 处理首次SSH连接确认流程
|
|
217
|
+
self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
218
|
+
|
|
219
|
+
self._ssh_client.connect(
|
|
220
|
+
hostname=self.host_ip,
|
|
221
|
+
port=self.port,
|
|
222
|
+
username=self.username,
|
|
223
|
+
password=self.password,
|
|
224
|
+
timeout=self.timeout
|
|
225
|
+
)
|
|
226
|
+
self.ssh_connected = True
|
|
227
|
+
logger.info(f"SSH连接成功:{self.host_ip} - {self.host_name}")
|
|
228
|
+
return self._ssh_client
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.error(f"SSH连接失败: :{self.host_ip} - {self.host_name}:{e}")
|
|
231
|
+
raise None
|
|
232
|
+
|
|
233
|
+
def sftp_connect(self):
|
|
234
|
+
"""
|
|
235
|
+
连接到远程主机SFTP
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
paramiko.SFTPClient: SFTP客户端实例
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
if not self._ssh_client:
|
|
242
|
+
self.ssh_connect()
|
|
243
|
+
|
|
244
|
+
self._sftp_client = self._ssh_client.open_sftp()
|
|
245
|
+
self.sftp_connected = True
|
|
246
|
+
logger.info(f"SFTP连接成功:{self.host_ip} - {self.host_name}")
|
|
247
|
+
return self._sftp_client
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.error(f"SFTP连接失败: {self.host_ip} - {self.host_name}:{e}")
|
|
250
|
+
raise None
|
|
251
|
+
|
|
252
|
+
def _execute_timeout_manage(self, stdin, stdout, stderr, timeout: int) -> dict:
|
|
253
|
+
"""
|
|
254
|
+
管理命令执行的超时和输出读取(类函数)
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
stdin: 标准输入通道
|
|
258
|
+
stdout: 标准输出通道
|
|
259
|
+
stderr: 错误输出通道
|
|
260
|
+
timeout (int): 超时时间(秒)
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
dict: 包含执行结果的字典,结构如下:
|
|
264
|
+
{
|
|
265
|
+
"stdout": "标准输出内容", # 命令的标准输出
|
|
266
|
+
"stderr": "错误输出内容", # 命令的错误输出
|
|
267
|
+
"exit_status": None, # 退出状态码(int),超时为None
|
|
268
|
+
"timeout": 30, # 超时时间设置(秒)
|
|
269
|
+
"do_break": False, # 是否主动断开(bool)
|
|
270
|
+
"error": "错误信息", # 异常时的错误描述
|
|
271
|
+
"result": False, # 执行结果(bool)
|
|
272
|
+
"channels_closed": True # 通道是否已关闭(bool,异常时存在)
|
|
273
|
+
}
|
|
274
|
+
"""
|
|
275
|
+
import time
|
|
276
|
+
|
|
277
|
+
response_dict = {
|
|
278
|
+
"stdout": "",
|
|
279
|
+
"stderr": "",
|
|
280
|
+
"exit_status": None,
|
|
281
|
+
"timeout": timeout,
|
|
282
|
+
"do_break":False,# 主动断开
|
|
283
|
+
"error":"",
|
|
284
|
+
"result":False
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
start_time = time.time()
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
while time.time() - start_time < timeout:
|
|
291
|
+
# 检查命令是否完成
|
|
292
|
+
if stdout.channel.exit_status_ready():
|
|
293
|
+
response_dict["exit_status"] = stdout.channel.recv_exit_status()
|
|
294
|
+
response_dict["stdout"] = stdout.read().decode('utf-8')
|
|
295
|
+
response_dict["stderr"] = stderr.read().decode('utf-8')
|
|
296
|
+
if response_dict["exit_status"] == 0:
|
|
297
|
+
response_dict["result"] = True
|
|
298
|
+
else:response_dict["result"] = False
|
|
299
|
+
break
|
|
300
|
+
|
|
301
|
+
# 非阻塞读取标准输出,超时时有用
|
|
302
|
+
if stdout.channel.recv_ready():
|
|
303
|
+
chunk = stdout.channel.recv(1024).decode('utf-8')
|
|
304
|
+
if chunk:
|
|
305
|
+
response_dict["stdout"] += chunk
|
|
306
|
+
else:
|
|
307
|
+
break
|
|
308
|
+
# 非阻塞读取错误输出,超时时有用
|
|
309
|
+
if stderr.channel.recv_stderr_ready():
|
|
310
|
+
chunk = stderr.channel.recv_stderr(1024).decode('utf-8')
|
|
311
|
+
if chunk:
|
|
312
|
+
response_dict["stderr"] += chunk
|
|
313
|
+
|
|
314
|
+
time.sleep(0.1)
|
|
315
|
+
|
|
316
|
+
# 超时处理
|
|
317
|
+
if time.time() - start_time >= timeout:
|
|
318
|
+
|
|
319
|
+
response_dict["error"] = f"命令执行超过设置时间: ({timeout}秒)"
|
|
320
|
+
response_dict["result"] = False
|
|
321
|
+
response_dict["do_break"] = True # 主动中断操作链接
|
|
322
|
+
|
|
323
|
+
# 关闭通道
|
|
324
|
+
self._close_channels_safely(stdout, stderr, stdin)
|
|
325
|
+
response_dict["channels_closed"] = True
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
response_dict["stderr"] = f"超时管理代码异常: {str(e)}"
|
|
329
|
+
logger.error(f"超时管理代码异常: {str(e)}")
|
|
330
|
+
self._close_channels_safely(stdout, stderr, stdin)
|
|
331
|
+
response_dict["channels_closed"] = True
|
|
332
|
+
|
|
333
|
+
return response_dict
|
|
334
|
+
|
|
335
|
+
def _close_channels_safely(self, stdout, stderr, stdin):
|
|
336
|
+
"""安全关闭SSH通道"""
|
|
337
|
+
channels = [stdout, stderr, stdin]
|
|
338
|
+
for channel_obj in channels:
|
|
339
|
+
try:
|
|
340
|
+
if hasattr(channel_obj, 'channel') and channel_obj.channel:
|
|
341
|
+
channel_obj.channel.close()
|
|
342
|
+
except:
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def execute_command(self, command: str, timeout: int = 30) -> dict:
|
|
347
|
+
"""
|
|
348
|
+
在远程主机上执行单个命令,支持超时控制
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
command (str): 要执行的命令
|
|
352
|
+
timeout (int): 超时时间(秒),默认30秒
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
dict: 包含执行结果的字典,结构如下:
|
|
356
|
+
{
|
|
357
|
+
"command": "执行的命令", # 执行的完整命令
|
|
358
|
+
"stdout": "标准输出内容", # 命令的标准输出
|
|
359
|
+
"stderr": "错误输出内容", # 命令的错误输出
|
|
360
|
+
"exit_status": 0, # 退出状态码(int),超时为None
|
|
361
|
+
"result": True, # 执行结果(bool)
|
|
362
|
+
"timeout": 30, # 超时时间设置(秒)
|
|
363
|
+
"do_break": False, # 是否主动断开(bool)
|
|
364
|
+
"error": "错误信息", # 异常时的错误描述
|
|
365
|
+
"channels_closed": True # 通道是否已关闭(bool)
|
|
366
|
+
}
|
|
367
|
+
"""
|
|
368
|
+
response_dict = dict()
|
|
369
|
+
response_dict["command"] = command
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
if not self.ssh_client:raise ConnectionError("SSH未连接")
|
|
373
|
+
|
|
374
|
+
stdin, stdout, stderr = self.ssh_client.exec_command(command)
|
|
375
|
+
|
|
376
|
+
# 管理命令执行的超时和输出读取
|
|
377
|
+
etm_response_dict = self._execute_timeout_manage(stdin, stdout, stderr, timeout)
|
|
378
|
+
# 合并字典
|
|
379
|
+
response_dict.update(etm_response_dict)
|
|
380
|
+
|
|
381
|
+
return response_dict
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def execute_multiple_commands(self, command_list: list,combine_str = "&&", timeout: int = 30) -> dict:
|
|
385
|
+
"""
|
|
386
|
+
在远程主机上执行多个命令,使用&&连接
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
command_list (list): 要执行的命令列表
|
|
390
|
+
cwd (str, optional): 工作目录,如果不为None,则在此目录下执行命令
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
dict: 包含执行结果的字典,结构如下:
|
|
394
|
+
{
|
|
395
|
+
"stdin": "", # 标准输入
|
|
396
|
+
"stdout": "", # 标准输出
|
|
397
|
+
"stderr": "", # 错误输出
|
|
398
|
+
"exit_status": 0, # 退出状态码(int)或None(异常时)
|
|
399
|
+
"command": "cmd1 && cmd2", # 执行的命令
|
|
400
|
+
"command_count": 2, # 命令数量
|
|
401
|
+
"command_list": ["cmd1", "cmd2"], # 命令列表
|
|
402
|
+
"result": True # 执行结果(bool)
|
|
403
|
+
}
|
|
404
|
+
"""
|
|
405
|
+
response_dict = dict()
|
|
406
|
+
response_dict["command_count"] = len(command_list)
|
|
407
|
+
response_dict["command"] = f" {combine_str}".join(command_list)
|
|
408
|
+
|
|
409
|
+
if not self.ssh_client:raise ConnectionError("SSH未连接")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
stdin, stdout, stderr = self.ssh_client.exec_command(response_dict["command"])
|
|
413
|
+
# 管理命令执行的超时和输出读取
|
|
414
|
+
etm_response_dict = self._execute_timeout_manage(stdin, stdout, stderr, timeout)
|
|
415
|
+
# 合并字典
|
|
416
|
+
response_dict.update(etm_response_dict)
|
|
417
|
+
|
|
418
|
+
return response_dict
|
|
419
|
+
|
|
420
|
+
def execute_shell_commands(self, commands: list, timeout: int = 30) -> dict:
|
|
421
|
+
"""
|
|
422
|
+
在同一个会话中执行多个命令,打包为shell脚本执行
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
commands (list): 要执行的命令列表
|
|
426
|
+
cwd (str, optional): 工作目录,如果不为None,则在此目录下执行命令
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
dict: 包含执行结果的字典,结构如下:
|
|
430
|
+
{
|
|
431
|
+
"stdin": "", # 标准输入
|
|
432
|
+
"stdout": "", # 标准输出
|
|
433
|
+
"stderr": "", # 错误输出
|
|
434
|
+
"exit_status": 0, # 退出状态码(int)
|
|
435
|
+
"command": "bash script", # 执行的完整脚本命令
|
|
436
|
+
"command_list": ["cmd1", "cmd2"], # 原始命令列表
|
|
437
|
+
"result": True # 执行结果(bool)
|
|
438
|
+
}
|
|
439
|
+
"""
|
|
440
|
+
if not self.ssh_client: raise ConnectionError("SSH未连接")
|
|
441
|
+
|
|
442
|
+
response_dict = dict()
|
|
443
|
+
# 将命令转换为脚本内容
|
|
444
|
+
script_lines = ["#!/bin/bash"]
|
|
445
|
+
script_lines.extend(commands)
|
|
446
|
+
script_content = "\n".join(script_lines)
|
|
447
|
+
# 使用heredoc方式执行脚本内容
|
|
448
|
+
combined_command = f'bash -s << "EOF"\n{script_content}\nEOF'
|
|
449
|
+
response_dict["command"] = combined_command
|
|
450
|
+
response_dict["command_list"] = commands
|
|
451
|
+
|
|
452
|
+
stdin, stdout, stderr = self.ssh_client.exec_command(combined_command)
|
|
453
|
+
# 管理命令执行的超时和输出读取
|
|
454
|
+
etm_response_dict = self._execute_timeout_manage(stdin, stdout, stderr, timeout)
|
|
455
|
+
# 合并字典
|
|
456
|
+
response_dict.update(etm_response_dict)
|
|
457
|
+
|
|
458
|
+
return response_dict
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def commands_to_shell_trans(self, command_list: list = None, command_str: str = None,delete_local_shell: bool = True) -> dict:
|
|
462
|
+
"""
|
|
463
|
+
将命令生成为shell文件并传输到远程主机,但不执行
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
command_list (list, optional): 命令列表
|
|
467
|
+
command_str (str, optional): 命令字符串
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
dict: 包含传输结果的字典,结构如下:
|
|
471
|
+
{
|
|
472
|
+
"file_name": "operation_xxx.sh", # 生成的shell文件名
|
|
473
|
+
"uuid": "uuid-string", # 操作唯一标识
|
|
474
|
+
"remote_path": "/home/user/file.sh", # 远程文件路径
|
|
475
|
+
"result": True, # 传输结果(bool)
|
|
476
|
+
"upload_response": {...} # 上传响应详情
|
|
477
|
+
}
|
|
478
|
+
"""
|
|
479
|
+
response_dict = dict()
|
|
480
|
+
response_dict["result"] = False
|
|
481
|
+
operation_uuid = str(uuid.uuid4())
|
|
482
|
+
file_name = "operation_" + operation_uuid + ".sh"
|
|
483
|
+
file_ready = False
|
|
484
|
+
# 分两种情况写shell文件
|
|
485
|
+
if command_list:
|
|
486
|
+
with open(file_name, "w") as f:
|
|
487
|
+
for command in command_list:
|
|
488
|
+
f.write(command + "\n")
|
|
489
|
+
file_ready = True
|
|
490
|
+
elif command_str:
|
|
491
|
+
command_str = command_str.replace("\r", "\n")# 处理可能存在的win换行符问题
|
|
492
|
+
with open(file_name, "w",newline="\n") as f:
|
|
493
|
+
f.write(command_str)
|
|
494
|
+
file_ready = True
|
|
495
|
+
|
|
496
|
+
if file_ready:
|
|
497
|
+
logger.debug(f"命令封装执行前流程,shell操作文件封装完毕:{file_name}")
|
|
498
|
+
remote_path = f"/home/{self.username}/{file_name}"
|
|
499
|
+
response_dict["file_name"] = file_name
|
|
500
|
+
response_dict["uuid"] = operation_uuid
|
|
501
|
+
response_dict["remote_path"] = remote_path
|
|
502
|
+
response_dict["result"] = True
|
|
503
|
+
|
|
504
|
+
# 上传后操作状态更新为上传操作
|
|
505
|
+
logger.debug(f"命令封装执行前流程,开始上传shell操作文件{file_name}到远程主机{self.host_name},目标远端目录:{remote_path}")
|
|
506
|
+
upload_response = self.sftp_upload_file(file_name, remote_path)
|
|
507
|
+
response_dict["upload_response"] = upload_response
|
|
508
|
+
response_dict["result"] = response_dict["result"] and upload_response["result"]
|
|
509
|
+
if delete_local_shell:os.remove(file_name)
|
|
510
|
+
if response_dict["result"]:
|
|
511
|
+
logger.debug(f"命令封装执行前流程,上传shell操作文件{file_name}到远程主机{self.host_name}成功")
|
|
512
|
+
else:
|
|
513
|
+
logger.warning(f"命令封装执行前流程,上传shell操作文件{file_name}到远程主机{self.host_name}失败,请检查反馈异常为:{upload_response['error']}")
|
|
514
|
+
else:
|
|
515
|
+
logger.warning(f"命令封装执行前流程,shell操作文件封装失败,未提供命令列表或命令字符串")
|
|
516
|
+
return response_dict
|
|
517
|
+
|
|
518
|
+
def commands_to_shell_run(self, trans_result: dict, sudo_to_user = False, timeout: int = 30) -> dict:
|
|
519
|
+
"""
|
|
520
|
+
运行已传输的shell文件
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
trans_result (dict): commands_to_shell_trans函数的返回结果
|
|
524
|
+
sudo_to_user (str, optional): 切换到指定用户执行,默认为False(不切换)
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
dict: 包含执行结果的字典,结构与execute_command一致,额外包含:
|
|
528
|
+
{
|
|
529
|
+
"remote_path": "/path/to/script.sh", # 执行的脚本路径
|
|
530
|
+
"file_name": "script.sh" # 脚本文件名
|
|
531
|
+
}
|
|
532
|
+
"""
|
|
533
|
+
shell_path = trans_result["remote_path"]
|
|
534
|
+
file_name = trans_result["file_name"]
|
|
535
|
+
if sudo_to_user:
|
|
536
|
+
logger.debug(f"命令封装执行前流程,触发sudo切换执行操作")
|
|
537
|
+
# 执行转移和变更操作
|
|
538
|
+
sudoers_shell_path = f"/home/{sudo_to_user}/{file_name}"
|
|
539
|
+
logger.info(f"命令封装执行后流程,开始将shell文件{shell_path}转移至用户{sudo_to_user},目标路径为:{sudoers_shell_path}")
|
|
540
|
+
mv_result = self.execute_command(f'sudo su - root -c "chown {sudo_to_user} {shell_path};mv {shell_path} {sudoers_shell_path}"')
|
|
541
|
+
if not mv_result["result"]:
|
|
542
|
+
logger.warning(f"命令封装执行后流程,将shell文件{shell_path}转移至用户{sudo_to_user}失败,请检查反馈异常为:{mv_result['stderr']}")
|
|
543
|
+
return mv_result
|
|
544
|
+
logger.debug(f"命令封装执行后流程,将shell文件{shell_path}转移至用户{sudo_to_user}成功")
|
|
545
|
+
|
|
546
|
+
response_dict = self.execute_command(f'sudo su - {sudo_to_user} -c "sh {sudoers_shell_path}"', timeout= timeout)
|
|
547
|
+
response_dict["remote_path"] = sudoers_shell_path
|
|
548
|
+
response_dict["file_name"] = file_name
|
|
549
|
+
logger.debug(f"命令封装执行后流程,sudo执行shell文件{sudoers_shell_path}")
|
|
550
|
+
|
|
551
|
+
self.execute_command(f'sudo su - root -c "rm -f {sudoers_shell_path}"')
|
|
552
|
+
logger.debug(f"命令封装执行后流程,sudo删除shell文件{sudoers_shell_path}")
|
|
553
|
+
else:
|
|
554
|
+
logger.debug(f"命令封装执行后流程,常规运行流程")
|
|
555
|
+
response_dict = self.execute_command(f"sh {shell_path}", timeout= timeout)
|
|
556
|
+
response_dict["remote_path"] = shell_path
|
|
557
|
+
response_dict["file_name"] = file_name
|
|
558
|
+
logger.debug(f"命令封装执行后流程,执行shell文件{shell_path}")
|
|
559
|
+
|
|
560
|
+
self.execute_command(f"rm -f {shell_path}")
|
|
561
|
+
logger.debug(f"命令封装执行后流程,删除shell文件{shell_path}")
|
|
562
|
+
return response_dict
|
|
563
|
+
|
|
564
|
+
def sftp_upload_file(self, local_path: str, remote_path: str = "") -> dict:
|
|
565
|
+
"""
|
|
566
|
+
上传单个文件到远程主机(不支持通配符)
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
local_path (str): 本地文件路径
|
|
570
|
+
remote_path (str, optional): 远程文件路径,为空时使用本地文件名
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
dict: 包含上传结果的字典,结构如下:
|
|
574
|
+
{
|
|
575
|
+
"local_path": "/local/file.txt", # 本地文件路径
|
|
576
|
+
"remote_path": "/remote/file.txt", # 远程文件路径
|
|
577
|
+
"result": True, # 上传结果(bool)
|
|
578
|
+
"file_size": 1024, # 文件大小(bytes)
|
|
579
|
+
"error": "" # 错误信息(失败时存在)
|
|
580
|
+
}
|
|
581
|
+
"""
|
|
582
|
+
response_dict = {
|
|
583
|
+
"local_path": local_path,
|
|
584
|
+
"remote_path": remote_path,
|
|
585
|
+
"result": False
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
# 本地文件验证
|
|
589
|
+
if not os.path.exists(local_path):
|
|
590
|
+
response_dict["error"] = f"本地文件不存在: {local_path}"
|
|
591
|
+
logger.warning(response_dict["error"])
|
|
592
|
+
return response_dict
|
|
593
|
+
|
|
594
|
+
if not os.path.isfile(local_path):
|
|
595
|
+
response_dict["error"] = f"指定路径不是文件: {local_path}"
|
|
596
|
+
logger.warning(response_dict["error"])
|
|
597
|
+
return response_dict
|
|
598
|
+
|
|
599
|
+
try:
|
|
600
|
+
sftp = self.sftp_client
|
|
601
|
+
# 执行上传
|
|
602
|
+
sftp.put(local_path, remote_path)
|
|
603
|
+
response_dict["result"] = True
|
|
604
|
+
response_dict["file_size"] = os.path.getsize(local_path)
|
|
605
|
+
|
|
606
|
+
logger.info(f"文件上传成功: {local_path} -> {remote_path}")
|
|
607
|
+
|
|
608
|
+
except Exception as e:
|
|
609
|
+
response_dict["result"] = False
|
|
610
|
+
response_dict["error"] = str(e)
|
|
611
|
+
logger.error(f"文件上传失败: {local_path} -> {remote_path}, 错误: {e}")
|
|
612
|
+
|
|
613
|
+
return response_dict
|
|
614
|
+
|
|
615
|
+
def sftp_upload_to(self, local_path: str, remote_dir: str = "") -> dict:
|
|
616
|
+
"""
|
|
617
|
+
支持通配符的批量SFTP上传方法
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
local_path (str): 本地路径,支持通配符模式(*, ?, **)
|
|
621
|
+
remote_dir (str, optional): 远程目录路径,为空时使用本地文件名
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
dict: 包含上传结果的字典,结构如下:
|
|
625
|
+
{
|
|
626
|
+
"local_pattern": "*.txt", # 本地匹配模式
|
|
627
|
+
"remote_base": "/remote/dir/", # 远程基础路径
|
|
628
|
+
"uploaded_files": [ # 成功上传的文件列表
|
|
629
|
+
{
|
|
630
|
+
"local_path": "/local/file.txt",
|
|
631
|
+
"remote_path": "/remote/file.txt",
|
|
632
|
+
"result": True
|
|
633
|
+
}
|
|
634
|
+
],
|
|
635
|
+
"failed_files": [ # 上传失败的文件列表
|
|
636
|
+
{
|
|
637
|
+
"local_path": "/local/failed.txt",
|
|
638
|
+
"remote_path": "/remote/failed.txt",
|
|
639
|
+
"error": "error message",
|
|
640
|
+
"result": False
|
|
641
|
+
}
|
|
642
|
+
],
|
|
643
|
+
"result": True # 总体结果(bool)
|
|
644
|
+
}
|
|
645
|
+
"""
|
|
646
|
+
import glob
|
|
647
|
+
import os
|
|
648
|
+
|
|
649
|
+
# 解析通配符,获取所有匹配的本地文件
|
|
650
|
+
matched_files = glob.glob(local_path, recursive=True)
|
|
651
|
+
|
|
652
|
+
response_dict = {
|
|
653
|
+
"local_pattern": local_path,
|
|
654
|
+
"remote_base": remote_dir,
|
|
655
|
+
"uploaded_files": [],
|
|
656
|
+
"failed_files": [],
|
|
657
|
+
"result": True
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
# 遍历所有匹配的文件进行上传
|
|
661
|
+
for local_file in matched_files:
|
|
662
|
+
# 检查是否为文件(而不是目录)
|
|
663
|
+
if os.path.isfile(local_file):
|
|
664
|
+
# 计算目标远程路径
|
|
665
|
+
if remote_dir:
|
|
666
|
+
# 如果指定了远程路径
|
|
667
|
+
remote_path = os.path.join(remote_dir, os.path.basename(local_file)).replace("\\", "/")
|
|
668
|
+
else:
|
|
669
|
+
# 如果未指定远程路径,则使用本地文件名
|
|
670
|
+
remote_path = os.path.basename(local_file)
|
|
671
|
+
|
|
672
|
+
# 确保远程目录存在
|
|
673
|
+
if remote_dir:
|
|
674
|
+
if not self._ensure_remote_directory(remote_dir):
|
|
675
|
+
response_dict["failed_files"].append({
|
|
676
|
+
"local_path": local_file,
|
|
677
|
+
"remote_path": remote_path,
|
|
678
|
+
"error": "无法创建远程目录,请检查是否是账户权限和目标路径不匹配",
|
|
679
|
+
"result": False
|
|
680
|
+
})
|
|
681
|
+
response_dict["result"] = False
|
|
682
|
+
logger.warning(f"无法创建远程目录: {remote_dir},请检查是否是账户权限和目标路径不匹配")
|
|
683
|
+
continue
|
|
684
|
+
|
|
685
|
+
# 执行上传
|
|
686
|
+
try:
|
|
687
|
+
sftp = self.sftp_client
|
|
688
|
+
sftp.put(local_file, remote_path)
|
|
689
|
+
response_dict["uploaded_files"].append({
|
|
690
|
+
"local_path": local_file,
|
|
691
|
+
"remote_path": remote_path,
|
|
692
|
+
"result": True
|
|
693
|
+
})
|
|
694
|
+
except Exception as e:
|
|
695
|
+
response_dict["failed_files"].append({
|
|
696
|
+
"local_path": local_file,
|
|
697
|
+
"remote_path": remote_path,
|
|
698
|
+
"error": str(e),
|
|
699
|
+
"result": False
|
|
700
|
+
})
|
|
701
|
+
response_dict["result"] = False
|
|
702
|
+
|
|
703
|
+
return response_dict
|
|
704
|
+
|
|
705
|
+
def sftp_download_file(self, remote_path: str, local_path: str) -> dict:
|
|
706
|
+
"""
|
|
707
|
+
从远程主机下载文件
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
remote_path (str): 远程文件路径
|
|
711
|
+
local_path (str): 本地文件路径
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
dict: 包含下载结果的字典,结构如下:
|
|
715
|
+
{
|
|
716
|
+
"remote_path": "/remote/file.txt", # 远程文件路径
|
|
717
|
+
"local_path": "/local/file.txt", # 本地文件路径
|
|
718
|
+
"result": True, # 下载结果(bool)
|
|
719
|
+
"error": "" # 错误信息(失败时存在)
|
|
720
|
+
}
|
|
721
|
+
"""
|
|
722
|
+
response_dict = dict()
|
|
723
|
+
response_dict["remote_path"] = remote_path
|
|
724
|
+
response_dict["local_path"] = local_path
|
|
725
|
+
|
|
726
|
+
try:
|
|
727
|
+
sftp = self.sftp_client
|
|
728
|
+
sftp.get(remote_path, local_path)
|
|
729
|
+
response_dict["result"] = True
|
|
730
|
+
except Exception as e:
|
|
731
|
+
response_dict["result"] = False
|
|
732
|
+
response_dict["error"] = str(e)
|
|
733
|
+
return response_dict
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def sftp_download_to(self, remote_path: str, local_dir: str = "") -> dict:
|
|
738
|
+
"""
|
|
739
|
+
支持多级通配符的批量SFTP下载方法
|
|
740
|
+
|
|
741
|
+
Args:
|
|
742
|
+
remote_path (str): 远程路径,支持多级通配符模式(*, ?, **)
|
|
743
|
+
local_dir (str, optional): 本地目录路径,为空时使用远程文件名
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
dict: 包含下载结果的字典,结构如下:
|
|
747
|
+
{
|
|
748
|
+
"remote_pattern": "*.txt", # 远程匹配模式
|
|
749
|
+
"local_base": "/local/dir/", # 本地基础路径
|
|
750
|
+
"downloaded_files": [ # 成功下载的文件列表
|
|
751
|
+
{
|
|
752
|
+
"remote_path": "/remote/file.txt",
|
|
753
|
+
"local_path": "/local/file.txt",
|
|
754
|
+
"result": True
|
|
755
|
+
}
|
|
756
|
+
],
|
|
757
|
+
"failed_files": [ # 下载失败的文件列表
|
|
758
|
+
{
|
|
759
|
+
"remote_path": "/remote/failed.txt",
|
|
760
|
+
"local_path": "/local/failed.txt",
|
|
761
|
+
"error": "error message",
|
|
762
|
+
"result": False
|
|
763
|
+
}
|
|
764
|
+
],
|
|
765
|
+
"result": True # 总体结果(bool)
|
|
766
|
+
}
|
|
767
|
+
"""
|
|
768
|
+
import os
|
|
769
|
+
|
|
770
|
+
response_dict = {
|
|
771
|
+
"remote_pattern": remote_path,
|
|
772
|
+
"local_base": local_dir,
|
|
773
|
+
"downloaded_files": [],
|
|
774
|
+
"failed_files": [],
|
|
775
|
+
"result": True
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
try:
|
|
779
|
+
sftp = self.sftp_client
|
|
780
|
+
|
|
781
|
+
# 获取所有匹配的远程文件路径
|
|
782
|
+
matched_files = self._pattern_remote_files(remote_path)
|
|
783
|
+
|
|
784
|
+
# 遍历所有匹配的文件进行下载
|
|
785
|
+
for remote_file_path in matched_files:
|
|
786
|
+
try:
|
|
787
|
+
# 计算目标本地路径
|
|
788
|
+
if local_dir:
|
|
789
|
+
local_file_path = os.path.join(local_dir, os.path.basename(remote_file_path))
|
|
790
|
+
else:
|
|
791
|
+
local_file_path = os.path.basename(remote_file_path)
|
|
792
|
+
|
|
793
|
+
# 确保本地目录存在
|
|
794
|
+
if local_dir and not os.path.exists(local_dir):
|
|
795
|
+
os.makedirs(local_dir, exist_ok=True)
|
|
796
|
+
|
|
797
|
+
# 执行下载
|
|
798
|
+
sftp.get(remote_file_path, local_file_path)
|
|
799
|
+
response_dict["downloaded_files"].append({
|
|
800
|
+
"remote_path": remote_file_path,
|
|
801
|
+
"local_path": local_file_path,
|
|
802
|
+
"result": True
|
|
803
|
+
})
|
|
804
|
+
logger.info(f"文件下载成功: {remote_file_path} -> {local_file_path}")
|
|
805
|
+
|
|
806
|
+
except Exception as e:
|
|
807
|
+
response_dict["failed_files"].append({
|
|
808
|
+
"remote_path": remote_file_path,
|
|
809
|
+
"local_path": local_file_path if 'local_file_path' in locals() else "",
|
|
810
|
+
"error": str(e),
|
|
811
|
+
"result": False
|
|
812
|
+
})
|
|
813
|
+
response_dict["result"] = False
|
|
814
|
+
logger.error(f"文件下载失败: {remote_file_path}, 错误: {e}")
|
|
815
|
+
|
|
816
|
+
except Exception as e:
|
|
817
|
+
response_dict["result"] = False
|
|
818
|
+
response_dict["error"] = str(e)
|
|
819
|
+
logger.error(f"批量下载初始化失败: {e}")
|
|
820
|
+
|
|
821
|
+
return response_dict
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def close(self):
|
|
825
|
+
"""关闭SSH和SFTP连接"""
|
|
826
|
+
if self._sftp_client:
|
|
827
|
+
self._sftp_client.close()
|
|
828
|
+
self.sftp_connected = False
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import datetime
|
|
3
|
+
import os
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup_logging(console_level="INFO", file_level="DEBUG", log_dir="batch_record"):
|
|
10
|
+
|
|
11
|
+
"""配置整个应用的日志系统"""
|
|
12
|
+
# 获取根记录器
|
|
13
|
+
root_logger = logging.getLogger()
|
|
14
|
+
|
|
15
|
+
# 避免重复添加处理器
|
|
16
|
+
# 也保证了在一次链式调用中,只会产生一个日志文件
|
|
17
|
+
if root_logger.handlers:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
timestr = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
21
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
22
|
+
log_path = os.path.join(log_dir, f'batch-{timestr}.log')
|
|
23
|
+
|
|
24
|
+
# 设置根记录器的级别为最低,确保消息能传递到处理器
|
|
25
|
+
level_dict = {
|
|
26
|
+
"INFO":logging.INFO,
|
|
27
|
+
"DEBUG":logging.DEBUG,
|
|
28
|
+
"WARNING":logging.WARNING,
|
|
29
|
+
"ERROR":logging.ERROR,
|
|
30
|
+
"CRITICAL":logging.CRITICAL
|
|
31
|
+
}
|
|
32
|
+
root_logger.setLevel(min(level_dict.get(console_level), level_dict.get(file_level)))
|
|
33
|
+
|
|
34
|
+
# 控制台输出日志,简化版本
|
|
35
|
+
console_formatter = logging.Formatter("%(levelname)s - %(name)s - %(message)s")
|
|
36
|
+
console_handler = logging.StreamHandler()
|
|
37
|
+
console_handler.setLevel(level_dict.get(console_level))
|
|
38
|
+
console_handler.setFormatter(console_formatter)
|
|
39
|
+
# 记录到文件的使用详细格式化的日志版本
|
|
40
|
+
file_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")
|
|
41
|
+
file_handler = logging.FileHandler(log_path, mode='a', encoding='utf-8')
|
|
42
|
+
file_handler.setLevel(level_dict.get(file_level))
|
|
43
|
+
file_handler.setFormatter(file_formatter)
|
|
44
|
+
|
|
45
|
+
root_logger.addHandler(console_handler)
|
|
46
|
+
root_logger.addHandler(file_handler)
|
|
47
|
+
|
|
48
|
+
# 顺手关掉第三方库的日志
|
|
49
|
+
logging.getLogger("paramiko").setLevel(logging.CRITICAL)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_logger(name):
|
|
54
|
+
"""获取预配置的日志记录器"""
|
|
55
|
+
logger_instance = logging.getLogger(name)
|
|
56
|
+
# 根记录器已经配置,不需要额外配置
|
|
57
|
+
return logger_instance
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ginkgo-tools-batchssh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 批量SSH操作和文件传输工具包
|
|
5
|
+
Author-email: 霍城 <495466557@qq.com>
|
|
6
|
+
Maintainer-email: 霍城 <495466557@qq.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: ssh,sftp,batch,automation,remote
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: paramiko>=2.7.0
|
|
25
|
+
|
|
26
|
+
# GINKGO-TOOLS-BATCHSSH
|
|
27
|
+
|
|
28
|
+
一个用于批量SSH操作和文件传输的Python工具包。
|
|
29
|
+
|
|
30
|
+
## 功能特性
|
|
31
|
+
|
|
32
|
+
- 🔧 **SSH连接管理** - 简化SSH连接建立和管理
|
|
33
|
+
- ⚡ **远程命令执行** - 支持超时控制的远程命令执行
|
|
34
|
+
- 📁 **SFTP文件传输** - 支持通配符的批量文件上传下载
|
|
35
|
+
- 🎯 **批量主机操作** - 支持多主机并行操作
|
|
36
|
+
- 📝 **智能日志系统** - 自动记录操作日志便于追踪
|
|
37
|
+
- 🛠️ **便捷工具函数** - 提供常用的主机管理和过滤工具
|
|
38
|
+
|
|
39
|
+
## 安装
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install ginkgo_tools_batchssh
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 快速开始
|
|
46
|
+
|
|
47
|
+
### 基本SSH连接和命令执行
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from ginkgo_tools_batchssh import SSHWrapper
|
|
51
|
+
|
|
52
|
+
# 创建SSH连接
|
|
53
|
+
ssh = SSHWrapper(
|
|
54
|
+
host_ip="192.168.1.100",
|
|
55
|
+
username="your_username",
|
|
56
|
+
password="your_password"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# 执行单个命令
|
|
60
|
+
result = ssh.execute_command("ls -la")
|
|
61
|
+
print(f"执行结果: {result['result']}")
|
|
62
|
+
print(f"输出内容: {result['stdout']}")
|
|
63
|
+
|
|
64
|
+
# 执行多个命令
|
|
65
|
+
commands = ["cd /tmp", "pwd", "ls"]
|
|
66
|
+
result = ssh.execute_shell_commands(commands)
|
|
67
|
+
print(result['stdout'])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 文件传输操作
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# 上传单个文件
|
|
74
|
+
upload_result = ssh.sftp_upload_file("local_file.txt", "/remote/path/file.txt")
|
|
75
|
+
|
|
76
|
+
# 批量上传文件(支持通配符)
|
|
77
|
+
upload_result = ssh.sftp_upload_to("*.log", "/remote/logs/")
|
|
78
|
+
|
|
79
|
+
# 下载文件
|
|
80
|
+
download_result = ssh.sftp_download_file("/remote/file.txt", "local_file.txt")
|
|
81
|
+
|
|
82
|
+
# 批量下载文件(支持通配符)
|
|
83
|
+
download_result = ssh.sftp_download_to("/remote/logs/*.log", "./local_logs/")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 主机管理工具
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from ginkgo_tools_batchssh import read_hosts_file, parse_and_filter_hosts
|
|
90
|
+
|
|
91
|
+
# 读取hosts文件
|
|
92
|
+
hosts_content = read_hosts_file()
|
|
93
|
+
|
|
94
|
+
# 过滤和解析主机
|
|
95
|
+
filter_list = ["web", "server"] # 只选择包含这些关键词的主机
|
|
96
|
+
target_hosts = parse_and_filter_hosts(hosts_content, filter_list)
|
|
97
|
+
|
|
98
|
+
# target_hosts 格式: {"ip_address": ["hostname1", "hostname2"]}
|
|
99
|
+
for ip, hostnames in target_hosts.items():
|
|
100
|
+
print(f"{ip}: {', '.join(hostnames)}")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 核心组件
|
|
104
|
+
|
|
105
|
+
### SSHWrapper 类
|
|
106
|
+
|
|
107
|
+
主要的SSH操作类,提供以下核心功能:
|
|
108
|
+
|
|
109
|
+
- `ssh_connect()` - 建立SSH连接
|
|
110
|
+
- `execute_command()` - 执行单个命令
|
|
111
|
+
- `execute_shell_commands()` - 执行多个命令
|
|
112
|
+
- `sftp_upload_*()` - 文件上传相关方法
|
|
113
|
+
- `sftp_download_*()` - 文件下载相关方法
|
|
114
|
+
- `commands_to_shell_*()` - Shell脚本执行相关方法
|
|
115
|
+
|
|
116
|
+
### 便捷工具函数
|
|
117
|
+
|
|
118
|
+
- `read_hosts_file()` - 读取hosts文件内容
|
|
119
|
+
- `parse_and_filter_hosts()` - 解析和过滤主机列表
|
|
120
|
+
|
|
121
|
+
### 日志系统
|
|
122
|
+
|
|
123
|
+
- `setup_logging()` - 配置日志系统
|
|
124
|
+
- `get_logger()` - 获取日志记录器
|
|
125
|
+
|
|
126
|
+
## 高级用法
|
|
127
|
+
|
|
128
|
+
### 超时控制
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
# 设置命令执行超时时间(秒)
|
|
132
|
+
result = ssh.execute_command("sleep 60", timeout=30)
|
|
133
|
+
if result['do_break']:
|
|
134
|
+
print("命令执行超时")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Shell脚本执行
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
# 将命令封装为shell脚本并执行
|
|
141
|
+
commands = [
|
|
142
|
+
"echo '开始执行任务'",
|
|
143
|
+
"date",
|
|
144
|
+
"ls -la"
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# 传输脚本文件
|
|
148
|
+
trans_result = ssh.commands_to_shell_trans(command_list=commands)
|
|
149
|
+
|
|
150
|
+
# 执行脚本(可选sudo切换用户)
|
|
151
|
+
exec_result = ssh.commands_to_shell_run(trans_result, sudo_to_user="root")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 批量主机操作示例
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from ginkgo_tools_batchssh import SSHWrapper, read_hosts_file, parse_and_filter_hosts
|
|
158
|
+
|
|
159
|
+
# 获取目标主机列表
|
|
160
|
+
hosts_content = read_hosts_file()
|
|
161
|
+
target_hosts = parse_and_filter_hosts(hosts_content, ["production"])
|
|
162
|
+
|
|
163
|
+
# 批量执行操作
|
|
164
|
+
results = {}
|
|
165
|
+
for ip, hostnames in target_hosts.items():
|
|
166
|
+
try:
|
|
167
|
+
ssh = SSHWrapper(
|
|
168
|
+
host_ip=ip,
|
|
169
|
+
username="admin",
|
|
170
|
+
password="password",
|
|
171
|
+
host_name=hostnames[0]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
result = ssh.execute_command("uptime")
|
|
175
|
+
results[ip] = result
|
|
176
|
+
|
|
177
|
+
ssh.close()
|
|
178
|
+
except Exception as e:
|
|
179
|
+
results[ip] = {"result": False, "error": str(e)}
|
|
180
|
+
|
|
181
|
+
# 查看结果
|
|
182
|
+
for ip, result in results.items():
|
|
183
|
+
if result['result']:
|
|
184
|
+
print(f"{ip}: {result['stdout'].strip()}")
|
|
185
|
+
else:
|
|
186
|
+
print(f"{ip}: 执行失败 - {result.get('error', '未知错误')}")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 返回值格式
|
|
190
|
+
|
|
191
|
+
大多数方法返回统一的字典格式:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
{
|
|
195
|
+
"result": True/False, # 操作是否成功
|
|
196
|
+
"stdout": "输出内容", # 标准输出
|
|
197
|
+
"stderr": "错误内容", # 错误输出
|
|
198
|
+
"exit_status": 0, # 退出状态码
|
|
199
|
+
"error": "错误信息", # 错误描述(失败时)
|
|
200
|
+
# 其他特定字段...
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## 依赖要求
|
|
205
|
+
|
|
206
|
+
- Python >= 3.8
|
|
207
|
+
- paramiko >= 2.7.0
|
|
208
|
+
|
|
209
|
+
## 许可证
|
|
210
|
+
|
|
211
|
+
MIT License
|
|
212
|
+
|
|
213
|
+
## 作者
|
|
214
|
+
|
|
215
|
+
霍城 <495466557@qq.com>
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
*注意:请根据实际使用场景调整连接参数和安全设置*
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ginkgo_tools_batchssh/__init__.py,sha256=S01AOuZJzGcIWh9XU_KL1xqHGh-xgmX44Rus8HwHGW4,1126
|
|
2
|
+
ginkgo_tools_batchssh/moduel_convenient_tools.py,sha256=srhVAx43J8gl9RVK3ZYWolJ6HgPEi0claxgPZwRu-zU,1653
|
|
3
|
+
ginkgo_tools_batchssh/module_SSHwrapper.py,sha256=-yJe7X7iKIyjsoAdowUoiGDNCtOn1z6ISXlrBgYb4ho,35597
|
|
4
|
+
ginkgo_tools_batchssh/module_logger.py,sha256=SFnxCKgyFZmzUl3Tr7nMrNdEkiotjU2cN0oCTZEgWuo,2039
|
|
5
|
+
ginkgo_tools_batchssh-0.1.0.dist-info/METADATA,sha256=MV52rpktac3X7vBpnZ35ex9lrIYOEaxe4ZpGdPEWO1Q,5938
|
|
6
|
+
ginkgo_tools_batchssh-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
7
|
+
ginkgo_tools_batchssh-0.1.0.dist-info/top_level.txt,sha256=LRf7tgGqGRB41Cit0RPETr5gjljt8IS2k5bl2SEPKxY,22
|
|
8
|
+
ginkgo_tools_batchssh-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ginkgo_tools_batchssh
|