remote-cmd-manager 2.7.0__tar.gz → 2.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {remote_cmd_manager-2.7.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.8.0}/PKG-INFO +24 -21
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/README.md +23 -20
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/pyproject.toml +2 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/__init__.py +7 -1
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/_version.py +1 -1
- remote_cmd_manager-2.8.0/remote_cmd/cli/formatters/__init__.py +27 -0
- remote_cmd_manager-2.8.0/remote_cmd/cli/formatters/base.py +71 -0
- remote_cmd_manager-2.8.0/remote_cmd/cli/formatters/json.py +82 -0
- remote_cmd_manager-2.8.0/remote_cmd/cli/formatters/table.py +84 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/cli/main.py +226 -14
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/host.py +3 -0
- remote_cmd_manager-2.8.0/remote_cmd/core/profile.py +114 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/json_host_repository.py +48 -4
- remote_cmd_manager-2.8.0/remote_cmd/repository/profile_store.py +48 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/sqlite_host_repository.py +94 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/_host_runner.py +10 -1
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/host_service.py +89 -8
- remote_cmd_manager-2.8.0/remote_cmd/service/profile_service.py +123 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0/remote_cmd_manager.egg-info}/PKG-INFO +24 -21
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/SOURCES.txt +12 -0
- remote_cmd_manager-2.8.0/tests/test_cli_profile.py +262 -0
- remote_cmd_manager-2.8.0/tests/test_formatters.py +149 -0
- remote_cmd_manager-2.8.0/tests/test_profile.py +72 -0
- remote_cmd_manager-2.8.0/tests/test_profile_merge.py +196 -0
- remote_cmd_manager-2.8.0/tests/test_profile_service.py +149 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/LICENSE +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/MANIFEST.in +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/config.example.yaml +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/examples/basic_usage.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/examples/deploy_script.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/examples/nginx_batch_update.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/examples/system_health_check.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/api/__init__.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/api/host_manager.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/cli/__init__.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/__init__.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/async_connection_pool.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/async_ssh_client.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/budget.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/host_manager.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/pool_policy.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/ssh_client.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/sync_connection_pool.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/__init__.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/host_repository.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/__init__.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/_pool_policy.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/_types.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/async_batch_executor.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/batch_executor.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/credential_provider.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/retry_policy.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/ssh_service.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/storage_factory.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/task_runner.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/__init__.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/config.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/credential_guard.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/crypto.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/exceptions.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/logging_utils.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/requires.txt +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/requirements.txt +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/setup.cfg +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/__init__.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/conftest.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/integration/conftest.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/integration/test_ssh_connection.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/performance/__init__.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/performance/conftest.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/performance/test_benchmarks.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/performance/test_paramiko_threads.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_async_batch_executor.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_async_ssh_client.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_batch_executor.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_cli.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_config.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_connection_budget.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_credential_provider.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_crypto.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_exceptions.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_host.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_host_manager.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_host_service.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_keyring_provider.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_logging_utils.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_output_policy.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_package_import.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_pool_policy.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_release_metadata.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_repository.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_retry_policy.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_sqlite_repository.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_ssh_client.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_ssh_service.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_storage_factory.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_sync_connection_pool.py +0 -0
- {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_task_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remote_cmd_manager
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0
|
|
4
4
|
Summary: Cross-platform SSH remote server management tool (Linux/macOS/Windows) — lightweight, zero-configuration CLI and Python API for managing servers over SSH. Supports host CRUD, batch commands, file transfer, credential encryption, async execution, and more.
|
|
5
5
|
Author-email: Vae-Scrooge <vaescrooge@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -89,36 +89,37 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
89
89
|
|
|
90
90
|
---
|
|
91
91
|
|
|
92
|
-
## v2.
|
|
92
|
+
## v2.8.0 Release Highlights
|
|
93
93
|
|
|
94
|
-
v2.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
instead of polling. Public APIs remain backward compatible. See the
|
|
94
|
+
v2.8.0 adds the first P3 user-facing features: **connection profiles** and
|
|
95
|
+
**machine-readable CLI output**. Public APIs remain backward compatible; the
|
|
96
|
+
default CLI output and all existing commands behave exactly as before. See the
|
|
98
97
|
[full migration notes](./CHANGELOG.md).
|
|
99
98
|
|
|
100
|
-
###
|
|
99
|
+
### Connection Profiles
|
|
101
100
|
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
101
|
+
- New `HostProfile` (exported from `remote_cmd`): reusable connection defaults — username, port, key file, tags, description — stored by both JSON and SQLite repositories via the optional `ProfileStore` capability (the `HostRepository` ABC is unchanged).
|
|
102
|
+
- Hosts reference a profile (`host add --profile aws`); values merge at connect time (reference model): port when the host uses the default 22, key file when unset, description when empty, tags as a union, username when the host stores none (live default — later profile username changes propagate).
|
|
103
|
+
- Changing a profile applies to every referencing host; unknown profiles fail with a clear `ConfigError` (batch execution reports them per host).
|
|
104
|
+
- `ProfileService` CRUD + `remote-cmd profile add/list/show/remove`, with deletion protection for profiles still referenced by hosts.
|
|
105
105
|
|
|
106
|
-
###
|
|
106
|
+
### Machine-Readable Output
|
|
107
107
|
|
|
108
|
-
- `
|
|
109
|
-
-
|
|
108
|
+
- `remote-cmd run` and `remote-cmd batch-run` accept `--format rich|json|table` (default `rich` = byte-compatible with previous output).
|
|
109
|
+
- JSON uses a stable schema with sorted result keys; table output is plain aligned text. Machine formats suppress the progress bar/header so stdout stays parseable.
|
|
110
|
+
- The formatter layer lives in `remote_cmd/cli/formatters/` and never touches the execution kernels.
|
|
110
111
|
|
|
111
|
-
###
|
|
112
|
+
### Try It
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
```bash
|
|
115
|
+
remote-cmd profile add aws -u ec2-user -k ~/.ssh/aws.pem -t cloud
|
|
116
|
+
remote-cmd host add web-01 10.0.0.10 --profile aws
|
|
117
|
+
remote-cmd batch-run web-01 web-02 "uptime" --format json | jq .
|
|
118
|
+
```
|
|
118
119
|
|
|
119
120
|
## Table of Contents
|
|
120
121
|
|
|
121
|
-
- [v2.
|
|
122
|
+
- [v2.8.0 Release Highlights](#v280-release-highlights)
|
|
122
123
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
123
124
|
- [Quick Start](#quick-start)
|
|
124
125
|
- [Use Cases](#use-cases)
|
|
@@ -265,7 +266,9 @@ with SSHClient(config) as client:
|
|
|
265
266
|
| **Commands** | Single, multi-line, sudo with password |
|
|
266
267
|
| **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
|
|
267
268
|
| **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
|
|
268
|
-
| **
|
|
269
|
+
| **Connection Profiles** | Reusable username/port/key/tag defaults referenced by hosts (`ProfileService`, `--profile`); never stores credentials |
|
|
270
|
+
| **Output Formats** | `--format rich\|json\|table` for `run` / `batch-run` (stable JSON schema, parseable stdout) |
|
|
271
|
+
| **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`); profile tags participate in filtering |
|
|
269
272
|
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
|
|
270
273
|
| **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
|
|
271
274
|
| **Global Connection Budget** | Optional `ConnectionBudget` caps live SSH connections process-wide across pools and executors |
|
|
@@ -36,36 +36,37 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
## v2.
|
|
39
|
+
## v2.8.0 Release Highlights
|
|
40
40
|
|
|
41
|
-
v2.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
instead of polling. Public APIs remain backward compatible. See the
|
|
41
|
+
v2.8.0 adds the first P3 user-facing features: **connection profiles** and
|
|
42
|
+
**machine-readable CLI output**. Public APIs remain backward compatible; the
|
|
43
|
+
default CLI output and all existing commands behave exactly as before. See the
|
|
45
44
|
[full migration notes](./CHANGELOG.md).
|
|
46
45
|
|
|
47
|
-
###
|
|
46
|
+
### Connection Profiles
|
|
48
47
|
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
48
|
+
- New `HostProfile` (exported from `remote_cmd`): reusable connection defaults — username, port, key file, tags, description — stored by both JSON and SQLite repositories via the optional `ProfileStore` capability (the `HostRepository` ABC is unchanged).
|
|
49
|
+
- Hosts reference a profile (`host add --profile aws`); values merge at connect time (reference model): port when the host uses the default 22, key file when unset, description when empty, tags as a union, username when the host stores none (live default — later profile username changes propagate).
|
|
50
|
+
- Changing a profile applies to every referencing host; unknown profiles fail with a clear `ConfigError` (batch execution reports them per host).
|
|
51
|
+
- `ProfileService` CRUD + `remote-cmd profile add/list/show/remove`, with deletion protection for profiles still referenced by hosts.
|
|
52
52
|
|
|
53
|
-
###
|
|
53
|
+
### Machine-Readable Output
|
|
54
54
|
|
|
55
|
-
- `
|
|
56
|
-
-
|
|
55
|
+
- `remote-cmd run` and `remote-cmd batch-run` accept `--format rich|json|table` (default `rich` = byte-compatible with previous output).
|
|
56
|
+
- JSON uses a stable schema with sorted result keys; table output is plain aligned text. Machine formats suppress the progress bar/header so stdout stays parseable.
|
|
57
|
+
- The formatter layer lives in `remote_cmd/cli/formatters/` and never touches the execution kernels.
|
|
57
58
|
|
|
58
|
-
###
|
|
59
|
+
### Try It
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
```bash
|
|
62
|
+
remote-cmd profile add aws -u ec2-user -k ~/.ssh/aws.pem -t cloud
|
|
63
|
+
remote-cmd host add web-01 10.0.0.10 --profile aws
|
|
64
|
+
remote-cmd batch-run web-01 web-02 "uptime" --format json | jq .
|
|
65
|
+
```
|
|
65
66
|
|
|
66
67
|
## Table of Contents
|
|
67
68
|
|
|
68
|
-
- [v2.
|
|
69
|
+
- [v2.8.0 Release Highlights](#v280-release-highlights)
|
|
69
70
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
70
71
|
- [Quick Start](#quick-start)
|
|
71
72
|
- [Use Cases](#use-cases)
|
|
@@ -212,7 +213,9 @@ with SSHClient(config) as client:
|
|
|
212
213
|
| **Commands** | Single, multi-line, sudo with password |
|
|
213
214
|
| **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
|
|
214
215
|
| **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
|
|
215
|
-
| **
|
|
216
|
+
| **Connection Profiles** | Reusable username/port/key/tag defaults referenced by hosts (`ProfileService`, `--profile`); never stores credentials |
|
|
217
|
+
| **Output Formats** | `--format rich\|json\|table` for `run` / `batch-run` (stable JSON schema, parseable stdout) |
|
|
218
|
+
| **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`); profile tags participate in filtering |
|
|
216
219
|
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
|
|
217
220
|
| **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
|
|
218
221
|
| **Global Connection Budget** | Optional `ConnectionBudget` caps live SSH connections process-wide across pools and executors |
|
|
@@ -124,6 +124,8 @@ ignore = [
|
|
|
124
124
|
"tests/test_async_ssh_client.py" = ["ARG002"]
|
|
125
125
|
# mock_paramiko fixture 以 patch 副作用生效,线程模型测试不直接引用其返回值
|
|
126
126
|
"tests/test_ssh_client.py" = ["ARG002"]
|
|
127
|
+
# Profile 合并测试中的 BareRepo stub 需保留与 HostRepository ABC 一致的签名
|
|
128
|
+
"tests/test_profile_merge.py" = ["ARG002"]
|
|
127
129
|
"tests/test_async_batch_executor.py" = ["ARG001", "ARG002"]
|
|
128
130
|
# 连接池测试中 patched_client fixture 触发 patch 副作用,factory 保留 cfg 参数以匹配真实签名
|
|
129
131
|
"tests/test_sync_connection_pool.py" = ["ARG001", "ARG002"]
|
|
@@ -48,7 +48,7 @@ Remote CMD - SSH 远程服务器管理工具
|
|
|
48
48
|
- 文档: 参见 docs/ 目录
|
|
49
49
|
|
|
50
50
|
Author: Vae-Scrooge
|
|
51
|
-
Version: 2.
|
|
51
|
+
Version: 2.8.0(单一真相源见 remote_cmd._version)
|
|
52
52
|
License: MIT
|
|
53
53
|
"""
|
|
54
54
|
|
|
@@ -78,6 +78,7 @@ if _HAS_ASYNC:
|
|
|
78
78
|
from remote_cmd.api.host_manager import HostManager
|
|
79
79
|
from remote_cmd.core.budget import ConnectionBudget
|
|
80
80
|
from remote_cmd.core.host import Host
|
|
81
|
+
from remote_cmd.core.profile import HostProfile
|
|
81
82
|
from remote_cmd.core.ssh_client import SSHClient
|
|
82
83
|
from remote_cmd.core.sync_connection_pool import SyncConnectionPool
|
|
83
84
|
|
|
@@ -105,6 +106,7 @@ from remote_cmd.service.batch_executor import (
|
|
|
105
106
|
OutputPolicy,
|
|
106
107
|
)
|
|
107
108
|
from remote_cmd.service.credential_provider import KeyringCredentialProvider
|
|
109
|
+
from remote_cmd.service.profile_service import ProfileService
|
|
108
110
|
from remote_cmd.service.task_runner import Task, TaskRunner, TaskStatus
|
|
109
111
|
from remote_cmd.utils.crypto import CredentialEncryption
|
|
110
112
|
from remote_cmd.utils.exceptions import BudgetTimeoutError, PlaintextCredentialWarning
|
|
@@ -124,11 +126,13 @@ if _HAS_ASYNC:
|
|
|
124
126
|
"AsyncConnectionPool",
|
|
125
127
|
"AsyncBatchExecutor",
|
|
126
128
|
"Host",
|
|
129
|
+
"HostProfile",
|
|
127
130
|
"HostManager",
|
|
128
131
|
# 新架构导出
|
|
129
132
|
"HostRepository",
|
|
130
133
|
"JsonHostRepository",
|
|
131
134
|
"HostService",
|
|
135
|
+
"ProfileService",
|
|
132
136
|
"SSHService",
|
|
133
137
|
"CredentialProvider",
|
|
134
138
|
"EnvCredentialProvider",
|
|
@@ -161,11 +165,13 @@ else:
|
|
|
161
165
|
# 原有导出(向后兼容,不含异步符号)
|
|
162
166
|
"SSHClient",
|
|
163
167
|
"Host",
|
|
168
|
+
"HostProfile",
|
|
164
169
|
"HostManager",
|
|
165
170
|
# 新架构导出
|
|
166
171
|
"HostRepository",
|
|
167
172
|
"JsonHostRepository",
|
|
168
173
|
"HostService",
|
|
174
|
+
"ProfileService",
|
|
169
175
|
"SSHService",
|
|
170
176
|
"CredentialProvider",
|
|
171
177
|
"EnvCredentialProvider",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
执行结果格式化层(CLI,v2.8)
|
|
3
|
+
|
|
4
|
+
职责边界:
|
|
5
|
+
- 本层只消费 ``CommandResult`` / ``BatchResult`` 数据对象并渲染字符串,
|
|
6
|
+
**绝不进入执行内核**(BatchExecutor / 连接池等不感知输出格式)。
|
|
7
|
+
- ``rich`` 格式(默认)保留在 CLI 原有渲染路径(含颜色与进度条),
|
|
8
|
+
本包提供 ``json`` / ``table`` 两种机器友好格式。
|
|
9
|
+
- JSON 输出为稳定 schema(字段与顺序固定),便于脚本消费。
|
|
10
|
+
|
|
11
|
+
用法:
|
|
12
|
+
>>> from remote_cmd.cli.formatters import format_batch_result
|
|
13
|
+
>>> text = format_batch_result(batch_result, fmt="json") # doctest: +SKIP
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from remote_cmd.cli.formatters.base import format_batch_result, format_single_result
|
|
17
|
+
from remote_cmd.cli.formatters.json import render_batch_json, render_single_json
|
|
18
|
+
from remote_cmd.cli.formatters.table import render_batch_table, render_single_table
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"format_batch_result",
|
|
22
|
+
"format_single_result",
|
|
23
|
+
"render_batch_json",
|
|
24
|
+
"render_batch_table",
|
|
25
|
+
"render_single_json",
|
|
26
|
+
"render_single_table",
|
|
27
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
格式化器分发与校验(CLI,v2.8)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from remote_cmd.cli.formatters.json import render_batch_json, render_single_json
|
|
8
|
+
from remote_cmd.cli.formatters.table import render_batch_table, render_single_table
|
|
9
|
+
from remote_cmd.core.ssh_client import CommandResult
|
|
10
|
+
from remote_cmd.service._types import BatchResult
|
|
11
|
+
from remote_cmd.utils.exceptions import ValidationError
|
|
12
|
+
|
|
13
|
+
#: CLI ``--format`` 可选值(rich 由 CLI 默认渲染路径处理)
|
|
14
|
+
MACHINE_FORMATS = ("json", "table")
|
|
15
|
+
FORMAT_CHOICES = ("rich", "json", "table")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_single_result(
|
|
19
|
+
host_name: str,
|
|
20
|
+
command: str,
|
|
21
|
+
result: CommandResult,
|
|
22
|
+
fmt: str,
|
|
23
|
+
duration: Optional[float] = None,
|
|
24
|
+
) -> str:
|
|
25
|
+
"""渲染单主机执行结果为 ``fmt`` 字符串。
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
host_name: 主机名
|
|
29
|
+
command: 执行的命令
|
|
30
|
+
result: 命令结果
|
|
31
|
+
fmt: ``json`` 或 ``table``
|
|
32
|
+
duration: 可选耗时(秒),JSON 中作为 ``duration`` 字段
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValidationError: fmt 不是机器格式
|
|
36
|
+
"""
|
|
37
|
+
if fmt == "json":
|
|
38
|
+
return render_single_json(host_name, command, result, duration)
|
|
39
|
+
if fmt == "table":
|
|
40
|
+
return render_single_table(host_name, command, result, duration)
|
|
41
|
+
raise ValidationError(f"unsupported machine format: {fmt!r} (expected json/table)")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def format_batch_result(
|
|
45
|
+
result: BatchResult,
|
|
46
|
+
fmt: str,
|
|
47
|
+
show_failures: bool = False,
|
|
48
|
+
) -> str:
|
|
49
|
+
"""渲染批量执行结果为 ``fmt`` 字符串。
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
result: 批量结果
|
|
53
|
+
fmt: ``json`` 或 ``table``
|
|
54
|
+
show_failures: table 格式下仅显示失败主机(JSON 始终完整)
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValidationError: fmt 不是机器格式
|
|
58
|
+
"""
|
|
59
|
+
if fmt == "json":
|
|
60
|
+
return render_batch_json(result)
|
|
61
|
+
if fmt == "table":
|
|
62
|
+
return render_batch_table(result, show_failures=show_failures)
|
|
63
|
+
raise ValidationError(f"unsupported machine format: {fmt!r} (expected json/table)")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"FORMAT_CHOICES",
|
|
68
|
+
"MACHINE_FORMATS",
|
|
69
|
+
"format_batch_result",
|
|
70
|
+
"format_single_result",
|
|
71
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON 格式化器(稳定 schema,机器友好;v2.8)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from remote_cmd.core.ssh_client import CommandResult
|
|
9
|
+
from remote_cmd.service._types import BatchResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _single_payload(
|
|
13
|
+
host_name: str,
|
|
14
|
+
command: str,
|
|
15
|
+
result: CommandResult,
|
|
16
|
+
duration: Optional[float],
|
|
17
|
+
) -> dict[str, Any]:
|
|
18
|
+
return {
|
|
19
|
+
"host": host_name,
|
|
20
|
+
"command": command,
|
|
21
|
+
"success": result.success,
|
|
22
|
+
"exit_code": result.exit_code,
|
|
23
|
+
"duration": duration,
|
|
24
|
+
"stdout": result.stdout,
|
|
25
|
+
"stderr": result.stderr,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def render_single_json(
|
|
30
|
+
host_name: str,
|
|
31
|
+
command: str,
|
|
32
|
+
result: CommandResult,
|
|
33
|
+
duration: Optional[float] = None,
|
|
34
|
+
) -> str:
|
|
35
|
+
"""渲染单主机结果为 JSON(字段顺序固定,ensure_ascii=False)。"""
|
|
36
|
+
return json.dumps(
|
|
37
|
+
_single_payload(host_name, command, result, duration),
|
|
38
|
+
ensure_ascii=False,
|
|
39
|
+
indent=2,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def render_batch_json(result: BatchResult) -> str:
|
|
44
|
+
"""渲染批量结果为 JSON。
|
|
45
|
+
|
|
46
|
+
schema(稳定)::
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
"total": 3, "success": 2, "failed": 1, "duration": 1.42,
|
|
50
|
+
"results": {
|
|
51
|
+
"web1": {"success": true, "exit_code": 0, "duration": 0.1,
|
|
52
|
+
"stdout": "...", "stderr": "", "error": null,
|
|
53
|
+
"command": "uptime"},
|
|
54
|
+
...
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
``results`` 按主机名排序,保证与完成顺序无关的确定性输出。
|
|
59
|
+
"""
|
|
60
|
+
results: dict[str, Any] = {}
|
|
61
|
+
for host in sorted(result.results):
|
|
62
|
+
host_result = result.results[host]
|
|
63
|
+
results[host] = {
|
|
64
|
+
"success": host_result.success,
|
|
65
|
+
"exit_code": host_result.exit_code,
|
|
66
|
+
"duration": host_result.duration,
|
|
67
|
+
"stdout": host_result.stdout,
|
|
68
|
+
"stderr": host_result.stderr,
|
|
69
|
+
"error": host_result.error,
|
|
70
|
+
"command": host_result.command,
|
|
71
|
+
}
|
|
72
|
+
payload = {
|
|
73
|
+
"total": result.total,
|
|
74
|
+
"success": result.success,
|
|
75
|
+
"failed": result.failed,
|
|
76
|
+
"duration": result.duration,
|
|
77
|
+
"results": results,
|
|
78
|
+
}
|
|
79
|
+
return json.dumps(payload, ensure_ascii=False, indent=2)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = ["render_batch_json", "render_single_json"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Table 格式化器(纯文本对齐,无颜色;v2.8)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from remote_cmd.core.ssh_client import CommandResult
|
|
8
|
+
from remote_cmd.service._types import BatchResult
|
|
9
|
+
|
|
10
|
+
# 单元格最大宽度(字符);超出截断并追加省略号
|
|
11
|
+
_MAX_CELL = 60
|
|
12
|
+
_ELLIPSIS = "…"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _shorten(text: str, limit: int = _MAX_CELL) -> str:
|
|
16
|
+
"""单元格内容单行化 + 截断(表格不展开多行输出)。"""
|
|
17
|
+
first_line = (text or "").splitlines()[0] if text else ""
|
|
18
|
+
if len(first_line) <= limit:
|
|
19
|
+
return first_line
|
|
20
|
+
return first_line[: limit - 1] + _ELLIPSIS
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _render_rows(headers: list[str], rows: list[list[str]]) -> str:
|
|
24
|
+
widths = [len(h) for h in headers]
|
|
25
|
+
for row in rows:
|
|
26
|
+
for i, cell in enumerate(row):
|
|
27
|
+
widths[i] = max(widths[i], len(cell))
|
|
28
|
+
lines = [" ".join(h.ljust(widths[i]) for i, h in enumerate(headers)).rstrip()]
|
|
29
|
+
for row in rows:
|
|
30
|
+
lines.append(" ".join(c.ljust(widths[i]) for i, c in enumerate(row)).rstrip())
|
|
31
|
+
return "\n".join(lines)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def render_single_table(
|
|
35
|
+
host_name: str,
|
|
36
|
+
command: str,
|
|
37
|
+
result: CommandResult,
|
|
38
|
+
duration: Optional[float] = None,
|
|
39
|
+
) -> str:
|
|
40
|
+
"""渲染单主机结果:摘要表 + stdout/stderr 分节。"""
|
|
41
|
+
duration_text = f"{duration:.2f}s" if duration is not None else "-"
|
|
42
|
+
summary = _render_rows(
|
|
43
|
+
["HOST", "COMMAND", "STATUS", "EXIT", "DURATION"],
|
|
44
|
+
[[host_name, _shorten(command), "ok" if result.success else "fail", str(result.exit_code), duration_text]],
|
|
45
|
+
)
|
|
46
|
+
parts = [summary, "", "--- stdout ---", result.stdout.rstrip("\n"), "", "--- stderr ---", result.stderr.rstrip("\n")]
|
|
47
|
+
return "\n".join(parts)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def render_batch_table(result: BatchResult, show_failures: bool = False) -> str:
|
|
51
|
+
"""渲染批量结果表:HOST/STATUS/EXIT/DURATION/OUTPUT。
|
|
52
|
+
|
|
53
|
+
输出列优先展示错误信息(失败主机),否则展示 stdout 首行;
|
|
54
|
+
``show_failures=True`` 时仅渲染失败主机。
|
|
55
|
+
"""
|
|
56
|
+
hosts = sorted(result.results)
|
|
57
|
+
if show_failures:
|
|
58
|
+
hosts = [h for h in hosts if not result.results[h].success]
|
|
59
|
+
|
|
60
|
+
rows: list[list[str]] = []
|
|
61
|
+
for host in hosts:
|
|
62
|
+
r = result.results[host]
|
|
63
|
+
output = r.error if (r.error and not r.success) else r.stdout
|
|
64
|
+
rows.append(
|
|
65
|
+
[
|
|
66
|
+
host,
|
|
67
|
+
"ok" if r.success else "fail",
|
|
68
|
+
str(r.exit_code),
|
|
69
|
+
f"{r.duration:.2f}s",
|
|
70
|
+
_shorten(output or ""),
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
header = _render_rows(["HOST", "STATUS", "EXIT", "DURATION", "OUTPUT"], rows)
|
|
75
|
+
summary = (
|
|
76
|
+
f"Total: {result.total} Succeeded: {result.success} "
|
|
77
|
+
f"Failed: {result.failed} Duration: {result.duration:.2f}s"
|
|
78
|
+
)
|
|
79
|
+
if not rows and not show_failures:
|
|
80
|
+
return f"{header}\n\nNo hosts.\n{summary}"
|
|
81
|
+
return f"{header}\n\n{summary}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
__all__ = ["render_batch_table", "render_single_table"]
|