ssh-handler 1.0.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.
@@ -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,163 @@
1
+ # ssh_handler
2
+
3
+ An extensive **SSH / SFTP / SCP / FTP** handler built on [Paramiko](https://www.paramiko.org/),
4
+ packaged so the *same* code works in three places:
5
+
6
+ - a **test-automation framework** (raise-on-error, pytest fixtures, parallel fleet ops),
7
+ - a **standalone CLI** (`python -m ssh_handler …`, argument-driven),
8
+ - a **PyQt5 tool** (safe mode + log streaming via Qt signals, off the GUI thread).
9
+
10
+ Passwords are wrapped in a `Secret` and stored in the **OS credential vault** — they
11
+ never appear in logs, reprs, tracebacks, or on disk in plaintext.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install -e . # core (paramiko)
17
+ pip install -e ".[all]" # + keyring, scp, PyQt5
18
+ # or just the pieces you need:
19
+ pip install -e ".[secure]" # keyring (confidential password storage)
20
+ pip install -e ".[scp]" # scp (SCP-protocol transfers)
21
+ pip install -e ".[gui]" # PyQt5 (the worker)
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ```python
27
+ from ssh_handler import SSHHandler, SSHConfig
28
+
29
+ with SSHHandler(SSHConfig(host="10.0.0.5", username="root", password="pw")) as ssh:
30
+ print(ssh.run("uptime").stdout)
31
+ ssh.run("systemctl restart nginx", check=True) # raises on non-zero exit
32
+ ssh.push("local.txt", "/tmp/remote.txt") # SFTP upload
33
+ ssh.pull("/etc/nginx", "./backup", recursive=True) # recursive download
34
+ ```
35
+
36
+ ## Connecting to your RDP / Windows hosts (domain login `EU\nkennedy`)
37
+
38
+ Those Windows machines need **OpenSSH Server** enabled (Settings → Optional Features →
39
+ "OpenSSH Server", or `Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0`).
40
+ Then you log in with your normal domain credentials.
41
+
42
+ **Pass `domain` and `username` separately** — never hard-code `"EU\nkennedy"` as a Python
43
+ string, because `\n` becomes a newline. The handler builds `EU\nkennedy` for you:
44
+
45
+ ```python
46
+ from ssh_handler import SSHHandler, SSHConfig, CredentialStore
47
+
48
+ store = CredentialStore(service="my_test_lab")
49
+ cfg = SSHConfig(
50
+ host="10.20.30.40",
51
+ domain="EU", username="nkennedy",
52
+ password=store.get("EU\\nkennedy"), # a Secret pulled from the OS vault
53
+ remote_os="windows", # skip the OS probe
54
+ fast_auth=True, # skip key probing -> faster login
55
+ )
56
+ with SSHHandler(cfg) as ssh:
57
+ print(ssh.run("whoami").stdout) # EU\nkennedy
58
+ print(ssh.run("powershell Get-Service sshd").stdout)
59
+ ssh.push("report.xlsx", "C:/Users/nkennedy/Desktop/report.xlsx")
60
+ ```
61
+
62
+ Store the password **once** so it's never typed or committed again:
63
+
64
+ ```python
65
+ CredentialStore("my_test_lab").set("EU\\nkennedy", prompt_password())
66
+ # or from the CLI:
67
+ python -m ssh_handler store-credential --user nkennedy --domain EU --service my_test_lab
68
+ ```
69
+
70
+ ## Confidential credentials
71
+
72
+ | Mechanism | What it does |
73
+ |-----------|--------------|
74
+ | `Secret` | wraps a password; `str()`/`repr()`/logs show `********`; only `.reveal()` exposes it |
75
+ | `mask()` | redacts secret values from any log line (applied automatically to all logging) |
76
+ | `CredentialStore` | stores/reads passwords in Windows Credential Manager (or macOS Keychain / Secret Service) via `keyring` — **no plaintext on disk** |
77
+ | `prompt_password()` | hidden terminal input |
78
+
79
+ ## Speed
80
+
81
+ - **`fast_auth`** (default on): when a password is supplied, key/agent probing is skipped —
82
+ faster logins and no "Too many authentication failures" from the server's `MaxAuthTries`.
83
+ - One SFTP channel is opened lazily and **reused** across operations.
84
+ - SFTP downloads use Paramiko **prefetch** for high throughput.
85
+ - **`SSHPool`** runs the same command/transfer across many hosts in parallel threads.
86
+ - `remote_os="windows"|"linux"` skips the one-time OS-detection probe.
87
+ - `compress=True` for slow/high-latency links; keepalives keep long sessions alive.
88
+
89
+ ## What's covered (Paramiko parity, plus extras)
90
+
91
+ - **Exec**: `run` (timeout, `check`, pty, env), `run_many`, `sudo` (password via stdin)
92
+ - **Interactive shell**: `open_shell()` → `ShellSession` with `send` / `read_until` / `read_available`
93
+ - **SFTP**: `listdir`, `listdir_attr`, `stat`, `lstat`, `exists`, `isdir`, `mkdir`, `makedirs`,
94
+ `rmdir`, `remove`, `rename`, `chmod`, `chown`, `symlink`, `readlink`, `open`,
95
+ `read_text`, `write_text`, `walk`, and `push`/`pull` (recursive, progress, stats)
96
+ - **SCP**: `scp_push` / `scp_pull` (needs the `scp` extra)
97
+ - **FTP/FTPS**: `FTPHandler` (stdlib `ftplib`, no extra dependency)
98
+ - **Jump host / bastion**: `SSHConfig(jump_host=…)` (ProxyJump-style)
99
+ - **Escape hatch**: `ssh.client` and `ssh.transport` expose the raw Paramiko objects
100
+
101
+ ## Result objects
102
+
103
+ Every action returns structured data, not bare strings:
104
+
105
+ - `CommandResult` — `exit_code`, `stdout`, `stderr`, `duration`, `host`, `.ok`, `.as_dict()`
106
+ - `TransferResult` — `size_bytes`, `duration`, `speed_bps`, `human_speed`, `files`
107
+ - `ShellResult` — `output`, `matched`, `timed_out`, `duration`
108
+ - `OperationResult` — safe-mode wrapper: `bool(res)`, `res.value`, `res.error`, `res.unwrap()`
109
+
110
+ ## Two error-handling styles
111
+
112
+ - **Raise (default)** — for tests/scripts. Typed exceptions: `SSHConnectionError`,
113
+ `SSHAuthenticationError`, `SSHTimeoutError`, `SSHCommandError`, `SSHTransferError`,
114
+ `FTPError`, `CredentialError` — all subclasses of `SSHError`. **Auto-retry** on connect
115
+ and **auto-reconnect** on dropped sessions are built in.
116
+ - **Safe mode** (`SSHHandler(cfg, safe=True)`) — for GUIs. Every call returns an
117
+ `OperationResult` instead of raising, so the event loop never dies. Override per call
118
+ with `safe=True/False`.
119
+
120
+ ## CLI
121
+
122
+ ```bash
123
+ python -m ssh_handler run --host H --user U --domain EU uptime
124
+ python -m ssh_handler push --host H --user U ./build /tmp/build --recursive
125
+ python -m ssh_handler pull --host H --user U /var/log ./logs --recursive
126
+ python -m ssh_handler info --host H --user U --json
127
+ python -m ssh_handler run --host H --user U --use-stored uptime # vault password
128
+ ```
129
+ Password options: `--password` (hidden prompt), `--use-stored` (OS vault), `--key FILE`.
130
+ Add `--json` for machine-readable output.
131
+
132
+ ## PyQt5
133
+
134
+ `ssh_handler.pyqt_worker.SSHWorker` is a `QObject` wrapping the handler in **safe mode**.
135
+ `moveToThread()` it, connect its signals (`log`, `connected`, `command_done`,
136
+ `transfer_done`, `progress`, `error`, `finished`), and drive it from the GUI — see recipe
137
+ in [examples/examples.py](examples/examples.py).
138
+
139
+ ## Project layout
140
+
141
+ ```
142
+ ssh_handler/
143
+ config.py SSHConfig, FTPConfig
144
+ credentials.py Secret, CredentialStore, mask, prompt_password
145
+ core.py SSHHandler, ShellSession (SSH + SFTP + SCP)
146
+ ftp.py FTPHandler (FTP/FTPS)
147
+ pool.py SSHPool (parallel multi-host)
148
+ cli.py argparse entry point (python -m ssh_handler)
149
+ pyqt_worker.py SSHWorker (PyQt5, lazy import)
150
+ results.py CommandResult, TransferResult, ShellResult, OperationResult
151
+ exceptions.py SSHError hierarchy
152
+ examples/examples.py
153
+ tests/test_offline.py (no network needed)
154
+ ```
155
+
156
+ ## Tests
157
+
158
+ `tests/test_offline.py` validates secret masking, domain-login building, fast-auth
159
+ kwargs, recursive-mkdir logic, transfer math, and both error-handling modes — all offline.
160
+
161
+ ```bash
162
+ python tests/test_offline.py
163
+ ```
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ssh-handler"
7
+ version = "1.0.0"
8
+ description = "Extensive SSH/SFTP/SCP/FTP handler built on Paramiko, for test automation, CLIs and PyQt5 tools."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ authors = [{ name = "Naveen" }]
12
+ dependencies = [
13
+ "paramiko>=3.0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ secure = ["keyring>=23.0"] # OS credential vault for confidential passwords
18
+ scp = ["scp>=0.14"] # SCP-protocol transfers (SFTP works without it)
19
+ gui = ["PyQt5>=5.15"] # PyQt5 worker integration
20
+ all = ["keyring>=23.0", "scp>=0.14", "PyQt5>=5.15"]
21
+
22
+ [project.scripts]
23
+ ssh-handler = "ssh_handler.cli:main"
24
+
25
+ [tool.setuptools]
26
+ packages = ["ssh_handler"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,52 @@
1
+ """
2
+ ssh_handler
3
+ ===========
4
+
5
+ An extensive SSH / SFTP / SCP / FTP handler built on Paramiko, usable from a
6
+ test-automation framework, a standalone CLI, or a PyQt5 tool.
7
+
8
+ Quick start
9
+ -----------
10
+ from ssh_handler import SSHHandler, SSHConfig
11
+
12
+ with SSHHandler(SSHConfig(host="10.0.0.5", domain="EU",
13
+ username="nkennedy", password="…")) as ssh:
14
+ print(ssh.run("hostname").stdout)
15
+ ssh.push("local.txt", "/tmp/remote.txt")
16
+ ssh.pull("/var/log/syslog", "syslog.txt")
17
+
18
+ The password is wrapped in a Secret and never appears in logs or reprs.
19
+ ``pyqt_worker`` and the ``scp`` extra are imported lazily, so missing optional
20
+ dependencies (PyQt5 / scp) don't break the core package.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ __version__ = "1.0.0"
26
+
27
+ from .config import SSHConfig, FTPConfig
28
+ from .core import SSHHandler, ShellSession
29
+ from .ftp import FTPHandler
30
+ from .pool import SSHPool
31
+ from .credentials import Secret, CredentialStore, prompt_password, mask
32
+ from .results import CommandResult, TransferResult, ShellResult, OperationResult
33
+ from .exceptions import (
34
+ SSHError,
35
+ SSHConnectionError,
36
+ SSHAuthenticationError,
37
+ SSHTimeoutError,
38
+ SSHCommandError,
39
+ SSHTransferError,
40
+ SSHNotConnectedError,
41
+ FTPError,
42
+ CredentialError,
43
+ )
44
+
45
+ __all__ = [
46
+ "SSHHandler", "ShellSession", "SSHConfig", "FTPConfig", "FTPHandler",
47
+ "SSHPool", "Secret", "CredentialStore", "prompt_password", "mask",
48
+ "CommandResult", "TransferResult", "ShellResult", "OperationResult",
49
+ "SSHError", "SSHConnectionError", "SSHAuthenticationError",
50
+ "SSHTimeoutError", "SSHCommandError", "SSHTransferError",
51
+ "SSHNotConnectedError", "FTPError", "CredentialError", "__version__",
52
+ ]
@@ -0,0 +1,5 @@
1
+ """Enables `python -m ssh_handler ...`."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,152 @@
1
+ """
2
+ Standalone command-line entry point.
3
+
4
+ python -m ssh_handler run --host H --user U [--domain EU] CMD...
5
+ python -m ssh_handler push --host H --user U LOCAL REMOTE [--recursive]
6
+ python -m ssh_handler pull --host H --user U REMOTE LOCAL [--recursive]
7
+ python -m ssh_handler info --host H --user U
8
+
9
+ Credentials (password never echoed, never stored in plaintext):
10
+ --password prompt interactively (hidden)
11
+ --use-stored read from the OS credential vault
12
+ --store-credential save to the OS vault, then exit
13
+ --key FILE use a private key instead of a password
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys
19
+ import json
20
+ import argparse
21
+
22
+ from .config import SSHConfig
23
+ from .core import SSHHandler
24
+ from .credentials import CredentialStore, prompt_password, Secret
25
+ from .exceptions import SSHError
26
+ from .results import CommandResult, TransferResult
27
+
28
+
29
+ def _add_conn_args(p: argparse.ArgumentParser) -> None:
30
+ p.add_argument("--host", required=True)
31
+ p.add_argument("--port", type=int, default=22)
32
+ p.add_argument("--user", required=True)
33
+ p.add_argument("--domain", default=None, help="e.g. EU for EU\\user logins")
34
+ p.add_argument("--key", default=None, help="private key file")
35
+ p.add_argument("--password", action="store_true",
36
+ help="prompt for password (hidden input)")
37
+ p.add_argument("--use-stored", action="store_true",
38
+ help="read password from the OS credential vault")
39
+ p.add_argument("--service", default="ssh_handler",
40
+ help="credential-vault namespace")
41
+ p.add_argument("--timeout", type=float, default=None)
42
+ p.add_argument("--no-fast-auth", action="store_true")
43
+ p.add_argument("--json", action="store_true", help="emit machine-readable JSON")
44
+
45
+
46
+ def _resolve_password(args, login_user: str) -> Secret | None:
47
+ if args.use_stored:
48
+ store = CredentialStore(args.service)
49
+ sec = store.get(login_user)
50
+ if sec is None:
51
+ print(f"No stored credential for {login_user!r} in service "
52
+ f"{args.service!r}.", file=sys.stderr)
53
+ sys.exit(2)
54
+ return sec
55
+ if args.password:
56
+ return prompt_password(f"Password for {login_user}: ")
57
+ return None
58
+
59
+
60
+ def _build_config(args) -> SSHConfig:
61
+ login_user = f"{args.domain}\\{args.user}" if args.domain else args.user
62
+ return SSHConfig(
63
+ host=args.host, port=args.port, username=args.user, domain=args.domain,
64
+ password=_resolve_password(args, login_user),
65
+ key_filename=args.key, command_timeout=args.timeout,
66
+ fast_auth=not args.no_fast_auth,
67
+ )
68
+
69
+
70
+ def _output(args, obj) -> None:
71
+ if args.json:
72
+ if isinstance(obj, (CommandResult, TransferResult)):
73
+ print(json.dumps(obj.as_dict(), default=str, indent=2))
74
+ else:
75
+ print(json.dumps(obj, default=str, indent=2))
76
+ else:
77
+ print(obj)
78
+
79
+
80
+ def main(argv=None) -> int:
81
+ parser = argparse.ArgumentParser(prog="ssh_handler",
82
+ description="Extensive SSH/SFTP/SCP handler CLI")
83
+ sub = parser.add_subparsers(dest="cmd", required=True)
84
+
85
+ # store-credential (no connection)
86
+ sc = sub.add_parser("store-credential", help="save a password to the OS vault")
87
+ sc.add_argument("--user", required=True)
88
+ sc.add_argument("--domain", default=None)
89
+ sc.add_argument("--service", default="ssh_handler")
90
+
91
+ p_run = sub.add_parser("run", help="execute a remote command")
92
+ _add_conn_args(p_run)
93
+ p_run.add_argument("command", nargs=argparse.REMAINDER)
94
+
95
+ p_push = sub.add_parser("push", help="upload a file/dir (SFTP)")
96
+ _add_conn_args(p_push)
97
+ p_push.add_argument("local")
98
+ p_push.add_argument("remote")
99
+ p_push.add_argument("--recursive", action="store_true")
100
+
101
+ p_pull = sub.add_parser("pull", help="download a file/dir (SFTP)")
102
+ _add_conn_args(p_pull)
103
+ p_pull.add_argument("remote")
104
+ p_pull.add_argument("local")
105
+ p_pull.add_argument("--recursive", action="store_true")
106
+
107
+ p_info = sub.add_parser("info", help="connect and report remote OS")
108
+ _add_conn_args(p_info)
109
+
110
+ args = parser.parse_args(argv)
111
+
112
+ # store-credential is handled without a connection
113
+ if args.cmd == "store-credential":
114
+ login_user = f"{args.domain}\\{args.user}" if args.domain else args.user
115
+ store = CredentialStore(args.service)
116
+ store.set(login_user, prompt_password(f"Password to store for {login_user}: "))
117
+ print(f"Stored credential for {login_user!r} in service {args.service!r}.")
118
+ return 0
119
+
120
+ try:
121
+ with SSHHandler(_build_config(args)) as ssh:
122
+ if args.cmd == "run":
123
+ cmd = " ".join(args.command).strip()
124
+ if not cmd:
125
+ print("No command given.", file=sys.stderr)
126
+ return 2
127
+ res = ssh.run(cmd, timeout=args.timeout)
128
+ if not args.json:
129
+ if res.stdout:
130
+ sys.stdout.write(res.stdout)
131
+ if res.stderr:
132
+ sys.stderr.write(res.stderr)
133
+ return res.exit_code
134
+ _output(args, res)
135
+ return res.exit_code
136
+ elif args.cmd == "push":
137
+ _output(args, ssh.push(args.local, args.remote,
138
+ recursive=args.recursive))
139
+ elif args.cmd == "pull":
140
+ _output(args, ssh.pull(args.remote, args.local,
141
+ recursive=args.recursive))
142
+ elif args.cmd == "info":
143
+ _output(args, {"host": args.host, "remote_os": ssh.detect_os(),
144
+ "connected": ssh.is_connected})
145
+ return 0
146
+ except SSHError as exc:
147
+ print(f"ERROR: {exc}", file=sys.stderr)
148
+ return 1
149
+
150
+
151
+ if __name__ == "__main__":
152
+ raise SystemExit(main())