ssh-handler 1.0.0__py3-none-any.whl
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/__init__.py +52 -0
- ssh_handler/__main__.py +5 -0
- ssh_handler/cli.py +152 -0
- ssh_handler/config.py +114 -0
- ssh_handler/core.py +793 -0
- ssh_handler/credentials.py +144 -0
- ssh_handler/exceptions.py +49 -0
- ssh_handler/ftp.py +186 -0
- ssh_handler/pool.py +103 -0
- ssh_handler/pyqt_worker.py +94 -0
- ssh_handler/results.py +127 -0
- ssh_handler-1.0.0.dist-info/METADATA +182 -0
- ssh_handler-1.0.0.dist-info/RECORD +16 -0
- ssh_handler-1.0.0.dist-info/WHEEL +5 -0
- ssh_handler-1.0.0.dist-info/entry_points.txt +2 -0
- ssh_handler-1.0.0.dist-info/top_level.txt +1 -0
ssh_handler/results.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Rich, structured result objects returned by every action."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass, field, asdict
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _human_size(num: float) -> str:
|
|
11
|
+
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
12
|
+
if abs(num) < 1024.0:
|
|
13
|
+
return f"{num:.1f}{unit}"
|
|
14
|
+
num /= 1024.0
|
|
15
|
+
return f"{num:.1f}PB"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CommandResult:
|
|
20
|
+
"""Result of a remote command execution."""
|
|
21
|
+
|
|
22
|
+
command: str
|
|
23
|
+
exit_code: int
|
|
24
|
+
stdout: str
|
|
25
|
+
stderr: str
|
|
26
|
+
duration: float
|
|
27
|
+
host: str = ""
|
|
28
|
+
started_at: float = field(default_factory=time.time)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def ok(self) -> bool:
|
|
32
|
+
return self.exit_code == 0
|
|
33
|
+
|
|
34
|
+
def __bool__(self) -> bool:
|
|
35
|
+
return self.ok
|
|
36
|
+
|
|
37
|
+
def as_dict(self) -> dict:
|
|
38
|
+
return asdict(self)
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return (
|
|
42
|
+
f"<CommandResult host={self.host} exit={self.exit_code} "
|
|
43
|
+
f"ok={self.ok} dur={self.duration:.2f}s cmd={self.command!r}>"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class TransferResult:
|
|
49
|
+
"""Result of a file/dir transfer (push/pull via SFTP or SCP)."""
|
|
50
|
+
|
|
51
|
+
source: str
|
|
52
|
+
dest: str
|
|
53
|
+
direction: str # "push" or "pull"
|
|
54
|
+
protocol: str # "sftp" or "scp"
|
|
55
|
+
size_bytes: int
|
|
56
|
+
duration: float
|
|
57
|
+
files: int = 1
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def speed_bps(self) -> float:
|
|
61
|
+
return self.size_bytes / self.duration if self.duration > 0 else 0.0
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def human_speed(self) -> str:
|
|
65
|
+
return f"{_human_size(self.speed_bps)}/s"
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def human_size(self) -> str:
|
|
69
|
+
return _human_size(self.size_bytes)
|
|
70
|
+
|
|
71
|
+
def as_dict(self) -> dict:
|
|
72
|
+
d = asdict(self)
|
|
73
|
+
d.update(speed_bps=self.speed_bps, human_speed=self.human_speed)
|
|
74
|
+
return d
|
|
75
|
+
|
|
76
|
+
def __str__(self) -> str:
|
|
77
|
+
return (
|
|
78
|
+
f"<TransferResult {self.direction}/{self.protocol} "
|
|
79
|
+
f"{self.source} -> {self.dest} {self.human_size} "
|
|
80
|
+
f"in {self.duration:.2f}s ({self.human_speed}), files={self.files}>"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class ShellResult:
|
|
86
|
+
"""Result of an interactive shell read/expect operation."""
|
|
87
|
+
|
|
88
|
+
output: str
|
|
89
|
+
matched: Optional[str]
|
|
90
|
+
timed_out: bool
|
|
91
|
+
duration: float
|
|
92
|
+
|
|
93
|
+
def __bool__(self) -> bool:
|
|
94
|
+
return not self.timed_out
|
|
95
|
+
|
|
96
|
+
def __str__(self) -> str:
|
|
97
|
+
return (
|
|
98
|
+
f"<ShellResult matched={self.matched!r} "
|
|
99
|
+
f"timed_out={self.timed_out} dur={self.duration:.2f}s>"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class OperationResult:
|
|
105
|
+
"""
|
|
106
|
+
Returned by safe-mode operations. Wraps a value or an error so GUI handlers
|
|
107
|
+
never have to try/except. Falsy on failure.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
success: bool
|
|
111
|
+
action: str = ""
|
|
112
|
+
value: object = None
|
|
113
|
+
error: Optional[Exception] = None
|
|
114
|
+
|
|
115
|
+
def __bool__(self) -> bool:
|
|
116
|
+
return self.success
|
|
117
|
+
|
|
118
|
+
def unwrap(self):
|
|
119
|
+
"""Return value on success, else re-raise the captured error."""
|
|
120
|
+
if self.success:
|
|
121
|
+
return self.value
|
|
122
|
+
raise self.error if self.error else RuntimeError(f"{self.action} failed")
|
|
123
|
+
|
|
124
|
+
def __str__(self) -> str:
|
|
125
|
+
if self.success:
|
|
126
|
+
return f"<OperationResult {self.action} ok value={self.value!r}>"
|
|
127
|
+
return f"<OperationResult {self.action} error={self.error!r}>"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ssh-handler
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Extensive SSH/SFTP/SCP/FTP handler built on Paramiko, for test automation, CLIs and PyQt5 tools.
|
|
5
|
+
Author: Naveen
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: paramiko>=3.0
|
|
9
|
+
Provides-Extra: secure
|
|
10
|
+
Requires-Dist: keyring>=23.0; extra == "secure"
|
|
11
|
+
Provides-Extra: scp
|
|
12
|
+
Requires-Dist: scp>=0.14; extra == "scp"
|
|
13
|
+
Provides-Extra: gui
|
|
14
|
+
Requires-Dist: PyQt5>=5.15; extra == "gui"
|
|
15
|
+
Provides-Extra: all
|
|
16
|
+
Requires-Dist: keyring>=23.0; extra == "all"
|
|
17
|
+
Requires-Dist: scp>=0.14; extra == "all"
|
|
18
|
+
Requires-Dist: PyQt5>=5.15; extra == "all"
|
|
19
|
+
|
|
20
|
+
# ssh_handler
|
|
21
|
+
|
|
22
|
+
An extensive **SSH / SFTP / SCP / FTP** handler built on [Paramiko](https://www.paramiko.org/),
|
|
23
|
+
packaged so the *same* code works in three places:
|
|
24
|
+
|
|
25
|
+
- a **test-automation framework** (raise-on-error, pytest fixtures, parallel fleet ops),
|
|
26
|
+
- a **standalone CLI** (`python -m ssh_handler …`, argument-driven),
|
|
27
|
+
- a **PyQt5 tool** (safe mode + log streaming via Qt signals, off the GUI thread).
|
|
28
|
+
|
|
29
|
+
Passwords are wrapped in a `Secret` and stored in the **OS credential vault** — they
|
|
30
|
+
never appear in logs, reprs, tracebacks, or on disk in plaintext.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e . # core (paramiko)
|
|
36
|
+
pip install -e ".[all]" # + keyring, scp, PyQt5
|
|
37
|
+
# or just the pieces you need:
|
|
38
|
+
pip install -e ".[secure]" # keyring (confidential password storage)
|
|
39
|
+
pip install -e ".[scp]" # scp (SCP-protocol transfers)
|
|
40
|
+
pip install -e ".[gui]" # PyQt5 (the worker)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from ssh_handler import SSHHandler, SSHConfig
|
|
47
|
+
|
|
48
|
+
with SSHHandler(SSHConfig(host="10.0.0.5", username="root", password="pw")) as ssh:
|
|
49
|
+
print(ssh.run("uptime").stdout)
|
|
50
|
+
ssh.run("systemctl restart nginx", check=True) # raises on non-zero exit
|
|
51
|
+
ssh.push("local.txt", "/tmp/remote.txt") # SFTP upload
|
|
52
|
+
ssh.pull("/etc/nginx", "./backup", recursive=True) # recursive download
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Connecting to your RDP / Windows hosts (domain login `EU\nkennedy`)
|
|
56
|
+
|
|
57
|
+
Those Windows machines need **OpenSSH Server** enabled (Settings → Optional Features →
|
|
58
|
+
"OpenSSH Server", or `Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0`).
|
|
59
|
+
Then you log in with your normal domain credentials.
|
|
60
|
+
|
|
61
|
+
**Pass `domain` and `username` separately** — never hard-code `"EU\nkennedy"` as a Python
|
|
62
|
+
string, because `\n` becomes a newline. The handler builds `EU\nkennedy` for you:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from ssh_handler import SSHHandler, SSHConfig, CredentialStore
|
|
66
|
+
|
|
67
|
+
store = CredentialStore(service="my_test_lab")
|
|
68
|
+
cfg = SSHConfig(
|
|
69
|
+
host="10.20.30.40",
|
|
70
|
+
domain="EU", username="nkennedy",
|
|
71
|
+
password=store.get("EU\\nkennedy"), # a Secret pulled from the OS vault
|
|
72
|
+
remote_os="windows", # skip the OS probe
|
|
73
|
+
fast_auth=True, # skip key probing -> faster login
|
|
74
|
+
)
|
|
75
|
+
with SSHHandler(cfg) as ssh:
|
|
76
|
+
print(ssh.run("whoami").stdout) # EU\nkennedy
|
|
77
|
+
print(ssh.run("powershell Get-Service sshd").stdout)
|
|
78
|
+
ssh.push("report.xlsx", "C:/Users/nkennedy/Desktop/report.xlsx")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Store the password **once** so it's never typed or committed again:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
CredentialStore("my_test_lab").set("EU\\nkennedy", prompt_password())
|
|
85
|
+
# or from the CLI:
|
|
86
|
+
python -m ssh_handler store-credential --user nkennedy --domain EU --service my_test_lab
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Confidential credentials
|
|
90
|
+
|
|
91
|
+
| Mechanism | What it does |
|
|
92
|
+
|-----------|--------------|
|
|
93
|
+
| `Secret` | wraps a password; `str()`/`repr()`/logs show `********`; only `.reveal()` exposes it |
|
|
94
|
+
| `mask()` | redacts secret values from any log line (applied automatically to all logging) |
|
|
95
|
+
| `CredentialStore` | stores/reads passwords in Windows Credential Manager (or macOS Keychain / Secret Service) via `keyring` — **no plaintext on disk** |
|
|
96
|
+
| `prompt_password()` | hidden terminal input |
|
|
97
|
+
|
|
98
|
+
## Speed
|
|
99
|
+
|
|
100
|
+
- **`fast_auth`** (default on): when a password is supplied, key/agent probing is skipped —
|
|
101
|
+
faster logins and no "Too many authentication failures" from the server's `MaxAuthTries`.
|
|
102
|
+
- One SFTP channel is opened lazily and **reused** across operations.
|
|
103
|
+
- SFTP downloads use Paramiko **prefetch** for high throughput.
|
|
104
|
+
- **`SSHPool`** runs the same command/transfer across many hosts in parallel threads.
|
|
105
|
+
- `remote_os="windows"|"linux"` skips the one-time OS-detection probe.
|
|
106
|
+
- `compress=True` for slow/high-latency links; keepalives keep long sessions alive.
|
|
107
|
+
|
|
108
|
+
## What's covered (Paramiko parity, plus extras)
|
|
109
|
+
|
|
110
|
+
- **Exec**: `run` (timeout, `check`, pty, env), `run_many`, `sudo` (password via stdin)
|
|
111
|
+
- **Interactive shell**: `open_shell()` → `ShellSession` with `send` / `read_until` / `read_available`
|
|
112
|
+
- **SFTP**: `listdir`, `listdir_attr`, `stat`, `lstat`, `exists`, `isdir`, `mkdir`, `makedirs`,
|
|
113
|
+
`rmdir`, `remove`, `rename`, `chmod`, `chown`, `symlink`, `readlink`, `open`,
|
|
114
|
+
`read_text`, `write_text`, `walk`, and `push`/`pull` (recursive, progress, stats)
|
|
115
|
+
- **SCP**: `scp_push` / `scp_pull` (needs the `scp` extra)
|
|
116
|
+
- **FTP/FTPS**: `FTPHandler` (stdlib `ftplib`, no extra dependency)
|
|
117
|
+
- **Jump host / bastion**: `SSHConfig(jump_host=…)` (ProxyJump-style)
|
|
118
|
+
- **Escape hatch**: `ssh.client` and `ssh.transport` expose the raw Paramiko objects
|
|
119
|
+
|
|
120
|
+
## Result objects
|
|
121
|
+
|
|
122
|
+
Every action returns structured data, not bare strings:
|
|
123
|
+
|
|
124
|
+
- `CommandResult` — `exit_code`, `stdout`, `stderr`, `duration`, `host`, `.ok`, `.as_dict()`
|
|
125
|
+
- `TransferResult` — `size_bytes`, `duration`, `speed_bps`, `human_speed`, `files`
|
|
126
|
+
- `ShellResult` — `output`, `matched`, `timed_out`, `duration`
|
|
127
|
+
- `OperationResult` — safe-mode wrapper: `bool(res)`, `res.value`, `res.error`, `res.unwrap()`
|
|
128
|
+
|
|
129
|
+
## Two error-handling styles
|
|
130
|
+
|
|
131
|
+
- **Raise (default)** — for tests/scripts. Typed exceptions: `SSHConnectionError`,
|
|
132
|
+
`SSHAuthenticationError`, `SSHTimeoutError`, `SSHCommandError`, `SSHTransferError`,
|
|
133
|
+
`FTPError`, `CredentialError` — all subclasses of `SSHError`. **Auto-retry** on connect
|
|
134
|
+
and **auto-reconnect** on dropped sessions are built in.
|
|
135
|
+
- **Safe mode** (`SSHHandler(cfg, safe=True)`) — for GUIs. Every call returns an
|
|
136
|
+
`OperationResult` instead of raising, so the event loop never dies. Override per call
|
|
137
|
+
with `safe=True/False`.
|
|
138
|
+
|
|
139
|
+
## CLI
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
python -m ssh_handler run --host H --user U --domain EU uptime
|
|
143
|
+
python -m ssh_handler push --host H --user U ./build /tmp/build --recursive
|
|
144
|
+
python -m ssh_handler pull --host H --user U /var/log ./logs --recursive
|
|
145
|
+
python -m ssh_handler info --host H --user U --json
|
|
146
|
+
python -m ssh_handler run --host H --user U --use-stored uptime # vault password
|
|
147
|
+
```
|
|
148
|
+
Password options: `--password` (hidden prompt), `--use-stored` (OS vault), `--key FILE`.
|
|
149
|
+
Add `--json` for machine-readable output.
|
|
150
|
+
|
|
151
|
+
## PyQt5
|
|
152
|
+
|
|
153
|
+
`ssh_handler.pyqt_worker.SSHWorker` is a `QObject` wrapping the handler in **safe mode**.
|
|
154
|
+
`moveToThread()` it, connect its signals (`log`, `connected`, `command_done`,
|
|
155
|
+
`transfer_done`, `progress`, `error`, `finished`), and drive it from the GUI — see recipe
|
|
156
|
+
in [examples/examples.py](examples/examples.py).
|
|
157
|
+
|
|
158
|
+
## Project layout
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
ssh_handler/
|
|
162
|
+
config.py SSHConfig, FTPConfig
|
|
163
|
+
credentials.py Secret, CredentialStore, mask, prompt_password
|
|
164
|
+
core.py SSHHandler, ShellSession (SSH + SFTP + SCP)
|
|
165
|
+
ftp.py FTPHandler (FTP/FTPS)
|
|
166
|
+
pool.py SSHPool (parallel multi-host)
|
|
167
|
+
cli.py argparse entry point (python -m ssh_handler)
|
|
168
|
+
pyqt_worker.py SSHWorker (PyQt5, lazy import)
|
|
169
|
+
results.py CommandResult, TransferResult, ShellResult, OperationResult
|
|
170
|
+
exceptions.py SSHError hierarchy
|
|
171
|
+
examples/examples.py
|
|
172
|
+
tests/test_offline.py (no network needed)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Tests
|
|
176
|
+
|
|
177
|
+
`tests/test_offline.py` validates secret masking, domain-login building, fast-auth
|
|
178
|
+
kwargs, recursive-mkdir logic, transfer math, and both error-handling modes — all offline.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
python tests/test_offline.py
|
|
182
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
ssh_handler/__init__.py,sha256=ZY-5qIgixXRCKs_AcONVIFegIOPYX3ImCZK-CyKHg5M,1737
|
|
2
|
+
ssh_handler/__main__.py,sha256=y-JCII5ssoxfEktEawpfYKBz-CCgxob8Q3b0AK76Lqo,122
|
|
3
|
+
ssh_handler/cli.py,sha256=AXG8u2hmRQXG2BsShwC_XFrHXPXf83CoE26DS-JOIrc,5926
|
|
4
|
+
ssh_handler/config.py,sha256=ElXOX-Al_o8PVv_i8cZponb1cdd1nkDBlwJjbV3g_oI,3982
|
|
5
|
+
ssh_handler/core.py,sha256=wGyr22I3MeuGXvBx4hi9Sv9Bm9ZLdwvpD1zPMB0VCCc,33491
|
|
6
|
+
ssh_handler/credentials.py,sha256=dhdun4U4lhngjbvT_Udv2Xpf9_NCmnB9Nk-S4ucOlNY,4848
|
|
7
|
+
ssh_handler/exceptions.py,sha256=O2Ksw1r0mTQkqd_w36KYwuL6me_bKFFXFJ9o8WO6Ul0,1319
|
|
8
|
+
ssh_handler/ftp.py,sha256=TOEJ3qHlo49A7rxf5iSunlr0zZGgxRpzbt27unHKWjA,7346
|
|
9
|
+
ssh_handler/pool.py,sha256=L7_dN75GYZYGedS3DHumgNmc6rYxJeIDmJ6r03QjxNk,3822
|
|
10
|
+
ssh_handler/pyqt_worker.py,sha256=FE1KiTC34fJTQxzVwo5CvKbb_j7FJes2f-aMgX3auXg,3636
|
|
11
|
+
ssh_handler/results.py,sha256=BRZ10UmIvuaLwOC1YSdDarNKJ7rz0jcOmm_5mVNi_8k,3262
|
|
12
|
+
ssh_handler-1.0.0.dist-info/METADATA,sha256=BCILCNpa6XbORrY2sMqRScUT988prMxHtg78Lov3q0U,7998
|
|
13
|
+
ssh_handler-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
ssh_handler-1.0.0.dist-info/entry_points.txt,sha256=AuXs00bXUYsd1ABut1If8dnWB7S8U9ZaWpsPClAHf2A,53
|
|
15
|
+
ssh_handler-1.0.0.dist-info/top_level.txt,sha256=QQ4IFeUXYOGWDo2uNzt6PEzQU_jZIBSONnD8oh5fvNg,12
|
|
16
|
+
ssh_handler-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ssh_handler
|