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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ ginkgo_tools_batchssh