remote-cmd-manager 2.0.0__tar.gz → 2.2.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.0.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.2.0}/PKG-INFO +57 -25
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/README.md +49 -20
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/examples/basic_usage.py +2 -2
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/pyproject.toml +10 -5
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/__init__.py +1 -1
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/_version.py +1 -1
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/cli/main.py +16 -3
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/async_connection_pool.py +24 -3
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/async_ssh_client.py +16 -4
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/ssh_client.py +155 -21
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/sync_connection_pool.py +10 -2
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/sqlite_host_repository.py +50 -7
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/__init__.py +3 -0
- remote_cmd_manager-2.2.0/remote_cmd/service/async_batch_executor.py +357 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/batch_executor.py +179 -83
- remote_cmd_manager-2.2.0/remote_cmd/service/retry_policy.py +158 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/crypto.py +9 -2
- remote_cmd_manager-2.2.0/remote_cmd/utils/exceptions.py +247 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0/remote_cmd_manager.egg-info}/PKG-INFO +57 -25
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/SOURCES.txt +4 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/requires.txt +1 -1
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/performance/conftest.py +18 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/performance/test_benchmarks.py +9 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_async_batch_executor.py +409 -23
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_async_ssh_client.py +54 -3
- remote_cmd_manager-2.2.0/tests/test_batch_executor.py +849 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_cli.py +46 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_crypto.py +7 -1
- remote_cmd_manager-2.2.0/tests/test_exceptions.py +115 -0
- remote_cmd_manager-2.2.0/tests/test_release_metadata.py +69 -0
- remote_cmd_manager-2.2.0/tests/test_retry_policy.py +154 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_sqlite_repository.py +167 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_ssh_client.py +198 -1
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_sync_connection_pool.py +74 -7
- remote_cmd_manager-2.0.0/remote_cmd/service/async_batch_executor.py +0 -235
- remote_cmd_manager-2.0.0/remote_cmd/utils/exceptions.py +0 -133
- remote_cmd_manager-2.0.0/tests/test_batch_executor.py +0 -437
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/LICENSE +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/MANIFEST.in +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/config.example.yaml +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/examples/deploy_script.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/examples/nginx_batch_update.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/examples/system_health_check.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/cli/__init__.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/__init__.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/host.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/host_manager.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/__init__.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/host_repository.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/repository/json_host_repository.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/_host_runner.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/_pool_policy.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/_types.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/credential_provider.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/host_service.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/ssh_service.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/storage_factory.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/service/task_runner.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/__init__.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/config.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/credential_guard.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/utils/logging_utils.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/requirements.txt +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/setup.cfg +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/__init__.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/conftest.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/integration/conftest.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/integration/test_ssh_connection.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/performance/__init__.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_config.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_credential_provider.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_host.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_host_manager.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_host_service.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_keyring_provider.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_logging_utils.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_pool_policy.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_repository.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_ssh_service.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_storage_factory.py +0 -0
- {remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/tests/test_task_runner.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remote_cmd_manager
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary: SSH
|
|
3
|
+
Version: 2.2.0
|
|
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
|
|
7
7
|
Project-URL: Homepage, https://github.com/Vae-Scrooge/remote-cmd
|
|
@@ -9,8 +9,8 @@ Project-URL: Repository, https://github.com/Vae-Scrooge/remote-cmd
|
|
|
9
9
|
Project-URL: Documentation, https://github.com/Vae-Scrooge/remote-cmd#readme
|
|
10
10
|
Project-URL: Bug Tracker, https://github.com/Vae-Scrooge/remote-cmd/issues
|
|
11
11
|
Project-URL: Download, https://pypi.org/project/remote_cmd_manager/
|
|
12
|
-
Keywords: ssh,remote,server-management,sysadmin,devops
|
|
13
|
-
Classifier: Development Status ::
|
|
12
|
+
Keywords: ssh,remote,server-management,sysadmin,devops,cross-platform
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Intended Audience :: System Administrators
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -21,6 +21,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
21
21
|
Classifier: Topic :: System :: Systems Administration
|
|
22
22
|
Classifier: Topic :: System :: Networking
|
|
23
23
|
Classifier: Environment :: Console
|
|
24
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
25
|
+
Classifier: Operating System :: MacOS
|
|
26
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
24
27
|
Requires-Python: >=3.9
|
|
25
28
|
Description-Content-Type: text/markdown
|
|
26
29
|
License-File: LICENSE
|
|
@@ -42,7 +45,7 @@ Requires-Dist: ruff>=0.4; extra == "dev"
|
|
|
42
45
|
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
43
46
|
Requires-Dist: keyring>=24.0; extra == "dev"
|
|
44
47
|
Provides-Extra: docs
|
|
45
|
-
Requires-Dist: pdoc
|
|
48
|
+
Requires-Dist: pdoc==15.0.4; extra == "docs"
|
|
46
49
|
Dynamic: license-file
|
|
47
50
|
|
|
48
51
|
<p align="center">
|
|
@@ -54,6 +57,7 @@ Dynamic: license-file
|
|
|
54
57
|
<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">
|
|
55
58
|
</p>
|
|
56
59
|
|
|
60
|
+
<p align="center"><strong>Tested on Linux, macOS, and Windows</strong></p>
|
|
57
61
|
<h1 align="center">Remote CMD — SSH Server Management<br><small>Without the Overhead</small></h1>
|
|
58
62
|
|
|
59
63
|
<p align="center">
|
|
@@ -71,15 +75,9 @@ Dynamic: license-file
|
|
|
71
75
|
<a href="#contributing">Contributing</a>
|
|
72
76
|
</p>
|
|
73
77
|
|
|
74
|
-
<p align="center">
|
|
75
|
-
<a href="https://asciinema.org/a/9yLeYj73muPUuAQY" target="_blank">
|
|
76
|
-
<img src="https://asciinema.org/a/9yLeYj73muPUuAQY.svg" width="720" alt="Demo">
|
|
77
|
-
</a>
|
|
78
|
-
</p>
|
|
79
|
-
|
|
80
78
|
---
|
|
81
79
|
|
|
82
|
-
**Remote CMD** is a lightweight Python CLI + API for managing servers over SSH. Add hosts, run commands, transfer files, and
|
|
80
|
+
**Remote CMD** is a lightweight Python CLI + API for managing servers over SSH. Add hosts, run commands, transfer files, and organize hosts with tags — no Ansible DSL or shell loops required.
|
|
83
81
|
|
|
84
82
|
```bash
|
|
85
83
|
# One command to get started
|
|
@@ -88,8 +86,27 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
88
86
|
|
|
89
87
|
---
|
|
90
88
|
|
|
89
|
+
## v2.2.0 Release Highlights
|
|
90
|
+
|
|
91
|
+
v2.2.0 bounds batch-execution resource usage, makes concurrent SQLite writers safe, tightens
|
|
92
|
+
retry classification, and hardens the release pipeline. See the
|
|
93
|
+
[full migration notes](./CHANGELOG.md) before upgrading automated callers.
|
|
94
|
+
|
|
95
|
+
### Resource & Reliability
|
|
96
|
+
|
|
97
|
+
- Internal batch pools are now created lazily per host, capped at one connection, and closed as soon as that host (including all retries) finishes; batch-wide live internal connections are bounded by `max_concurrency` instead of retaining one idle connection per host until the batch ends.
|
|
98
|
+
- SQLite writes now use `BEGIN IMMEDIATE` with a configurable `busy_timeout` (default 5000 ms), and the initial `journal_mode=WAL` switch is retried; concurrent `remote-cmd` processes writing the same `hosts.db` no longer fail with `database is locked` or lose updates. Schema and `db_version` are unchanged.
|
|
99
|
+
- Pool closure raises the new `PoolClosedError` (still catchable as `RuntimeError`); retry classification is corrected so bare `RuntimeError` is retryable again (v2.0 behavior) while pool closure remains non-retryable. Typed permanent and transient errors are unchanged.
|
|
100
|
+
|
|
101
|
+
### Release Engineering
|
|
102
|
+
|
|
103
|
+
- Publish workflow: artifact actions aligned, run-scoped artifact names, duplicate version publication now fails instead of being skipped, `twine check` validates distributions, and the release tag is verified against the package version. Trusted Publishing and the GitHub Release → PyPI flow are unchanged.
|
|
104
|
+
- CI documentation drift detection: `docs/api` is regenerated and compared on relevant changes, failing when tracked generated docs are stale (`pdoc` pinned to `15.0.4` on Python 3.12).
|
|
105
|
+
- Release metadata validation: a lightweight gate checks the single-source version, the `pyproject.toml` dynamic-version attribute, `[Unreleased]` and matching `[x.y.z]` CHANGELOG headings, and (on release) that the git tag matches the package version.
|
|
106
|
+
|
|
91
107
|
## Table of Contents
|
|
92
108
|
|
|
109
|
+
- [v2.2.0 Release Highlights](#v220-release-highlights)
|
|
93
110
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
94
111
|
- [Quick Start](#quick-start)
|
|
95
112
|
- [Use Cases](#use-cases)
|
|
@@ -132,8 +149,8 @@ remote-cmd host add web-01 192.168.1.10 ubuntu --key ~/.ssh/id_rsa
|
|
|
132
149
|
# 3. Run a command
|
|
133
150
|
remote-cmd run web-01 "uptime"
|
|
134
151
|
|
|
135
|
-
# 4. Run across
|
|
136
|
-
remote-cmd batch-run -
|
|
152
|
+
# 4. Run across named production servers
|
|
153
|
+
remote-cmd batch-run web-01 web-02 db-01 "df -h /"
|
|
137
154
|
```
|
|
138
155
|
|
|
139
156
|
---
|
|
@@ -143,7 +160,7 @@ remote-cmd batch-run -t production "df -h /"
|
|
|
143
160
|
### 🖥️ System Administrators — Check disk across 20 servers in one command
|
|
144
161
|
|
|
145
162
|
```bash
|
|
146
|
-
remote-cmd batch-run -
|
|
163
|
+
remote-cmd batch-run web-01 web-02 db-01 "df -h / | tail -1"
|
|
147
164
|
# Output:
|
|
148
165
|
# ✓ web-01 → /dev/sda1 32G 12G 19G 40% /
|
|
149
166
|
# ✓ web-02 → /dev/sda1 32G 28G 3G 90% / ⚠️
|
|
@@ -167,7 +184,7 @@ for host in service.list_hosts(tag="staging"):
|
|
|
167
184
|
### 🔥 Incident Response — Check logs across all servers
|
|
168
185
|
|
|
169
186
|
```bash
|
|
170
|
-
remote-cmd batch-run -
|
|
187
|
+
remote-cmd batch-run web-01 web-02 "journalctl -xe -n 50 | grep -i error"
|
|
171
188
|
```
|
|
172
189
|
|
|
173
190
|
### 🔧 Config Update — Upload and reload nginx across tagged hosts
|
|
@@ -190,10 +207,10 @@ All operations are available from the terminal:
|
|
|
190
207
|
| `remote-cmd host show <name>` | Show one host's details |
|
|
191
208
|
| `remote-cmd host test <name>` | Test connectivity to a host |
|
|
192
209
|
| `remote-cmd host remove <name>` | Remove a host |
|
|
193
|
-
| `remote-cmd run <name> "<cmd>"` | Run a command on one host |
|
|
210
|
+
| `remote-cmd run <name> "<cmd>" [-T SECONDS]` | Run a command on one host (`--timeout/-T` sets the wall-clock limit) |
|
|
194
211
|
| `remote-cmd upload <name> <local> <remote>` | Upload a file via SFTP |
|
|
195
212
|
| `remote-cmd download <name> <remote> <local>` | Download a file via SFTP |
|
|
196
|
-
| `remote-cmd batch-run
|
|
213
|
+
| `remote-cmd batch-run <name>... "<cmd>"` | Run across named hosts (`-C` concurrency, `-T` timeout, `-r` retries, `--async`, `--show-failures`) |
|
|
197
214
|
|
|
198
215
|
---
|
|
199
216
|
|
|
@@ -221,7 +238,7 @@ with SSHClient(config) as client:
|
|
|
221
238
|
|
|
222
239
|
# List remote directory
|
|
223
240
|
for entry in client.list_remote_directory("/var/log"):
|
|
224
|
-
print(f"{entry
|
|
241
|
+
print(f"{entry.name}: {entry.size} bytes")
|
|
225
242
|
```
|
|
226
243
|
|
|
227
244
|
---
|
|
@@ -232,7 +249,7 @@ with SSHClient(config) as client:
|
|
|
232
249
|
|---|---|
|
|
233
250
|
| **SSH Auth** | Password + key file + ssh-agent, with pluggable credential providers |
|
|
234
251
|
| **Credential Chain** | Source passwords from environment, keyring, or arbitrary providers, in priority order |
|
|
235
|
-
| **Credential Encryption** |
|
|
252
|
+
| **Credential Encryption** | Fernet-encrypt secrets at rest (`CredentialEncryption`) |
|
|
236
253
|
| **Commands** | Single, multi-line, sudo with password |
|
|
237
254
|
| **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
|
|
238
255
|
| **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
|
|
@@ -240,12 +257,26 @@ with SSHClient(config) as client:
|
|
|
240
257
|
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously |
|
|
241
258
|
| **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
|
|
242
259
|
| **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
|
|
243
|
-
| **Connection Test** |
|
|
260
|
+
| **Connection Test** | Test all host SSH connections and report status |
|
|
244
261
|
| **Secure Logging** | Structured logging that filters sensitive data (`SensitiveDataFilter`) |
|
|
245
262
|
| **Type Safety** | Full type annotations + mypy strict |
|
|
246
263
|
|
|
247
264
|
---
|
|
248
265
|
|
|
266
|
+
## Cross-Platform Support
|
|
267
|
+
|
|
268
|
+
`remote-cmd` is a pure-Python client, **tested and supported on Linux, macOS, and Windows**. No native dependencies or platform-specific builds are required.
|
|
269
|
+
|
|
270
|
+
| Platform | Status | Notes |
|
|
271
|
+
|----------|--------|-------|
|
|
272
|
+
| Linux | ✅ Fully tested | Primary development platform |
|
|
273
|
+
| macOS | ✅ Supported | Use `brew install openssh` for a newer OpenSSH |
|
|
274
|
+
| Windows | ✅ Supported | PowerShell / CMD / WSL all work |
|
|
275
|
+
|
|
276
|
+
> **Note on Windows file permissions:** Credential encryption keys are protected with `0600` permissions on Unix. On Windows this restriction is not enforced by the filesystem — the key file is still encrypted at rest. For production Windows use, consider adding filesystem-level ACLs.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
249
280
|
## Installation
|
|
250
281
|
|
|
251
282
|
```bash
|
|
@@ -263,8 +294,9 @@ pip install -e ".[dev]"
|
|
|
263
294
|
|
|
264
295
|
The `[async]` extra installs `asyncssh` and enables the native async execution
|
|
265
296
|
kernel: `AsyncSSHClient`, `AsyncConnectionPool` and `AsyncBatchExecutor`
|
|
266
|
-
(also available via `BatchExecutor(use_async=True)`).
|
|
267
|
-
still works — the async symbols
|
|
297
|
+
(also available via `BatchExecutor(use_async=True)`). `BatchExecutor(use_async=True)`
|
|
298
|
+
also requires this extra. Without it, `import remote_cmd` still works — the async symbols
|
|
299
|
+
are simply not exported.
|
|
268
300
|
|
|
269
301
|
---
|
|
270
302
|
|
|
@@ -284,14 +316,14 @@ still works — the async symbols are simply not exported.
|
|
|
284
316
|
| [Changelog](./CHANGELOG.md) | Release history |
|
|
285
317
|
| [Mobile Remote Guide](./MOBILE-REMOTE-GUIDE.md) | Manage servers from your phone |
|
|
286
318
|
|
|
287
|
-
> **Note:** The documentation center and tutorials are maintained in **
|
|
319
|
+
> **Note:** The documentation center and tutorials are maintained in **English**. See
|
|
288
320
|
> [README.zh-CN.md](./README.zh-CN.md) for the Chinese version of this page.
|
|
289
321
|
|
|
290
322
|
---
|
|
291
323
|
|
|
292
324
|
## Project Status
|
|
293
325
|
|
|
294
|
-
**
|
|
326
|
+
**Stable.** The core API is stable and versioned under semantic versioning. Breaking changes are communicated via major-version bumps, and the public API surface has been stable since the 1.x line.
|
|
295
327
|
|
|
296
328
|
**Roadmap:**
|
|
297
329
|
- [x] Async SSH operations (parallel execution) — v1.1.0
|
|
@@ -7,6 +7,7 @@
|
|
|
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>
|
|
9
9
|
|
|
10
|
+
<p align="center"><strong>Tested on Linux, macOS, and Windows</strong></p>
|
|
10
11
|
<h1 align="center">Remote CMD — SSH Server Management<br><small>Without the Overhead</small></h1>
|
|
11
12
|
|
|
12
13
|
<p align="center">
|
|
@@ -24,15 +25,9 @@
|
|
|
24
25
|
<a href="#contributing">Contributing</a>
|
|
25
26
|
</p>
|
|
26
27
|
|
|
27
|
-
<p align="center">
|
|
28
|
-
<a href="https://asciinema.org/a/9yLeYj73muPUuAQY" target="_blank">
|
|
29
|
-
<img src="https://asciinema.org/a/9yLeYj73muPUuAQY.svg" width="720" alt="Demo">
|
|
30
|
-
</a>
|
|
31
|
-
</p>
|
|
32
|
-
|
|
33
28
|
---
|
|
34
29
|
|
|
35
|
-
**Remote CMD** is a lightweight Python CLI + API for managing servers over SSH. Add hosts, run commands, transfer files, and
|
|
30
|
+
**Remote CMD** is a lightweight Python CLI + API for managing servers over SSH. Add hosts, run commands, transfer files, and organize hosts with tags — no Ansible DSL or shell loops required.
|
|
36
31
|
|
|
37
32
|
```bash
|
|
38
33
|
# One command to get started
|
|
@@ -41,8 +36,27 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
41
36
|
|
|
42
37
|
---
|
|
43
38
|
|
|
39
|
+
## v2.2.0 Release Highlights
|
|
40
|
+
|
|
41
|
+
v2.2.0 bounds batch-execution resource usage, makes concurrent SQLite writers safe, tightens
|
|
42
|
+
retry classification, and hardens the release pipeline. See the
|
|
43
|
+
[full migration notes](./CHANGELOG.md) before upgrading automated callers.
|
|
44
|
+
|
|
45
|
+
### Resource & Reliability
|
|
46
|
+
|
|
47
|
+
- Internal batch pools are now created lazily per host, capped at one connection, and closed as soon as that host (including all retries) finishes; batch-wide live internal connections are bounded by `max_concurrency` instead of retaining one idle connection per host until the batch ends.
|
|
48
|
+
- SQLite writes now use `BEGIN IMMEDIATE` with a configurable `busy_timeout` (default 5000 ms), and the initial `journal_mode=WAL` switch is retried; concurrent `remote-cmd` processes writing the same `hosts.db` no longer fail with `database is locked` or lose updates. Schema and `db_version` are unchanged.
|
|
49
|
+
- Pool closure raises the new `PoolClosedError` (still catchable as `RuntimeError`); retry classification is corrected so bare `RuntimeError` is retryable again (v2.0 behavior) while pool closure remains non-retryable. Typed permanent and transient errors are unchanged.
|
|
50
|
+
|
|
51
|
+
### Release Engineering
|
|
52
|
+
|
|
53
|
+
- Publish workflow: artifact actions aligned, run-scoped artifact names, duplicate version publication now fails instead of being skipped, `twine check` validates distributions, and the release tag is verified against the package version. Trusted Publishing and the GitHub Release → PyPI flow are unchanged.
|
|
54
|
+
- CI documentation drift detection: `docs/api` is regenerated and compared on relevant changes, failing when tracked generated docs are stale (`pdoc` pinned to `15.0.4` on Python 3.12).
|
|
55
|
+
- Release metadata validation: a lightweight gate checks the single-source version, the `pyproject.toml` dynamic-version attribute, `[Unreleased]` and matching `[x.y.z]` CHANGELOG headings, and (on release) that the git tag matches the package version.
|
|
56
|
+
|
|
44
57
|
## Table of Contents
|
|
45
58
|
|
|
59
|
+
- [v2.2.0 Release Highlights](#v220-release-highlights)
|
|
46
60
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
47
61
|
- [Quick Start](#quick-start)
|
|
48
62
|
- [Use Cases](#use-cases)
|
|
@@ -85,8 +99,8 @@ remote-cmd host add web-01 192.168.1.10 ubuntu --key ~/.ssh/id_rsa
|
|
|
85
99
|
# 3. Run a command
|
|
86
100
|
remote-cmd run web-01 "uptime"
|
|
87
101
|
|
|
88
|
-
# 4. Run across
|
|
89
|
-
remote-cmd batch-run -
|
|
102
|
+
# 4. Run across named production servers
|
|
103
|
+
remote-cmd batch-run web-01 web-02 db-01 "df -h /"
|
|
90
104
|
```
|
|
91
105
|
|
|
92
106
|
---
|
|
@@ -96,7 +110,7 @@ remote-cmd batch-run -t production "df -h /"
|
|
|
96
110
|
### 🖥️ System Administrators — Check disk across 20 servers in one command
|
|
97
111
|
|
|
98
112
|
```bash
|
|
99
|
-
remote-cmd batch-run -
|
|
113
|
+
remote-cmd batch-run web-01 web-02 db-01 "df -h / | tail -1"
|
|
100
114
|
# Output:
|
|
101
115
|
# ✓ web-01 → /dev/sda1 32G 12G 19G 40% /
|
|
102
116
|
# ✓ web-02 → /dev/sda1 32G 28G 3G 90% / ⚠️
|
|
@@ -120,7 +134,7 @@ for host in service.list_hosts(tag="staging"):
|
|
|
120
134
|
### 🔥 Incident Response — Check logs across all servers
|
|
121
135
|
|
|
122
136
|
```bash
|
|
123
|
-
remote-cmd batch-run -
|
|
137
|
+
remote-cmd batch-run web-01 web-02 "journalctl -xe -n 50 | grep -i error"
|
|
124
138
|
```
|
|
125
139
|
|
|
126
140
|
### 🔧 Config Update — Upload and reload nginx across tagged hosts
|
|
@@ -143,10 +157,10 @@ All operations are available from the terminal:
|
|
|
143
157
|
| `remote-cmd host show <name>` | Show one host's details |
|
|
144
158
|
| `remote-cmd host test <name>` | Test connectivity to a host |
|
|
145
159
|
| `remote-cmd host remove <name>` | Remove a host |
|
|
146
|
-
| `remote-cmd run <name> "<cmd>"` | Run a command on one host |
|
|
160
|
+
| `remote-cmd run <name> "<cmd>" [-T SECONDS]` | Run a command on one host (`--timeout/-T` sets the wall-clock limit) |
|
|
147
161
|
| `remote-cmd upload <name> <local> <remote>` | Upload a file via SFTP |
|
|
148
162
|
| `remote-cmd download <name> <remote> <local>` | Download a file via SFTP |
|
|
149
|
-
| `remote-cmd batch-run
|
|
163
|
+
| `remote-cmd batch-run <name>... "<cmd>"` | Run across named hosts (`-C` concurrency, `-T` timeout, `-r` retries, `--async`, `--show-failures`) |
|
|
150
164
|
|
|
151
165
|
---
|
|
152
166
|
|
|
@@ -174,7 +188,7 @@ with SSHClient(config) as client:
|
|
|
174
188
|
|
|
175
189
|
# List remote directory
|
|
176
190
|
for entry in client.list_remote_directory("/var/log"):
|
|
177
|
-
print(f"{entry
|
|
191
|
+
print(f"{entry.name}: {entry.size} bytes")
|
|
178
192
|
```
|
|
179
193
|
|
|
180
194
|
---
|
|
@@ -185,7 +199,7 @@ with SSHClient(config) as client:
|
|
|
185
199
|
|---|---|
|
|
186
200
|
| **SSH Auth** | Password + key file + ssh-agent, with pluggable credential providers |
|
|
187
201
|
| **Credential Chain** | Source passwords from environment, keyring, or arbitrary providers, in priority order |
|
|
188
|
-
| **Credential Encryption** |
|
|
202
|
+
| **Credential Encryption** | Fernet-encrypt secrets at rest (`CredentialEncryption`) |
|
|
189
203
|
| **Commands** | Single, multi-line, sudo with password |
|
|
190
204
|
| **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
|
|
191
205
|
| **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
|
|
@@ -193,12 +207,26 @@ with SSHClient(config) as client:
|
|
|
193
207
|
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously |
|
|
194
208
|
| **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
|
|
195
209
|
| **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
|
|
196
|
-
| **Connection Test** |
|
|
210
|
+
| **Connection Test** | Test all host SSH connections and report status |
|
|
197
211
|
| **Secure Logging** | Structured logging that filters sensitive data (`SensitiveDataFilter`) |
|
|
198
212
|
| **Type Safety** | Full type annotations + mypy strict |
|
|
199
213
|
|
|
200
214
|
---
|
|
201
215
|
|
|
216
|
+
## Cross-Platform Support
|
|
217
|
+
|
|
218
|
+
`remote-cmd` is a pure-Python client, **tested and supported on Linux, macOS, and Windows**. No native dependencies or platform-specific builds are required.
|
|
219
|
+
|
|
220
|
+
| Platform | Status | Notes |
|
|
221
|
+
|----------|--------|-------|
|
|
222
|
+
| Linux | ✅ Fully tested | Primary development platform |
|
|
223
|
+
| macOS | ✅ Supported | Use `brew install openssh` for a newer OpenSSH |
|
|
224
|
+
| Windows | ✅ Supported | PowerShell / CMD / WSL all work |
|
|
225
|
+
|
|
226
|
+
> **Note on Windows file permissions:** Credential encryption keys are protected with `0600` permissions on Unix. On Windows this restriction is not enforced by the filesystem — the key file is still encrypted at rest. For production Windows use, consider adding filesystem-level ACLs.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
202
230
|
## Installation
|
|
203
231
|
|
|
204
232
|
```bash
|
|
@@ -216,8 +244,9 @@ pip install -e ".[dev]"
|
|
|
216
244
|
|
|
217
245
|
The `[async]` extra installs `asyncssh` and enables the native async execution
|
|
218
246
|
kernel: `AsyncSSHClient`, `AsyncConnectionPool` and `AsyncBatchExecutor`
|
|
219
|
-
(also available via `BatchExecutor(use_async=True)`).
|
|
220
|
-
still works — the async symbols
|
|
247
|
+
(also available via `BatchExecutor(use_async=True)`). `BatchExecutor(use_async=True)`
|
|
248
|
+
also requires this extra. Without it, `import remote_cmd` still works — the async symbols
|
|
249
|
+
are simply not exported.
|
|
221
250
|
|
|
222
251
|
---
|
|
223
252
|
|
|
@@ -237,14 +266,14 @@ still works — the async symbols are simply not exported.
|
|
|
237
266
|
| [Changelog](./CHANGELOG.md) | Release history |
|
|
238
267
|
| [Mobile Remote Guide](./MOBILE-REMOTE-GUIDE.md) | Manage servers from your phone |
|
|
239
268
|
|
|
240
|
-
> **Note:** The documentation center and tutorials are maintained in **
|
|
269
|
+
> **Note:** The documentation center and tutorials are maintained in **English**. See
|
|
241
270
|
> [README.zh-CN.md](./README.zh-CN.md) for the Chinese version of this page.
|
|
242
271
|
|
|
243
272
|
---
|
|
244
273
|
|
|
245
274
|
## Project Status
|
|
246
275
|
|
|
247
|
-
**
|
|
276
|
+
**Stable.** The core API is stable and versioned under semantic versioning. Breaking changes are communicated via major-version bumps, and the public API surface has been stable since the 1.x line.
|
|
248
277
|
|
|
249
278
|
**Roadmap:**
|
|
250
279
|
- [x] Async SSH operations (parallel execution) — v1.1.0
|
|
@@ -107,8 +107,8 @@ def example_file_transfer():
|
|
|
107
107
|
entries = client.list_remote_directory("/tmp")
|
|
108
108
|
print("\n/tmp 目录内容:")
|
|
109
109
|
for entry in entries:
|
|
110
|
-
type_str = "D" if entry
|
|
111
|
-
print(f" [{type_str}] {entry
|
|
110
|
+
type_str = "D" if entry.is_dir else "F"
|
|
111
|
+
print(f" [{type_str}] {entry.name:30} {entry.size:>10} bytes")
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
def example_sudo_command():
|
|
@@ -5,16 +5,16 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "remote_cmd_manager"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "SSH
|
|
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
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "Vae-Scrooge", email = "vaescrooge@gmail.com" },
|
|
14
14
|
]
|
|
15
|
-
keywords = ["ssh", "remote", "server-management", "sysadmin", "devops"]
|
|
15
|
+
keywords = ["ssh", "remote", "server-management", "sysadmin", "devops", "cross-platform"]
|
|
16
16
|
classifiers = [
|
|
17
|
-
"Development Status ::
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"Intended Audience :: System Administrators",
|
|
20
20
|
"License :: OSI Approved :: MIT License",
|
|
@@ -25,6 +25,9 @@ classifiers = [
|
|
|
25
25
|
"Topic :: System :: Systems Administration",
|
|
26
26
|
"Topic :: System :: Networking",
|
|
27
27
|
"Environment :: Console",
|
|
28
|
+
"Operating System :: POSIX :: Linux",
|
|
29
|
+
"Operating System :: MacOS",
|
|
30
|
+
"Operating System :: Microsoft :: Windows",
|
|
28
31
|
]
|
|
29
32
|
dependencies = [
|
|
30
33
|
"paramiko>=3.0",
|
|
@@ -50,8 +53,10 @@ dev = [
|
|
|
50
53
|
# 测试 keyring 凭据提供者(生产为可选依赖,测试环境固定安装)
|
|
51
54
|
"keyring>=24.0",
|
|
52
55
|
]
|
|
53
|
-
# 文档生成(pdoc>=16 需 Python>=3.10,固定到 15.x 以兼容 requires-python>=3.9
|
|
54
|
-
|
|
56
|
+
# 文档生成(pdoc>=16 需 Python>=3.10,固定到 15.x 以兼容 requires-python>=3.9)。
|
|
57
|
+
# 精确 pin 15.0.4:pdoc HTML 输出随版本/解释器变化,docs/api 漂移门禁
|
|
58
|
+
# (scripts/check_docs_drift.py)要求生成结果可复现。
|
|
59
|
+
docs = ["pdoc==15.0.4"]
|
|
55
60
|
|
|
56
61
|
[project.urls]
|
|
57
62
|
Homepage = "https://github.com/Vae-Scrooge/remote-cmd"
|
|
@@ -310,19 +310,27 @@ def host_test(ctx: click.Context, name: str) -> None:
|
|
|
310
310
|
@cli.command()
|
|
311
311
|
@click.argument("host_name", required=True)
|
|
312
312
|
@click.argument("command", required=True)
|
|
313
|
+
@click.option(
|
|
314
|
+
"--timeout",
|
|
315
|
+
"-T",
|
|
316
|
+
default=None,
|
|
317
|
+
type=int,
|
|
318
|
+
help="Command timeout in seconds (default: no timeout)",
|
|
319
|
+
)
|
|
313
320
|
@click.pass_context
|
|
314
|
-
def run(ctx: click.Context, host_name: str, command: str) -> None:
|
|
321
|
+
def run(ctx: click.Context, host_name: str, command: str, timeout: Optional[int]) -> None:
|
|
315
322
|
"""
|
|
316
323
|
Execute a command on a remote host
|
|
317
324
|
|
|
318
325
|
HOST_NAME: host name
|
|
326
|
+
|
|
319
327
|
COMMAND: command to execute
|
|
320
328
|
"""
|
|
321
329
|
service: HostService = ctx.obj["service"]
|
|
322
330
|
|
|
323
331
|
try:
|
|
324
332
|
with service.connect_to_host(host_name) as client:
|
|
325
|
-
result = client.execute(command)
|
|
333
|
+
result = client.execute(command, timeout=timeout)
|
|
326
334
|
|
|
327
335
|
if result.stdout:
|
|
328
336
|
click.echo(result.stdout)
|
|
@@ -397,7 +405,12 @@ def download(ctx: click.Context, host_name: str, local_path: str, remote_path: s
|
|
|
397
405
|
@click.option("--concurrency", "-C", default=10, help="Max concurrency (default: 10)")
|
|
398
406
|
@click.option("--timeout", "-T", default=30, help="Command timeout in seconds (default: 30)")
|
|
399
407
|
@click.option("--retry", "-r", default=0, help="Failure retry count (default: 0)")
|
|
400
|
-
@click.option(
|
|
408
|
+
@click.option(
|
|
409
|
+
"--retry-delay",
|
|
410
|
+
default=1.0,
|
|
411
|
+
help="Base retry delay in seconds; actual wait is exponential "
|
|
412
|
+
"backoff with jitter (default: 1.0)",
|
|
413
|
+
)
|
|
401
414
|
@click.option("--async", "use_async", is_flag=True, help="Use async execution engine (asyncssh)")
|
|
402
415
|
@click.option("--show-failures", is_flag=True, help="Show only failed hosts")
|
|
403
416
|
@click.pass_context
|
{remote_cmd_manager-2.0.0 → remote_cmd_manager-2.2.0}/remote_cmd/core/async_connection_pool.py
RENAMED
|
@@ -26,6 +26,7 @@ from remote_cmd.service._pool_policy import (
|
|
|
26
26
|
lifetime_expired,
|
|
27
27
|
should_close,
|
|
28
28
|
)
|
|
29
|
+
from remote_cmd.utils.exceptions import PoolClosedError
|
|
29
30
|
|
|
30
31
|
logger = logging.getLogger(__name__)
|
|
31
32
|
|
|
@@ -48,12 +49,25 @@ class AsyncConnectionPool:
|
|
|
48
49
|
max_lifetime: int = 3600,
|
|
49
50
|
idle_timeout: int = 300,
|
|
50
51
|
health_check_interval: int = 60,
|
|
52
|
+
client_factory: Optional[Any] = None,
|
|
51
53
|
) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Args:
|
|
56
|
+
config: 用于建立 SSH 连接的配置
|
|
57
|
+
max_connections: 同一最大连接数(同一配置可复用)
|
|
58
|
+
max_lifetime: 连接最大生命周期(秒),超过自动关闭
|
|
59
|
+
idle_timeout: 空闲超时(秒),超过自动关闭
|
|
60
|
+
health_check_interval: 后台清理任务周期(秒)
|
|
61
|
+
client_factory: 客户端工厂,默认为 AsyncSSHClient;测试可注入
|
|
62
|
+
mock(与 SyncConnectionPool 对齐)
|
|
63
|
+
"""
|
|
52
64
|
self.config = config
|
|
53
65
|
self._max = max_connections
|
|
54
66
|
self._max_lifetime = max_lifetime
|
|
55
67
|
self._idle_timeout = idle_timeout
|
|
56
68
|
self._health_check_interval = health_check_interval
|
|
69
|
+
# 客户端工厂:默认为 AsyncSSHClient;测试可注入 mock
|
|
70
|
+
self._client_factory = client_factory or AsyncSSHClient
|
|
57
71
|
|
|
58
72
|
# 容器
|
|
59
73
|
self._connections: list[AsyncSSHClient] = []
|
|
@@ -107,11 +121,18 @@ class AsyncConnectionPool:
|
|
|
107
121
|
|
|
108
122
|
Raises:
|
|
109
123
|
SSHConnectionError: 创建连接失败
|
|
110
|
-
|
|
124
|
+
PoolClosedError: 连接池已关闭(close_all 之后),
|
|
125
|
+
同时是 RuntimeError 子类(既有捕获行为不变)
|
|
111
126
|
"""
|
|
112
127
|
if self._closed:
|
|
113
|
-
raise
|
|
128
|
+
raise PoolClosedError("connection pool is closed")
|
|
114
129
|
await self._semaphore.acquire()
|
|
130
|
+
# 竞态守卫:等待信号量期间 close_all() 可能已完成——
|
|
131
|
+
# 取得槽位后必须复查,已关闭则归还槽位并抛出既有错误,
|
|
132
|
+
# 否则会向调用方发放来自已关闭池的连接
|
|
133
|
+
if self._closed:
|
|
134
|
+
self._semaphore.release()
|
|
135
|
+
raise PoolClosedError("connection pool is closed")
|
|
115
136
|
try:
|
|
116
137
|
# 优先复用空闲连接
|
|
117
138
|
while not self._free.empty():
|
|
@@ -168,7 +189,7 @@ class AsyncConnectionPool:
|
|
|
168
189
|
# 内部
|
|
169
190
|
# ------------------------------------------------------------------
|
|
170
191
|
async def _create_connection(self) -> AsyncSSHClient:
|
|
171
|
-
client =
|
|
192
|
+
client = self._client_factory(self.config)
|
|
172
193
|
try:
|
|
173
194
|
await client.connect()
|
|
174
195
|
except Exception: # noqa: BLE001
|
|
@@ -22,11 +22,18 @@ from typing import Any, Optional
|
|
|
22
22
|
|
|
23
23
|
import asyncssh
|
|
24
24
|
|
|
25
|
-
from remote_cmd.core.ssh_client import
|
|
25
|
+
from remote_cmd.core.ssh_client import (
|
|
26
|
+
CommandResult,
|
|
27
|
+
ConnectionConfig,
|
|
28
|
+
RemoteFileEntry,
|
|
29
|
+
validate_environment,
|
|
30
|
+
)
|
|
26
31
|
from remote_cmd.utils.exceptions import (
|
|
32
|
+
SSHAuthenticationError,
|
|
27
33
|
SSHCommandError,
|
|
28
34
|
SSHConnectionError,
|
|
29
35
|
SSHFileTransferError,
|
|
36
|
+
SSHTimeoutError,
|
|
30
37
|
)
|
|
31
38
|
|
|
32
39
|
logger = logging.getLogger(__name__)
|
|
@@ -94,11 +101,12 @@ class AsyncSSHClient:
|
|
|
94
101
|
try:
|
|
95
102
|
self._conn = await asyncssh.connect(**connect_kwargs)
|
|
96
103
|
except asyncssh.PermissionDenied as e:
|
|
97
|
-
|
|
104
|
+
# 永久性错误:重试同一凭据只会加剧账号锁定(见 service/retry_policy.py)
|
|
105
|
+
raise SSHAuthenticationError(f"authentication failed: {e}") from e
|
|
98
106
|
except (OSError, asyncssh.Error) as e:
|
|
99
107
|
msg = str(e).lower()
|
|
100
108
|
if "timed out" in msg or "timeout" in msg or isinstance(e, asyncssh.TimeoutError):
|
|
101
|
-
raise
|
|
109
|
+
raise SSHTimeoutError(f"connection timeout: {self.config.hostname}") from e
|
|
102
110
|
raise SSHConnectionError(f"connection error: {e}") from e
|
|
103
111
|
|
|
104
112
|
logger.info(f"connected to {self.config.hostname}")
|
|
@@ -187,6 +195,8 @@ class AsyncSSHClient:
|
|
|
187
195
|
SSHConnectionError: 未连接时抛出
|
|
188
196
|
"""
|
|
189
197
|
conn = await self._get_conn()
|
|
198
|
+
# 安全:键必须为合法 shell 标识符(与同步实现一致,防止命令注入)
|
|
199
|
+
validate_environment(environment)
|
|
190
200
|
# 安全:对 value 做 shlex.quote 转义,防止 shell 元字符注入
|
|
191
201
|
env_str = ""
|
|
192
202
|
if environment:
|
|
@@ -198,11 +208,13 @@ class AsyncSSHClient:
|
|
|
198
208
|
# 安全:不记录命令全文(可能含敏感参数),仅记录执行事件
|
|
199
209
|
logger.debug("executing remote command")
|
|
200
210
|
try:
|
|
211
|
+
# 环境变量仅通过命令前缀的 export 注入(与同步 SSHClient 行为
|
|
212
|
+
# 一致):conn.run(env=...) 依赖服务端 AcceptEnv 且语义分叉,
|
|
213
|
+
# 不再重复传递
|
|
201
214
|
result = await conn.run(
|
|
202
215
|
full_command,
|
|
203
216
|
timeout=timeout,
|
|
204
217
|
check=False,
|
|
205
|
-
env={k: str(v) for k, v in (environment or {}).items()},
|
|
206
218
|
)
|
|
207
219
|
except (OSError, asyncssh.Error) as e:
|
|
208
220
|
raise SSHCommandError(f"command execution failed: {e}") from e
|