remote-cmd-manager 2.3.0__tar.gz → 2.5.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.3.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.5.0}/PKG-INFO +29 -21
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/README.md +24 -16
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/pyproject.toml +15 -8
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/__init__.py +12 -2
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/_version.py +1 -1
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/json_host_repository.py +61 -2
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/sqlite_host_repository.py +43 -2
- remote_cmd_manager-2.5.0/remote_cmd/service/_host_runner.py +202 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/_types.py +33 -1
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/async_batch_executor.py +95 -46
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/batch_executor.py +31 -3
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/storage_factory.py +37 -11
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/credential_guard.py +18 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/exceptions.py +17 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/logging_utils.py +7 -2
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0/remote_cmd_manager.egg-info}/PKG-INFO +29 -21
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/SOURCES.txt +1 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/requires.txt +3 -2
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_async_batch_executor.py +165 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_batch_executor.py +100 -1
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_logging_utils.py +14 -0
- remote_cmd_manager-2.5.0/tests/test_output_policy.py +172 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_release_metadata.py +13 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_repository.py +69 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_sqlite_repository.py +49 -2
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_storage_factory.py +40 -2
- remote_cmd_manager-2.3.0/remote_cmd/service/_host_runner.py +0 -115
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/LICENSE +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/MANIFEST.in +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/config.example.yaml +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/examples/basic_usage.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/examples/deploy_script.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/examples/nginx_batch_update.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/examples/system_health_check.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/cli/__init__.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/cli/main.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/__init__.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/async_connection_pool.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/async_ssh_client.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/host.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/host_manager.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/ssh_client.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/sync_connection_pool.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/__init__.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/host_repository.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/__init__.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/_pool_policy.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/credential_provider.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/host_service.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/retry_policy.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/ssh_service.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/task_runner.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/__init__.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/config.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/crypto.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/requirements.txt +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/setup.cfg +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/__init__.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/conftest.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/integration/conftest.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/integration/test_ssh_connection.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/performance/__init__.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/performance/conftest.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/performance/test_benchmarks.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_async_ssh_client.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_cli.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_config.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_credential_provider.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_crypto.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_exceptions.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_host.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_host_manager.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_host_service.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_keyring_provider.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_pool_policy.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_retry_policy.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_ssh_client.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_ssh_service.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/tests/test_sync_connection_pool.py +0 -0
- {remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.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.5.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
|
|
@@ -15,7 +15,6 @@ Classifier: Intended Audience :: Developers
|
|
|
15
15
|
Classifier: Intended Audience :: System Administrators
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -27,17 +26,17 @@ Classifier: Environment :: Console
|
|
|
27
26
|
Classifier: Operating System :: POSIX :: Linux
|
|
28
27
|
Classifier: Operating System :: MacOS
|
|
29
28
|
Classifier: Operating System :: Microsoft :: Windows
|
|
30
|
-
Requires-Python: >=3.
|
|
29
|
+
Requires-Python: >=3.10
|
|
31
30
|
Description-Content-Type: text/markdown
|
|
32
31
|
License-File: LICENSE
|
|
33
|
-
Requires-Dist: paramiko
|
|
32
|
+
Requires-Dist: paramiko<6,>=5.0
|
|
34
33
|
Requires-Dist: click>=8.0
|
|
35
34
|
Requires-Dist: cryptography>=41.0
|
|
36
35
|
Requires-Dist: rich>=13.0
|
|
37
36
|
Requires-Dist: PyYAML>=6.0
|
|
38
37
|
Requires-Dist: aiofiles>=3.0
|
|
39
38
|
Provides-Extra: async
|
|
40
|
-
Requires-Dist: asyncssh
|
|
39
|
+
Requires-Dist: asyncssh<3,>=2.24.0; extra == "async"
|
|
41
40
|
Provides-Extra: cloud
|
|
42
41
|
Requires-Dist: boto3>=1.34; extra == "cloud"
|
|
43
42
|
Provides-Extra: dev
|
|
@@ -46,6 +45,7 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
|
46
45
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
47
46
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
48
47
|
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
48
|
+
Requires-Dist: asyncssh<3,>=2.24.0; extra == "dev"
|
|
49
49
|
Requires-Dist: keyring>=24.0; extra == "dev"
|
|
50
50
|
Provides-Extra: docs
|
|
51
51
|
Requires-Dist: pdoc==15.0.4; extra == "docs"
|
|
@@ -55,7 +55,7 @@ Dynamic: license-file
|
|
|
55
55
|
<img src="https://img.shields.io/pypi/v/remote_cmd_manager?style=for-the-badge&logo=pypi&logoColor=white&label=PyPI" alt="PyPI">
|
|
56
56
|
<img src="https://img.shields.io/pypi/dm/remote_cmd_manager?style=for-the-badge&logo=python&logoColor=white&label=Downloads" alt="Downloads">
|
|
57
57
|
<img src="https://img.shields.io/github/stars/Vae-Scrooge/remote-cmd?style=for-the-badge&logo=github" alt="Stars">
|
|
58
|
-
<img src="https://img.shields.io/badge/python-3.
|
|
58
|
+
<img src="https://img.shields.io/badge/python-3.10%2B-blue?style=for-the-badge&logo=python" alt="Python">
|
|
59
59
|
<img src="https://img.shields.io/github/license/Vae-Scrooge/remote-cmd?style=for-the-badge" alt="License">
|
|
60
60
|
<img src="https://img.shields.io/github/actions/workflow/status/Vae-Scrooge/remote-cmd/ci.yml?style=for-the-badge&logo=githubactions&label=CI" alt="CI">
|
|
61
61
|
</p>
|
|
@@ -89,28 +89,36 @@ 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.5.0 Release Highlights
|
|
93
93
|
|
|
94
|
-
v2.
|
|
95
|
-
|
|
94
|
+
v2.5.0 is a hardening and scalability release: dependency security floors, Python 3.9
|
|
95
|
+
removal, bounded async scheduling, and explicit output-retention / credential-persistence
|
|
96
|
+
policies. Backward-compatible defaults are preserved where behavior is involved. See the
|
|
96
97
|
[full migration notes](./CHANGELOG.md) before upgrading automated callers.
|
|
97
98
|
|
|
98
|
-
###
|
|
99
|
+
### Security & Platform
|
|
99
100
|
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
- The asynchronous `AsyncSSHClient` bounds SFTP channel startup with a timeout and uses a progress-based inactivity watchdog to cancel stalled transfers, awaiting the aborted operation and preserving caller cancellation during cleanup.
|
|
103
|
-
- Timed-out or cancelled SFTP sessions are closed and discarded, so a stale or desynchronized channel can never be reused.
|
|
101
|
+
- Minimum supported Python is now **3.10** (3.9 reached EOL on 2025-10-31).
|
|
102
|
+
- Security floors raised: **Paramiko >= 5.0,<6** and **AsyncSSH >= 2.24.0,<3** (2.23.x and earlier are affected by 2026 AsyncSSH advisories).
|
|
104
103
|
|
|
105
|
-
###
|
|
104
|
+
### Scalability
|
|
106
105
|
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
|
|
106
|
+
- `AsyncBatchExecutor` now uses a **bounded worker queue**: only `min(max_concurrency, host count)` worker tasks are created instead of one task per host, so scheduling memory no longer grows with fleet size.
|
|
107
|
+
- New `OutputPolicy` (exported from `remote_cmd`) gives an explicit, mutually exclusive alternative to `max_output_bytes`; `None` still means full retention in v2.5, and a bounded default is planned for v3.0.
|
|
108
|
+
|
|
109
|
+
### Credential Safety
|
|
110
|
+
|
|
111
|
+
- `JsonHostRepository` / `SqliteHostRepository` accept `allow_plaintext_credentials` (default `None`): plaintext persistence now emits `PlaintextCredentialWarning`; pass `True` to opt in silently or `False` to reject it with `CredentialError`. v3.0 plans to reject by default.
|
|
112
|
+
- JSON repository concurrency semantics are now explicit: single-process/single-writer; use SQLite for multi-process writers.
|
|
113
|
+
- The `dev` extra now includes `asyncssh`, so `pip install -e ".[dev]"` runs the full test suite out of the box.
|
|
114
|
+
|
|
115
|
+
### Compatibility
|
|
116
|
+
|
|
117
|
+
- `max_output_bytes=None` still retains full output, and the public `BatchResult` / `BatchHostResult` schemas are unchanged; the legacy `max_output_bytes` parameter continues to work alongside `OutputPolicy`.
|
|
110
118
|
|
|
111
119
|
## Table of Contents
|
|
112
120
|
|
|
113
|
-
- [v2.
|
|
121
|
+
- [v2.5.0 Release Highlights](#v250-release-highlights)
|
|
114
122
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
115
123
|
- [Quick Start](#quick-start)
|
|
116
124
|
- [Use Cases](#use-cases)
|
|
@@ -258,7 +266,7 @@ with SSHClient(config) as client:
|
|
|
258
266
|
| **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
|
|
259
267
|
| **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
|
|
260
268
|
| **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`) |
|
|
261
|
-
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously |
|
|
269
|
+
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
|
|
262
270
|
| **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
|
|
263
271
|
| **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
|
|
264
272
|
| **Connection Test** | Test all host SSH connections and report status |
|
|
@@ -348,7 +356,7 @@ Good first issues are labelled `good first issue` in the
|
|
|
348
356
|
developed independently as a focused alternative to heavyweight tools for the
|
|
349
357
|
ad-hoc SSH tasks that come up in day-to-day server work.
|
|
350
358
|
|
|
351
|
-
- **Project health:** CI runs on every PR, Python 3.
|
|
359
|
+
- **Project health:** CI runs on every PR, Python 3.10+ is supported, and the
|
|
352
360
|
public API is versioned under [semantic versioning](https://semver.org/).
|
|
353
361
|
- **Your code, your servers:** usage stays open under the MIT license — nothing
|
|
354
362
|
is telemetry-driven or locked behind a service.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="https://img.shields.io/pypi/v/remote_cmd_manager?style=for-the-badge&logo=pypi&logoColor=white&label=PyPI" alt="PyPI">
|
|
3
3
|
<img src="https://img.shields.io/pypi/dm/remote_cmd_manager?style=for-the-badge&logo=python&logoColor=white&label=Downloads" alt="Downloads">
|
|
4
4
|
<img src="https://img.shields.io/github/stars/Vae-Scrooge/remote-cmd?style=for-the-badge&logo=github" alt="Stars">
|
|
5
|
-
<img src="https://img.shields.io/badge/python-3.
|
|
5
|
+
<img src="https://img.shields.io/badge/python-3.10%2B-blue?style=for-the-badge&logo=python" alt="Python">
|
|
6
6
|
<img src="https://img.shields.io/github/license/Vae-Scrooge/remote-cmd?style=for-the-badge" alt="License">
|
|
7
7
|
<img src="https://img.shields.io/github/actions/workflow/status/Vae-Scrooge/remote-cmd/ci.yml?style=for-the-badge&logo=githubactions&label=CI" alt="CI">
|
|
8
8
|
</p>
|
|
@@ -36,28 +36,36 @@ 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.5.0 Release Highlights
|
|
40
40
|
|
|
41
|
-
v2.
|
|
42
|
-
|
|
41
|
+
v2.5.0 is a hardening and scalability release: dependency security floors, Python 3.9
|
|
42
|
+
removal, bounded async scheduling, and explicit output-retention / credential-persistence
|
|
43
|
+
policies. Backward-compatible defaults are preserved where behavior is involved. See the
|
|
43
44
|
[full migration notes](./CHANGELOG.md) before upgrading automated callers.
|
|
44
45
|
|
|
45
|
-
###
|
|
46
|
+
### Security & Platform
|
|
46
47
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
- The asynchronous `AsyncSSHClient` bounds SFTP channel startup with a timeout and uses a progress-based inactivity watchdog to cancel stalled transfers, awaiting the aborted operation and preserving caller cancellation during cleanup.
|
|
50
|
-
- Timed-out or cancelled SFTP sessions are closed and discarded, so a stale or desynchronized channel can never be reused.
|
|
48
|
+
- Minimum supported Python is now **3.10** (3.9 reached EOL on 2025-10-31).
|
|
49
|
+
- Security floors raised: **Paramiko >= 5.0,<6** and **AsyncSSH >= 2.24.0,<3** (2.23.x and earlier are affected by 2026 AsyncSSH advisories).
|
|
51
50
|
|
|
52
|
-
###
|
|
51
|
+
### Scalability
|
|
53
52
|
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
|
|
53
|
+
- `AsyncBatchExecutor` now uses a **bounded worker queue**: only `min(max_concurrency, host count)` worker tasks are created instead of one task per host, so scheduling memory no longer grows with fleet size.
|
|
54
|
+
- New `OutputPolicy` (exported from `remote_cmd`) gives an explicit, mutually exclusive alternative to `max_output_bytes`; `None` still means full retention in v2.5, and a bounded default is planned for v3.0.
|
|
55
|
+
|
|
56
|
+
### Credential Safety
|
|
57
|
+
|
|
58
|
+
- `JsonHostRepository` / `SqliteHostRepository` accept `allow_plaintext_credentials` (default `None`): plaintext persistence now emits `PlaintextCredentialWarning`; pass `True` to opt in silently or `False` to reject it with `CredentialError`. v3.0 plans to reject by default.
|
|
59
|
+
- JSON repository concurrency semantics are now explicit: single-process/single-writer; use SQLite for multi-process writers.
|
|
60
|
+
- The `dev` extra now includes `asyncssh`, so `pip install -e ".[dev]"` runs the full test suite out of the box.
|
|
61
|
+
|
|
62
|
+
### Compatibility
|
|
63
|
+
|
|
64
|
+
- `max_output_bytes=None` still retains full output, and the public `BatchResult` / `BatchHostResult` schemas are unchanged; the legacy `max_output_bytes` parameter continues to work alongside `OutputPolicy`.
|
|
57
65
|
|
|
58
66
|
## Table of Contents
|
|
59
67
|
|
|
60
|
-
- [v2.
|
|
68
|
+
- [v2.5.0 Release Highlights](#v250-release-highlights)
|
|
61
69
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
62
70
|
- [Quick Start](#quick-start)
|
|
63
71
|
- [Use Cases](#use-cases)
|
|
@@ -205,7 +213,7 @@ with SSHClient(config) as client:
|
|
|
205
213
|
| **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
|
|
206
214
|
| **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
|
|
207
215
|
| **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`) |
|
|
208
|
-
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously |
|
|
216
|
+
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
|
|
209
217
|
| **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
|
|
210
218
|
| **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
|
|
211
219
|
| **Connection Test** | Test all host SSH connections and report status |
|
|
@@ -295,7 +303,7 @@ Good first issues are labelled `good first issue` in the
|
|
|
295
303
|
developed independently as a focused alternative to heavyweight tools for the
|
|
296
304
|
ad-hoc SSH tasks that come up in day-to-day server work.
|
|
297
305
|
|
|
298
|
-
- **Project health:** CI runs on every PR, Python 3.
|
|
306
|
+
- **Project health:** CI runs on every PR, Python 3.10+ is supported, and the
|
|
299
307
|
public API is versioned under [semantic versioning](https://semver.org/).
|
|
300
308
|
- **Your code, your servers:** usage stays open under the MIT license — nothing
|
|
301
309
|
is telemetry-driven or locked behind a service.
|
|
@@ -7,7 +7,7 @@ name = "remote_cmd_manager"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "Vae-Scrooge", email = "vaescrooge@gmail.com" },
|
|
@@ -19,7 +19,6 @@ classifiers = [
|
|
|
19
19
|
"Intended Audience :: System Administrators",
|
|
20
20
|
"License :: OSI Approved :: MIT License",
|
|
21
21
|
"Programming Language :: Python :: 3",
|
|
22
|
-
"Programming Language :: Python :: 3.9",
|
|
23
22
|
"Programming Language :: Python :: 3.10",
|
|
24
23
|
"Programming Language :: Python :: 3.11",
|
|
25
24
|
"Programming Language :: Python :: 3.12",
|
|
@@ -33,7 +32,7 @@ classifiers = [
|
|
|
33
32
|
"Operating System :: Microsoft :: Windows",
|
|
34
33
|
]
|
|
35
34
|
dependencies = [
|
|
36
|
-
"paramiko>=
|
|
35
|
+
"paramiko>=5.0,<6",
|
|
37
36
|
"click>=8.0",
|
|
38
37
|
"cryptography>=41.0",
|
|
39
38
|
"rich>=13.0",
|
|
@@ -43,22 +42,24 @@ dependencies = [
|
|
|
43
42
|
|
|
44
43
|
[project.optional-dependencies]
|
|
45
44
|
# 异步原生执行内核:启用 BatchExecutor(use_async=True) / AsyncSSHClient / AsyncConnectionPool 时需要
|
|
46
|
-
|
|
45
|
+
# 下限锁定在 2.24.x:2.23.x 及更早版本存在 2026 年公开 CVE(SCP 路径穿越等)。
|
|
46
|
+
async = ["asyncssh>=2.24.0,<3"]
|
|
47
47
|
# 云平台主机发现(Phase 4 预留)
|
|
48
48
|
cloud = ["boto3>=1.34"]
|
|
49
49
|
# 开发与测试
|
|
50
|
+
# 包含 async extra:默认测试套件会 import 异步模块,仅装 dev 无法收集。
|
|
50
51
|
dev = [
|
|
51
52
|
"pytest>=8.0",
|
|
52
53
|
"pytest-asyncio>=0.23",
|
|
53
54
|
"pytest-cov>=4.0",
|
|
54
55
|
"ruff>=0.4",
|
|
55
56
|
"mypy>=1.10",
|
|
57
|
+
"asyncssh>=2.24.0,<3",
|
|
56
58
|
# 测试 keyring 凭据提供者(生产为可选依赖,测试环境固定安装)
|
|
57
59
|
"keyring>=24.0",
|
|
58
60
|
]
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
# (scripts/check_docs_drift.py)要求生成结果可复现。
|
|
61
|
+
# 文档生成。pdoc 固定到 15.0.4:docs/api 漂移门禁(scripts/check_docs_drift.py)
|
|
62
|
+
# 要求生成结果可复现。
|
|
62
63
|
docs = ["pdoc==15.0.4"]
|
|
63
64
|
|
|
64
65
|
[project.urls]
|
|
@@ -88,7 +89,7 @@ markers = [
|
|
|
88
89
|
]
|
|
89
90
|
|
|
90
91
|
[tool.ruff]
|
|
91
|
-
target-version = "
|
|
92
|
+
target-version = "py310"
|
|
92
93
|
line-length = 100
|
|
93
94
|
|
|
94
95
|
[tool.ruff.lint]
|
|
@@ -109,6 +110,12 @@ ignore = [
|
|
|
109
110
|
"N812",
|
|
110
111
|
"N999",
|
|
111
112
|
"B904",
|
|
113
|
+
# PEP 604 / 新式导入重写推迟到 v2.6(保持 v2.5 变更面最小)
|
|
114
|
+
"UP006", # dict/list 泛型重写
|
|
115
|
+
"UP007", # Union -> X|Y
|
|
116
|
+
"UP035", # typing.* 导入重写
|
|
117
|
+
"UP041", # IOError -> TimeoutError 别名
|
|
118
|
+
"UP045", # Optional -> X|None
|
|
112
119
|
]
|
|
113
120
|
|
|
114
121
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -48,7 +48,7 @@ Remote CMD - SSH 远程服务器管理工具
|
|
|
48
48
|
- 文档: 参见 docs/ 目录
|
|
49
49
|
|
|
50
50
|
Author: Vae-Scrooge
|
|
51
|
-
Version: 2.
|
|
51
|
+
Version: 2.5.0(单一真相源见 remote_cmd._version)
|
|
52
52
|
License: MIT
|
|
53
53
|
"""
|
|
54
54
|
|
|
@@ -96,10 +96,16 @@ from remote_cmd.service import (
|
|
|
96
96
|
HostService,
|
|
97
97
|
SSHService,
|
|
98
98
|
)
|
|
99
|
-
from remote_cmd.service.batch_executor import
|
|
99
|
+
from remote_cmd.service.batch_executor import (
|
|
100
|
+
BatchExecutor,
|
|
101
|
+
BatchHostResult,
|
|
102
|
+
BatchResult,
|
|
103
|
+
OutputPolicy,
|
|
104
|
+
)
|
|
100
105
|
from remote_cmd.service.credential_provider import KeyringCredentialProvider
|
|
101
106
|
from remote_cmd.service.task_runner import Task, TaskRunner, TaskStatus
|
|
102
107
|
from remote_cmd.utils.crypto import CredentialEncryption
|
|
108
|
+
from remote_cmd.utils.exceptions import PlaintextCredentialWarning
|
|
103
109
|
from remote_cmd.utils.logging_utils import (
|
|
104
110
|
SensitiveDataFilter,
|
|
105
111
|
get_logger,
|
|
@@ -134,6 +140,8 @@ if _HAS_ASYNC:
|
|
|
134
140
|
"BatchExecutor",
|
|
135
141
|
"BatchResult",
|
|
136
142
|
"BatchHostResult",
|
|
143
|
+
"OutputPolicy",
|
|
144
|
+
"PlaintextCredentialWarning",
|
|
137
145
|
"SyncConnectionPool",
|
|
138
146
|
"TaskRunner",
|
|
139
147
|
"Task",
|
|
@@ -167,6 +175,8 @@ else:
|
|
|
167
175
|
"BatchExecutor",
|
|
168
176
|
"BatchResult",
|
|
169
177
|
"BatchHostResult",
|
|
178
|
+
"OutputPolicy",
|
|
179
|
+
"PlaintextCredentialWarning",
|
|
170
180
|
"SyncConnectionPool",
|
|
171
181
|
"TaskRunner",
|
|
172
182
|
"Task",
|
{remote_cmd_manager-2.3.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/json_host_repository.py
RENAMED
|
@@ -4,8 +4,14 @@ JSON 文件主机仓库实现
|
|
|
4
4
|
使用 JSON 文件存储主机配置,支持:
|
|
5
5
|
- 原子写入(先写临时文件再重命名,防止崩溃导致数据丢失)
|
|
6
6
|
- 可选加密(通过 CredentialEncryption 加密 password 字段)
|
|
7
|
+
- 明文持久化策略(v2.5)
|
|
7
8
|
- config version management
|
|
8
9
|
- 自动从旧版本迁移
|
|
10
|
+
|
|
11
|
+
并发语义(single-writer):
|
|
12
|
+
- 本实现面向**单进程/单写入者**"; 多进程并发写同一 JSON 文件
|
|
13
|
+
可能丢失更新(读-改-写无跨进程锁)。需要多进程/多写入者时,
|
|
14
|
+
请使用 SqliteHostRepository(WAL + busy_timeout 处理并发写)。
|
|
9
15
|
"""
|
|
10
16
|
|
|
11
17
|
import builtins
|
|
@@ -14,13 +20,15 @@ import json
|
|
|
14
20
|
import logging
|
|
15
21
|
import os
|
|
16
22
|
import tempfile
|
|
23
|
+
import warnings
|
|
17
24
|
from pathlib import Path
|
|
18
25
|
from typing import Optional
|
|
19
26
|
|
|
20
27
|
from remote_cmd.core.host import Host
|
|
21
28
|
from remote_cmd.repository.host_repository import HostRepository
|
|
22
|
-
from remote_cmd.utils.credential_guard import PasswordGuard
|
|
29
|
+
from remote_cmd.utils.credential_guard import PasswordGuard, is_plaintext_password
|
|
23
30
|
from remote_cmd.utils.crypto import CredentialEncryption
|
|
31
|
+
from remote_cmd.utils.exceptions import CredentialError, PlaintextCredentialWarning
|
|
24
32
|
|
|
25
33
|
logger = logging.getLogger(__name__)
|
|
26
34
|
|
|
@@ -36,6 +44,16 @@ class JsonHostRepository(HostRepository):
|
|
|
36
44
|
filepath: JSON 文件路径
|
|
37
45
|
encryption: 可选的凭据加密器(设置后自动加密 password)
|
|
38
46
|
auto_load: 初始化时是否自动加载已有文件(默认 True)
|
|
47
|
+
allow_plaintext_credentials: 明文密码持久化策略(v2.5;默认 None
|
|
48
|
+
为兼容模式)。三态:
|
|
49
|
+
- None:允许,但在 flush 会持久化明文密码时发出
|
|
50
|
+
PlaintextCredentialWarning(兼容 v2.4 及更早)
|
|
51
|
+
- True:显式允许明文落盘(不再告警)
|
|
52
|
+
- False:flush 遇到明文密码时抛出 CredentialError
|
|
53
|
+
v3.0 起默认值计划切换为 False。
|
|
54
|
+
|
|
55
|
+
并发语义:单进程/单写入者(见模块 docstring);多进程场景请使用
|
|
56
|
+
SqliteHostRepository。
|
|
39
57
|
"""
|
|
40
58
|
|
|
41
59
|
def __init__(
|
|
@@ -43,10 +61,12 @@ class JsonHostRepository(HostRepository):
|
|
|
43
61
|
filepath: str,
|
|
44
62
|
encryption: Optional[CredentialEncryption] = None,
|
|
45
63
|
auto_load: bool = True,
|
|
64
|
+
allow_plaintext_credentials: Optional[bool] = None,
|
|
46
65
|
) -> None:
|
|
47
66
|
self._filepath = Path(filepath)
|
|
48
67
|
self._encryption = encryption
|
|
49
68
|
self._guard = PasswordGuard(encryption)
|
|
69
|
+
self._allow_plaintext = allow_plaintext_credentials
|
|
50
70
|
self._hosts: dict[str, Host] = {}
|
|
51
71
|
|
|
52
72
|
if auto_load and self._filepath.exists():
|
|
@@ -100,7 +120,14 @@ class JsonHostRepository(HostRepository):
|
|
|
100
120
|
# ========================================================================
|
|
101
121
|
|
|
102
122
|
def flush(self) -> None:
|
|
103
|
-
"""原子写入 JSON 文件
|
|
123
|
+
"""原子写入 JSON 文件
|
|
124
|
+
|
|
125
|
+
注意:JSON 持久化是单进程/单写入者语义,多进程并发写同一文件
|
|
126
|
+
可能丢失更新(无跨进程锁);多进程场景请使用 SqliteHostRepository。
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
CredentialError: allow_plaintext_credentials=False 且存在明文密码
|
|
130
|
+
"""
|
|
104
131
|
data = self._serialize_hosts()
|
|
105
132
|
self._atomic_write(data)
|
|
106
133
|
|
|
@@ -112,12 +139,44 @@ class JsonHostRepository(HostRepository):
|
|
|
112
139
|
if self._guard.enabled:
|
|
113
140
|
for host_data in hosts_dict.values():
|
|
114
141
|
host_data["password"] = self._guard.encrypt(host_data.get("password"))
|
|
142
|
+
else:
|
|
143
|
+
self._enforce_plaintext_policy(hosts_dict)
|
|
115
144
|
|
|
116
145
|
return {
|
|
117
146
|
"version": CONFIG_VERSION,
|
|
118
147
|
"hosts": hosts_dict,
|
|
119
148
|
}
|
|
120
149
|
|
|
150
|
+
def _enforce_plaintext_policy(self, hosts_dict: dict) -> None:
|
|
151
|
+
"""执行明文密码持久化策略(未配置加密器时)。
|
|
152
|
+
|
|
153
|
+
集合所有违规主机,一次 flush 最多发出一次警告或抛出一次异常。
|
|
154
|
+
"""
|
|
155
|
+
if self._allow_plaintext is True:
|
|
156
|
+
return
|
|
157
|
+
offenders = [
|
|
158
|
+
name
|
|
159
|
+
for name, data in hosts_dict.items()
|
|
160
|
+
if is_plaintext_password(data.get("password"))
|
|
161
|
+
]
|
|
162
|
+
if not offenders:
|
|
163
|
+
return
|
|
164
|
+
if self._allow_plaintext is False:
|
|
165
|
+
raise CredentialError(
|
|
166
|
+
"refusing to persist plaintext credentials for hosts: "
|
|
167
|
+
f"{', '.join(sorted(offenders))}. "
|
|
168
|
+
"Pass encryption=... to encrypt at rest, or set "
|
|
169
|
+
"allow_plaintext_credentials=True to explicitly opt in."
|
|
170
|
+
)
|
|
171
|
+
warnings.warn(
|
|
172
|
+
f"Plaintext credentials will be persisted for {len(offenders)} host(s): "
|
|
173
|
+
f"{', '.join(sorted(offenders))}. Pass encryption=... to encrypt at rest, or "
|
|
174
|
+
"allow_plaintext_credentials=True to suppress this warning "
|
|
175
|
+
"(v3.0 will reject plaintext persistence by default).",
|
|
176
|
+
PlaintextCredentialWarning,
|
|
177
|
+
stacklevel=4,
|
|
178
|
+
)
|
|
179
|
+
|
|
121
180
|
def _load(self) -> None:
|
|
122
181
|
"""从 JSON 文件加载主机配置"""
|
|
123
182
|
try:
|
|
@@ -22,12 +22,14 @@ import logging
|
|
|
22
22
|
import sqlite3
|
|
23
23
|
import threading
|
|
24
24
|
import time
|
|
25
|
+
import warnings
|
|
25
26
|
from typing import Optional
|
|
26
27
|
|
|
27
28
|
from remote_cmd.core.host import Host
|
|
28
29
|
from remote_cmd.repository.host_repository import HostRepository
|
|
29
|
-
from remote_cmd.utils.credential_guard import PasswordGuard
|
|
30
|
+
from remote_cmd.utils.credential_guard import PasswordGuard, is_plaintext_password
|
|
30
31
|
from remote_cmd.utils.crypto import CredentialEncryption
|
|
32
|
+
from remote_cmd.utils.exceptions import CredentialError, PlaintextCredentialWarning
|
|
31
33
|
|
|
32
34
|
logger = logging.getLogger(__name__)
|
|
33
35
|
|
|
@@ -79,9 +81,19 @@ class SqliteHostRepository(HostRepository):
|
|
|
79
81
|
auto_create: 是否自动创建表和数据库,默认 True
|
|
80
82
|
encryption: 可选的凭据加密器(设置后 save() 自动加密 password)
|
|
81
83
|
busy_timeout_ms: 写锁等待上限(毫秒),默认 5000
|
|
84
|
+
allow_plaintext_credentials: 明文密码持久化策略(v2.5;默认 None
|
|
85
|
+
为兼容模式)。三态:
|
|
86
|
+
- None:允许,但 save() 将要持久化明文密码时发出
|
|
87
|
+
PlaintextCredentialWarning(兼容 v2.4 及更早)
|
|
88
|
+
- True:显式允许明文落盘(不再告警)
|
|
89
|
+
- False:save() 遇到明文密码时抛出 CredentialError
|
|
90
|
+
v3.0 起默认值计划切换为 False。
|
|
82
91
|
|
|
83
92
|
注意: 密码的加密依赖传入 encryption。若直接以明文密码调用 save()
|
|
84
93
|
且未提供 encryption,明文会被持久化到数据库。请勿绕过 HostService。
|
|
94
|
+
|
|
95
|
+
并发:WAL + busy_timeout 处理多进程并发写,适合作为多写入者后端
|
|
96
|
+
(与 JsonHostRepository 的 single-writer 语义不同)。
|
|
85
97
|
"""
|
|
86
98
|
|
|
87
99
|
def __init__(
|
|
@@ -91,12 +103,14 @@ class SqliteHostRepository(HostRepository):
|
|
|
91
103
|
auto_create: bool = True,
|
|
92
104
|
encryption: Optional[CredentialEncryption] = None,
|
|
93
105
|
busy_timeout_ms: int = DEFAULT_BUSY_TIMEOUT_MS,
|
|
106
|
+
allow_plaintext_credentials: Optional[bool] = None,
|
|
94
107
|
) -> None:
|
|
95
108
|
self._db_path = db_path
|
|
96
109
|
self._lock = threading.Lock()
|
|
97
110
|
self._encryption = encryption
|
|
98
111
|
self._guard = PasswordGuard(encryption)
|
|
99
112
|
self._busy_timeout_ms = busy_timeout_ms
|
|
113
|
+
self._allow_plaintext = allow_plaintext_credentials
|
|
100
114
|
|
|
101
115
|
if auto_create:
|
|
102
116
|
self._init_db()
|
|
@@ -246,7 +260,13 @@ class SqliteHostRepository(HostRepository):
|
|
|
246
260
|
# ========================================================================
|
|
247
261
|
|
|
248
262
|
def save(self, host: Host) -> None:
|
|
249
|
-
"""保存或更新主机
|
|
263
|
+
"""保存或更新主机
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
CredentialError: allow_plaintext_credentials=False 且密码为明文
|
|
267
|
+
"""
|
|
268
|
+
if not self._guard.enabled:
|
|
269
|
+
self._enforce_plaintext_policy(host.name, host.password)
|
|
250
270
|
with self._lock, self._txn(write=True) as conn:
|
|
251
271
|
tags_json = json.dumps(host.tags or [], ensure_ascii=False)
|
|
252
272
|
# 配置了加密器时,明文密码先加密再落库
|
|
@@ -289,6 +309,27 @@ class SqliteHostRepository(HostRepository):
|
|
|
289
309
|
|
|
290
310
|
return self._row_to_host(row)
|
|
291
311
|
|
|
312
|
+
def _enforce_plaintext_policy(self, name: str, password: Optional[str]) -> None:
|
|
313
|
+
"""执行明文密码持久化策略(未配置加密器时)。"""
|
|
314
|
+
if self._allow_plaintext is True:
|
|
315
|
+
return
|
|
316
|
+
if not is_plaintext_password(password):
|
|
317
|
+
return
|
|
318
|
+
if self._allow_plaintext is False:
|
|
319
|
+
raise CredentialError(
|
|
320
|
+
f"refusing to persist plaintext credential for host '{name}'. "
|
|
321
|
+
"Pass encryption=... to encrypt at rest, or set "
|
|
322
|
+
"allow_plaintext_credentials=True to explicitly opt in."
|
|
323
|
+
)
|
|
324
|
+
warnings.warn(
|
|
325
|
+
f"Plaintext credential will be persisted for host '{name}'. "
|
|
326
|
+
"Pass encryption=... to encrypt at rest, or "
|
|
327
|
+
"allow_plaintext_credentials=True to suppress this warning "
|
|
328
|
+
"(v3.0 will reject plaintext persistence by default).",
|
|
329
|
+
PlaintextCredentialWarning,
|
|
330
|
+
stacklevel=4,
|
|
331
|
+
)
|
|
332
|
+
|
|
292
333
|
def delete(self, name: str) -> None:
|
|
293
334
|
"""按名称删除主机"""
|
|
294
335
|
with self._lock, self._txn(write=True) as conn:
|