ssh-handler 1.0.0__tar.gz → 1.0.1__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.
- ssh_handler-1.0.1/LICENSE +21 -0
- ssh_handler-1.0.1/PKG-INFO +342 -0
- ssh_handler-1.0.1/README.md +314 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/pyproject.toml +36 -26
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/__init__.py +52 -52
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/cli.py +2 -2
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/config.py +4 -3
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/credentials.py +1 -1
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/pyqt_worker.py +2 -2
- ssh_handler-1.0.1/ssh_handler.egg-info/PKG-INFO +342 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler.egg-info/SOURCES.txt +1 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/tests/test_offline.py +3 -3
- ssh_handler-1.0.0/PKG-INFO +0 -182
- ssh_handler-1.0.0/README.md +0 -163
- ssh_handler-1.0.0/ssh_handler.egg-info/PKG-INFO +0 -182
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/setup.cfg +0 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/__main__.py +0 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/core.py +0 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/exceptions.py +0 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/ftp.py +0 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/pool.py +0 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/results.py +0 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler.egg-info/dependency_links.txt +0 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler.egg-info/entry_points.txt +0 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler.egg-info/requires.txt +0 -0
- {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ssh-handler contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ssh-handler
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Extensive SSH/SFTP/SCP/FTP handler built on Paramiko, for test automation, CLIs and PyQt5 tools.
|
|
5
|
+
Author: ssh-handler contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: ssh,sftp,scp,ftp,paramiko,automation,pyqt5,testing
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: System :: Networking
|
|
11
|
+
Classifier: Topic :: Software Development :: Testing
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: paramiko>=3.0
|
|
17
|
+
Provides-Extra: secure
|
|
18
|
+
Requires-Dist: keyring>=23.0; extra == "secure"
|
|
19
|
+
Provides-Extra: scp
|
|
20
|
+
Requires-Dist: scp>=0.14; extra == "scp"
|
|
21
|
+
Provides-Extra: gui
|
|
22
|
+
Requires-Dist: PyQt5>=5.15; extra == "gui"
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: keyring>=23.0; extra == "all"
|
|
25
|
+
Requires-Dist: scp>=0.14; extra == "all"
|
|
26
|
+
Requires-Dist: PyQt5>=5.15; extra == "all"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# ssh-handler
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/ssh-handler/)
|
|
32
|
+
[](https://pypi.org/project/ssh-handler/)
|
|
33
|
+
|
|
34
|
+
An extensive **SSH / SFTP / SCP / FTP** automation handler built on
|
|
35
|
+
[Paramiko](https://www.paramiko.org/). One package, three ways to use it:
|
|
36
|
+
|
|
37
|
+
- **Test-automation framework** — raise-on-error API, pytest fixtures, parallel fleet ops.
|
|
38
|
+
- **Standalone CLI** — `python -m ssh_handler …`, fully argument-driven.
|
|
39
|
+
- **PyQt5 tool** — safe mode + log streaming over Qt signals, runs off the GUI thread.
|
|
40
|
+
|
|
41
|
+
Passwords are wrapped in a `Secret` and stored in the **OS credential vault** — they
|
|
42
|
+
never appear in logs, reprs, tracebacks, or on disk in plaintext.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install ssh-handler
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Table of contents
|
|
51
|
+
|
|
52
|
+
- [Why this package](#why-this-package)
|
|
53
|
+
- [Install](#install)
|
|
54
|
+
- [Quick start](#quick-start)
|
|
55
|
+
- [What you can do](#what-you-can-do) — full capability list
|
|
56
|
+
- [Domain / Windows (RDP) hosts](#domain--windows-rdp-hosts)
|
|
57
|
+
- [Confidential credentials](#confidential-credentials)
|
|
58
|
+
- [Performance](#performance)
|
|
59
|
+
- [Error handling: two styles](#error-handling-two-styles)
|
|
60
|
+
- [Result objects](#result-objects)
|
|
61
|
+
- [CLI reference](#cli-reference)
|
|
62
|
+
- [PyQt5 integration](#pyqt5-integration)
|
|
63
|
+
- [Parallel fleet operations](#parallel-fleet-operations)
|
|
64
|
+
- [FTP / FTPS](#ftp--ftps)
|
|
65
|
+
- [API map](#api-map)
|
|
66
|
+
- [Releasing](#releasing)
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Why this package
|
|
71
|
+
|
|
72
|
+
Paramiko is powerful but low-level: you manage clients, transports, channels,
|
|
73
|
+
SFTP sessions, timeouts, retries, host-key policies and error handling yourself,
|
|
74
|
+
and you repeat that boilerplate in every project. `ssh-handler` wraps all of it
|
|
75
|
+
behind one object that:
|
|
76
|
+
|
|
77
|
+
- auto-selects the right authentication strategy (password, key, agent, empty password),
|
|
78
|
+
- retries connections and transparently reconnects dropped sessions,
|
|
79
|
+
- returns **structured results** for every action instead of raw strings,
|
|
80
|
+
- keeps **passwords confidential** end-to-end,
|
|
81
|
+
- and exposes the same surface whether you're in a test, a CLI, or a GUI.
|
|
82
|
+
|
|
83
|
+
## Install
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install ssh-handler # core (paramiko only)
|
|
87
|
+
pip install "ssh-handler[secure]" # + keyring (OS credential vault)
|
|
88
|
+
pip install "ssh-handler[scp]" # + scp (SCP-protocol transfers)
|
|
89
|
+
pip install "ssh-handler[gui]" # + PyQt5 (the GUI worker)
|
|
90
|
+
pip install "ssh-handler[all]" # everything
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`scp`, `keyring`, and `PyQt5` are optional — the core works without them, and
|
|
94
|
+
those features raise a clear, actionable message if you use them without the extra.
|
|
95
|
+
|
|
96
|
+
## Quick start
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from ssh_handler import SSHHandler, SSHConfig
|
|
100
|
+
|
|
101
|
+
with SSHHandler(SSHConfig(host="10.0.0.5", username="root", password="pw")) as ssh:
|
|
102
|
+
print(ssh.run("uptime").stdout)
|
|
103
|
+
ssh.run("systemctl restart nginx", check=True) # raises on non-zero exit
|
|
104
|
+
ssh.push("local.txt", "/tmp/remote.txt") # SFTP upload
|
|
105
|
+
ssh.pull("/etc/nginx", "./backup", recursive=True) # recursive download
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## What you can do
|
|
109
|
+
|
|
110
|
+
**Connection & session**
|
|
111
|
+
- Connect with password, private key (+ passphrase), SSH agent, auto-discovered
|
|
112
|
+
keys, or an **empty-password** account — all auto-tried in a smart order.
|
|
113
|
+
- **Auto-retry** connects with backoff; **auto-reconnect** if a session drops.
|
|
114
|
+
- Keepalives, per-command and connection timeouts, optional compression.
|
|
115
|
+
- **Jump host / bastion** chaining (ProxyJump-style) via `SSHConfig(jump_host=…)`.
|
|
116
|
+
- Host-key policy: `auto` / `reject` / `warn`, with an optional `known_hosts` file.
|
|
117
|
+
- Remote-OS awareness (`detect_os()`, `is_windows`) for Linux **and** Windows targets.
|
|
118
|
+
- Raw escape hatch: `ssh.client` and `ssh.transport` expose the underlying Paramiko objects.
|
|
119
|
+
|
|
120
|
+
**Command execution**
|
|
121
|
+
- `run()` — timeout, `check` (raise on non-zero), PTY allocation, custom environment.
|
|
122
|
+
- `run_many()` — batch with stop-on-error.
|
|
123
|
+
- `sudo()` — runs `sudo -S` and feeds the password on stdin.
|
|
124
|
+
- `open_shell()` — a persistent interactive `ShellSession` with `send` /
|
|
125
|
+
`read_until` (send-expect) / `read_available`.
|
|
126
|
+
|
|
127
|
+
**File operations (SFTP) — full Paramiko parity**
|
|
128
|
+
- Transfers: `push` / `pull` (single file **or** recursive directory, with progress
|
|
129
|
+
callbacks and transfer statistics), plus `scp_push` / `scp_pull` (SCP protocol).
|
|
130
|
+
- Listing & metadata: `listdir`, `listdir_attr`, `stat`, `lstat`, `exists`, `isdir`, `walk`.
|
|
131
|
+
- Directories: `mkdir`, `makedirs` (recursive `mkdir -p`), `rmdir`.
|
|
132
|
+
- Files: `remove`, `rename`, `open` (remote file object), `read_text`, `write_text`.
|
|
133
|
+
- Permissions & links: `chmod`, `chown`, `symlink`, `readlink`.
|
|
134
|
+
|
|
135
|
+
**Other protocols**
|
|
136
|
+
- **FTP / FTPS** via `FTPHandler` (standard-library `ftplib`, no extra dependency):
|
|
137
|
+
connect, login, TLS, `push`, `pull`, `listdir`, `cwd`, `pwd`, `mkdir`, `rmdir`,
|
|
138
|
+
`remove`, `rename`, `size`, `exists`.
|
|
139
|
+
|
|
140
|
+
**Scale & integration**
|
|
141
|
+
- `SSHPool` — run the same command/transfer across many hosts in parallel threads.
|
|
142
|
+
- Safe mode + log callback for GUIs; structured result objects everywhere.
|
|
143
|
+
- Confidential credential storage in the OS vault (`CredentialStore`, `Secret`, `mask`).
|
|
144
|
+
|
|
145
|
+
## Domain / Windows (RDP) hosts
|
|
146
|
+
|
|
147
|
+
The Windows machines you normally RDP into can be driven over SSH once **OpenSSH
|
|
148
|
+
Server** is enabled on them:
|
|
149
|
+
|
|
150
|
+
```powershell
|
|
151
|
+
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
|
|
152
|
+
Start-Service sshd ; Set-Service -Name sshd -StartupType Automatic
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Then log in with your normal domain credentials. **Pass `domain` and `username`
|
|
156
|
+
separately** — never hard-code a single `"DOMAIN\user"` Python string, because a
|
|
157
|
+
backslash escape (e.g. `\n`, `\t`) silently becomes a control character. The
|
|
158
|
+
handler builds the `DOMAIN\user` login string for you:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from ssh_handler import SSHHandler, SSHConfig, CredentialStore
|
|
162
|
+
|
|
163
|
+
store = CredentialStore(service="my_test_lab")
|
|
164
|
+
cfg = SSHConfig(
|
|
165
|
+
host="10.20.30.40",
|
|
166
|
+
domain="CORP", username="myuser", # -> login "CORP\myuser"
|
|
167
|
+
password=store.get("CORP\\myuser"), # a Secret pulled from the OS vault
|
|
168
|
+
remote_os="windows", # skip the OS probe
|
|
169
|
+
fast_auth=True, # skip key probing -> faster login
|
|
170
|
+
)
|
|
171
|
+
with SSHHandler(cfg) as ssh:
|
|
172
|
+
print(ssh.run("whoami").stdout) # CORP\myuser
|
|
173
|
+
print(ssh.run("powershell Get-Service sshd").stdout)
|
|
174
|
+
ssh.push("report.xlsx", "C:/Users/myuser/Desktop/report.xlsx")
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Store the password **once** so it never appears in code again:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from ssh_handler import CredentialStore, prompt_password
|
|
181
|
+
CredentialStore("my_test_lab").set("CORP\\myuser", prompt_password())
|
|
182
|
+
```
|
|
183
|
+
```bash
|
|
184
|
+
# …or from the CLI:
|
|
185
|
+
python -m ssh_handler store-credential --user myuser --domain CORP --service my_test_lab
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Confidential credentials
|
|
189
|
+
|
|
190
|
+
| Mechanism | What it does |
|
|
191
|
+
|-----------|--------------|
|
|
192
|
+
| `Secret` | wraps a password; `str()`/`repr()`/logs show `********`; only `.reveal()` exposes it |
|
|
193
|
+
| `mask()` | redacts secret values from any string (applied automatically to all logging) |
|
|
194
|
+
| `CredentialStore` | stores/reads passwords in the OS vault (Windows Credential Manager / macOS Keychain / Secret Service) via `keyring` — **no plaintext on disk** |
|
|
195
|
+
| `prompt_password()` | hidden terminal input, returns a `Secret` |
|
|
196
|
+
|
|
197
|
+
Pass a `Secret` (or a plain string, which is wrapped automatically) anywhere a
|
|
198
|
+
password is accepted. It stays redacted across the whole stack.
|
|
199
|
+
|
|
200
|
+
## Performance
|
|
201
|
+
|
|
202
|
+
- **`fast_auth`** (default on): when a password is supplied, the slow key/agent
|
|
203
|
+
probing is skipped — faster logins and no "Too many authentication failures"
|
|
204
|
+
from the server's `MaxAuthTries`.
|
|
205
|
+
- One SFTP channel is opened lazily and **reused** across operations.
|
|
206
|
+
- SFTP downloads use Paramiko **prefetch** for high throughput.
|
|
207
|
+
- `remote_os="windows"|"linux"` skips the one-time OS-detection probe.
|
|
208
|
+
- `compress=True` for slow/high-latency links; keepalives keep long sessions alive.
|
|
209
|
+
- `SSHPool` parallelizes across hosts with a thread pool.
|
|
210
|
+
|
|
211
|
+
## Error handling: two styles
|
|
212
|
+
|
|
213
|
+
**Raise (default)** — best for tests/scripts. Typed exceptions, all subclasses of
|
|
214
|
+
`SSHError`:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
SSHConnectionError SSHAuthenticationError SSHTimeoutError
|
|
218
|
+
SSHCommandError SSHTransferError SSHNotConnectedError
|
|
219
|
+
FTPError CredentialError
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Safe mode** (`SSHHandler(cfg, safe=True)`) — best for GUIs. Every call returns an
|
|
223
|
+
`OperationResult` instead of raising, so an event loop never dies:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
res = ssh.connect()
|
|
227
|
+
if not res: # OperationResult is falsy on failure
|
|
228
|
+
show_error(res.error)
|
|
229
|
+
else:
|
|
230
|
+
data = res.value # or res.unwrap() to re-raise on failure
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Override per call with `safe=True` / `safe=False`.
|
|
234
|
+
|
|
235
|
+
## Result objects
|
|
236
|
+
|
|
237
|
+
Every action returns structured data, not bare strings:
|
|
238
|
+
|
|
239
|
+
- **`CommandResult`** — `exit_code`, `stdout`, `stderr`, `duration`, `host`, `.ok`, `.as_dict()`
|
|
240
|
+
- **`TransferResult`** — `size_bytes`, `duration`, `speed_bps`, `human_speed`, `human_size`, `files`
|
|
241
|
+
- **`ShellResult`** — `output`, `matched`, `timed_out`, `duration`
|
|
242
|
+
- **`OperationResult`** — safe-mode wrapper: `bool(res)`, `res.value`, `res.error`, `res.unwrap()`
|
|
243
|
+
|
|
244
|
+
## CLI reference
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
python -m ssh_handler run --host H --user U --domain CORP uptime
|
|
248
|
+
python -m ssh_handler push --host H --user U ./build /tmp/build --recursive
|
|
249
|
+
python -m ssh_handler pull --host H --user U /var/log ./logs --recursive
|
|
250
|
+
python -m ssh_handler info --host H --user U --json
|
|
251
|
+
python -m ssh_handler store-credential --user U --domain CORP --service my_test_lab
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Password options: `--password` (hidden prompt), `--use-stored` (read from the OS
|
|
255
|
+
vault), `--key FILE` (private key). Add `--json` for machine-readable output.
|
|
256
|
+
After `pip install`, a `ssh-handler` console script is also available
|
|
257
|
+
(`ssh-handler run --host …`).
|
|
258
|
+
|
|
259
|
+
## PyQt5 integration
|
|
260
|
+
|
|
261
|
+
`ssh_handler.pyqt_worker.SSHWorker` is a `QObject` wrapping the handler in **safe
|
|
262
|
+
mode**. Move it to a `QThread`, connect its signals, and drive it from the GUI:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from PyQt5.QtCore import QThread
|
|
266
|
+
from ssh_handler import SSHConfig
|
|
267
|
+
from ssh_handler.pyqt_worker import SSHWorker
|
|
268
|
+
|
|
269
|
+
worker = SSHWorker(SSHConfig(host="10.0.0.5", username="root", password=secret))
|
|
270
|
+
thread = QThread(); worker.moveToThread(thread)
|
|
271
|
+
worker.log.connect(text_edit.append) # live, secret-masked log lines
|
|
272
|
+
worker.command_done.connect(on_command_done)
|
|
273
|
+
worker.progress.connect(progress_bar.setValue) # bytes_done, bytes_total
|
|
274
|
+
thread.started.connect(lambda: worker.run_command("uptime"))
|
|
275
|
+
thread.start()
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Signals: `log`, `connected`, `command_done`, `transfer_done`, `progress`, `error`,
|
|
279
|
+
`finished`. The import is lazy, so the rest of the package works where PyQt5 isn't installed.
|
|
280
|
+
|
|
281
|
+
## Parallel fleet operations
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from ssh_handler import SSHPool, SSHConfig
|
|
285
|
+
|
|
286
|
+
configs = [SSHConfig(host=h, username="root", password="pw")
|
|
287
|
+
for h in ("10.0.0.1", "10.0.0.2", "10.0.0.3")]
|
|
288
|
+
|
|
289
|
+
with SSHPool(configs, max_workers=8) as pool:
|
|
290
|
+
for host, res in pool.run("uptime").items():
|
|
291
|
+
print(host, res.value.stdout.strip() if res else res.error)
|
|
292
|
+
pool.pull("/var/log/syslog", "logs/{host}_syslog.txt") # {host} avoids collisions
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## FTP / FTPS
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from ssh_handler import FTPHandler, FTPConfig
|
|
299
|
+
|
|
300
|
+
with FTPHandler(FTPConfig(host="ftp.example.com", username="u",
|
|
301
|
+
password="p", use_tls=True)) as ftp:
|
|
302
|
+
ftp.push("local.txt", "remote.txt")
|
|
303
|
+
ftp.pull("remote.txt", "copy.txt")
|
|
304
|
+
print(ftp.listdir("/"))
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## API map
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
ssh_handler/
|
|
311
|
+
config.py SSHConfig, FTPConfig
|
|
312
|
+
credentials.py Secret, CredentialStore, mask, prompt_password
|
|
313
|
+
core.py SSHHandler, ShellSession (SSH + SFTP + SCP)
|
|
314
|
+
ftp.py FTPHandler (FTP / FTPS)
|
|
315
|
+
pool.py SSHPool (parallel multi-host)
|
|
316
|
+
cli.py argparse entry point (python -m ssh_handler / ssh-handler)
|
|
317
|
+
pyqt_worker.py SSHWorker (PyQt5, lazy import)
|
|
318
|
+
results.py CommandResult, TransferResult, ShellResult, OperationResult
|
|
319
|
+
exceptions.py SSHError hierarchy
|
|
320
|
+
examples/examples.py copy-paste recipes
|
|
321
|
+
tests/test_offline.py offline checks (no network needed)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Releasing
|
|
325
|
+
|
|
326
|
+
Maintainers: use the helper to build and publish a new version.
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
python scripts/release.py 1.0.1 # bump -> build -> twine check -> upload
|
|
330
|
+
python scripts/release.py 1.0.1 --dry-run # build + check only, no upload
|
|
331
|
+
python scripts/release.py patch # auto-bump patch/minor/major
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The token is read from the `TWINE_PASSWORD` environment variable (username
|
|
335
|
+
`__token__`), never hard-coded. See [`scripts/release.py`](scripts/release.py) and
|
|
336
|
+
the optional [GitHub Actions workflow](.github/workflows/publish.yml) (publishes on
|
|
337
|
+
a `v*` tag). PyPI permanently forbids re-uploading an existing version, so each
|
|
338
|
+
release must use a new version number.
|
|
339
|
+
|
|
340
|
+
## License
|
|
341
|
+
|
|
342
|
+
MIT
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# ssh-handler
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/ssh-handler/)
|
|
4
|
+
[](https://pypi.org/project/ssh-handler/)
|
|
5
|
+
|
|
6
|
+
An extensive **SSH / SFTP / SCP / FTP** automation handler built on
|
|
7
|
+
[Paramiko](https://www.paramiko.org/). One package, three ways to use it:
|
|
8
|
+
|
|
9
|
+
- **Test-automation framework** — raise-on-error API, pytest fixtures, parallel fleet ops.
|
|
10
|
+
- **Standalone CLI** — `python -m ssh_handler …`, fully argument-driven.
|
|
11
|
+
- **PyQt5 tool** — safe mode + log streaming over Qt signals, runs off the GUI thread.
|
|
12
|
+
|
|
13
|
+
Passwords are wrapped in a `Secret` and stored in the **OS credential vault** — they
|
|
14
|
+
never appear in logs, reprs, tracebacks, or on disk in plaintext.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install ssh-handler
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Table of contents
|
|
23
|
+
|
|
24
|
+
- [Why this package](#why-this-package)
|
|
25
|
+
- [Install](#install)
|
|
26
|
+
- [Quick start](#quick-start)
|
|
27
|
+
- [What you can do](#what-you-can-do) — full capability list
|
|
28
|
+
- [Domain / Windows (RDP) hosts](#domain--windows-rdp-hosts)
|
|
29
|
+
- [Confidential credentials](#confidential-credentials)
|
|
30
|
+
- [Performance](#performance)
|
|
31
|
+
- [Error handling: two styles](#error-handling-two-styles)
|
|
32
|
+
- [Result objects](#result-objects)
|
|
33
|
+
- [CLI reference](#cli-reference)
|
|
34
|
+
- [PyQt5 integration](#pyqt5-integration)
|
|
35
|
+
- [Parallel fleet operations](#parallel-fleet-operations)
|
|
36
|
+
- [FTP / FTPS](#ftp--ftps)
|
|
37
|
+
- [API map](#api-map)
|
|
38
|
+
- [Releasing](#releasing)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Why this package
|
|
43
|
+
|
|
44
|
+
Paramiko is powerful but low-level: you manage clients, transports, channels,
|
|
45
|
+
SFTP sessions, timeouts, retries, host-key policies and error handling yourself,
|
|
46
|
+
and you repeat that boilerplate in every project. `ssh-handler` wraps all of it
|
|
47
|
+
behind one object that:
|
|
48
|
+
|
|
49
|
+
- auto-selects the right authentication strategy (password, key, agent, empty password),
|
|
50
|
+
- retries connections and transparently reconnects dropped sessions,
|
|
51
|
+
- returns **structured results** for every action instead of raw strings,
|
|
52
|
+
- keeps **passwords confidential** end-to-end,
|
|
53
|
+
- and exposes the same surface whether you're in a test, a CLI, or a GUI.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install ssh-handler # core (paramiko only)
|
|
59
|
+
pip install "ssh-handler[secure]" # + keyring (OS credential vault)
|
|
60
|
+
pip install "ssh-handler[scp]" # + scp (SCP-protocol transfers)
|
|
61
|
+
pip install "ssh-handler[gui]" # + PyQt5 (the GUI worker)
|
|
62
|
+
pip install "ssh-handler[all]" # everything
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`scp`, `keyring`, and `PyQt5` are optional — the core works without them, and
|
|
66
|
+
those features raise a clear, actionable message if you use them without the extra.
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from ssh_handler import SSHHandler, SSHConfig
|
|
72
|
+
|
|
73
|
+
with SSHHandler(SSHConfig(host="10.0.0.5", username="root", password="pw")) as ssh:
|
|
74
|
+
print(ssh.run("uptime").stdout)
|
|
75
|
+
ssh.run("systemctl restart nginx", check=True) # raises on non-zero exit
|
|
76
|
+
ssh.push("local.txt", "/tmp/remote.txt") # SFTP upload
|
|
77
|
+
ssh.pull("/etc/nginx", "./backup", recursive=True) # recursive download
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## What you can do
|
|
81
|
+
|
|
82
|
+
**Connection & session**
|
|
83
|
+
- Connect with password, private key (+ passphrase), SSH agent, auto-discovered
|
|
84
|
+
keys, or an **empty-password** account — all auto-tried in a smart order.
|
|
85
|
+
- **Auto-retry** connects with backoff; **auto-reconnect** if a session drops.
|
|
86
|
+
- Keepalives, per-command and connection timeouts, optional compression.
|
|
87
|
+
- **Jump host / bastion** chaining (ProxyJump-style) via `SSHConfig(jump_host=…)`.
|
|
88
|
+
- Host-key policy: `auto` / `reject` / `warn`, with an optional `known_hosts` file.
|
|
89
|
+
- Remote-OS awareness (`detect_os()`, `is_windows`) for Linux **and** Windows targets.
|
|
90
|
+
- Raw escape hatch: `ssh.client` and `ssh.transport` expose the underlying Paramiko objects.
|
|
91
|
+
|
|
92
|
+
**Command execution**
|
|
93
|
+
- `run()` — timeout, `check` (raise on non-zero), PTY allocation, custom environment.
|
|
94
|
+
- `run_many()` — batch with stop-on-error.
|
|
95
|
+
- `sudo()` — runs `sudo -S` and feeds the password on stdin.
|
|
96
|
+
- `open_shell()` — a persistent interactive `ShellSession` with `send` /
|
|
97
|
+
`read_until` (send-expect) / `read_available`.
|
|
98
|
+
|
|
99
|
+
**File operations (SFTP) — full Paramiko parity**
|
|
100
|
+
- Transfers: `push` / `pull` (single file **or** recursive directory, with progress
|
|
101
|
+
callbacks and transfer statistics), plus `scp_push` / `scp_pull` (SCP protocol).
|
|
102
|
+
- Listing & metadata: `listdir`, `listdir_attr`, `stat`, `lstat`, `exists`, `isdir`, `walk`.
|
|
103
|
+
- Directories: `mkdir`, `makedirs` (recursive `mkdir -p`), `rmdir`.
|
|
104
|
+
- Files: `remove`, `rename`, `open` (remote file object), `read_text`, `write_text`.
|
|
105
|
+
- Permissions & links: `chmod`, `chown`, `symlink`, `readlink`.
|
|
106
|
+
|
|
107
|
+
**Other protocols**
|
|
108
|
+
- **FTP / FTPS** via `FTPHandler` (standard-library `ftplib`, no extra dependency):
|
|
109
|
+
connect, login, TLS, `push`, `pull`, `listdir`, `cwd`, `pwd`, `mkdir`, `rmdir`,
|
|
110
|
+
`remove`, `rename`, `size`, `exists`.
|
|
111
|
+
|
|
112
|
+
**Scale & integration**
|
|
113
|
+
- `SSHPool` — run the same command/transfer across many hosts in parallel threads.
|
|
114
|
+
- Safe mode + log callback for GUIs; structured result objects everywhere.
|
|
115
|
+
- Confidential credential storage in the OS vault (`CredentialStore`, `Secret`, `mask`).
|
|
116
|
+
|
|
117
|
+
## Domain / Windows (RDP) hosts
|
|
118
|
+
|
|
119
|
+
The Windows machines you normally RDP into can be driven over SSH once **OpenSSH
|
|
120
|
+
Server** is enabled on them:
|
|
121
|
+
|
|
122
|
+
```powershell
|
|
123
|
+
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
|
|
124
|
+
Start-Service sshd ; Set-Service -Name sshd -StartupType Automatic
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Then log in with your normal domain credentials. **Pass `domain` and `username`
|
|
128
|
+
separately** — never hard-code a single `"DOMAIN\user"` Python string, because a
|
|
129
|
+
backslash escape (e.g. `\n`, `\t`) silently becomes a control character. The
|
|
130
|
+
handler builds the `DOMAIN\user` login string for you:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from ssh_handler import SSHHandler, SSHConfig, CredentialStore
|
|
134
|
+
|
|
135
|
+
store = CredentialStore(service="my_test_lab")
|
|
136
|
+
cfg = SSHConfig(
|
|
137
|
+
host="10.20.30.40",
|
|
138
|
+
domain="CORP", username="myuser", # -> login "CORP\myuser"
|
|
139
|
+
password=store.get("CORP\\myuser"), # a Secret pulled from the OS vault
|
|
140
|
+
remote_os="windows", # skip the OS probe
|
|
141
|
+
fast_auth=True, # skip key probing -> faster login
|
|
142
|
+
)
|
|
143
|
+
with SSHHandler(cfg) as ssh:
|
|
144
|
+
print(ssh.run("whoami").stdout) # CORP\myuser
|
|
145
|
+
print(ssh.run("powershell Get-Service sshd").stdout)
|
|
146
|
+
ssh.push("report.xlsx", "C:/Users/myuser/Desktop/report.xlsx")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Store the password **once** so it never appears in code again:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from ssh_handler import CredentialStore, prompt_password
|
|
153
|
+
CredentialStore("my_test_lab").set("CORP\\myuser", prompt_password())
|
|
154
|
+
```
|
|
155
|
+
```bash
|
|
156
|
+
# …or from the CLI:
|
|
157
|
+
python -m ssh_handler store-credential --user myuser --domain CORP --service my_test_lab
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Confidential credentials
|
|
161
|
+
|
|
162
|
+
| Mechanism | What it does |
|
|
163
|
+
|-----------|--------------|
|
|
164
|
+
| `Secret` | wraps a password; `str()`/`repr()`/logs show `********`; only `.reveal()` exposes it |
|
|
165
|
+
| `mask()` | redacts secret values from any string (applied automatically to all logging) |
|
|
166
|
+
| `CredentialStore` | stores/reads passwords in the OS vault (Windows Credential Manager / macOS Keychain / Secret Service) via `keyring` — **no plaintext on disk** |
|
|
167
|
+
| `prompt_password()` | hidden terminal input, returns a `Secret` |
|
|
168
|
+
|
|
169
|
+
Pass a `Secret` (or a plain string, which is wrapped automatically) anywhere a
|
|
170
|
+
password is accepted. It stays redacted across the whole stack.
|
|
171
|
+
|
|
172
|
+
## Performance
|
|
173
|
+
|
|
174
|
+
- **`fast_auth`** (default on): when a password is supplied, the slow key/agent
|
|
175
|
+
probing is skipped — faster logins and no "Too many authentication failures"
|
|
176
|
+
from the server's `MaxAuthTries`.
|
|
177
|
+
- One SFTP channel is opened lazily and **reused** across operations.
|
|
178
|
+
- SFTP downloads use Paramiko **prefetch** for high throughput.
|
|
179
|
+
- `remote_os="windows"|"linux"` skips the one-time OS-detection probe.
|
|
180
|
+
- `compress=True` for slow/high-latency links; keepalives keep long sessions alive.
|
|
181
|
+
- `SSHPool` parallelizes across hosts with a thread pool.
|
|
182
|
+
|
|
183
|
+
## Error handling: two styles
|
|
184
|
+
|
|
185
|
+
**Raise (default)** — best for tests/scripts. Typed exceptions, all subclasses of
|
|
186
|
+
`SSHError`:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
SSHConnectionError SSHAuthenticationError SSHTimeoutError
|
|
190
|
+
SSHCommandError SSHTransferError SSHNotConnectedError
|
|
191
|
+
FTPError CredentialError
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Safe mode** (`SSHHandler(cfg, safe=True)`) — best for GUIs. Every call returns an
|
|
195
|
+
`OperationResult` instead of raising, so an event loop never dies:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
res = ssh.connect()
|
|
199
|
+
if not res: # OperationResult is falsy on failure
|
|
200
|
+
show_error(res.error)
|
|
201
|
+
else:
|
|
202
|
+
data = res.value # or res.unwrap() to re-raise on failure
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Override per call with `safe=True` / `safe=False`.
|
|
206
|
+
|
|
207
|
+
## Result objects
|
|
208
|
+
|
|
209
|
+
Every action returns structured data, not bare strings:
|
|
210
|
+
|
|
211
|
+
- **`CommandResult`** — `exit_code`, `stdout`, `stderr`, `duration`, `host`, `.ok`, `.as_dict()`
|
|
212
|
+
- **`TransferResult`** — `size_bytes`, `duration`, `speed_bps`, `human_speed`, `human_size`, `files`
|
|
213
|
+
- **`ShellResult`** — `output`, `matched`, `timed_out`, `duration`
|
|
214
|
+
- **`OperationResult`** — safe-mode wrapper: `bool(res)`, `res.value`, `res.error`, `res.unwrap()`
|
|
215
|
+
|
|
216
|
+
## CLI reference
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python -m ssh_handler run --host H --user U --domain CORP uptime
|
|
220
|
+
python -m ssh_handler push --host H --user U ./build /tmp/build --recursive
|
|
221
|
+
python -m ssh_handler pull --host H --user U /var/log ./logs --recursive
|
|
222
|
+
python -m ssh_handler info --host H --user U --json
|
|
223
|
+
python -m ssh_handler store-credential --user U --domain CORP --service my_test_lab
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Password options: `--password` (hidden prompt), `--use-stored` (read from the OS
|
|
227
|
+
vault), `--key FILE` (private key). Add `--json` for machine-readable output.
|
|
228
|
+
After `pip install`, a `ssh-handler` console script is also available
|
|
229
|
+
(`ssh-handler run --host …`).
|
|
230
|
+
|
|
231
|
+
## PyQt5 integration
|
|
232
|
+
|
|
233
|
+
`ssh_handler.pyqt_worker.SSHWorker` is a `QObject` wrapping the handler in **safe
|
|
234
|
+
mode**. Move it to a `QThread`, connect its signals, and drive it from the GUI:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from PyQt5.QtCore import QThread
|
|
238
|
+
from ssh_handler import SSHConfig
|
|
239
|
+
from ssh_handler.pyqt_worker import SSHWorker
|
|
240
|
+
|
|
241
|
+
worker = SSHWorker(SSHConfig(host="10.0.0.5", username="root", password=secret))
|
|
242
|
+
thread = QThread(); worker.moveToThread(thread)
|
|
243
|
+
worker.log.connect(text_edit.append) # live, secret-masked log lines
|
|
244
|
+
worker.command_done.connect(on_command_done)
|
|
245
|
+
worker.progress.connect(progress_bar.setValue) # bytes_done, bytes_total
|
|
246
|
+
thread.started.connect(lambda: worker.run_command("uptime"))
|
|
247
|
+
thread.start()
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Signals: `log`, `connected`, `command_done`, `transfer_done`, `progress`, `error`,
|
|
251
|
+
`finished`. The import is lazy, so the rest of the package works where PyQt5 isn't installed.
|
|
252
|
+
|
|
253
|
+
## Parallel fleet operations
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from ssh_handler import SSHPool, SSHConfig
|
|
257
|
+
|
|
258
|
+
configs = [SSHConfig(host=h, username="root", password="pw")
|
|
259
|
+
for h in ("10.0.0.1", "10.0.0.2", "10.0.0.3")]
|
|
260
|
+
|
|
261
|
+
with SSHPool(configs, max_workers=8) as pool:
|
|
262
|
+
for host, res in pool.run("uptime").items():
|
|
263
|
+
print(host, res.value.stdout.strip() if res else res.error)
|
|
264
|
+
pool.pull("/var/log/syslog", "logs/{host}_syslog.txt") # {host} avoids collisions
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## FTP / FTPS
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from ssh_handler import FTPHandler, FTPConfig
|
|
271
|
+
|
|
272
|
+
with FTPHandler(FTPConfig(host="ftp.example.com", username="u",
|
|
273
|
+
password="p", use_tls=True)) as ftp:
|
|
274
|
+
ftp.push("local.txt", "remote.txt")
|
|
275
|
+
ftp.pull("remote.txt", "copy.txt")
|
|
276
|
+
print(ftp.listdir("/"))
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## API map
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
ssh_handler/
|
|
283
|
+
config.py SSHConfig, FTPConfig
|
|
284
|
+
credentials.py Secret, CredentialStore, mask, prompt_password
|
|
285
|
+
core.py SSHHandler, ShellSession (SSH + SFTP + SCP)
|
|
286
|
+
ftp.py FTPHandler (FTP / FTPS)
|
|
287
|
+
pool.py SSHPool (parallel multi-host)
|
|
288
|
+
cli.py argparse entry point (python -m ssh_handler / ssh-handler)
|
|
289
|
+
pyqt_worker.py SSHWorker (PyQt5, lazy import)
|
|
290
|
+
results.py CommandResult, TransferResult, ShellResult, OperationResult
|
|
291
|
+
exceptions.py SSHError hierarchy
|
|
292
|
+
examples/examples.py copy-paste recipes
|
|
293
|
+
tests/test_offline.py offline checks (no network needed)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Releasing
|
|
297
|
+
|
|
298
|
+
Maintainers: use the helper to build and publish a new version.
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
python scripts/release.py 1.0.1 # bump -> build -> twine check -> upload
|
|
302
|
+
python scripts/release.py 1.0.1 --dry-run # build + check only, no upload
|
|
303
|
+
python scripts/release.py patch # auto-bump patch/minor/major
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The token is read from the `TWINE_PASSWORD` environment variable (username
|
|
307
|
+
`__token__`), never hard-coded. See [`scripts/release.py`](scripts/release.py) and
|
|
308
|
+
the optional [GitHub Actions workflow](.github/workflows/publish.yml) (publishes on
|
|
309
|
+
a `v*` tag). PyPI permanently forbids re-uploading an existing version, so each
|
|
310
|
+
release must use a new version number.
|
|
311
|
+
|
|
312
|
+
## License
|
|
313
|
+
|
|
314
|
+
MIT
|