sshscript 3.1.4__tar.gz → 3.1.5__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.
- {sshscript-3.1.4 → sshscript-3.1.5}/CHANGELOG.md +47 -1
- sshscript-3.1.5/CODE_OF_CONDUCT.md +61 -0
- sshscript-3.1.5/CONTRIBUTING.md +87 -0
- sshscript-3.1.5/EXCEPTIONS.md +54 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/MANIFEST.in +1 -1
- sshscript-3.1.5/PKG-INFO +375 -0
- sshscript-3.1.5/README.md +344 -0
- sshscript-3.1.5/SECURITY.md +111 -0
- sshscript-3.1.5/SUPPORT.md +86 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/pyproject.toml +14 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/__init__.py +6 -2
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/_version.py +1 -1
- sshscript-3.1.5/src/sshscript/commandresult.py +28 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/dollar.py +3 -2
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/dollarchanger.py +1 -1
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/dollarparser.py +4 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/session.py +86 -17
- sshscript-3.1.5/src/sshscript/sshconfig.py +127 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/sshscript.py +36 -10
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/dollar_syntax.spy +7 -13
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/language.spy +2 -2
- sshscript-3.1.5/src/sshscript/unittest/test_check_file.py +60 -0
- sshscript-3.1.5/src/sshscript/unittest/test_command_api.py +128 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_logger_integration.py +2 -1
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_openssh_integration.py +6 -3
- sshscript-3.1.5/src/sshscript/unittest/test_ssh_config.py +128 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_ssh_security.py +2 -1
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_sshscript_module.py +8 -8
- sshscript-3.1.5/src/sshscript.egg-info/PKG-INFO +375 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript.egg-info/SOURCES.txt +6 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/tools/check_release.py +1 -1
- {sshscript-3.1.4 → sshscript-3.1.5}/tools/prepare_release.py +7 -2
- sshscript-3.1.4/CONTRIBUTING.md +0 -56
- sshscript-3.1.4/EXCEPTIONS.md +0 -45
- sshscript-3.1.4/PKG-INFO +0 -178
- sshscript-3.1.4/README.md +0 -153
- sshscript-3.1.4/SECURITY.md +0 -37
- sshscript-3.1.4/SUPPORT.md +0 -23
- sshscript-3.1.4/src/sshscript.egg-info/PKG-INFO +0 -178
- {sshscript-3.1.4 → sshscript-3.1.5}/.github/dependabot.yml +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/.github/workflows/ci.yml +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/.github/workflows/codeql.yml +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/.github/workflows/release.yml +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/LICENSE.txt +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/RELEASING.md +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/setup.cfg +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/channelgeneric.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/channelssh.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/channelsubprocess.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/channelutils.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/errorutils.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/patching.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/sessionwrapper.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/spyimporter.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/stdio.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/tokenparser.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/check_package_asserts.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/dollar_syntax_fixture.spy +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/language_fixture.spy +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_channelgeneric_expect.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_file_transfer.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_logger_api.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_production_contract.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_session_close_reporting.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_session_proxy_cleanup.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_spy_source_mapping.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_spy_thread_session.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_sshscript_dollar_syntax.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_stdio_dynamic_string.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_syntax_error.spy +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript/unittest/test_update_check.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript.egg-info/dependency_links.txt +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript.egg-info/entry_points.txt +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript.egg-info/requires.txt +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/src/sshscript.egg-info/top_level.txt +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/tools/publish_release.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/tools/run_checks.py +0 -0
- {sshscript-3.1.4 → sshscript-3.1.5}/tools/setup_openssh_ci.sh +0 -0
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This project records user-visible changes here. Release artifacts and their
|
|
4
|
+
provenance are available from the linked GitHub Release and PyPI pages.
|
|
5
|
+
|
|
6
|
+
## [3.1.5] - 2026-09-27
|
|
7
|
+
|
|
8
|
+
### Command API and SSH configuration
|
|
9
|
+
|
|
10
|
+
- Accept argv lists/tuples for a single command, with local direct execution
|
|
11
|
+
and POSIX quoting over SSH; shell mode remains explicit for strings.
|
|
12
|
+
- Return immutable CommandResult snapshots with stdout, stderr, exitcode,
|
|
13
|
+
host, duration, and command. Unpacking/indexing now yields three values:
|
|
14
|
+
stdout, stderr, exitcode. Two-value unpacking must be updated.
|
|
15
|
+
Output values are now strings rather than live buffer objects.
|
|
16
|
+
- Apply check=True consistently to local and remote one-shot commands after
|
|
17
|
+
preserving results; CalledProcessError carries text output and result.
|
|
18
|
+
- Resolve common ~/.ssh/config settings with explicit API overrides; add
|
|
19
|
+
config opt-out, alternate files, and a connection-free settings preview.
|
|
20
|
+
Support ProxyCommand and ProxyJump (the latter uses local OpenSSH).
|
|
21
|
+
- Reject unsupported Match/Include/canonicalization rules; warn about other
|
|
22
|
+
unapplied config settings. ProxyJump requires noninteractive authentication
|
|
23
|
+
and verified jump-host keys. Existing host-key checks remain enabled.
|
|
24
|
+
|
|
25
|
+
### Script validation
|
|
26
|
+
|
|
27
|
+
- Add check_file(path) and --check FILE to compile Python/dollar syntax without
|
|
28
|
+
executing user code, imports, or commands. The no-file --check update alias
|
|
29
|
+
remains compatible; --check-updates is the explicit update command.
|
|
30
|
+
- Map tokenizer indentation errors to the original source file; retain source
|
|
31
|
+
mapping coverage for nested commands, interpolation, multiline expressions,
|
|
32
|
+
and imported .spy modules. Editor integration is deferred.
|
|
33
|
+
|
|
34
|
+
### Documentation and community
|
|
35
|
+
|
|
36
|
+
- Reorganize the README around installation, secure first use, compatibility,
|
|
37
|
+
support, and release provenance.
|
|
38
|
+
- Add structured issue forms, a pull request template, and a Code of Conduct.
|
|
39
|
+
- Clarify supported environments, security boundaries, and public test paths.
|
|
40
|
+
|
|
41
|
+
## [3.1.4] - 2026-09-24
|
|
42
|
+
|
|
43
|
+
First Production/Stable release in the 3.1 line. Published through
|
|
44
|
+
[PyPI Trusted Publishing](https://pypi.org/project/sshscript/3.1.4/) with
|
|
45
|
+
verified artifacts in the
|
|
46
|
+
[GitHub Release](https://github.com/iapyeh/sshscript/releases/tag/v3.1.4).
|
|
4
47
|
|
|
5
48
|
### Production hardening
|
|
6
49
|
|
|
@@ -53,3 +96,6 @@
|
|
|
53
96
|
- Add packaging metadata, license, contributor guidance, and CI configuration.
|
|
54
97
|
- Add a disposable loopback OpenSSH integration gate covering host keys, SFTP,
|
|
55
98
|
PTY behavior, sudo/su, and timeout handling.
|
|
99
|
+
|
|
100
|
+
[Unreleased]: https://github.com/iapyeh/sshscript/compare/v3.1.4...release
|
|
101
|
+
[3.1.4]: https://github.com/iapyeh/sshscript/releases/tag/v3.1.4
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our commitment
|
|
4
|
+
|
|
5
|
+
SSHScript welcomes people of every background and experience level. We are
|
|
6
|
+
committed to a collaborative environment in which participation is respectful,
|
|
7
|
+
constructive, and free from harassment.
|
|
8
|
+
|
|
9
|
+
## Expected behavior
|
|
10
|
+
|
|
11
|
+
- Be respectful and assume good faith while still discussing technical risks
|
|
12
|
+
directly.
|
|
13
|
+
- Give actionable feedback about work rather than making personal judgments.
|
|
14
|
+
- Acknowledge different experience levels, environments, and communication
|
|
15
|
+
styles.
|
|
16
|
+
- Protect credentials, personal information, internal infrastructure details,
|
|
17
|
+
and embargoed vulnerability information.
|
|
18
|
+
- Accept moderation decisions and correct harmful behavior promptly.
|
|
19
|
+
|
|
20
|
+
## Unacceptable behavior
|
|
21
|
+
|
|
22
|
+
- Harassment, intimidation, threats, stalking, or discriminatory language.
|
|
23
|
+
- Sexualized language, imagery, or unwelcome attention.
|
|
24
|
+
- Personal attacks, deliberate humiliation, or sustained disruption.
|
|
25
|
+
- Publishing another person's private information without permission.
|
|
26
|
+
- Retaliation against someone who reports a concern in good faith.
|
|
27
|
+
- Using project spaces to distribute malware, credentials, or knowingly unsafe
|
|
28
|
+
instructions.
|
|
29
|
+
|
|
30
|
+
Reasonable technical disagreement, security review, and criticism of code are
|
|
31
|
+
not harassment when expressed professionally and focused on the work.
|
|
32
|
+
|
|
33
|
+
## Scope
|
|
34
|
+
|
|
35
|
+
This policy applies to repository issues, pull requests, reviews, discussions,
|
|
36
|
+
release and documentation spaces, and other public interactions in which a
|
|
37
|
+
person represents the SSHScript project.
|
|
38
|
+
|
|
39
|
+
## Reporting conduct concerns
|
|
40
|
+
|
|
41
|
+
Do not post a conduct report publicly. Contact the maintainer at
|
|
42
|
+
[iapyeh@gmail.com](mailto:iapyeh@gmail.com) with links or screenshots, the
|
|
43
|
+
relevant dates, a factual description, and any immediate safety concern. Share
|
|
44
|
+
only information needed to evaluate the report.
|
|
45
|
+
|
|
46
|
+
Suspected software vulnerabilities belong in
|
|
47
|
+
[GitHub private vulnerability reporting](https://github.com/iapyeh/sshscript/security/advisories/new),
|
|
48
|
+
as described in [SECURITY.md](SECURITY.md).
|
|
49
|
+
|
|
50
|
+
Reports will be reviewed as privately as practical. A maintainer named in or
|
|
51
|
+
directly involved with a report should not make the enforcement decision when a
|
|
52
|
+
reasonable alternative reviewer is available. Knowingly false reports may
|
|
53
|
+
themselves violate this policy; an unproven good-faith report does not.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Project maintainers may edit or remove content, request a correction, issue a
|
|
58
|
+
warning, limit participation, or permanently exclude a participant. Responses
|
|
59
|
+
will be based on severity, impact, pattern of behavior, and willingness to
|
|
60
|
+
correct the problem. Illegal activity or a credible threat may be referred to
|
|
61
|
+
the appropriate service provider or authority.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thank you for helping improve SSHScript. Participation is governed by the
|
|
4
|
+
[Code of Conduct](CODE_OF_CONDUCT.md). Please use a private security advisory,
|
|
5
|
+
not an issue or pull request, for a suspected vulnerability.
|
|
6
|
+
|
|
7
|
+
## Development setup
|
|
8
|
+
|
|
9
|
+
Use Python 3.11 or newer. The public release checkout uses a `src/` package
|
|
10
|
+
layout. From the repository root:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
python3 -m venv .venv
|
|
14
|
+
. .venv/bin/activate
|
|
15
|
+
python3 -m pip install --upgrade pip
|
|
16
|
+
python3 -m pip install -e .
|
|
17
|
+
python3 -m pip install build twine
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Keep test credentials, host inventories, local configuration, build output,
|
|
21
|
+
and editor state untracked.
|
|
22
|
+
|
|
23
|
+
## Required checks
|
|
24
|
+
|
|
25
|
+
Run the complete credential-free gate before proposing a change:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
python3 tools/run_checks.py
|
|
29
|
+
python3 tools/check_release.py --output /tmp/sshscript-candidate-UNIQUE
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The output path must not already exist. The first command finds the public test
|
|
33
|
+
suite under `src/sshscript/unittest/` and runs normal and optimized tests,
|
|
34
|
+
compile checks, the package assertion scan, and dollar-syntax smoke tests. The
|
|
35
|
+
second command builds the allowlisted wheel and source distribution, checks
|
|
36
|
+
their metadata, installs the wheel in an isolated environment, and performs an
|
|
37
|
+
installed-package smoke test.
|
|
38
|
+
|
|
39
|
+
Tests under `src/sshscript/unittest/` must not require network access, SSH
|
|
40
|
+
agents, private keys, passwords, or host-specific configuration unless they are
|
|
41
|
+
part of the isolated OpenSSH fixture. A regression fix should add a
|
|
42
|
+
credential-free test whenever the behavior can be reproduced with a fake
|
|
43
|
+
Paramiko client or local subprocess.
|
|
44
|
+
|
|
45
|
+
## SSH integration tests
|
|
46
|
+
|
|
47
|
+
Public CI provisions a disposable loopback OpenSSH server and runs
|
|
48
|
+
`src/sshscript/unittest/test_openssh_integration.py` against the built wheel.
|
|
49
|
+
The setup uses ephemeral users, passwords, and keys generated inside the CI
|
|
50
|
+
runner; it must never target a persistent host. See
|
|
51
|
+
`tools/setup_openssh_ci.sh` for the exact environment contract.
|
|
52
|
+
|
|
53
|
+
Historical site-specific and credentialed tests are intentionally not shipped
|
|
54
|
+
in the public release checkout. Do not add production credentials or internal
|
|
55
|
+
host scenarios to a pull request. Convert a failure into a disposable fixture,
|
|
56
|
+
redacted fake, or credential-free unit test first.
|
|
57
|
+
|
|
58
|
+
## Compatibility and public APIs
|
|
59
|
+
|
|
60
|
+
Changes to `Session`, `run_file()`, CLI exit statuses, dollar syntax, logging,
|
|
61
|
+
or SSH security defaults require synchronized implementation, tests, README,
|
|
62
|
+
and changelog updates. Avoid silently accepting insecure behavior.
|
|
63
|
+
|
|
64
|
+
The module API is the primary public interface. Keep dollar syntax behavior
|
|
65
|
+
aligned with it, and document compatibility aliases or deprecations explicitly.
|
|
66
|
+
Tests should cover both normal Python and `python -O` where the exception
|
|
67
|
+
contract is involved. Do not use runtime `assert` statements for package input
|
|
68
|
+
or lifecycle validation.
|
|
69
|
+
|
|
70
|
+
Before release, run all normal, optimized, compile, and AST gates listed in
|
|
71
|
+
[EXCEPTIONS.md](EXCEPTIONS.md). CI targets Python 3.11 through 3.14.
|
|
72
|
+
|
|
73
|
+
## Pull requests
|
|
74
|
+
|
|
75
|
+
Target the protected `release` branch from a topic branch or fork. Keep each
|
|
76
|
+
pull request focused and include:
|
|
77
|
+
|
|
78
|
+
- the problem and intended behavior;
|
|
79
|
+
- tests that fail before the change and pass afterward when practical;
|
|
80
|
+
- documentation and changelog updates for user-visible behavior;
|
|
81
|
+
- the commands used to validate the change;
|
|
82
|
+
- any compatibility, security, logging, or migration impact.
|
|
83
|
+
|
|
84
|
+
Complete the pull request template and respond to review feedback. A passing CI
|
|
85
|
+
run is required but does not guarantee acceptance. By submitting a
|
|
86
|
+
contribution, you agree that it may be distributed under the repository's MIT
|
|
87
|
+
License.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Stable exception contract
|
|
2
|
+
|
|
3
|
+
The following contract applies with and without `python -O`.
|
|
4
|
+
|
|
5
|
+
| Failure | Exception |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| Wrong argument type | `TypeError` |
|
|
8
|
+
| Correct type but invalid value, content, or combination | `ValueError` |
|
|
9
|
+
| Invalid session, console, or channel lifecycle state | `RuntimeError` |
|
|
10
|
+
| Operation on a closed channel or transport | `BrokenPipeError` |
|
|
11
|
+
| Channel ends while waiting | `EOFError` |
|
|
12
|
+
| Timeout | `TimeoutError` |
|
|
13
|
+
| Disconnected `Session.sftp`, upload, or download | `SSHScriptException` |
|
|
14
|
+
| Filesystem failure | Appropriate `OSError` subclass |
|
|
15
|
+
| Paramiko failure | Original Paramiko exception and traceback |
|
|
16
|
+
| Internal AST or data-structure invariant failure | Descriptive `RuntimeError` |
|
|
17
|
+
| Stack indexing outside its bounds | `IndexError`, following `deque` |
|
|
18
|
+
|
|
19
|
+
Nonzero command exit status remains data by default. One-shot local and remote
|
|
20
|
+
`Session.exec_command(..., check=True)` raise `subprocess.CalledProcessError`
|
|
21
|
+
after saving `session.last_result` and channel output/status. The exception's
|
|
22
|
+
`output`/`stdout` and `stderr` are text snapshots on both backends; `cmd` is a
|
|
23
|
+
string or argument tuple, and `result` is the complete immutable CommandResult.
|
|
24
|
+
CommandResult unpacks as exactly three values: stdout, stderr, exitcode; indexing
|
|
25
|
+
and slicing use the same order. Persistent consoles retain two-buffer returns.
|
|
26
|
+
This replaces the local backend's former raw subprocess exception (byte
|
|
27
|
+
output and generated argv). `AssertionError` was never a supported API contract.
|
|
28
|
+
|
|
29
|
+
User-written `assert` in `.spy` files is preserved as Python syntax. Python's
|
|
30
|
+
optimized mode removes these statements, including any calls inside them.
|
|
31
|
+
Production scripts must not depend on `assert` for command-success handling.
|
|
32
|
+
For local or remote one-shot execution, use `session.exec_command(command, check=True)`, or
|
|
33
|
+
explicitly inspect `session.exitcode` and raise an application exception when
|
|
34
|
+
appropriate. Persistent consoles retain their separate command/prompt contract.
|
|
35
|
+
|
|
36
|
+
`for_with` must be strictly bool. `get_pty` must be None or bool. Commands must
|
|
37
|
+
be nonempty strings or nonempty lists/tuples of string arguments. Argument
|
|
38
|
+
sequences disallow NUL, an empty executable, and shell mode. Persistent
|
|
39
|
+
commands must contain only one line.
|
|
40
|
+
Compiled bytes regular expressions are not accepted by text-output matching.
|
|
41
|
+
Listener removal requires the identical top listener; failed removal, duplicate
|
|
42
|
+
hijack/release, and last-layer removal leave their associated state unchanged.
|
|
43
|
+
|
|
44
|
+
Run the canonical credential-free gates from the public release repository root
|
|
45
|
+
using a supported interpreter:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
python3 tools/run_checks.py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The runner locates the package and tests under `src/sshscript/`, then executes
|
|
52
|
+
normal and optimized test suites, the dollar-syntax smoke suite, compile checks,
|
|
53
|
+
and the package-assertion gate. CI runs these checks on Python 3.11–3.14 on
|
|
54
|
+
Linux and macOS. The AST gate scans package runtime modules and excludes tests.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
include README.md LICENSE.txt RELEASING.md
|
|
2
|
-
include CHANGELOG.md CONTRIBUTING.md EXCEPTIONS.md SECURITY.md SUPPORT.md
|
|
2
|
+
include CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md EXCEPTIONS.md SECURITY.md SUPPORT.md
|
|
3
3
|
recursive-include tools *.py
|
|
4
4
|
include tools/setup_openssh_ci.sh
|
|
5
5
|
include .github/dependabot.yml
|
sshscript-3.1.5/PKG-INFO
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sshscript
|
|
3
|
+
Version: 3.1.5
|
|
4
|
+
Summary: Python automation for local processes, SSH, and SSHScript .spy files
|
|
5
|
+
Author-email: "Yeh, Hsin-Yuan" <iapyeh@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/iapyeh/sshscript
|
|
8
|
+
Project-URL: Documentation, https://iapyeh.github.io/sshscript/v3a/
|
|
9
|
+
Project-URL: Changelog, https://github.com/iapyeh/sshscript/blob/release/CHANGELOG.md
|
|
10
|
+
Project-URL: Releases, https://github.com/iapyeh/sshscript/releases
|
|
11
|
+
Project-URL: Source, https://github.com/iapyeh/sshscript
|
|
12
|
+
Project-URL: Issues, https://github.com/iapyeh/sshscript/issues
|
|
13
|
+
Project-URL: Security, https://github.com/iapyeh/sshscript/security/policy
|
|
14
|
+
Project-URL: Support, https://github.com/iapyeh/sshscript/discussions
|
|
15
|
+
Keywords: automation,paramiko,remote-execution,sftp,ssh,subprocess,system-administration
|
|
16
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: System :: Systems Administration
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE.txt
|
|
28
|
+
Requires-Dist: paramiko<5,>=2.11
|
|
29
|
+
Requires-Dist: packaging>=21
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# SSHScript
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/sshscript/)
|
|
35
|
+
[](https://pypi.org/project/sshscript/)
|
|
36
|
+
[](https://github.com/iapyeh/sshscript/actions/workflows/ci.yml)
|
|
37
|
+
[](https://github.com/iapyeh/sshscript/actions/workflows/codeql.yml)
|
|
38
|
+
[](https://github.com/iapyeh/sshscript/blob/release/LICENSE.txt)
|
|
39
|
+
|
|
40
|
+
SSHScript is a Python automation library for running commands locally and over
|
|
41
|
+
SSH through one `Session` API. It also provides optional dollar syntax for
|
|
42
|
+
compact `.spy` automation files.
|
|
43
|
+
|
|
44
|
+
**Current release:** [3.1.5](https://github.com/iapyeh/sshscript/releases/tag/v3.1.5)
|
|
45
|
+
(Production/Stable) · **Python:** 3.11 or newer · **Tested:** Python
|
|
46
|
+
3.11–3.14 on Linux and macOS
|
|
47
|
+
|
|
48
|
+
[Documentation](https://iapyeh.github.io/sshscript/v3a/) ·
|
|
49
|
+
[PyPI](https://pypi.org/project/sshscript/) ·
|
|
50
|
+
[Changelog](https://github.com/iapyeh/sshscript/blob/release/CHANGELOG.md) ·
|
|
51
|
+
[Security](https://github.com/iapyeh/sshscript/security/policy) ·
|
|
52
|
+
[Support](https://github.com/iapyeh/sshscript/blob/release/SUPPORT.md)
|
|
53
|
+
|
|
54
|
+
## Why SSHScript?
|
|
55
|
+
|
|
56
|
+
- Use the same interface for local subprocesses and remote SSH commands.
|
|
57
|
+
- Traverse nested SSH connections without rebuilding connection logic.
|
|
58
|
+
- Keep ordinary Python functions, packages, exceptions, data processing, and
|
|
59
|
+
threading around your automation.
|
|
60
|
+
- Scope connections, privilege changes, persistent shells, and interactive
|
|
61
|
+
programs with context managers.
|
|
62
|
+
- Read stdout, stderr, and exit status directly after each command.
|
|
63
|
+
- Add concise dollar syntax only where command-shaped notation improves a
|
|
64
|
+
script.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
SSHScript requires Python 3.11 or newer. Installing in a virtual environment is
|
|
69
|
+
recommended:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
python3 -m venv .venv
|
|
73
|
+
. .venv/bin/activate
|
|
74
|
+
python3 -m pip install --upgrade pip
|
|
75
|
+
python3 -m pip install sshscript
|
|
76
|
+
sshscript --version
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For a production deployment that requires repeatable dependency resolution,
|
|
80
|
+
pin SSHScript and all transitive dependencies in your application's lock file.
|
|
81
|
+
To install this release explicitly:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
python3 -m pip install "sshscript==3.1.5"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Use `python3 -m pip install --upgrade sshscript` to upgrade. The optional
|
|
88
|
+
`sshscript --check-updates` command queries PyPI and prints an upgrade command;
|
|
89
|
+
it never installs an update by itself.
|
|
90
|
+
|
|
91
|
+
## 60-second local quickstart
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import shlex
|
|
95
|
+
import sys
|
|
96
|
+
|
|
97
|
+
from sshscript import Session
|
|
98
|
+
|
|
99
|
+
command = shlex.join([
|
|
100
|
+
sys.executable,
|
|
101
|
+
"-c",
|
|
102
|
+
"print('sshscript is ready')",
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
with Session() as local:
|
|
106
|
+
stdout, stderr, exitcode = local.exec_command(
|
|
107
|
+
command,
|
|
108
|
+
shell=False,
|
|
109
|
+
check=True,
|
|
110
|
+
)
|
|
111
|
+
print(str(stdout).strip())
|
|
112
|
+
print(f"exit code: {local.exitcode}")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Expected output:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
sshscript is ready
|
|
119
|
+
exit code: 0
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`exec_command()` accepts one nonempty command string or a nonempty list/tuple
|
|
123
|
+
of string arguments. An argument sequence means **one command**, not a batch.
|
|
124
|
+
Sequences execute directly on the local host and are quoted for a POSIX login
|
|
125
|
+
shell over SSH; shell operators inside them are literal arguments. They accept
|
|
126
|
+
only `shell=None` or `shell=False`, without `shell_executable`. Use a string
|
|
127
|
+
with `shell=True` when you intentionally need shell operators.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
with Session() as local:
|
|
131
|
+
result = local.exec_command(
|
|
132
|
+
[sys.executable, "-c", "import sys; print(sys.argv[1])", "a; b"],
|
|
133
|
+
check=True,
|
|
134
|
+
timeout=30,
|
|
135
|
+
)
|
|
136
|
+
print(result.stdout, result.exitcode, result.duration)
|
|
137
|
+
stdout, stderr, exitcode = result # three-value unpacking
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Each call returns an immutable `CommandResult` containing text `stdout` and
|
|
141
|
+
`stderr`, `exitcode`, `host`, `duration`, and `command`. `host` snapshots
|
|
142
|
+
`session.host` (`None` locally); it does not execute `hostname`. `duration` is
|
|
143
|
+
elapsed monotonic seconds for command execution and output collection,
|
|
144
|
+
including communication and worker cleanup but excluding connection setup.
|
|
145
|
+
`command` is the normalized string or an immutable tuple of arguments.
|
|
146
|
+
`session.last_result` references the most recently completed command; saved
|
|
147
|
+
results remain valid after later commands or session closure. Validation
|
|
148
|
+
failures leave it alone; starting a command clears it until completion.
|
|
149
|
+
|
|
150
|
+
Both local and remote commands accept `check=True` to raise
|
|
151
|
+
`subprocess.CalledProcessError` for a nonzero status **after** preserving the
|
|
152
|
+
result. The exception has text `stdout`/`stderr`, the normalized `cmd`, and a
|
|
153
|
+
`result` attribute. With the default `check=False`, a nonzero status is result
|
|
154
|
+
data. Connection errors and timeouts propagate unchanged.
|
|
155
|
+
|
|
156
|
+
Compatibility: the returned object is no longer a tuple of live output
|
|
157
|
+
buffers. Unpack exactly three values: stdout, stderr, exitcode. Indexing and
|
|
158
|
+
slicing use that same three-value order; old two-value unpacking must change.
|
|
159
|
+
Use `session.stdout`/`session.stderr` for the existing buffer interface.
|
|
160
|
+
Persistent shell and interactive console APIs retain their existing buffer
|
|
161
|
+
and prompt semantics; this result/check contract applies to one-shot Session
|
|
162
|
+
commands, including `$` commands outside persistent consoles.
|
|
163
|
+
|
|
164
|
+
## First secure SSH connection
|
|
165
|
+
|
|
166
|
+
SSHScript uses Paramiko and verifies system host keys by default. Before the
|
|
167
|
+
first connection, place the server key in the account's standard
|
|
168
|
+
`known_hosts` file and verify its fingerprint through an independent trusted
|
|
169
|
+
channel. Prefer an SSH agent, managed private key, or secret manager over a
|
|
170
|
+
password embedded in source code.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from sshscript import Session
|
|
174
|
+
|
|
175
|
+
with Session() as local:
|
|
176
|
+
with local.connect(
|
|
177
|
+
"ops@example.net",
|
|
178
|
+
timeout=30,
|
|
179
|
+
banner_timeout=30,
|
|
180
|
+
auth_timeout=30,
|
|
181
|
+
) as remote:
|
|
182
|
+
result = remote.exec_command(
|
|
183
|
+
["uname", "-s"],
|
|
184
|
+
check=True,
|
|
185
|
+
timeout=30,
|
|
186
|
+
)
|
|
187
|
+
print(result.stdout.strip())
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Unknown and changed host keys are rejected unless the caller explicitly
|
|
191
|
+
supplies a different Paramiko policy. Do not use automatic key acceptance in a
|
|
192
|
+
production workflow unless a separate trusted bootstrap process has already
|
|
193
|
+
verified the key. See the
|
|
194
|
+
[SSHScript v3 documentation](https://iapyeh.github.io/sshscript/v3a/) for
|
|
195
|
+
nested connections, timeouts, file transfer, `sudo`, `su`, and interactive
|
|
196
|
+
programs.
|
|
197
|
+
|
|
198
|
+
## Reusing SSH configuration
|
|
199
|
+
|
|
200
|
+
Connections from a local session read `~/.ssh/config` if it exists. Supported
|
|
201
|
+
settings are `Host` patterns, `HostName`, `User`, `Port`, `IdentityFile`,
|
|
202
|
+
`ProxyCommand`, and `ProxyJump`. Explicit API arguments override config, then
|
|
203
|
+
built-in defaults apply. `port=None` means unspecified; an explicit `port=22`
|
|
204
|
+
overrides a configured port. Explicit `pkey`, `pkey_path`, or `key_filename`
|
|
205
|
+
overrides configured identity files. Host-key verification remains enabled.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
# Inspect effective settings without connecting or starting a proxy process.
|
|
209
|
+
settings = Session.resolve_connection("production", port=2222)
|
|
210
|
+
|
|
211
|
+
with Session() as local:
|
|
212
|
+
with local.connect("production") as remote:
|
|
213
|
+
result = remote(["uname", "-s"], check=True)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Pass `ssh_config=False` to disable config lookup, or `ssh_config="/path/config"`
|
|
217
|
+
to require a particular file. Nested connections do not read local config
|
|
218
|
+
unless an explicit file is supplied; proxy options remain unsupported on
|
|
219
|
+
nested sessions. `session.host` and `result.host` use the resolved HostName.
|
|
220
|
+
`resolve_connection()` returns effective Paramiko keyword arguments plus
|
|
221
|
+
`proxyCommand`, when applicable; it performs no network operations.
|
|
222
|
+
|
|
223
|
+
Config tokens `%h`, `%n`, `%p`, `%r`, `%u`, `%d`, and `%%` are expanded after
|
|
224
|
+
explicit overrides for identity paths and configured proxy commands. Other
|
|
225
|
+
tokens fail clearly. Explicit `proxyCommand` strings retain their historical
|
|
226
|
+
verbatim behavior. Explicit `proxyCommand=None` disables configured proxies.
|
|
227
|
+
You can also supply `proxyJump="user@bastion:2222"` or a comma-separated chain.
|
|
228
|
+
When both configured proxy types are active, select one explicitly or remove
|
|
229
|
+
the conflict; SSHScript does not implement OpenSSH's first-proxy-wins rule.
|
|
230
|
+
|
|
231
|
+
`ProxyJump` uses the local `ssh` executable to forward to the target, with
|
|
232
|
+
batch authentication and strict host-key checks on jump hosts. It requires
|
|
233
|
+
known host keys and noninteractive authentication for those hops. The target
|
|
234
|
+
connection remains managed and verified by Paramiko. A custom config file is
|
|
235
|
+
also passed to `ssh`; otherwise its user config is used when present.
|
|
236
|
+
|
|
237
|
+
This is a subset of OpenSSH configuration. `Match`, `Include`, and hostname
|
|
238
|
+
canonicalization are rejected before lookup; other unapplied options produce
|
|
239
|
+
a warning. In particular, alternate known-hosts files and identity-agent
|
|
240
|
+
settings are not imported. Treat config and proxy commands as trusted local
|
|
241
|
+
inputs; connecting may execute configured proxy programs.
|
|
242
|
+
|
|
243
|
+
## Optional dollar syntax
|
|
244
|
+
|
|
245
|
+
Dollar syntax is not ordinary Python syntax. It is normally stored in `.spy`
|
|
246
|
+
files; `run_script(source)` also accepts Dollar syntax from an in-memory string.
|
|
247
|
+
Both forms use the same session and transport implementation as the module API:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
# health.spy
|
|
251
|
+
$hostname
|
|
252
|
+
if $.exitcode != 0:
|
|
253
|
+
raise RuntimeError("hostname failed")
|
|
254
|
+
print($.stdout.strip())
|
|
255
|
+
|
|
256
|
+
with $.connect("ops@example.net"):
|
|
257
|
+
$uname -s
|
|
258
|
+
if $.exitcode != 0:
|
|
259
|
+
raise RuntimeError("remote uname failed")
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Run exactly one file with:
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
sshscript health.spy
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Check one file without executing its Python, imports, or commands:
|
|
269
|
+
|
|
270
|
+
```sh
|
|
271
|
+
sshscript --check health.spy
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The exit status is 0 for valid syntax, 1 for a syntax/read failure, and 2 for
|
|
275
|
+
invalid CLI usage. Syntax diagnostics show the original file, line, source,
|
|
276
|
+
and caret. `sshscript.check_file(path)` provides the same compile-only check
|
|
277
|
+
and raises source-located `SyntaxError` or filesystem errors. It validates
|
|
278
|
+
Python/dollar syntax, not shell commands, imported modules, or remote hosts.
|
|
279
|
+
`--script` remains available to inspect generated Python without execution.
|
|
280
|
+
For compatibility, **`--check` without a file still checks PyPI for updates**;
|
|
281
|
+
use `--check-updates` explicitly for that purpose.
|
|
282
|
+
|
|
283
|
+
In version 3, a single `$` supports direct commands and shell features such as
|
|
284
|
+
pipelines and redirection. The old `$$` form is retained for compatibility but
|
|
285
|
+
is deprecated. New applications should start with the regular Python module
|
|
286
|
+
API and adopt dollar syntax only when its notation is useful.
|
|
287
|
+
|
|
288
|
+
The CLI and `run_file()` execute one file. Directories, globs, and multiple
|
|
289
|
+
paths are intentionally unsupported. Importing SSHScript does not globally
|
|
290
|
+
enable imports of `.spy` modules; use the temporary `sshscript.spy_imports()`
|
|
291
|
+
context manager when a regular Python program needs that behavior.
|
|
292
|
+
|
|
293
|
+
## Security model
|
|
294
|
+
|
|
295
|
+
SSHScript executes commands and Python code; it is not a sandbox. Treat every
|
|
296
|
+
`.spy` file, Python module, command string, remote host, and command output as a
|
|
297
|
+
trust boundary. In particular:
|
|
298
|
+
|
|
299
|
+
- never run unreviewed automation with production credentials;
|
|
300
|
+
- avoid shell interpolation of external data;
|
|
301
|
+
- keep credentials, private keys, inventories, and captured production output
|
|
302
|
+
out of source control;
|
|
303
|
+
- verify host keys independently and retain timeouts around network operations;
|
|
304
|
+
- validate site-specific PAM, `sudoers`, shell, and network policy in a
|
|
305
|
+
disposable environment before rollout.
|
|
306
|
+
|
|
307
|
+
See the [security policy](https://github.com/iapyeh/sshscript/security/policy)
|
|
308
|
+
for the complete reporting and security model, and the
|
|
309
|
+
[stable exception contract](https://github.com/iapyeh/sshscript/blob/release/EXCEPTIONS.md)
|
|
310
|
+
for runtime behavior.
|
|
311
|
+
|
|
312
|
+
## Release confidence and provenance
|
|
313
|
+
|
|
314
|
+
The 3.1 release line uses the following public controls:
|
|
315
|
+
|
|
316
|
+
- CI on Linux and macOS with Python 3.11, 3.12, 3.13, and 3.14;
|
|
317
|
+
- normal and optimized-mode tests, syntax smoke tests, compile checks, and a
|
|
318
|
+
runtime-assertion gate;
|
|
319
|
+
- disposable loopback OpenSSH integration tests for host keys, SFTP, PTY,
|
|
320
|
+
`sudo`/`su`, and timeout behavior;
|
|
321
|
+
- CodeQL and Dependabot;
|
|
322
|
+
- PyPI Trusted Publishing with short-lived OIDC credentials;
|
|
323
|
+
- GitHub build-provenance attestations and a SHA-256 `verified.json` manifest
|
|
324
|
+
attached to the GitHub Release.
|
|
325
|
+
|
|
326
|
+
Download distributions from [PyPI](https://pypi.org/project/sshscript/) or the
|
|
327
|
+
[GitHub Release](https://github.com/iapyeh/sshscript/releases/tag/v3.1.5), not
|
|
328
|
+
from unverified mirrors. These controls establish tested behavior, artifact
|
|
329
|
+
integrity, and release provenance; they are not a substitute for reviewing the
|
|
330
|
+
automation you run or for validating your production environment.
|
|
331
|
+
|
|
332
|
+
## Compatibility and support
|
|
333
|
+
|
|
334
|
+
| Component | Current policy |
|
|
335
|
+
| --- | --- |
|
|
336
|
+
| SSHScript | Latest 3.1.x receives bug and security fixes |
|
|
337
|
+
| Python | 3.11–3.14 are continuously tested |
|
|
338
|
+
| Platforms | Current Linux and macOS releases |
|
|
339
|
+
| SSH | OpenSSH integration is tested on Ubuntu; other servers are best effort |
|
|
340
|
+
| Paramiko | Runtime dependency is `>=2.11,<5`; CI resolves a compatible release |
|
|
341
|
+
| Windows | Not currently tested or supported |
|
|
342
|
+
|
|
343
|
+
See the [support policy](https://github.com/iapyeh/sshscript/blob/release/SUPPORT.md)
|
|
344
|
+
for scope, support channels, and the information needed in a useful bug report.
|
|
345
|
+
|
|
346
|
+
## Development and verification
|
|
347
|
+
|
|
348
|
+
A release checkout uses the `src/sshscript/` package layout:
|
|
349
|
+
|
|
350
|
+
```sh
|
|
351
|
+
python3 -m pip install 'paramiko>=2.11,<5' 'packaging>=21' build twine
|
|
352
|
+
python3 tools/run_checks.py
|
|
353
|
+
python3 tools/check_release.py --output /tmp/sshscript-candidate-UNIQUE
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
The output directory must not already exist. `run_checks.py` locates the test
|
|
357
|
+
suite under `src/sshscript/unittest/` automatically. See the
|
|
358
|
+
[contributing guide](https://github.com/iapyeh/sshscript/blob/release/CONTRIBUTING.md)
|
|
359
|
+
before proposing a change and the
|
|
360
|
+
[release guide](https://github.com/iapyeh/sshscript/blob/release/RELEASING.md)
|
|
361
|
+
for maintainer-only release steps.
|
|
362
|
+
|
|
363
|
+
## Project policies
|
|
364
|
+
|
|
365
|
+
- [Changelog](https://github.com/iapyeh/sshscript/blob/release/CHANGELOG.md)
|
|
366
|
+
- [Support policy](https://github.com/iapyeh/sshscript/blob/release/SUPPORT.md)
|
|
367
|
+
- [Security policy](https://github.com/iapyeh/sshscript/security/policy)
|
|
368
|
+
- [Stable exception contract](https://github.com/iapyeh/sshscript/blob/release/EXCEPTIONS.md)
|
|
369
|
+
- [Contributing guide](https://github.com/iapyeh/sshscript/blob/release/CONTRIBUTING.md)
|
|
370
|
+
- [Code of Conduct](https://github.com/iapyeh/sshscript/blob/release/CODE_OF_CONDUCT.md)
|
|
371
|
+
|
|
372
|
+
## License
|
|
373
|
+
|
|
374
|
+
SSHScript is released under the
|
|
375
|
+
[MIT License](https://github.com/iapyeh/sshscript/blob/release/LICENSE.txt).
|