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 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."""