remote-cmd-manager 1.2.0__tar.gz → 1.2.2__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-1.2.0/remote_cmd_manager.egg-info → remote_cmd_manager-1.2.2}/PKG-INFO +94 -20
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/README.md +93 -19
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/pyproject.toml +1 -1
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/__init__.py +5 -2
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/cli/main.py +27 -35
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/core/async_connection_pool.py +26 -8
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/core/async_ssh_client.py +30 -13
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/core/host.py +4 -1
- remote_cmd_manager-1.2.2/remote_cmd/core/host_manager.py +165 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/core/ssh_client.py +8 -3
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/core/sync_connection_pool.py +16 -6
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/repository/json_host_repository.py +7 -1
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/repository/sqlite_host_repository.py +23 -4
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/service/async_batch_executor.py +44 -13
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/service/batch_executor.py +18 -10
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/service/credential_provider.py +22 -3
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/service/host_service.py +25 -7
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/service/storage_factory.py +3 -3
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/utils/crypto.py +20 -5
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2/remote_cmd_manager.egg-info}/PKG-INFO +94 -20
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd_manager.egg-info/SOURCES.txt +2 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_async_batch_executor.py +4 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_batch_executor.py +28 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_cli.py +54 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_credential_provider.py +37 -0
- remote_cmd_manager-1.2.2/tests/test_host_manager.py +132 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_sqlite_repository.py +37 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/LICENSE +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/MANIFEST.in +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/config.example.yaml +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/examples/basic_usage.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/examples/deploy_script.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/examples/nginx_batch_update.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/examples/system_health_check.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/cli/__init__.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/core/__init__.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/repository/__init__.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/repository/host_repository.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/service/__init__.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/service/ssh_service.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/service/task_runner.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/utils/__init__.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/utils/config.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/utils/exceptions.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd/utils/logging_utils.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd_manager.egg-info/requires.txt +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/remote_cmd_manager.egg-info/top_level.txt +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/requirements.txt +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/setup.cfg +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/__init__.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/conftest.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/integration/conftest.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/integration/test_ssh_connection.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/performance/__init__.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/performance/conftest.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/performance/test_benchmarks.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_async_ssh_client.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_config.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_crypto.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_host.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_host_service.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_keyring_provider.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_logging_utils.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_repository.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_ssh_client.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_ssh_service.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_storage_factory.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/tests/test_sync_connection_pool.py +0 -0
- {remote_cmd_manager-1.2.0 → remote_cmd_manager-1.2.2}/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: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: SSH 远程服务器管理工具 / Python SSH remote server management tool
|
|
5
5
|
Author-email: Vae-Scrooge <vaescrooge@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -52,18 +52,23 @@ Dynamic: license-file
|
|
|
52
52
|
<img src="https://img.shields.io/badge/python-3.9%2B-blue?style=for-the-badge&logo=python" alt="Python">
|
|
53
53
|
<img src="https://img.shields.io/github/license/Vae-Scrooge/remote-cmd?style=for-the-badge" alt="License">
|
|
54
54
|
<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
|
-
<img src="https://img.shields.io/badge/code%20style-black-black?style=for-the-badge" alt="Code Style">
|
|
56
55
|
</p>
|
|
57
56
|
|
|
58
57
|
<h1 align="center">Remote CMD — SSH Server Management<br><small>Without the Overhead</small></h1>
|
|
59
58
|
|
|
59
|
+
<p align="center">
|
|
60
|
+
<img src="https://img.shields.io/badge/English-blue?style=flat-square" alt="English"> ·
|
|
61
|
+
<a href="./README.zh-CN.md"><img src="https://img.shields.io/badge/中文-gray?style=flat-square" alt="中文"></a>
|
|
62
|
+
</p>
|
|
63
|
+
|
|
60
64
|
<p align="center">
|
|
61
65
|
<b><code>pip install remote_cmd_manager</code></b> ·
|
|
62
66
|
<a href="#quick-start">Quick Start</a> ·
|
|
63
67
|
<a href="#use-cases">Use Cases</a> ·
|
|
68
|
+
<a href="#cli-reference">CLI Reference</a> ·
|
|
64
69
|
<a href="#python-api">Python API</a> ·
|
|
65
|
-
<a href="
|
|
66
|
-
<a href="
|
|
70
|
+
<a href="#documentation">Documentation</a> ·
|
|
71
|
+
<a href="#contributing">Contributing</a>
|
|
67
72
|
</p>
|
|
68
73
|
|
|
69
74
|
<p align="center">
|
|
@@ -83,6 +88,23 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
83
88
|
|
|
84
89
|
---
|
|
85
90
|
|
|
91
|
+
## Table of Contents
|
|
92
|
+
|
|
93
|
+
- [Why Remote CMD?](#why-remote-cmd)
|
|
94
|
+
- [Quick Start](#quick-start)
|
|
95
|
+
- [Use Cases](#use-cases)
|
|
96
|
+
- [CLI Reference](#cli-reference)
|
|
97
|
+
- [Python API](#python-api)
|
|
98
|
+
- [Features](#features)
|
|
99
|
+
- [Installation](#installation)
|
|
100
|
+
- [Documentation](#documentation)
|
|
101
|
+
- [Project Status](#project-status)
|
|
102
|
+
- [Maintainership](#maintainership)
|
|
103
|
+
- [Contributing](#contributing)
|
|
104
|
+
- [License](#license)
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
86
108
|
## Why Remote CMD?
|
|
87
109
|
|
|
88
110
|
| Feature | `remote-cmd` | `ssh` + shell | Ansible | Fabric |
|
|
@@ -91,7 +113,7 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
91
113
|
| Batch commands across hosts | ✅ `batch-run` | ❌ Write a loop | ✅ Playbook | ✅ |
|
|
92
114
|
| File transfer (upload/download) | ✅ Built-in | ✅ scp | ✅ copy module | ✅ |
|
|
93
115
|
| Python API | ✅ `from remote_cmd import ...` | ❌ | ❌ YAML-only | ✅ |
|
|
94
|
-
| Zero setup | ✅ `pip install → go` | ❌
|
|
116
|
+
| Zero setup | ✅ `pip install → go` | ❌ Configure SSH | ❌ `ansible.cfg` | ❌ |
|
|
95
117
|
| Learning curve | **Low** | Low | **High** | Medium |
|
|
96
118
|
|
|
97
119
|
**Use `remote-cmd` when** you need a CLI that works immediately for ad-hoc SSH tasks. **Use Ansible when** you need full configuration management and idempotent playbooks.
|
|
@@ -118,7 +140,8 @@ remote-cmd batch-run -t production "df -h /"
|
|
|
118
140
|
|
|
119
141
|
## Use Cases
|
|
120
142
|
|
|
121
|
-
### 🖥️ System
|
|
143
|
+
### 🖥️ System Administrators — Check disk across 20 servers in one command
|
|
144
|
+
|
|
122
145
|
```bash
|
|
123
146
|
remote-cmd batch-run -t production "df -h / | tail -1"
|
|
124
147
|
# Output:
|
|
@@ -127,10 +150,11 @@ remote-cmd batch-run -t production "df -h / | tail -1"
|
|
|
127
150
|
# ✗ db-01 → Connection refused
|
|
128
151
|
```
|
|
129
152
|
|
|
130
|
-
### 🚀 Deploy — Pull code and restart service
|
|
153
|
+
### 🚀 Deploy — Pull code and restart a service
|
|
154
|
+
|
|
131
155
|
```python
|
|
132
156
|
from remote_cmd.service.host_service import HostService
|
|
133
|
-
from remote_cmd.repository
|
|
157
|
+
from remote_cmd.repository import JsonHostRepository
|
|
134
158
|
|
|
135
159
|
service = HostService(repository=JsonHostRepository("hosts.json"))
|
|
136
160
|
for host in service.list_hosts(tag="staging"):
|
|
@@ -141,19 +165,38 @@ for host in service.list_hosts(tag="staging"):
|
|
|
141
165
|
```
|
|
142
166
|
|
|
143
167
|
### 🔥 Incident Response — Check logs across all servers
|
|
168
|
+
|
|
144
169
|
```bash
|
|
145
170
|
remote-cmd batch-run -t web "journalctl -xe -n 50 | grep -i error"
|
|
146
171
|
```
|
|
147
172
|
|
|
148
173
|
### 🔧 Config Update — Upload and reload nginx across tagged hosts
|
|
174
|
+
|
|
149
175
|
```bash
|
|
150
|
-
# Upload new config
|
|
151
|
-
scp nginx.conf user@server:/tmp/nginx.conf # or use the upload command
|
|
176
|
+
# Upload new config, reload across web servers
|
|
152
177
|
remote-cmd run web-01 "sudo cp /tmp/nginx.conf /etc/nginx/nginx.conf && sudo nginx -t && sudo systemctl reload nginx"
|
|
153
178
|
```
|
|
154
179
|
|
|
155
180
|
---
|
|
156
181
|
|
|
182
|
+
## CLI Reference
|
|
183
|
+
|
|
184
|
+
All operations are available from the terminal:
|
|
185
|
+
|
|
186
|
+
| Command | Description |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `remote-cmd host add <name> <host> <user>` | Register a server (`-k/--key`, `-p/--port`, `-t/--tag`, repeatable) |
|
|
189
|
+
| `remote-cmd host list [-t TAG]` | List hosts, optionally filtered by tag |
|
|
190
|
+
| `remote-cmd host show <name>` | Show one host's details |
|
|
191
|
+
| `remote-cmd host test <name>` | Test connectivity to a host |
|
|
192
|
+
| `remote-cmd host remove <name>` | Remove a host |
|
|
193
|
+
| `remote-cmd run <name> "<cmd>"` | Run a command on one host |
|
|
194
|
+
| `remote-cmd upload <name> <local> <remote>` | Upload a file via SFTP |
|
|
195
|
+
| `remote-cmd download <name> <remote> <local>` | Download a file via SFTP |
|
|
196
|
+
| `remote-cmd batch-run -t <tag> "<cmd>"` | Run across all hosts in a tag (`-C` concurrency, `-T` timeout, `--async`, `--show-failures`) |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
157
200
|
## Python API
|
|
158
201
|
|
|
159
202
|
Use Remote CMD inside your own scripts and automation:
|
|
@@ -187,13 +230,18 @@ with SSHClient(config) as client:
|
|
|
187
230
|
|
|
188
231
|
| Category | Details |
|
|
189
232
|
|---|---|
|
|
190
|
-
| **SSH Auth** | Password + key file + ssh-agent |
|
|
233
|
+
| **SSH Auth** | Password + key file + ssh-agent, with pluggable credential providers |
|
|
234
|
+
| **Credential Chain** | Source passwords from environment, keyring, or arbitrary providers, in priority order |
|
|
235
|
+
| **Credential Encryption** | AES-encrypt secrets at rest (`CredentialEncryption`) |
|
|
191
236
|
| **Commands** | Single, multi-line, sudo with password |
|
|
192
|
-
| **File Transfer** | Upload/download via SFTP |
|
|
193
|
-
| **Host Management** | CRUD with JSON
|
|
237
|
+
| **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
|
|
238
|
+
| **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
|
|
194
239
|
| **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`) |
|
|
195
|
-
| **Batch Ops** | Run commands across any host group |
|
|
240
|
+
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously |
|
|
241
|
+
| **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
|
|
242
|
+
| **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
|
|
196
243
|
| **Connection Test** | Ping all hosts and report status |
|
|
244
|
+
| **Secure Logging** | Structured logging that filters sensitive data (`SensitiveDataFilter`) |
|
|
197
245
|
| **Type Safety** | Full type annotations + mypy strict |
|
|
198
246
|
|
|
199
247
|
---
|
|
@@ -201,10 +249,10 @@ with SSHClient(config) as client:
|
|
|
201
249
|
## Installation
|
|
202
250
|
|
|
203
251
|
```bash
|
|
204
|
-
# From PyPI (recommended) —
|
|
252
|
+
# From PyPI (recommended) — keeps API and CLI in sync
|
|
205
253
|
pip install remote_cmd_manager
|
|
206
254
|
|
|
207
|
-
# With async
|
|
255
|
+
# With native async support (AsyncSSHClient / AsyncConnectionPool / AsyncBatchExecutor)
|
|
208
256
|
pip install "remote_cmd_manager[async]"
|
|
209
257
|
|
|
210
258
|
# From source
|
|
@@ -216,13 +264,13 @@ pip install -e ".[dev]"
|
|
|
216
264
|
The `[async]` extra installs `asyncssh` and enables the native async execution
|
|
217
265
|
kernel: `AsyncSSHClient`, `AsyncConnectionPool` and `AsyncBatchExecutor`
|
|
218
266
|
(also available via `BatchExecutor(use_async=True)`). Without it, `import remote_cmd`
|
|
219
|
-
still works — async symbols are simply not exported.
|
|
267
|
+
still works — the async symbols are simply not exported.
|
|
220
268
|
|
|
221
269
|
---
|
|
222
270
|
|
|
223
271
|
## Documentation
|
|
224
272
|
|
|
225
|
-
📚 **[
|
|
273
|
+
📚 **[Full Documentation Center](./docs/README.md)** — tutorials, API reference, architecture, and troubleshooting
|
|
226
274
|
|
|
227
275
|
| Document | Contents |
|
|
228
276
|
|---|---|
|
|
@@ -230,11 +278,15 @@ still works — async symbols are simply not exported.
|
|
|
230
278
|
| [API Docs (auto-generated)](./docs/api/remote_cmd.html) | Complete API reference generated by pdoc |
|
|
231
279
|
| [Quickstart Tutorial](./docs/tutorial-quickstart.md) | Step-by-step walkthrough |
|
|
232
280
|
| [Advanced Tutorial](./docs/tutorial-advanced.md) | Batch ops, error handling, production patterns |
|
|
233
|
-
| [
|
|
281
|
+
| [Architecture](./docs/architecture.md) | System architecture and design decisions |
|
|
282
|
+
| [Development Guide](./docs/DEVELOPMENT.md) | Set up the dev environment, contributing |
|
|
234
283
|
| [Troubleshooting](./docs/TROUBLESHOOTING.md) | Common issues and solutions |
|
|
235
284
|
| [Changelog](./CHANGELOG.md) | Release history |
|
|
236
285
|
| [Mobile Remote Guide](./MOBILE-REMOTE-GUIDE.md) | Manage servers from your phone |
|
|
237
286
|
|
|
287
|
+
> **Note:** The documentation center and tutorials are maintained in **Chinese**. See
|
|
288
|
+
> [README.zh-CN.md](./README.zh-CN.md) for the Chinese version of this page.
|
|
289
|
+
|
|
238
290
|
---
|
|
239
291
|
|
|
240
292
|
## Project Status
|
|
@@ -243,8 +295,30 @@ still works — async symbols are simply not exported.
|
|
|
243
295
|
|
|
244
296
|
**Roadmap:**
|
|
245
297
|
- [x] Async SSH operations (parallel execution) — v1.1.0
|
|
298
|
+
- [x] Pluggable storage backends (JSON + SQLite) — v1.2.x
|
|
299
|
+
- [x] Chainable credential providers + at-rest encryption — v1.2.x
|
|
246
300
|
- [ ] Configuration profiles (AWS, GCP, custom)
|
|
247
301
|
- [ ] Output formatting (JSON, table)
|
|
302
|
+
- [ ] Templated command recipes
|
|
303
|
+
|
|
304
|
+
Good first issues are labelled `good first issue` in the
|
|
305
|
+
[issue tracker](https://github.com/Vae-Scrooge/remote-cmd/issues) — contributions welcome.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Maintainership
|
|
310
|
+
|
|
311
|
+
**Remote CMD is an actively maintained open-source project.** It is designed and
|
|
312
|
+
developed independently as a focused alternative to heavyweight tools for the
|
|
313
|
+
ad-hoc SSH tasks that come up in day-to-day server work.
|
|
314
|
+
|
|
315
|
+
- **Project health:** CI runs on every PR, Python 3.9+ is supported, and the
|
|
316
|
+
public API is versioned under [semantic versioning](https://semver.org/).
|
|
317
|
+
- **Your code, your servers:** usage stays open under the MIT license — nothing
|
|
318
|
+
is telemetry-driven or locked behind a service.
|
|
319
|
+
- **Why open source?** The tooling around ad-hoc SSH administration was either
|
|
320
|
+
too heavy (Ansible) or too bare (raw shell loops). Remote CMD exists so that
|
|
321
|
+
a single command can cover the common 90% of remote admin.
|
|
248
322
|
|
|
249
323
|
---
|
|
250
324
|
|
|
@@ -258,7 +332,7 @@ Before contributing, please read our [Code of Conduct](./CODE_OF_CONDUCT.md).
|
|
|
258
332
|
|
|
259
333
|
## License
|
|
260
334
|
|
|
261
|
-
MIT © [Vae-Scrooge](https://github.com/Vae-Scrooge)
|
|
335
|
+
MIT © [Vae-Scrooge](https://github.com/Vae-Scrooge/remote-cmd)
|
|
262
336
|
|
|
263
337
|
---
|
|
264
338
|
|
|
@@ -5,18 +5,23 @@
|
|
|
5
5
|
<img src="https://img.shields.io/badge/python-3.9%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
|
-
<img src="https://img.shields.io/badge/code%20style-black-black?style=for-the-badge" alt="Code Style">
|
|
9
8
|
</p>
|
|
10
9
|
|
|
11
10
|
<h1 align="center">Remote CMD — SSH Server Management<br><small>Without the Overhead</small></h1>
|
|
12
11
|
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://img.shields.io/badge/English-blue?style=flat-square" alt="English"> ·
|
|
14
|
+
<a href="./README.zh-CN.md"><img src="https://img.shields.io/badge/中文-gray?style=flat-square" alt="中文"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
13
17
|
<p align="center">
|
|
14
18
|
<b><code>pip install remote_cmd_manager</code></b> ·
|
|
15
19
|
<a href="#quick-start">Quick Start</a> ·
|
|
16
20
|
<a href="#use-cases">Use Cases</a> ·
|
|
21
|
+
<a href="#cli-reference">CLI Reference</a> ·
|
|
17
22
|
<a href="#python-api">Python API</a> ·
|
|
18
|
-
<a href="
|
|
19
|
-
<a href="
|
|
23
|
+
<a href="#documentation">Documentation</a> ·
|
|
24
|
+
<a href="#contributing">Contributing</a>
|
|
20
25
|
</p>
|
|
21
26
|
|
|
22
27
|
<p align="center">
|
|
@@ -36,6 +41,23 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
36
41
|
|
|
37
42
|
---
|
|
38
43
|
|
|
44
|
+
## Table of Contents
|
|
45
|
+
|
|
46
|
+
- [Why Remote CMD?](#why-remote-cmd)
|
|
47
|
+
- [Quick Start](#quick-start)
|
|
48
|
+
- [Use Cases](#use-cases)
|
|
49
|
+
- [CLI Reference](#cli-reference)
|
|
50
|
+
- [Python API](#python-api)
|
|
51
|
+
- [Features](#features)
|
|
52
|
+
- [Installation](#installation)
|
|
53
|
+
- [Documentation](#documentation)
|
|
54
|
+
- [Project Status](#project-status)
|
|
55
|
+
- [Maintainership](#maintainership)
|
|
56
|
+
- [Contributing](#contributing)
|
|
57
|
+
- [License](#license)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
39
61
|
## Why Remote CMD?
|
|
40
62
|
|
|
41
63
|
| Feature | `remote-cmd` | `ssh` + shell | Ansible | Fabric |
|
|
@@ -44,7 +66,7 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
44
66
|
| Batch commands across hosts | ✅ `batch-run` | ❌ Write a loop | ✅ Playbook | ✅ |
|
|
45
67
|
| File transfer (upload/download) | ✅ Built-in | ✅ scp | ✅ copy module | ✅ |
|
|
46
68
|
| Python API | ✅ `from remote_cmd import ...` | ❌ | ❌ YAML-only | ✅ |
|
|
47
|
-
| Zero setup | ✅ `pip install → go` | ❌
|
|
69
|
+
| Zero setup | ✅ `pip install → go` | ❌ Configure SSH | ❌ `ansible.cfg` | ❌ |
|
|
48
70
|
| Learning curve | **Low** | Low | **High** | Medium |
|
|
49
71
|
|
|
50
72
|
**Use `remote-cmd` when** you need a CLI that works immediately for ad-hoc SSH tasks. **Use Ansible when** you need full configuration management and idempotent playbooks.
|
|
@@ -71,7 +93,8 @@ remote-cmd batch-run -t production "df -h /"
|
|
|
71
93
|
|
|
72
94
|
## Use Cases
|
|
73
95
|
|
|
74
|
-
### 🖥️ System
|
|
96
|
+
### 🖥️ System Administrators — Check disk across 20 servers in one command
|
|
97
|
+
|
|
75
98
|
```bash
|
|
76
99
|
remote-cmd batch-run -t production "df -h / | tail -1"
|
|
77
100
|
# Output:
|
|
@@ -80,10 +103,11 @@ remote-cmd batch-run -t production "df -h / | tail -1"
|
|
|
80
103
|
# ✗ db-01 → Connection refused
|
|
81
104
|
```
|
|
82
105
|
|
|
83
|
-
### 🚀 Deploy — Pull code and restart service
|
|
106
|
+
### 🚀 Deploy — Pull code and restart a service
|
|
107
|
+
|
|
84
108
|
```python
|
|
85
109
|
from remote_cmd.service.host_service import HostService
|
|
86
|
-
from remote_cmd.repository
|
|
110
|
+
from remote_cmd.repository import JsonHostRepository
|
|
87
111
|
|
|
88
112
|
service = HostService(repository=JsonHostRepository("hosts.json"))
|
|
89
113
|
for host in service.list_hosts(tag="staging"):
|
|
@@ -94,19 +118,38 @@ for host in service.list_hosts(tag="staging"):
|
|
|
94
118
|
```
|
|
95
119
|
|
|
96
120
|
### 🔥 Incident Response — Check logs across all servers
|
|
121
|
+
|
|
97
122
|
```bash
|
|
98
123
|
remote-cmd batch-run -t web "journalctl -xe -n 50 | grep -i error"
|
|
99
124
|
```
|
|
100
125
|
|
|
101
126
|
### 🔧 Config Update — Upload and reload nginx across tagged hosts
|
|
127
|
+
|
|
102
128
|
```bash
|
|
103
|
-
# Upload new config
|
|
104
|
-
scp nginx.conf user@server:/tmp/nginx.conf # or use the upload command
|
|
129
|
+
# Upload new config, reload across web servers
|
|
105
130
|
remote-cmd run web-01 "sudo cp /tmp/nginx.conf /etc/nginx/nginx.conf && sudo nginx -t && sudo systemctl reload nginx"
|
|
106
131
|
```
|
|
107
132
|
|
|
108
133
|
---
|
|
109
134
|
|
|
135
|
+
## CLI Reference
|
|
136
|
+
|
|
137
|
+
All operations are available from the terminal:
|
|
138
|
+
|
|
139
|
+
| Command | Description |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `remote-cmd host add <name> <host> <user>` | Register a server (`-k/--key`, `-p/--port`, `-t/--tag`, repeatable) |
|
|
142
|
+
| `remote-cmd host list [-t TAG]` | List hosts, optionally filtered by tag |
|
|
143
|
+
| `remote-cmd host show <name>` | Show one host's details |
|
|
144
|
+
| `remote-cmd host test <name>` | Test connectivity to a host |
|
|
145
|
+
| `remote-cmd host remove <name>` | Remove a host |
|
|
146
|
+
| `remote-cmd run <name> "<cmd>"` | Run a command on one host |
|
|
147
|
+
| `remote-cmd upload <name> <local> <remote>` | Upload a file via SFTP |
|
|
148
|
+
| `remote-cmd download <name> <remote> <local>` | Download a file via SFTP |
|
|
149
|
+
| `remote-cmd batch-run -t <tag> "<cmd>"` | Run across all hosts in a tag (`-C` concurrency, `-T` timeout, `--async`, `--show-failures`) |
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
110
153
|
## Python API
|
|
111
154
|
|
|
112
155
|
Use Remote CMD inside your own scripts and automation:
|
|
@@ -140,13 +183,18 @@ with SSHClient(config) as client:
|
|
|
140
183
|
|
|
141
184
|
| Category | Details |
|
|
142
185
|
|---|---|
|
|
143
|
-
| **SSH Auth** | Password + key file + ssh-agent |
|
|
186
|
+
| **SSH Auth** | Password + key file + ssh-agent, with pluggable credential providers |
|
|
187
|
+
| **Credential Chain** | Source passwords from environment, keyring, or arbitrary providers, in priority order |
|
|
188
|
+
| **Credential Encryption** | AES-encrypt secrets at rest (`CredentialEncryption`) |
|
|
144
189
|
| **Commands** | Single, multi-line, sudo with password |
|
|
145
|
-
| **File Transfer** | Upload/download via SFTP |
|
|
146
|
-
| **Host Management** | CRUD with JSON
|
|
190
|
+
| **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
|
|
191
|
+
| **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
|
|
147
192
|
| **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`) |
|
|
148
|
-
| **Batch Ops** | Run commands across any host group |
|
|
193
|
+
| **Batch Ops** | Run commands across any host group, synchronously or asynchronously |
|
|
194
|
+
| **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
|
|
195
|
+
| **Task Runner** | Track and schedule long-running remote tasks with statuses (`TaskRunner`) |
|
|
149
196
|
| **Connection Test** | Ping all hosts and report status |
|
|
197
|
+
| **Secure Logging** | Structured logging that filters sensitive data (`SensitiveDataFilter`) |
|
|
150
198
|
| **Type Safety** | Full type annotations + mypy strict |
|
|
151
199
|
|
|
152
200
|
---
|
|
@@ -154,10 +202,10 @@ with SSHClient(config) as client:
|
|
|
154
202
|
## Installation
|
|
155
203
|
|
|
156
204
|
```bash
|
|
157
|
-
# From PyPI (recommended) —
|
|
205
|
+
# From PyPI (recommended) — keeps API and CLI in sync
|
|
158
206
|
pip install remote_cmd_manager
|
|
159
207
|
|
|
160
|
-
# With async
|
|
208
|
+
# With native async support (AsyncSSHClient / AsyncConnectionPool / AsyncBatchExecutor)
|
|
161
209
|
pip install "remote_cmd_manager[async]"
|
|
162
210
|
|
|
163
211
|
# From source
|
|
@@ -169,13 +217,13 @@ pip install -e ".[dev]"
|
|
|
169
217
|
The `[async]` extra installs `asyncssh` and enables the native async execution
|
|
170
218
|
kernel: `AsyncSSHClient`, `AsyncConnectionPool` and `AsyncBatchExecutor`
|
|
171
219
|
(also available via `BatchExecutor(use_async=True)`). Without it, `import remote_cmd`
|
|
172
|
-
still works — async symbols are simply not exported.
|
|
220
|
+
still works — the async symbols are simply not exported.
|
|
173
221
|
|
|
174
222
|
---
|
|
175
223
|
|
|
176
224
|
## Documentation
|
|
177
225
|
|
|
178
|
-
📚 **[
|
|
226
|
+
📚 **[Full Documentation Center](./docs/README.md)** — tutorials, API reference, architecture, and troubleshooting
|
|
179
227
|
|
|
180
228
|
| Document | Contents |
|
|
181
229
|
|---|---|
|
|
@@ -183,11 +231,15 @@ still works — async symbols are simply not exported.
|
|
|
183
231
|
| [API Docs (auto-generated)](./docs/api/remote_cmd.html) | Complete API reference generated by pdoc |
|
|
184
232
|
| [Quickstart Tutorial](./docs/tutorial-quickstart.md) | Step-by-step walkthrough |
|
|
185
233
|
| [Advanced Tutorial](./docs/tutorial-advanced.md) | Batch ops, error handling, production patterns |
|
|
186
|
-
| [
|
|
234
|
+
| [Architecture](./docs/architecture.md) | System architecture and design decisions |
|
|
235
|
+
| [Development Guide](./docs/DEVELOPMENT.md) | Set up the dev environment, contributing |
|
|
187
236
|
| [Troubleshooting](./docs/TROUBLESHOOTING.md) | Common issues and solutions |
|
|
188
237
|
| [Changelog](./CHANGELOG.md) | Release history |
|
|
189
238
|
| [Mobile Remote Guide](./MOBILE-REMOTE-GUIDE.md) | Manage servers from your phone |
|
|
190
239
|
|
|
240
|
+
> **Note:** The documentation center and tutorials are maintained in **Chinese**. See
|
|
241
|
+
> [README.zh-CN.md](./README.zh-CN.md) for the Chinese version of this page.
|
|
242
|
+
|
|
191
243
|
---
|
|
192
244
|
|
|
193
245
|
## Project Status
|
|
@@ -196,8 +248,30 @@ still works — async symbols are simply not exported.
|
|
|
196
248
|
|
|
197
249
|
**Roadmap:**
|
|
198
250
|
- [x] Async SSH operations (parallel execution) — v1.1.0
|
|
251
|
+
- [x] Pluggable storage backends (JSON + SQLite) — v1.2.x
|
|
252
|
+
- [x] Chainable credential providers + at-rest encryption — v1.2.x
|
|
199
253
|
- [ ] Configuration profiles (AWS, GCP, custom)
|
|
200
254
|
- [ ] Output formatting (JSON, table)
|
|
255
|
+
- [ ] Templated command recipes
|
|
256
|
+
|
|
257
|
+
Good first issues are labelled `good first issue` in the
|
|
258
|
+
[issue tracker](https://github.com/Vae-Scrooge/remote-cmd/issues) — contributions welcome.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Maintainership
|
|
263
|
+
|
|
264
|
+
**Remote CMD is an actively maintained open-source project.** It is designed and
|
|
265
|
+
developed independently as a focused alternative to heavyweight tools for the
|
|
266
|
+
ad-hoc SSH tasks that come up in day-to-day server work.
|
|
267
|
+
|
|
268
|
+
- **Project health:** CI runs on every PR, Python 3.9+ is supported, and the
|
|
269
|
+
public API is versioned under [semantic versioning](https://semver.org/).
|
|
270
|
+
- **Your code, your servers:** usage stays open under the MIT license — nothing
|
|
271
|
+
is telemetry-driven or locked behind a service.
|
|
272
|
+
- **Why open source?** The tooling around ad-hoc SSH administration was either
|
|
273
|
+
too heavy (Ansible) or too bare (raw shell loops). Remote CMD exists so that
|
|
274
|
+
a single command can cover the common 90% of remote admin.
|
|
201
275
|
|
|
202
276
|
---
|
|
203
277
|
|
|
@@ -211,7 +285,7 @@ Before contributing, please read our [Code of Conduct](./CODE_OF_CONDUCT.md).
|
|
|
211
285
|
|
|
212
286
|
## License
|
|
213
287
|
|
|
214
|
-
MIT © [Vae-Scrooge](https://github.com/Vae-Scrooge)
|
|
288
|
+
MIT © [Vae-Scrooge](https://github.com/Vae-Scrooge/remote-cmd)
|
|
215
289
|
|
|
216
290
|
---
|
|
217
291
|
|
|
@@ -48,11 +48,11 @@ Remote CMD - SSH 远程服务器管理工具
|
|
|
48
48
|
- 文档: 参见 docs/ 目录
|
|
49
49
|
|
|
50
50
|
Author: Vae-Scrooge
|
|
51
|
-
Version: 1.
|
|
51
|
+
Version: 1.2.2
|
|
52
52
|
License: MIT
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
|
-
__version__ = "1.2.
|
|
55
|
+
__version__ = "1.2.2"
|
|
56
56
|
__author__ = "Vae-Scrooge"
|
|
57
57
|
__email__ = "vae-scrooge@example.com"
|
|
58
58
|
__license__ = "MIT"
|
|
@@ -73,6 +73,7 @@ except ImportError: # pragma: no cover - 依赖 asyncssh,未安装时不导
|
|
|
73
73
|
_HAS_ASYNC = False
|
|
74
74
|
|
|
75
75
|
from remote_cmd.core.host import Host
|
|
76
|
+
from remote_cmd.core.host_manager import HostManager
|
|
76
77
|
from remote_cmd.core.ssh_client import SSHClient
|
|
77
78
|
from remote_cmd.core.sync_connection_pool import SyncConnectionPool
|
|
78
79
|
|
|
@@ -113,6 +114,7 @@ if _HAS_ASYNC:
|
|
|
113
114
|
"AsyncConnectionPool",
|
|
114
115
|
"AsyncBatchExecutor",
|
|
115
116
|
"Host",
|
|
117
|
+
"HostManager",
|
|
116
118
|
# 新架构导出
|
|
117
119
|
"HostRepository",
|
|
118
120
|
"JsonHostRepository",
|
|
@@ -145,6 +147,7 @@ else:
|
|
|
145
147
|
# 原有导出(向后兼容,不含异步符号)
|
|
146
148
|
"SSHClient",
|
|
147
149
|
"Host",
|
|
150
|
+
"HostManager",
|
|
148
151
|
# 新架构导出
|
|
149
152
|
"HostRepository",
|
|
150
153
|
"JsonHostRepository",
|
|
@@ -62,9 +62,17 @@ def _build_service(config_file: str, storage_backend: Optional[str] = None) -> H
|
|
|
62
62
|
@click.group()
|
|
63
63
|
@click.version_option(version=__version__, prog_name="remote-cmd")
|
|
64
64
|
@click.option("--config", "-c", type=click.Path(), help="Path to config file")
|
|
65
|
+
@click.option(
|
|
66
|
+
"--hosts-file",
|
|
67
|
+
"hosts_file_override",
|
|
68
|
+
type=click.Path(),
|
|
69
|
+
help="Override hosts storage file (e.g. hosts.db for SQLite). "
|
|
70
|
+
"Takes precedence over the config's hosts_file; backend is inferred "
|
|
71
|
+
"from the extension (.json/.db/.sqlite) unless storage_backend is set.",
|
|
72
|
+
)
|
|
65
73
|
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output mode")
|
|
66
74
|
@click.pass_context
|
|
67
|
-
def cli(ctx, config: Optional[str], verbose: bool):
|
|
75
|
+
def cli(ctx, config: Optional[str], hosts_file_override: Optional[str], verbose: bool):
|
|
68
76
|
"""
|
|
69
77
|
Remote CMD - SSH remote server management tool
|
|
70
78
|
|
|
@@ -90,12 +98,15 @@ def cli(ctx, config: Optional[str], verbose: bool):
|
|
|
90
98
|
ctx.obj["config"] = load_config(config_path)
|
|
91
99
|
ctx.obj["verbose"] = verbose
|
|
92
100
|
|
|
93
|
-
|
|
101
|
+
# CLI --hosts-file 优先于配置文件中的 hosts_file;storage_backend 仍由配置提供
|
|
102
|
+
# (扩展名推断覆盖 .json/.db/.sqlite 常见场景)
|
|
103
|
+
hosts_file = hosts_file_override or ctx.obj["config"].get("hosts_file", "hosts.json")
|
|
94
104
|
storage_backend = ctx.obj["config"].get("storage_backend")
|
|
95
105
|
ctx.obj["service"] = _build_service(hosts_file, storage_backend)
|
|
96
106
|
|
|
97
107
|
if verbose:
|
|
98
108
|
click.echo(f"Using config file: {config_path}")
|
|
109
|
+
click.echo(f"Using hosts file: {hosts_file}")
|
|
99
110
|
|
|
100
111
|
|
|
101
112
|
@cli.group()
|
|
@@ -120,16 +131,6 @@ def host():
|
|
|
120
131
|
@click.argument("hostname", required=True)
|
|
121
132
|
@click.argument("username", required=True)
|
|
122
133
|
@click.option("--port", "-p", default=22, help="SSH port (default: 22)")
|
|
123
|
-
@click.option(
|
|
124
|
-
"--password",
|
|
125
|
-
"-P",
|
|
126
|
-
default=None,
|
|
127
|
-
help=(
|
|
128
|
-
"Login password (INSECURE: visible in shell history and process list). "
|
|
129
|
-
"Prefer the REMOTE_CMD_PASSWORD environment variable, or omit this "
|
|
130
|
-
"option to be prompted interactively."
|
|
131
|
-
),
|
|
132
|
-
)
|
|
133
134
|
@click.option("--key", "-k", help="Path to SSH private key file")
|
|
134
135
|
@click.option("--tag", "-t", multiple=True, help="Host tag (may be repeated)")
|
|
135
136
|
@click.option("--description", "-d", default="", help="Host description")
|
|
@@ -140,7 +141,6 @@ def host_add(
|
|
|
140
141
|
hostname: str,
|
|
141
142
|
username: str,
|
|
142
143
|
port: int,
|
|
143
|
-
password: Optional[str],
|
|
144
144
|
key: Optional[str],
|
|
145
145
|
tag: tuple,
|
|
146
146
|
description: str,
|
|
@@ -151,15 +151,20 @@ def host_add(
|
|
|
151
151
|
NAME: host name (unique identifier)
|
|
152
152
|
HOSTNAME: host address (IP or domain)
|
|
153
153
|
USERNAME: SSH login username
|
|
154
|
+
|
|
155
|
+
Password input (in order of security):
|
|
156
|
+
|
|
157
|
+
1. REMOTE_CMD_PASSWORD environment variable (safest: not in shell
|
|
158
|
+
history or process list).
|
|
159
|
+
2. Interactive prompt via getpass (no echo, not recorded).
|
|
160
|
+
|
|
161
|
+
The insecure ``--password`` option has been removed to prevent passwords
|
|
162
|
+
from leaking via shell history and ``ps``/``/proc``. Use one of the above.
|
|
154
163
|
"""
|
|
155
164
|
service: HostService = ctx.obj["service"]
|
|
156
165
|
|
|
157
166
|
# Resolve password: priority REMOTE_CMD_PASSWORD env var > interactive
|
|
158
|
-
# getpass
|
|
159
|
-
# Security order: env var is safest (not in command line/history); getpass
|
|
160
|
-
# is interactive without echo and is not recorded.
|
|
161
|
-
# --password appears in shell history and process list, so it is the lowest
|
|
162
|
-
# priority and triggers a warning.
|
|
167
|
+
# getpass. Both are secure (not visible in command line/history).
|
|
163
168
|
env_password = os.environ.get("REMOTE_CMD_PASSWORD")
|
|
164
169
|
resolved_password: Optional[str] = None
|
|
165
170
|
|
|
@@ -173,22 +178,6 @@ def host_add(
|
|
|
173
178
|
click.echo(click.style("\n✗ cancelled", fg="red"), err=True)
|
|
174
179
|
ctx.exit(1)
|
|
175
180
|
|
|
176
|
-
if password and not env_password:
|
|
177
|
-
# Only warn when the user explicitly used --password (not env var)
|
|
178
|
-
click.echo(
|
|
179
|
-
click.style(
|
|
180
|
-
"⚠ Warning: passing the password via --password is insecure "
|
|
181
|
-
"(visible in shell history and process list). "
|
|
182
|
-
"Use the REMOTE_CMD_PASSWORD environment variable or "
|
|
183
|
-
"interactive input instead.",
|
|
184
|
-
fg="yellow",
|
|
185
|
-
),
|
|
186
|
-
err=True,
|
|
187
|
-
)
|
|
188
|
-
# --password as the last-resort fallback
|
|
189
|
-
if not resolved_password:
|
|
190
|
-
resolved_password = password
|
|
191
|
-
|
|
192
181
|
host = Host(
|
|
193
182
|
name=name,
|
|
194
183
|
hostname=hostname,
|
|
@@ -279,6 +268,7 @@ def host_show(ctx, name: str):
|
|
|
279
268
|
if host.key_filename:
|
|
280
269
|
# Sanitize key path: show only the filename, not full path
|
|
281
270
|
from pathlib import Path
|
|
271
|
+
|
|
282
272
|
key_name = Path(host.key_filename).name
|
|
283
273
|
click.echo(f" Key file: {key_name}")
|
|
284
274
|
tags_str = ", ".join(host.tags) if host.tags else "-"
|
|
@@ -435,7 +425,9 @@ def batch_run(
|
|
|
435
425
|
use_async=use_async,
|
|
436
426
|
)
|
|
437
427
|
|
|
438
|
-
click.echo(
|
|
428
|
+
click.echo(
|
|
429
|
+
f"Batch running on {len(host_names)} hosts, command='{command}', concurrency={concurrency}"
|
|
430
|
+
)
|
|
439
431
|
click.echo()
|
|
440
432
|
|
|
441
433
|
bar: Any
|