remote-cmd-manager 1.0.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.
- remote_cmd/__init__.py +125 -0
- remote_cmd/cli/__init__.py +1 -0
- remote_cmd/cli/main.py +434 -0
- remote_cmd/core/__init__.py +1 -0
- remote_cmd/core/async_client.py +349 -0
- remote_cmd/core/host.py +107 -0
- remote_cmd/core/host_manager.py +159 -0
- remote_cmd/core/ssh_client.py +644 -0
- remote_cmd/repository/__init__.py +4 -0
- remote_cmd/repository/host_repository.py +57 -0
- remote_cmd/repository/json_host_repository.py +181 -0
- remote_cmd/repository/sqlite_host_repository.py +385 -0
- remote_cmd/service/__init__.py +17 -0
- remote_cmd/service/batch_executor.py +317 -0
- remote_cmd/service/credential_provider.py +196 -0
- remote_cmd/service/host_service.py +256 -0
- remote_cmd/service/ssh_service.py +111 -0
- remote_cmd/service/task_runner.py +423 -0
- remote_cmd/utils/__init__.py +1 -0
- remote_cmd/utils/config.py +200 -0
- remote_cmd/utils/crypto.py +137 -0
- remote_cmd/utils/exceptions.py +133 -0
- remote_cmd/utils/logging_utils.py +194 -0
- remote_cmd_manager-1.0.0.dist-info/METADATA +694 -0
- remote_cmd_manager-1.0.0.dist-info/RECORD +29 -0
- remote_cmd_manager-1.0.0.dist-info/WHEEL +5 -0
- remote_cmd_manager-1.0.0.dist-info/entry_points.txt +2 -0
- remote_cmd_manager-1.0.0.dist-info/licenses/LICENSE +21 -0
- remote_cmd_manager-1.0.0.dist-info/top_level.txt +1 -0
remote_cmd/__init__.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Remote CMD - SSH 远程服务器管理工具
|
|
3
|
+
|
|
4
|
+
一个功能强大的 Python 库,用于管理远程服务器。
|
|
5
|
+
提供简洁的 API 用于 SSH 连接、命令执行和文件传输。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- SSH 连接管理(支持密码和密钥认证)
|
|
9
|
+
- 远程命令执行(包括 sudo 命令)
|
|
10
|
+
- 文件上传和下载
|
|
11
|
+
- 主机配置管理
|
|
12
|
+
- 标签分类系统
|
|
13
|
+
- 批量连接测试
|
|
14
|
+
- 凭据加密存储
|
|
15
|
+
- 结构化日志系统
|
|
16
|
+
|
|
17
|
+
快速开始:
|
|
18
|
+
>>> from remote_cmd import SSHClient, HostManager
|
|
19
|
+
>>> from remote_cmd.core.ssh_client import ConnectionConfig
|
|
20
|
+
>>>
|
|
21
|
+
>>> # 创建连接配置
|
|
22
|
+
>>> config = ConnectionConfig(
|
|
23
|
+
... hostname="192.168.1.100",
|
|
24
|
+
... username="admin",
|
|
25
|
+
... key_filename="~/.ssh/id_rsa"
|
|
26
|
+
... )
|
|
27
|
+
>>>
|
|
28
|
+
>>> # 执行远程命令
|
|
29
|
+
>>> with SSHClient(config) as client:
|
|
30
|
+
... result = client.execute("ls -la")
|
|
31
|
+
... print(result.stdout)
|
|
32
|
+
|
|
33
|
+
新架构(推荐):
|
|
34
|
+
>>> from remote_cmd.repository.json_host_repository import JsonHostRepository
|
|
35
|
+
>>> from remote_cmd.service.host_service import HostService
|
|
36
|
+
>>>
|
|
37
|
+
>>> repo = JsonHostRepository("hosts.json")
|
|
38
|
+
>>> service = HostService(repo)
|
|
39
|
+
>>> service.add_host(host)
|
|
40
|
+
|
|
41
|
+
命令行使用:
|
|
42
|
+
$ remote-cmd host add server1 192.168.1.100 admin -k ~/.ssh/id_rsa
|
|
43
|
+
$ remote-cmd host list
|
|
44
|
+
$ remote-cmd run server1 "uptime"
|
|
45
|
+
|
|
46
|
+
更多信息:
|
|
47
|
+
- GitHub: https://github.com/Vae-Scrooge/remote-cmd-test
|
|
48
|
+
- 文档: 参见 docs/ 目录
|
|
49
|
+
|
|
50
|
+
Author: Vae-Scrooge
|
|
51
|
+
Version: 1.0.0
|
|
52
|
+
License: MIT
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
__version__ = "1.0.0"
|
|
56
|
+
__author__ = "Vae-Scrooge"
|
|
57
|
+
__email__ = "vae-scrooge@example.com"
|
|
58
|
+
__license__ = "MIT"
|
|
59
|
+
|
|
60
|
+
import logging
|
|
61
|
+
|
|
62
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
63
|
+
|
|
64
|
+
# 向后兼容导出(原有 API)
|
|
65
|
+
from remote_cmd.core.async_client import AsyncSSHClient, ConnectionPool
|
|
66
|
+
from remote_cmd.core.host import Host
|
|
67
|
+
from remote_cmd.core.host_manager import HostManager
|
|
68
|
+
from remote_cmd.core.ssh_client import SSHClient
|
|
69
|
+
|
|
70
|
+
# 新架构导出(推荐)
|
|
71
|
+
from remote_cmd.repository import HostRepository, JsonHostRepository
|
|
72
|
+
|
|
73
|
+
# Phase 2 新组件
|
|
74
|
+
from remote_cmd.repository.sqlite_host_repository import SqliteHostRepository
|
|
75
|
+
from remote_cmd.service import (
|
|
76
|
+
ChainCredentialProvider,
|
|
77
|
+
CredentialProvider,
|
|
78
|
+
EnvCredentialProvider,
|
|
79
|
+
HostService,
|
|
80
|
+
SSHService,
|
|
81
|
+
)
|
|
82
|
+
from remote_cmd.service.batch_executor import BatchExecutor, BatchHostResult, BatchResult
|
|
83
|
+
from remote_cmd.service.credential_provider import KeyringCredentialProvider
|
|
84
|
+
from remote_cmd.service.task_runner import Task, TaskRunner, TaskStatus
|
|
85
|
+
from remote_cmd.utils.crypto import CredentialEncryption
|
|
86
|
+
from remote_cmd.utils.logging_utils import (
|
|
87
|
+
SensitiveDataFilter,
|
|
88
|
+
get_logger,
|
|
89
|
+
setup_logging,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# 定义公开 API
|
|
93
|
+
__all__ = [
|
|
94
|
+
# 原有导出(向后兼容)
|
|
95
|
+
"SSHClient",
|
|
96
|
+
"AsyncSSHClient",
|
|
97
|
+
"ConnectionPool",
|
|
98
|
+
"HostManager",
|
|
99
|
+
"Host",
|
|
100
|
+
# 新架构导出
|
|
101
|
+
"HostRepository",
|
|
102
|
+
"JsonHostRepository",
|
|
103
|
+
"HostService",
|
|
104
|
+
"SSHService",
|
|
105
|
+
"CredentialProvider",
|
|
106
|
+
"EnvCredentialProvider",
|
|
107
|
+
"ChainCredentialProvider",
|
|
108
|
+
"CredentialEncryption",
|
|
109
|
+
"setup_logging",
|
|
110
|
+
"SensitiveDataFilter",
|
|
111
|
+
"get_logger",
|
|
112
|
+
# Phase 2 新组件
|
|
113
|
+
"SqliteHostRepository",
|
|
114
|
+
"BatchExecutor",
|
|
115
|
+
"BatchResult",
|
|
116
|
+
"BatchHostResult",
|
|
117
|
+
"TaskRunner",
|
|
118
|
+
"Task",
|
|
119
|
+
"TaskStatus",
|
|
120
|
+
"KeyringCredentialProvider",
|
|
121
|
+
# 元信息
|
|
122
|
+
"__version__",
|
|
123
|
+
"__author__",
|
|
124
|
+
"__license__",
|
|
125
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI module for command-line interface."""
|
remote_cmd/cli/main.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""
|
|
2
|
+
命令行接口模块
|
|
3
|
+
|
|
4
|
+
提供完整的命令行工具,用于管理远程主机和执行 SSH 操作。
|
|
5
|
+
基于 Click 框架构建,使用 HostService + Repository 架构。
|
|
6
|
+
|
|
7
|
+
主要命令:
|
|
8
|
+
- host: 主机管理命令组
|
|
9
|
+
- add: 添加新主机
|
|
10
|
+
- list: 列出所有主机
|
|
11
|
+
- remove: 移除主机
|
|
12
|
+
- test: 测试主机连接
|
|
13
|
+
- run: 在远程主机上执行命令
|
|
14
|
+
- upload: 上传文件到远程主机
|
|
15
|
+
- download: 从远程主机下载文件
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
|
|
23
|
+
from remote_cmd.core.host import Host
|
|
24
|
+
from remote_cmd.repository.json_host_repository import JsonHostRepository
|
|
25
|
+
from remote_cmd.service.batch_executor import BatchExecutor
|
|
26
|
+
from remote_cmd.service.credential_provider import (
|
|
27
|
+
ChainCredentialProvider,
|
|
28
|
+
EnvCredentialProvider,
|
|
29
|
+
)
|
|
30
|
+
from remote_cmd.service.host_service import HostService
|
|
31
|
+
from remote_cmd.utils.config import get_default_config_path, load_config
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_service(config_file: str) -> HostService:
|
|
35
|
+
"""从配置文件构建 HostService"""
|
|
36
|
+
repo = JsonHostRepository(filepath=config_file, auto_load=True)
|
|
37
|
+
cred_provider = ChainCredentialProvider(
|
|
38
|
+
[
|
|
39
|
+
EnvCredentialProvider(),
|
|
40
|
+
]
|
|
41
|
+
)
|
|
42
|
+
return HostService(repository=repo, credential_provider=cred_provider)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@click.group()
|
|
46
|
+
@click.version_option(version="1.0.0", prog_name="remote-cmd")
|
|
47
|
+
@click.option("--config", "-c", type=click.Path(), help="配置文件路径")
|
|
48
|
+
@click.option("--verbose", "-v", is_flag=True, help="启用详细输出模式")
|
|
49
|
+
@click.pass_context
|
|
50
|
+
def cli(ctx, config: Optional[str], verbose: bool):
|
|
51
|
+
"""
|
|
52
|
+
Remote CMD - SSH 远程服务器管理工具
|
|
53
|
+
|
|
54
|
+
一个功能强大的命令行工具,用于管理远程主机配置、
|
|
55
|
+
执行命令和传输文件。
|
|
56
|
+
|
|
57
|
+
快速开始:
|
|
58
|
+
# 添加主机
|
|
59
|
+
remote-cmd host add my-server 192.168.1.100 admin -k ~/.ssh/id_rsa
|
|
60
|
+
|
|
61
|
+
# 列出所有主机
|
|
62
|
+
remote-cmd host list
|
|
63
|
+
|
|
64
|
+
# 执行远程命令
|
|
65
|
+
remote-cmd run my-server "ls -la"
|
|
66
|
+
|
|
67
|
+
# 上传文件
|
|
68
|
+
remote-cmd upload my-server ./local.txt /remote/path.txt
|
|
69
|
+
"""
|
|
70
|
+
ctx.ensure_object(dict)
|
|
71
|
+
|
|
72
|
+
config_path = config or get_default_config_path()
|
|
73
|
+
ctx.obj["config"] = load_config(config_path)
|
|
74
|
+
ctx.obj["verbose"] = verbose
|
|
75
|
+
|
|
76
|
+
hosts_file = ctx.obj["config"].get("hosts_file", "hosts.json")
|
|
77
|
+
ctx.obj["service"] = _build_service(hosts_file)
|
|
78
|
+
|
|
79
|
+
if verbose:
|
|
80
|
+
click.echo(f"使用配置文件: {config_path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@cli.group()
|
|
84
|
+
def host():
|
|
85
|
+
"""
|
|
86
|
+
主机管理命令组
|
|
87
|
+
|
|
88
|
+
用于添加、删除、列出、查看和测试远程主机连接。
|
|
89
|
+
|
|
90
|
+
可用命令:
|
|
91
|
+
add 添加新主机
|
|
92
|
+
list 列出所有主机
|
|
93
|
+
show 查看主机详情
|
|
94
|
+
remove 移除主机
|
|
95
|
+
test 测试主机连接
|
|
96
|
+
"""
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@host.command("add")
|
|
101
|
+
@click.argument("name", required=True)
|
|
102
|
+
@click.argument("hostname", required=True)
|
|
103
|
+
@click.argument("username", required=True)
|
|
104
|
+
@click.option("--port", "-p", default=22, help="SSH 端口号(默认:22)")
|
|
105
|
+
@click.option(
|
|
106
|
+
"--password",
|
|
107
|
+
"-P",
|
|
108
|
+
default=None,
|
|
109
|
+
help="登录密码(不推荐直接在命令行输入,建议使用 REMOTE_CMD_PASSWORD 环境变量或交互式输入)",
|
|
110
|
+
)
|
|
111
|
+
@click.option("--key", "-k", help="SSH 私钥文件路径")
|
|
112
|
+
@click.option("--tag", "-t", multiple=True, help="主机标签(可多次指定)")
|
|
113
|
+
@click.option("--description", "-d", default="", help="主机描述")
|
|
114
|
+
@click.pass_context
|
|
115
|
+
def host_add(
|
|
116
|
+
ctx,
|
|
117
|
+
name: str,
|
|
118
|
+
hostname: str,
|
|
119
|
+
username: str,
|
|
120
|
+
port: int,
|
|
121
|
+
password: Optional[str],
|
|
122
|
+
key: Optional[str],
|
|
123
|
+
tag: tuple,
|
|
124
|
+
description: str,
|
|
125
|
+
):
|
|
126
|
+
"""
|
|
127
|
+
添加新主机
|
|
128
|
+
|
|
129
|
+
NAME: 主机名称(唯一标识符)
|
|
130
|
+
HOSTNAME: 主机地址(IP 或域名)
|
|
131
|
+
USERNAME: SSH 登录用户名
|
|
132
|
+
"""
|
|
133
|
+
service: HostService = ctx.obj["service"]
|
|
134
|
+
|
|
135
|
+
# 获取密码:优先级 REMOTE_CMD_PASSWORD > --password > 交互式输入
|
|
136
|
+
resolved_password = os.environ.get("REMOTE_CMD_PASSWORD") or password
|
|
137
|
+
if not resolved_password and not key:
|
|
138
|
+
resolved_password = click.prompt(
|
|
139
|
+
"SSH 密码", hide_input=True, default="", show_default=False
|
|
140
|
+
)
|
|
141
|
+
elif password and not os.environ.get("REMOTE_CMD_PASSWORD"):
|
|
142
|
+
click.echo(
|
|
143
|
+
click.style(
|
|
144
|
+
"⚠ 警告: 在命令行参数中传递密码不安全,建议使用 REMOTE_CMD_PASSWORD 环境变量",
|
|
145
|
+
fg="yellow",
|
|
146
|
+
),
|
|
147
|
+
err=True,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
host = Host(
|
|
151
|
+
name=name,
|
|
152
|
+
hostname=hostname,
|
|
153
|
+
username=username,
|
|
154
|
+
port=port,
|
|
155
|
+
password=resolved_password or password,
|
|
156
|
+
key_filename=key,
|
|
157
|
+
tags=list(tag),
|
|
158
|
+
description=description,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
service.add_host(host)
|
|
163
|
+
click.echo(f"✓ 主机 '{name}' 添加成功")
|
|
164
|
+
except ValueError as e:
|
|
165
|
+
click.echo(f"✗ 错误: {e}", err=True)
|
|
166
|
+
ctx.exit(1)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@host.command("list")
|
|
170
|
+
@click.option("--tag", "-t", help="按标签筛选主机")
|
|
171
|
+
@click.pass_context
|
|
172
|
+
def host_list(ctx, tag: Optional[str]):
|
|
173
|
+
"""列出所有主机"""
|
|
174
|
+
service: HostService = ctx.obj["service"]
|
|
175
|
+
hosts = service.list_hosts(tag=tag)
|
|
176
|
+
|
|
177
|
+
if not hosts:
|
|
178
|
+
if tag:
|
|
179
|
+
click.echo(f"没有找到标签为 '{tag}' 的主机")
|
|
180
|
+
else:
|
|
181
|
+
click.echo("没有配置任何主机")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
click.echo(f"\n{'名称':<20} {'主机地址':<25} {'用户名':<15} {'标签':<20}")
|
|
185
|
+
click.echo("-" * 80)
|
|
186
|
+
|
|
187
|
+
for host in hosts:
|
|
188
|
+
tags_str = ", ".join(host.tags) if host.tags else "-"
|
|
189
|
+
click.echo(f"{host.name:<20} {host.hostname:<25} {host.username:<15} {tags_str:<20}")
|
|
190
|
+
|
|
191
|
+
click.echo()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@host.command("remove")
|
|
195
|
+
@click.argument("name", required=True)
|
|
196
|
+
@click.confirmation_option(prompt="确定要移除这个主机吗?")
|
|
197
|
+
@click.pass_context
|
|
198
|
+
def host_remove(ctx, name: str):
|
|
199
|
+
"""移除主机"""
|
|
200
|
+
service: HostService = ctx.obj["service"]
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
service.remove_host(name)
|
|
204
|
+
click.echo(f"✓ 主机 '{name}' 已移除")
|
|
205
|
+
except KeyError as e:
|
|
206
|
+
click.echo(f"✗ 错误: {e}", err=True)
|
|
207
|
+
ctx.exit(1)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@host.command("show")
|
|
211
|
+
@click.argument("name", required=True)
|
|
212
|
+
@click.pass_context
|
|
213
|
+
def host_show(ctx, name: str):
|
|
214
|
+
"""显示主机详细信息"""
|
|
215
|
+
service: HostService = ctx.obj["service"]
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
host = service.get_host(name)
|
|
219
|
+
click.echo(f"\n{'=' * 50}")
|
|
220
|
+
click.echo(f" 主机详情: {host.name}")
|
|
221
|
+
click.echo(f"{'=' * 50}")
|
|
222
|
+
click.echo(f" 名称: {host.name}")
|
|
223
|
+
click.echo(f" 主机地址: {host.hostname}")
|
|
224
|
+
click.echo(f" 用户名: {host.username}")
|
|
225
|
+
click.echo(f" 端口: {host.port}")
|
|
226
|
+
click.echo(
|
|
227
|
+
f" 认证方式: {'密码' if host.password else 'SSH 密钥' if host.key_filename else 'SSH Agent'}"
|
|
228
|
+
)
|
|
229
|
+
if host.key_filename:
|
|
230
|
+
click.echo(f" 密钥路径: {host.key_filename}")
|
|
231
|
+
tags_str = ", ".join(host.tags) if host.tags else "-"
|
|
232
|
+
click.echo(f" 标签: {tags_str}")
|
|
233
|
+
click.echo(f" 描述: {host.description or '-'}")
|
|
234
|
+
click.echo(f"{'=' * 50}\n")
|
|
235
|
+
except KeyError as e:
|
|
236
|
+
click.echo(f"✗ 错误: {e}", err=True)
|
|
237
|
+
ctx.exit(1)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@host.command("test")
|
|
241
|
+
@click.argument("name", required=True)
|
|
242
|
+
@click.pass_context
|
|
243
|
+
def host_test(ctx, name: str):
|
|
244
|
+
"""测试主机连接"""
|
|
245
|
+
service: HostService = ctx.obj["service"]
|
|
246
|
+
|
|
247
|
+
click.echo(f"正在测试 '{name}' 的连接...")
|
|
248
|
+
|
|
249
|
+
if service.test_connection(name):
|
|
250
|
+
click.echo(f"✓ 主机 '{name}' 连接成功")
|
|
251
|
+
else:
|
|
252
|
+
click.echo(f"✗ 主机 '{name}' 连接失败", err=True)
|
|
253
|
+
ctx.exit(1)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@cli.command()
|
|
257
|
+
@click.argument("host_name", required=True)
|
|
258
|
+
@click.argument("command", required=True)
|
|
259
|
+
@click.pass_context
|
|
260
|
+
def run(ctx, host_name: str, command: str):
|
|
261
|
+
"""
|
|
262
|
+
在远程主机上执行命令
|
|
263
|
+
|
|
264
|
+
HOST_NAME: 主机名称
|
|
265
|
+
COMMAND: 要执行的命令
|
|
266
|
+
"""
|
|
267
|
+
service: HostService = ctx.obj["service"]
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
with service.connect_to_host(host_name) as client:
|
|
271
|
+
result = client.execute(command)
|
|
272
|
+
|
|
273
|
+
if result.stdout:
|
|
274
|
+
click.echo(result.stdout)
|
|
275
|
+
|
|
276
|
+
if result.stderr:
|
|
277
|
+
click.echo(result.stderr, err=True)
|
|
278
|
+
|
|
279
|
+
ctx.exit(result.exit_code)
|
|
280
|
+
|
|
281
|
+
except Exception as e: # noqa: BLE001
|
|
282
|
+
click.echo(f"✗ 错误: {e}", err=True)
|
|
283
|
+
ctx.exit(1)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@cli.command()
|
|
287
|
+
@click.argument("host_name", required=True)
|
|
288
|
+
@click.argument("local_path", required=True)
|
|
289
|
+
@click.argument("remote_path", required=True)
|
|
290
|
+
@click.pass_context
|
|
291
|
+
def upload(ctx, host_name: str, local_path: str, remote_path: str):
|
|
292
|
+
"""
|
|
293
|
+
上传文件到远程主机
|
|
294
|
+
|
|
295
|
+
HOST_NAME: 主机名称
|
|
296
|
+
LOCAL_PATH: 本地文件路径
|
|
297
|
+
REMOTE_PATH: 远程目标路径
|
|
298
|
+
"""
|
|
299
|
+
service: HostService = ctx.obj["service"]
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
with service.connect_to_host(host_name) as client:
|
|
303
|
+
client.upload_file(local_path, remote_path)
|
|
304
|
+
click.echo(f"✓ 上传成功: {local_path} -> {host_name}:{remote_path}")
|
|
305
|
+
except Exception as e: # noqa: BLE001
|
|
306
|
+
click.echo(f"✗ 错误: {e}", err=True)
|
|
307
|
+
ctx.exit(1)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@cli.command()
|
|
311
|
+
@click.argument("host_name", required=True)
|
|
312
|
+
@click.argument("local_path", required=True)
|
|
313
|
+
@click.argument("remote_path", required=True)
|
|
314
|
+
@click.pass_context
|
|
315
|
+
def download(ctx, host_name: str, local_path: str, remote_path: str):
|
|
316
|
+
"""
|
|
317
|
+
从远程主机下载文件
|
|
318
|
+
|
|
319
|
+
HOST_NAME: 主机名称
|
|
320
|
+
LOCAL_PATH: 本地文件路径
|
|
321
|
+
REMOTE_PATH: 远程目标路径
|
|
322
|
+
"""
|
|
323
|
+
service: HostService = ctx.obj["service"]
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
with service.connect_to_host(host_name) as client:
|
|
327
|
+
client.download_file(remote_path, local_path)
|
|
328
|
+
click.echo(f"✓ 下载成功: {host_name}:{remote_path} -> {local_path}")
|
|
329
|
+
except Exception as e: # noqa: BLE001
|
|
330
|
+
click.echo(f"✗ 错误: {e}", err=True)
|
|
331
|
+
ctx.exit(1)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@cli.command()
|
|
335
|
+
@click.argument("host_names", nargs=-1, required=True)
|
|
336
|
+
@click.argument("command", required=True)
|
|
337
|
+
@click.option("--concurrency", "-C", default=10, help="最大并发数(默认:10)")
|
|
338
|
+
@click.option("--timeout", "-T", default=30, help="单个命令超时秒数(默认:30)")
|
|
339
|
+
@click.option("--retry", "-r", default=0, help="失败重试次数(默认:0)")
|
|
340
|
+
@click.option("--retry-delay", default=1.0, help="重试间隔秒数(默认:1.0)")
|
|
341
|
+
@click.option("--show-failures", is_flag=True, help="仅显示失败主机")
|
|
342
|
+
@click.pass_context
|
|
343
|
+
def batch_run(
|
|
344
|
+
ctx,
|
|
345
|
+
host_names: tuple,
|
|
346
|
+
command: str,
|
|
347
|
+
concurrency: int,
|
|
348
|
+
timeout: int,
|
|
349
|
+
retry: int,
|
|
350
|
+
retry_delay: float,
|
|
351
|
+
show_failures: bool,
|
|
352
|
+
):
|
|
353
|
+
"""
|
|
354
|
+
在多个主机上批量执行命令
|
|
355
|
+
|
|
356
|
+
HOST_NAMES: 主机名称列表(可指定多个)
|
|
357
|
+
COMMAND: 要执行的命令
|
|
358
|
+
|
|
359
|
+
示例:
|
|
360
|
+
|
|
361
|
+
remote-cmd batch-run web-1 web-2 db-1 "uptime"
|
|
362
|
+
|
|
363
|
+
remote-cmd batch-run web-1 web-2 "df -h" -C 5 -r 2
|
|
364
|
+
"""
|
|
365
|
+
service: HostService = ctx.obj["service"]
|
|
366
|
+
executor = BatchExecutor(
|
|
367
|
+
host_service=service,
|
|
368
|
+
max_concurrency=concurrency,
|
|
369
|
+
command_timeout=timeout,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
click.echo(f"批量执行: {len(host_names)} 台主机, 命令='{command}', 并发={concurrency}")
|
|
373
|
+
click.echo()
|
|
374
|
+
|
|
375
|
+
bar: Any
|
|
376
|
+
with click.progressbar(
|
|
377
|
+
length=len(host_names),
|
|
378
|
+
label="执行进度",
|
|
379
|
+
show_eta=True,
|
|
380
|
+
show_percent=True,
|
|
381
|
+
) as bar:
|
|
382
|
+
|
|
383
|
+
def progress(_completed, _total, _host_name):
|
|
384
|
+
bar.update(1)
|
|
385
|
+
|
|
386
|
+
result = executor.execute(
|
|
387
|
+
host_names=list(host_names),
|
|
388
|
+
command=command,
|
|
389
|
+
retry_count=retry,
|
|
390
|
+
retry_delay=retry_delay,
|
|
391
|
+
progress_callback=progress,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
click.echo()
|
|
395
|
+
click.echo("=" * 50)
|
|
396
|
+
click.echo(" 执行结果汇总")
|
|
397
|
+
click.echo("=" * 50)
|
|
398
|
+
click.echo(f" 总执行: {result.total}")
|
|
399
|
+
click.echo(f" 成功: {result.success}")
|
|
400
|
+
click.echo(f" 失败: {result.failed}")
|
|
401
|
+
click.echo(f" 耗时: {result.duration:.1f}s")
|
|
402
|
+
click.echo(f" 成功率: {result.success_rate:.1%}")
|
|
403
|
+
click.echo("=" * 50)
|
|
404
|
+
|
|
405
|
+
if result.failed_hosts:
|
|
406
|
+
click.echo()
|
|
407
|
+
click.echo(click.style("失败主机:", fg="red"))
|
|
408
|
+
for host in result.failed_hosts:
|
|
409
|
+
host_result = result.results[host]
|
|
410
|
+
error_msg = host_result.error or f"exit_code={host_result.exit_code}"
|
|
411
|
+
click.echo(
|
|
412
|
+
click.style(
|
|
413
|
+
f" ✗ {host}: {error_msg}",
|
|
414
|
+
fg="red",
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if not show_failures and result.success_hosts:
|
|
419
|
+
click.echo()
|
|
420
|
+
click.echo(click.style("成功主机:", fg="green"))
|
|
421
|
+
for host in result.success_hosts:
|
|
422
|
+
click.echo(click.style(f" ✓ {host}", fg="green"))
|
|
423
|
+
|
|
424
|
+
if result.failed > 0:
|
|
425
|
+
ctx.exit(1)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def main():
|
|
429
|
+
"""CLI 程序入口点"""
|
|
430
|
+
cli()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
if __name__ == "__main__":
|
|
434
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core module for SSH operations."""
|