├── .codecov.yml ├── .coveragerc ├── .darglint ├── .flake8 ├── .git_archival.txt ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.rst ├── CONTRIBUTING.rst ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── documentation_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.rst ├── codecov.yml ├── config.yml ├── dependabot.yml └── workflows │ ├── build-manylinux-container-images.yml │ ├── ci-cd.yml │ ├── reusable-cibuildwheel.yml │ ├── reusable-linters.yml │ └── reusable-tests.yml ├── .gitignore ├── .isort.cfg ├── .packit.yml ├── .pip-tools.toml ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .yamllint ├── AUTHORS.rst ├── CHANGELOG.rst ├── LICENSE.rst ├── README.rst ├── build-scripts └── manylinux-container-image │ ├── Dockerfile │ ├── LICENSE.APACHE │ ├── LICENSE.BSD │ ├── README.md │ ├── activate-userspace-tools.sh │ ├── build_utils.sh │ ├── get-static-deps-dir.sh │ ├── install-userspace-tools.sh │ ├── install_libffi.sh │ ├── install_libssh.sh │ ├── install_openssl.sh │ ├── install_perl.sh │ ├── install_virtualenv.sh │ ├── install_zlib.sh │ ├── make-static-deps-dir.sh │ ├── manylinux_mapping.py │ └── openssl-version.sh ├── contrib └── sphinx_cython_parsing_notes.txt ├── docs ├── _samples │ ├── copy_files_scp.py │ ├── copy_files_sftp.py │ ├── get_version.py │ ├── gssapi.py │ └── shell.py ├── _static │ └── .gitkeep ├── _templates │ └── .gitkeep ├── changelog-fragments │ ├── .gitignore │ ├── 1dfbf70fdfd99ae75068fdb3630790c96101a96a.contrib.rst │ ├── 532.breaking.rst │ ├── 562.breaking.rst │ ├── 562.contrib.rst │ ├── 562.packaging.rst │ ├── 636.contrib.rst │ ├── 638.bugfix.rst │ ├── 640.doc.rst │ ├── 646.doc.rst │ ├── 658.bugfix.rst │ ├── 658.packaging.rst │ ├── 661.bugfix.rst │ ├── 664.bugfix.rst │ ├── 675.feature.rst │ ├── 676.contrib.rst │ ├── 688.contrib.rst │ ├── 692.contrib.rst │ ├── 692.packaging.rst │ ├── 706.contrib.rst │ ├── 707.contrib.rst │ ├── 708.deprecation.rst │ ├── 709.bugfix.rst │ ├── 713.contrib.rst │ ├── 713.packaging.rst │ ├── 714.packaging.rst │ ├── 715.contrib.rst │ ├── 716.contrib.rst │ ├── 718.breaking.rst │ ├── 730.contrib.rst │ ├── 731.contrib.rst │ ├── 732.contrib.rst │ ├── 733.contrib.rst │ ├── 734.contrib.rst │ ├── 735.packaging.rst │ ├── README.rst │ ├── ea34887831a0c6547b32cd8c6a035bb77b91e771.contrib.rst │ └── template.j2 ├── changelog.rst ├── conf.py ├── contributing │ ├── code_of_conduct.rst │ ├── guidelines.rst │ ├── release_guide.rst │ ├── security.rst │ └── testing_guide.rst ├── index.rst ├── installation_guide.rst ├── requirements.in ├── requirements.txt └── user_guide.rst ├── packaging ├── README.md ├── pep517_backend │ ├── __init__.py │ ├── __main__.py │ ├── _backend.py │ ├── _compat.py │ ├── _cython_configuration.py │ ├── _transformers.py │ ├── cli.py │ └── hooks.py └── rpm │ └── ansible-pylibssh.spec ├── pyproject.toml ├── pytest.ini ├── requirements-build.txt ├── setup.cfg ├── src └── pylibsshext │ ├── .gitignore │ ├── __init__.py │ ├── _libssh_version.pyx │ ├── _version.py │ ├── channel.pxd │ ├── channel.pyx │ ├── errors.pxd │ ├── errors.pyx │ ├── includes │ ├── __init__.py │ ├── callbacks.pxd │ ├── libssh.pxd │ └── sftp.pxd │ ├── scp.pxd │ ├── scp.pyx │ ├── session.pxd │ ├── session.pyx │ ├── sftp.pxd │ └── sftp.pyx ├── tests ├── _service_utils.py ├── conftest.py ├── integration │ └── sshd_test.py └── unit │ ├── channel_test.py │ ├── scp_test.py │ ├── session_test.py │ ├── sftp_test.py │ └── version_test.py └── tox.ini /.codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | codecov: 4 | notify: 5 | after_n_builds: 26 # Number of test matrix+lint jobs uploading coverage 6 | wait_for_ci: false 7 | 8 | require_ci_to_pass: false 9 | 10 | token: >- # notsecret # repo-scoped, upload-only, stability in fork PRs 11 | 78c6cd01-1f6b-428a-8be0-9069e6073d6d 12 | 13 | comment: 14 | require_changes: true 15 | 16 | coverage: 17 | range: 100..100 18 | status: 19 | patch: 20 | default: 21 | target: 100% 22 | pytest: 23 | target: 100% 24 | flags: 25 | - pytest 26 | typing: 27 | flags: 28 | - MyPy 29 | project: 30 | default: 31 | target: 100% 32 | lib: 33 | flags: 34 | - pytest 35 | paths: 36 | - src/ 37 | target: 100% 38 | packaging: 39 | paths: 40 | - packaging/ 41 | target: 75.24% 42 | tests: 43 | flags: 44 | - pytest 45 | paths: 46 | - tests/ 47 | target: 100% 48 | typing: 49 | flags: 50 | - MyPy 51 | target: 100% 52 | 53 | ... 54 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [html] 2 | directory = .test-results/pytest/cov/ 3 | show_contexts = true 4 | skip_covered = false 5 | 6 | [paths] 7 | source = 8 | src 9 | */src 10 | *\src 11 | */lib/python*/site-packages 12 | */pypy*/site-packages 13 | *\Lib\site-packages 14 | 15 | [report] 16 | exclude_also = 17 | \#\s*pragma: no cover 18 | ^\s*raise AssertionError\b 19 | ^\s*raise NotImplementedError\b 20 | ^\s*return NotImplemented\b 21 | ^\s*raise$ 22 | ^if __name__ == ['"]__main__['"]:$ 23 | # fail_under = 100 24 | skip_covered = true 25 | skip_empty = true 26 | show_missing = true 27 | 28 | [run] 29 | branch = true 30 | cover_pylib = false 31 | # NOTE: `disable_warnings` is needed when `pytest-cov` runs in tandem 32 | # NOTE: with `pytest-xdist`. These warnings are false negative in this 33 | # NOTE: context. 34 | # 35 | # NOTE: It's `coveragepy` that emits the warnings and previously they 36 | # NOTE: wouldn't get on the radar of `pytest`'s `filterwarnings` 37 | # NOTE: mechanism. This changed, however, with `pytest >= 8.4`. And 38 | # NOTE: since we set `filterwarnings = error`, those warnings are being 39 | # NOTE: raised as exceptions, cascading into `pytest`'s internals and 40 | # NOTE: causing tracebacks and crashes of the test sessions. 41 | # 42 | # Ref: 43 | # * https://github.com/pytest-dev/pytest-cov/issues/693 44 | # * https://github.com/pytest-dev/pytest-cov/pull/695 45 | # * https://github.com/pytest-dev/pytest-cov/pull/696 46 | disable_warnings = 47 | module-not-measured 48 | plugins = 49 | # covdefaults 50 | Cython.Coverage 51 | relative_files = true 52 | source = 53 | . 54 | source_pkgs = 55 | pylibsshext 56 | -------------------------------------------------------------------------------- /.darglint: -------------------------------------------------------------------------------- 1 | [darglint] 2 | docstring_style=sphinx 3 | enable=DAR104 4 | strictness=long 5 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | 3 | # Print the total number of errors: 4 | count = true 5 | 6 | # Don't even try to analyze these: 7 | extend-exclude = 8 | # Circle CI configs 9 | .circleci, 10 | # No need to traverse egg info dir 11 | *.egg-info, 12 | # GitHub configs 13 | .github, 14 | # Cache files of MyPy 15 | .mypy_cache, 16 | # Cache files of pytest 17 | .pytest_cache, 18 | # Temp dir of pytest-testmon 19 | .tmontmp, 20 | # Countless third-party libs in venvs 21 | .tox, 22 | # Occasional virtualenv dir 23 | .venv, 24 | # VS Code 25 | .vscode, 26 | # Temporary build dir 27 | build, 28 | # This contains sdists and wheels that we don't want to check 29 | dist, 30 | # Metadata of `pip wheel` cmd is autogenerated 31 | pip-wheel-metadata, 32 | 33 | # IMPORTANT: avoid using ignore option, always use extend-ignore instead 34 | # Completely and unconditionally ignore the following errors: 35 | extend-ignore = 36 | # Legitimate cases, no need to "fix" these violations: 37 | # E501: "line too long", its function is replaced by `flake8-length` 38 | E501, 39 | # W505: "doc line too long", its function is replaced by `flake8-length` 40 | W505, 41 | # I: flake8-isort is drunk + we have isort integrated into pre-commit 42 | I, 43 | # WPS300: "Found local folder import" -- nothing bad about this 44 | WPS300, 45 | # An opposite consistency expectation is currently enforced 46 | # by pylint via: useless-object-inheritance (R0205): 47 | # WPS306: "Found class without a base class: *" -- we have metaclass shims 48 | WPS306, 49 | # WPS326: "Found implicit string concatenation" -- nothing bad about this 50 | WPS326, 51 | # WPS422: "Found future import: *" -- we need these for multipython 52 | WPS422, 53 | 54 | filename = 55 | # Normal Python files (default): 56 | *.py, 57 | # Cython files: 58 | *.pyx 59 | 60 | format = default 61 | 62 | # Let's not overcomplicate the code: 63 | max-complexity = 10 64 | 65 | # Accessibility/large fonts and PEP8 friendly: 66 | #max-line-length = 79 67 | # Accessibility/large fonts and PEP8 unfriendly: 68 | max-line-length = 160 69 | 70 | # Allow certain violations in certain files: 71 | # Please keep both sections of this list sorted, as it will be easier for others to find and add entries in the future 72 | per-file-ignores = 73 | # The following ignores have been researched and should be considered permanent 74 | # each should be preceded with an explanation of each of the error codes 75 | # If other ignores are added for a specific file in the section following this, 76 | # these will need to be added to that line as well. 77 | 78 | # Sphinx builds aren't supposed to be run under Python 2: 79 | docs/conf.py: WPS305 80 | 81 | # in-tree PEP517 build backend needs a lot of legit `noqa`s, 82 | # members and imports: 83 | packaging/pep517_backend/_backend.py: WPS201, WPS202, WPS402 84 | 85 | # F401 imported but unused 86 | packaging/pep517_backend/hooks.py: F401 87 | 88 | # The package has imports exposing private things to the public: 89 | src/pylibsshext/__init__.py: WPS412 90 | 91 | # Exclude errors that don't make sense for Cython 92 | # Examples: 93 | # * "E211 whitespace before '('" happening to "typedef int (*smth) ..." 94 | # * "E226 missing whitespace around arithmetic operator" 95 | src/pylibsshext/*.pxd: E211, E225, E226, E227, E999 96 | src/pylibsshext/*.pyx: E225, E226, E227, E999 97 | 98 | # There are multiple `assert`s (S101) 99 | # and subprocesses (import – S404; call – S603) in tests; 100 | # also, using fixtures looks like shadowing the outer scope (WPS442); 101 | # furthermore, we should be able to import and test private attributes 102 | # (WPS450) and modules (WPS436), and finally it's impossible to 103 | # have <= members in tests (WPS202), including many local vars (WPS210): 104 | tests/**.py: S101, S404, S603, WPS202, WPS210, WPS436, WPS442, WPS450 105 | 106 | 107 | # Count the number of occurrences of each error/warning code and print a report: 108 | statistics = true 109 | 110 | # ## Plugin-provided settings: ## 111 | 112 | # flake8-eradicate 113 | # E800: 114 | eradicate-whitelist-extend = distutils:\s+libraries\s+=\s+|isort:\s+\w+|NAME = "VALUE" 115 | 116 | # flake8-pytest-style 117 | # PT001: 118 | pytest-fixture-no-parentheses = true 119 | # PT006: 120 | pytest-parametrize-names-type = tuple 121 | # PT007: 122 | pytest-parametrize-values-type = tuple 123 | pytest-parametrize-values-row-type = tuple 124 | # PT023: 125 | pytest-mark-no-parentheses = true 126 | 127 | # flake8-rst-docstrings 128 | rst-directives = 129 | spelling 130 | rst-roles = 131 | # Built-in Sphinx roles: 132 | class, 133 | data, 134 | file, 135 | exc, 136 | meth, 137 | mod, 138 | term, 139 | py:class, 140 | py:data, 141 | py:exc, 142 | py:meth, 143 | py:term, 144 | # Sphinx's internal role: 145 | event, 146 | 147 | # wemake-python-styleguide 148 | show-source = true 149 | -------------------------------------------------------------------------------- /.git_archival.txt: -------------------------------------------------------------------------------- 1 | node: cd2970fc2a3d67ffa87d2fde45f2d033700d3c50 2 | node-date: 2025-06-07T02:30:54+02:00 3 | describe-name: v1.2.2-221-gcd2970fc2 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force LF line endings for text files 2 | * text=auto eol=lf 3 | 4 | # Needed for setuptools-scm-git-archive 5 | .git_archival.txt export-subst 6 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Community Code of Conduct 3 | ========================= 4 | 5 | Please see the official `Ansible Community Code of Conduct`_. 6 | 7 | .. _Ansible Community Code of Conduct: 8 | https://docs.ansible.com/ansible/latest/community 9 | /code_of_conduct.html 10 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | Contributing to ansible-pylibssh 3 | ================================ 4 | 5 | .. attention:: 6 | 7 | ansible-pylibssh project exists solely to allow Ansible connection 8 | plugins to use libssh_ SSH implementation by importing it in 9 | Python-land. At the moment we don't accept any contributions, nor 10 | feature requests that are unrelated to this goal. 11 | 12 | But if you want to contribute a bug fix or send a pull-request 13 | improving our CI, testing and packaging, we will gladly review it. 14 | 15 | 16 | In order to contribute, you'll need to: 17 | 18 | 1. Fork the repository. 19 | 20 | 2. Create a branch, push your changes there. Don't forget to 21 | :ref:`include news files for the changelog `. 23 | 24 | 3. Send it to us as a PR. 25 | 26 | 4. Iterate on your PR, incorporating the requested improvements 27 | and participating in the discussions. 28 | 29 | Prerequisites: 30 | 31 | 1. Have libssh_. 32 | 33 | 2. Use tox_ to build the C-extension, docs and run the tests. 34 | 35 | 3. Before sending a PR, make sure that the linters pass: 36 | 37 | .. code-block:: shell-session 38 | 39 | $ tox -e lint 40 | 41 | 42 | .. _libssh: https://www.libssh.org 43 | .. _tox: https://tox.readthedocs.io 44 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | custom: https://www.ansible.com 4 | 5 | ... 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | 7 | 8 | 9 | ##### SUMMARY 10 | 11 | 12 | ##### ISSUE TYPE 13 | - Bug Report 14 | 15 | ##### PYLISSH and LIBSSH VERSION 16 | 17 | ```paste below 18 | 19 | ``` 20 | 21 | ##### OS / ENVIRONMENT 22 | 23 | 24 | 25 | ##### STEPS TO REPRODUCE 26 | 27 | 28 | 29 | ```paste below 30 | 31 | ``` 32 | 33 | 34 | 35 | ##### EXPECTED RESULTS 36 | 37 | 38 | 39 | ##### ACTUAL RESULTS 40 | 41 | 42 | 43 | ```paste below 44 | 45 | ``` 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yamllint disable rule:line-length 3 | # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser 4 | 5 | blank_issues_enabled: true # default 6 | contact_links: 7 | - name: 🔐 Security bug report 🔥 8 | url: https://docs.ansible.com/ansible/latest/community/reporting_bugs_and_features.html 9 | about: | 10 | Please learn how to report security vulnerabilities here. 11 | 12 | For all security related bugs, email security@ansible.com 13 | instead of using this issue tracker and you will receive 14 | a prompt response. 15 | 16 | For more information, see https://docs.ansible.com/ansible/latest/community/reporting_bugs_and_features.html 17 | - name: 📝 Ansible Code of Conduct 18 | url: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html 19 | about: ❤ Be nice to other members of the community. ☮ Behave. 20 | 21 | ... 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 📝 Documentation Report 3 | about: Ask us about docs 4 | --- 5 | 6 | 7 | 8 | 9 | ##### SUMMARY 10 | 11 | 12 | 13 | 14 | ##### ISSUE TYPE 15 | - Documentation Report 16 | 17 | ##### PYLIBSSH and LIBSSH VERSION 18 | 19 | ```paste below 20 | 21 | ``` 22 | 23 | ##### OS / ENVIRONMENT 24 | 25 | 26 | ##### ADDITIONAL INFORMATION 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | 7 | 8 | ##### SUMMARY 9 | 10 | 11 | ##### ISSUE TYPE 12 | - Feature Idea 13 | 14 | ##### ADDITIONAL INFORMATION 15 | 16 | 17 | 18 | ``` 19 | 20 | ``` 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ##### SUMMARY 2 | 3 | 4 | 5 | 6 | ##### ISSUE TYPE 7 | 8 | - Bugfix Pull Request 9 | - Docs Pull Request 10 | - Feature Pull Request 11 | 12 | ##### ADDITIONAL INFORMATION 13 | 14 | 15 | 16 | 17 | ```paste below 18 | 19 | ``` 20 | -------------------------------------------------------------------------------- /.github/SECURITY.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Security Policy 3 | =============== 4 | 5 | ------------------ 6 | Supported Versions 7 | ------------------ 8 | 9 | Ansible applies security fixes according to the 3-versions-back support 10 | policy. Please find more information in `our docs `_. 12 | 13 | .. _Ansible release and maintenance: 14 | https://docs.ansible.com/ansible/devel/reference_appendices 15 | /release_and_maintenance.html#release-status 16 | 17 | ------------------------- 18 | Reporting a Vulnerability 19 | ------------------------- 20 | 21 | .. attention:: 22 | 23 | **⚠️ Please do not file public GitHub issues for security 24 | vulnerabilities as they are open for everyone to see! ⚠️** 25 | 26 | We encourage responsible disclosure practices for security 27 | vulnerabilities. Please read `our policies for reporting bugs `_ if you want to report a security issue 29 | that might affect Ansible. 30 | 31 | .. _Ansible policies for reporting bugs: 32 | https://docs.ansible.com/ansible/devel/community 33 | /reporting_bugs_and_features.html#reporting-a-bug 34 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yamllint disable rule:truthy 3 | 4 | comment: 5 | layout: header, reach, diff, files 6 | behavior: new 7 | require_changes: no 8 | after_n_builds: 25 # Wait for the most of the GHA matrix before posting 9 | 10 | coverage: 11 | precision: 2 12 | round: nearest 13 | range: 68..100 14 | status: 15 | # Only consider coverage of the code snippet changed in PR 16 | # https://docs.codecov.io/docs/commit-status 17 | project: no 18 | patch: yes 19 | changes: yes 20 | 21 | notify: 22 | after_n_builds: 25 # Wait for the most of the GHA matrix before posting 23 | 24 | ... 25 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | chronographer: 4 | enforce_name: 5 | suffix: .rst 6 | 7 | ... 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: daily 9 | - package-ecosystem: pip 10 | directory: / 11 | schedule: 12 | interval: weekly 13 | open-pull-requests-limit: 3 14 | versioning-strategy: lockfile-only 15 | labels: 16 | - dependabot-deps-update 17 | 18 | ... 19 | -------------------------------------------------------------------------------- /.github/workflows/build-manylinux-container-images.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: ♲ manylinux containers 4 | 5 | on: # yamllint disable-line rule:truthy 6 | workflow_dispatch: 7 | schedule: 8 | # Run once a week on Mondays 9 | - cron: 0 0 * * MON 10 | pull_request: 11 | paths: 12 | - .github/workflows/build-manylinux-container-images.yml 13 | - build-scripts/manylinux-container-image/** 14 | push: 15 | branches: 16 | - devel 17 | paths: 18 | - .github/workflows/build-manylinux-container-images.yml 19 | - build-scripts/manylinux-container-image/** 20 | 21 | jobs: 22 | build: 23 | runs-on: ${{ matrix.IMAGE.HOST_OS || 'ubuntu-latest' }} 24 | 25 | timeout-minutes: 45 26 | 27 | strategy: 28 | matrix: 29 | # All cached images we prebuild now target PEP 600 tags only 30 | IMAGE: 31 | # Build containers for x86_64 32 | - ARCH: x86_64 33 | QEMU_ARCH: amd64 34 | # Build containers for aarch64 (ARM 64) 35 | - ARCH: aarch64 36 | HOST_OS: ubuntu-24.04-arm 37 | # Build containers for ppc64le 38 | - ARCH: ppc64le 39 | # Build containers for s390x 40 | - ARCH: s390x 41 | # There are no base containers for these archs 42 | # at https://quay.io/organization/pypa. 43 | # Build containers for armv7l (ARM v7) 44 | # - ARCH: armv7l 45 | # QEMU_ARCH: arm/v7 46 | # Build containers for ppc64 47 | # - ARCH: ppc64 48 | YEAR: 49 | - _2_24 50 | - _2_28 51 | 52 | env: 53 | LIBSSH_VERSION: 0.11.1 54 | PYPA_MANYLINUX_TAG: >- 55 | manylinux${{ matrix.YEAR }}_${{ matrix.IMAGE.ARCH }} 56 | FULL_IMAGE_NAME: >- 57 | ${{ 58 | github.repository 59 | }}-manylinux${{ 60 | matrix.YEAR 61 | }}_${{ 62 | matrix.IMAGE.ARCH 63 | }} 64 | QEMU_ARCH: ${{ matrix.IMAGE.QEMU_ARCH || matrix.IMAGE.ARCH }} 65 | 66 | defaults: 67 | run: 68 | working-directory: build-scripts/manylinux-container-image/ 69 | 70 | name: >- # can't use `env` in this context: 71 | 🐳 72 | manylinux${{ matrix.YEAR }}_${{ matrix.IMAGE.ARCH }} 73 | steps: 74 | - name: Fetch the repo src 75 | uses: actions/checkout@v4.1.6 76 | - name: >- 77 | Set up QEMU ${{ env.QEMU_ARCH }} arch emulation 78 | with Podman 79 | if: env.QEMU_ARCH != 'amd64' 80 | run: > 81 | sudo podman run 82 | --rm --privileged 83 | tonistiigi/binfmt 84 | --uninstall 'qemu-*' 85 | 86 | 87 | sudo podman run 88 | --rm --privileged 89 | tonistiigi/binfmt 90 | --install all 91 | - name: Build the image with Buildah 92 | id: build-image 93 | uses: redhat-actions/buildah-build@v2.13 94 | with: 95 | arch: ${{ env.QEMU_ARCH }} 96 | image: ${{ env.FULL_IMAGE_NAME }} 97 | tags: >- 98 | ${{ github.sha }} 99 | libssh-v${{ env.LIBSSH_VERSION }}_gh-${{ github.sha }} 100 | libssh-v${{ env.LIBSSH_VERSION }} 101 | latest 102 | dockerfiles: build-scripts/manylinux-container-image/Dockerfile 103 | context: build-scripts/manylinux-container-image/ 104 | oci: true # Should be alright because we don't publish to Docker Hub 105 | build-args: | 106 | LIBSSH_VERSION=${{ env.LIBSSH_VERSION }} 107 | RELEASE=${{ env.PYPA_MANYLINUX_TAG }} 108 | - name: Push to GitHub Container Registry 109 | if: >- 110 | (github.event_name == 'push' || github.event_name == 'schedule') 111 | && github.ref == format( 112 | 'refs/heads/{0}', github.event.repository.default_branch 113 | ) 114 | id: push-to-ghcr 115 | uses: redhat-actions/push-to-registry@v2.8 116 | with: 117 | image: ${{ steps.build-image.outputs.image }} 118 | tags: ${{ steps.build-image.outputs.tags }} 119 | registry: ghcr.io 120 | username: ${{ github.actor }} 121 | password: ${{ secrets.GITHUB_TOKEN }} 122 | - name: Log the upload result 123 | if: >- 124 | (github.event_name == 'push' || github.event_name == 'schedule') 125 | && github.ref == format( 126 | 'refs/heads/{0}', github.event.repository.default_branch 127 | ) 128 | run: >- 129 | echo 130 | 'New image has been pushed to 131 | ${{ steps.push-to-ghcr.outputs.registry-paths }}' 132 | 133 | ... 134 | -------------------------------------------------------------------------------- /.github/workflows/reusable-cibuildwheel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: ♲ 👷 Build wheel 🛞📦 4 | 5 | on: # yamllint disable-line rule:truthy 6 | workflow_call: 7 | inputs: 8 | dists-artifact-name: 9 | description: Workflow artifact name containing dists 10 | required: true 11 | type: string 12 | check-name: 13 | description: A custom name for the Checks API-reported status 14 | required: false 15 | type: string 16 | cython-tracing: 17 | description: Whether to build Cython modules with line tracing 18 | default: '0' 19 | required: false 20 | type: string 21 | os: 22 | description: VM OS to use, without version suffix 23 | default: ubuntu 24 | required: false 25 | type: string 26 | qemu: 27 | description: Emulated QEMU architecture 28 | default: '' 29 | required: false 30 | type: string 31 | source-tarball-name: 32 | description: Sdist filename wildcard 33 | required: true 34 | type: string 35 | wheel-tags-to-skip: 36 | description: Wheel tags to skip building 37 | default: '' 38 | required: false 39 | type: string 40 | 41 | env: 42 | FORCE_COLOR: "1" # Make tools pretty. 43 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 44 | PIP_NO_PYTHON_VERSION_WARNING: "1" 45 | 46 | jobs: 47 | 48 | build-wheel: 49 | name: >- 50 | ${{ 51 | inputs.check-name 52 | && inputs.check-name 53 | || format('Build wheels on {0} {1}', inputs.os, inputs.qemu) 54 | }} 55 | runs-on: ${{ inputs.os }} 56 | timeout-minutes: ${{ inputs.qemu && 70 || 9 }} 57 | steps: 58 | - name: Compute GHA artifact name ending 59 | id: gha-artifact-name 60 | run: | 61 | from hashlib import sha512 62 | from os import environ 63 | from pathlib import Path 64 | 65 | FILE_APPEND_MODE = 'a' 66 | 67 | inputs_json_str = """${{ toJSON(inputs) }}""" 68 | 69 | hash = sha512(inputs_json_str.encode()).hexdigest() 70 | 71 | with Path(environ['GITHUB_OUTPUT']).open( 72 | mode=FILE_APPEND_MODE, 73 | ) as outputs_file: 74 | print(f'hash={hash}', file=outputs_file) 75 | shell: python 76 | 77 | - name: Retrieve the project source from an sdist inside the GHA artifact 78 | uses: re-actors/checkout-python-sdist@release/v2 79 | with: 80 | source-tarball-name: ${{ inputs.source-tarball-name }} 81 | workflow-artifact-name: ${{ inputs.dists-artifact-name }} 82 | 83 | - name: Set up QEMU 84 | if: inputs.qemu 85 | uses: docker/setup-qemu-action@v3 86 | with: 87 | platforms: all 88 | id: qemu 89 | - name: Prepare emulation 90 | if: inputs.qemu 91 | run: | 92 | # Build emulated architectures only if QEMU is set, 93 | # use default "auto" otherwise 94 | echo "CIBW_ARCHS_LINUX=${{ inputs.qemu }}" >> "${GITHUB_ENV}" 95 | shell: bash 96 | 97 | - name: Skip building some wheel tags 98 | if: inputs.wheel-tags-to-skip 99 | run: | 100 | echo "CIBW_SKIP=${{ inputs.wheel-tags-to-skip }}" >> "${GITHUB_ENV}" 101 | shell: bash 102 | 103 | - name: Build wheels 104 | uses: pypa/cibuildwheel@v2.17.0 105 | env: 106 | # CIBW_ARCHS_MACOS: all x86_64 arm64 universal2 107 | CIBW_ARCHS_MACOS: native 108 | CIBW_CONFIG_SETTINGS: >- # Cython line tracing for coverage collection 109 | with-cython-tracing=${{ inputs.cython-tracing }} 110 | 111 | - name: Upload built artifacts for testing and publishing 112 | uses: actions/upload-artifact@v4 113 | with: 114 | name: ${{ inputs.dists-artifact-name }}- 115 | ${{ inputs.os }}- 116 | ${{ inputs.qemu }}- 117 | ${{ steps.gha-artifact-name.outputs.hash }} 118 | path: ./wheelhouse/*.whl 119 | 120 | ... 121 | -------------------------------------------------------------------------------- /.github/workflows/reusable-linters.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: ♲ 🚨 4 | 5 | on: # yamllint disable-line rule:truthy 6 | workflow_call: 7 | 8 | env: 9 | TOX_VERSION: tox 10 | 11 | jobs: 12 | linters: 13 | name: >- 14 | ${{ matrix.toxenv }}/${{ matrix.python-version }}@${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | 17 | timeout-minutes: 2 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | python-version: 23 | - 3.11 24 | os: 25 | - ubuntu-latest 26 | toxenv: 27 | - lint 28 | - build-docs 29 | - make-changelog 30 | 31 | env: 32 | PY_COLORS: 1 33 | TOX_PARALLEL_NO_SPINNER: 1 34 | TOXENV: ${{ matrix.toxenv }} 35 | 36 | steps: 37 | - uses: actions/checkout@v4.1.6 38 | - name: Set up Python ${{ matrix.python-version }} 39 | uses: actions/setup-python@v5.3.0 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | - name: >- 43 | Calculate Python interpreter version hash value 44 | for use in the cache key 45 | id: calc-cache-key-py 46 | run: | 47 | from hashlib import sha512 48 | from os import environ 49 | from pathlib import Path 50 | from sys import version 51 | 52 | FILE_APPEND_MODE = 'a' 53 | 54 | hash = sha512(version.encode()).hexdigest() 55 | 56 | with Path(environ['GITHUB_OUTPUT']).open( 57 | mode=FILE_APPEND_MODE, 58 | ) as outputs_file: 59 | print(f'py-hash-key={hash}', file=outputs_file) 60 | shell: python 61 | - name: Pre-commit cache 62 | uses: actions/cache@v4 63 | with: 64 | path: ~/.cache/pre-commit 65 | key: >- 66 | ${{ runner.os }}-pre-commit-${{ 67 | steps.calc-cache-key-py.outputs.py-hash-key }}-${{ 68 | hashFiles('setup.cfg') }}-${{ hashFiles('tox.ini') }}-${{ 69 | hashFiles('pyproject.toml') }}-${{ 70 | hashFiles('.pre-commit-config.yaml') }}-${{ 71 | hashFiles('pytest.ini') }} 72 | - name: Pip cache 73 | uses: actions/cache@v4 74 | with: 75 | path: ~/.cache/pip 76 | key: >- 77 | ${{ runner.os }}-pip-${{ 78 | steps.calc-cache-key-py.outputs.py-hash-key }}-${{ 79 | hashFiles('setup.cfg') }}-${{ hashFiles('tox.ini') }}-${{ 80 | hashFiles('pyproject.toml') }}-${{ 81 | hashFiles('.pre-commit-config.yaml') }}-${{ 82 | hashFiles('pytest.ini') }} 83 | restore-keys: | 84 | ${{ runner.os }}-pip- 85 | ${{ runner.os }}- 86 | - name: Install tox 87 | run: >- 88 | python -m 89 | pip install 90 | --user 91 | '${{ env.TOX_VERSION }}' 92 | - name: Log installed dists 93 | run: python -m pip freeze --all 94 | - name: Initialize tox envs 95 | run: | 96 | python -m tox --parallel auto --parallel-live --notest 97 | - name: Initialize pre-commit envs if needed 98 | run: >- 99 | test -d .tox/lint 100 | && .tox/lint/bin/python -m pre_commit install-hooks 101 | || : 102 | - name: Test with tox 103 | run: python -m tox --parallel auto --parallel-live 104 | 105 | ... 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv 3 | # Edit at https://www.gitignore.io/?templates=git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv 4 | 5 | ### dotenv ### 6 | .env 7 | 8 | ### Emacs ### 9 | # -*- mode: gitignore; -*- 10 | *~ 11 | \#*\# 12 | /.emacs.desktop 13 | /.emacs.desktop.lock 14 | *.elc 15 | auto-save-list 16 | tramp 17 | .\#* 18 | 19 | # Org-mode 20 | .org-id-locations 21 | *_archive 22 | 23 | # flymake-mode 24 | *_flymake.* 25 | 26 | # eshell files 27 | /eshell/history 28 | /eshell/lastdir 29 | 30 | # elpa packages 31 | /elpa/ 32 | 33 | # reftex files 34 | *.rel 35 | 36 | # AUCTeX auto folder 37 | /auto/ 38 | 39 | # cask packages 40 | .cask/ 41 | dist/ 42 | 43 | # Flycheck 44 | flycheck_*.el 45 | 46 | # server auth directory 47 | /server/ 48 | 49 | # projectiles files 50 | .projectile 51 | 52 | # directory configuration 53 | .dir-locals.el 54 | 55 | # network security 56 | /network-security.data 57 | 58 | 59 | ### Git ### 60 | # Created by git for backups. To disable backups in Git: 61 | # $ git config --global mergetool.keepBackup false 62 | *.orig 63 | 64 | # Created by git when using merge tools for conflicts 65 | *.BACKUP.* 66 | *.BASE.* 67 | *.LOCAL.* 68 | *.REMOTE.* 69 | *_BACKUP_*.txt 70 | *_BASE_*.txt 71 | *_LOCAL_*.txt 72 | *_REMOTE_*.txt 73 | 74 | #!! ERROR: jupyternotebook is undefined. Use list command to see defined gitignore types !!# 75 | 76 | ### Linux ### 77 | 78 | # temporary files which can be created if a process still has a handle open of a deleted file 79 | .fuse_hidden* 80 | 81 | # KDE directory preferences 82 | .directory 83 | 84 | # Linux trash folder which might appear on any partition or disk 85 | .Trash-* 86 | 87 | # .nfs files are created when an open file is removed but is still being accessed 88 | .nfs* 89 | 90 | ### PyCharm+all ### 91 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 92 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 93 | 94 | # User-specific stuff 95 | .idea/**/workspace.xml 96 | .idea/**/tasks.xml 97 | .idea/**/usage.statistics.xml 98 | .idea/**/dictionaries 99 | .idea/**/shelf 100 | 101 | # Generated files 102 | .idea/**/contentModel.xml 103 | 104 | # Sensitive or high-churn files 105 | .idea/**/dataSources/ 106 | .idea/**/dataSources.ids 107 | .idea/**/dataSources.local.xml 108 | .idea/**/sqlDataSources.xml 109 | .idea/**/dynamic.xml 110 | .idea/**/uiDesigner.xml 111 | .idea/**/dbnavigator.xml 112 | 113 | # Gradle 114 | .idea/**/gradle.xml 115 | .idea/**/libraries 116 | 117 | # Gradle and Maven with auto-import 118 | # When using Gradle or Maven with auto-import, you should exclude module files, 119 | # since they will be recreated, and may cause churn. Uncomment if using 120 | # auto-import. 121 | # .idea/modules.xml 122 | # .idea/*.iml 123 | # .idea/modules 124 | # *.iml 125 | # *.ipr 126 | 127 | # CMake 128 | cmake-build-*/ 129 | 130 | # Mongo Explorer plugin 131 | .idea/**/mongoSettings.xml 132 | 133 | # File-based project format 134 | *.iws 135 | 136 | # IntelliJ 137 | out/ 138 | 139 | # mpeltonen/sbt-idea plugin 140 | .idea_modules/ 141 | 142 | # JIRA plugin 143 | atlassian-ide-plugin.xml 144 | 145 | # Cursive Clojure plugin 146 | .idea/replstate.xml 147 | 148 | # Crashlytics plugin (for Android Studio and IntelliJ) 149 | com_crashlytics_export_strings.xml 150 | crashlytics.properties 151 | crashlytics-build.properties 152 | fabric.properties 153 | 154 | # Editor-based Rest Client 155 | .idea/httpRequests 156 | 157 | # Android studio 3.1+ serialized cache file 158 | .idea/caches/build_file_checksums.ser 159 | 160 | ### PyCharm+all Patch ### 161 | # Ignores the whole .idea folder and all .iml files 162 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 163 | 164 | .idea/ 165 | 166 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 167 | 168 | *.iml 169 | modules.xml 170 | .idea/misc.xml 171 | *.ipr 172 | 173 | # Sonarlint plugin 174 | .idea/sonarlint 175 | 176 | ### pydev ### 177 | .pydevproject 178 | 179 | ### Python ### 180 | # Byte-compiled / optimized / DLL files 181 | __pycache__/ 182 | *.py[cod] 183 | *$py.class 184 | 185 | # C extensions 186 | *.so 187 | 188 | # Distribution / packaging 189 | .Python 190 | build/ 191 | develop-eggs/ 192 | downloads/ 193 | eggs/ 194 | .eggs/ 195 | lib/ 196 | lib64/ 197 | parts/ 198 | sdist/ 199 | var/ 200 | wheels/ 201 | pip-wheel-metadata/ 202 | share/python-wheels/ 203 | *.egg-info/ 204 | .installed.cfg 205 | *.egg 206 | MANIFEST 207 | 208 | # PyInstaller 209 | # Usually these files are written by a python script from a template 210 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 211 | *.manifest 212 | *.spec 213 | 214 | # Installer logs 215 | pip-log.txt 216 | pip-delete-this-directory.txt 217 | 218 | # Unit test / coverage reports 219 | htmlcov/ 220 | .tox/ 221 | .nox/ 222 | .coverage 223 | .coverage.* 224 | .cache 225 | nosetests.xml 226 | coverage.xml 227 | *.cover 228 | .hypothesis/ 229 | .pytest_cache/ 230 | 231 | # Translations 232 | *.mo 233 | *.pot 234 | 235 | # Scrapy stuff: 236 | .scrapy 237 | 238 | # Sphinx documentation 239 | docs/_build/ 240 | 241 | # PyBuilder 242 | target/ 243 | 244 | # pyenv 245 | .python-version 246 | 247 | # pipenv 248 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 249 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 250 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 251 | # install all needed dependencies. 252 | #Pipfile.lock 253 | 254 | # celery beat schedule file 255 | celerybeat-schedule 256 | 257 | # SageMath parsed files 258 | *.sage.py 259 | 260 | # Spyder project settings 261 | .spyderproject 262 | .spyproject 263 | 264 | # Rope project settings 265 | .ropeproject 266 | 267 | # Mr Developer 268 | .mr.developer.cfg 269 | .project 270 | 271 | # mkdocs documentation 272 | /site 273 | 274 | # mypy 275 | .mypy_cache/ 276 | .dmypy.json 277 | dmypy.json 278 | 279 | # Pyre type checker 280 | .pyre/ 281 | 282 | ### Vim ### 283 | # Swap 284 | [._]*.s[a-v][a-z] 285 | [._]*.sw[a-p] 286 | [._]s[a-rt-v][a-z] 287 | [._]ss[a-gi-z] 288 | [._]sw[a-p] 289 | 290 | # Session 291 | Session.vim 292 | Sessionx.vim 293 | 294 | # Temporary 295 | .netrwhist 296 | 297 | # Auto-generated tag files 298 | tags 299 | 300 | # Persistent undo 301 | [._]*.un~ 302 | 303 | # Coc configuration directory 304 | .vim 305 | 306 | ### WebStorm ### 307 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 308 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 309 | 310 | # User-specific stuff 311 | 312 | # Generated files 313 | 314 | # Sensitive or high-churn files 315 | 316 | # Gradle 317 | 318 | # Gradle and Maven with auto-import 319 | # When using Gradle or Maven with auto-import, you should exclude module files, 320 | # since they will be recreated, and may cause churn. Uncomment if using 321 | # auto-import. 322 | # .idea/modules.xml 323 | # .idea/*.iml 324 | # .idea/modules 325 | # *.iml 326 | # *.ipr 327 | 328 | # CMake 329 | 330 | # Mongo Explorer plugin 331 | 332 | # File-based project format 333 | 334 | # IntelliJ 335 | 336 | # mpeltonen/sbt-idea plugin 337 | 338 | # JIRA plugin 339 | 340 | # Cursive Clojure plugin 341 | 342 | # Crashlytics plugin (for Android Studio and IntelliJ) 343 | 344 | # Editor-based Rest Client 345 | 346 | # Android studio 3.1+ serialized cache file 347 | 348 | ### WebStorm Patch ### 349 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 350 | 351 | # *.iml 352 | # modules.xml 353 | # .idea/misc.xml 354 | # *.ipr 355 | 356 | # Sonarlint plugin 357 | .idea/**/sonarlint/ 358 | 359 | # SonarQube Plugin 360 | .idea/**/sonarIssues.xml 361 | 362 | # Markdown Navigator plugin 363 | .idea/**/markdown-navigator.xml 364 | .idea/**/markdown-navigator/ 365 | 366 | ### Windows ### 367 | # Windows thumbnail cache files 368 | Thumbs.db 369 | Thumbs.db:encryptable 370 | ehthumbs.db 371 | ehthumbs_vista.db 372 | 373 | # Dump file 374 | *.stackdump 375 | 376 | # Folder config file 377 | [Dd]esktop.ini 378 | 379 | # Recycle Bin used on file shares 380 | $RECYCLE.BIN/ 381 | 382 | # Windows Installer files 383 | *.cab 384 | *.msi 385 | *.msix 386 | *.msm 387 | *.msp 388 | 389 | # Windows shortcuts 390 | *.lnk 391 | 392 | # End of https://www.gitignore.io/api/git,linux,pydev,python,windows,pycharm+all,jupyternotebook,vim,webstorm,emacs,dotenv 393 | 394 | /.github/workflows/.tmp 395 | /.test-results 396 | 397 | # setuptools-scm write_to: 398 | /src/pylibsshext/_scm_version.py 399 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | # https://github.com/timothycrosley/isort/wiki/isort-Settings 2 | [settings] 3 | default_section = THIRDPARTY 4 | # force_to_top=file1.py,file2.py 5 | # forced_separate = django.contrib,django.utils 6 | include_trailing_comma = true 7 | indent = 4 8 | known_first_party = pylibsshext 9 | # known_future_library = future,pies 10 | # known_standard_library = std,std2 11 | known_testing = pytest,unittest,_service_utils 12 | known_cython = libc,cpython,Cython 13 | # known_third_party = Cython 14 | # length_sort = 1 15 | # Should be: 80 - 1 16 | line_length = 79 17 | lines_after_imports = 2 18 | # https://github.com/timothycrosley/isort#multi-line-output-modes 19 | multi_line_output = 5 20 | no_lines_before = LOCALFOLDER 21 | sections=FUTURE,STDLIB,CYTHON,TESTING,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 22 | # skip=file3.py,file4.py 23 | use_parentheses = true 24 | verbose = true 25 | -------------------------------------------------------------------------------- /.packit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | actions: 4 | changelog-entry: >- 5 | echo Dummy log https://github.com/packit/packit-service/issues/1659 6 | # post-upstream-clone: [] 7 | create-archive: 8 | - >- 9 | bash -c ' 10 | echo %upstream_version "${PACKIT_PROJECT_VERSION}" 11 | | 12 | tee ~/.rpmmacros 13 | ' 14 | - >- # NOTE: defining `%{rhel}` lets the spectool download all the sources 15 | bash -c ' 16 | spectool 17 | --define "upstream_version ${PACKIT_PROJECT_VERSION}" 18 | --define "rhel 666" 19 | --all --get-files 20 | --sourcedir packaging/rpm/ansible-pylibssh.spec 21 | ' 22 | - tox -e build-dists -qq -- --sdist 23 | - sh -c 'echo dist/"${PACKIT_PROJECT_NAME_VERSION}".tar.gz' 24 | get-current-version: 25 | - >- 26 | bash -c " 27 | python3 -m setuptools_scm | sed 's/.*Guessed Version\s\+//g' 28 | " 29 | 30 | # allowed_gpg_keys: 31 | # - 5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB23 # GH web UI commit signing key 32 | 33 | archive_root_dir_template: >- 34 | {upstream_pkg_name}-{version} 35 | 36 | copy_upstream_release_description: false 37 | 38 | create_pr: true # in dist-git 39 | 40 | dist_git_base_url: https://src.fedoraproject.org/ 41 | dist_git_namespace: rpms 42 | 43 | downstream_package_name: python3-ansible-pylibssh 44 | 45 | jobs: 46 | - job: copr_build 47 | enable_net: true 48 | targets: 49 | # - epel-9 50 | # - fedora-all 51 | - fedora-development 52 | - fedora-stable 53 | trigger: pull_request 54 | - job: tests 55 | enable_net: true 56 | targets: 57 | # - epel-9 58 | - fedora-development 59 | - fedora-stable 60 | trigger: pull_request 61 | - job: propose_downstream 62 | enable_net: true 63 | dist_git_branches: fedora-all 64 | trigger: release 65 | 66 | notifications: 67 | pull_request: 68 | successful_build: true 69 | 70 | patch_generation_ignore_paths: [] 71 | patch_generation_patch_id_digits: 4 72 | 73 | parse_time_macros: 74 | upstream_version: 666 75 | 76 | # sources: [] 77 | 78 | specfile_path: packaging/rpm/ansible-pylibssh.spec 79 | # spec_source_id: 0 80 | 81 | srpm_build_deps: # The presense of `srpm_build_deps` enforces Copr env 82 | - python3-setuptools_scm+toml 83 | - tox 84 | 85 | sync_changelog: false 86 | # synced_files: [] 87 | 88 | upstream_package_name: ansible-pylibssh 89 | upstream_project_url: https://github.com/ansible/pylibssh 90 | upstream_tag_template: v{version} 91 | 92 | ... 93 | -------------------------------------------------------------------------------- /.pip-tools.toml: -------------------------------------------------------------------------------- 1 | [tool.pip-tools] 2 | allow-unsafe = true # weird outdated default 3 | # generate-hashes = false # pip bug https://github.com/pypa/pip/issues/9243 4 | resolver = "backtracking" # modern depresolver 5 | strip-extras = true # so that output files are true pip constraints 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ci: 4 | autoupdate_schedule: quarterly 5 | 6 | repos: 7 | - repo: https://github.com/asottile/add-trailing-comma.git 8 | rev: v3.1.0 9 | hooks: 10 | - id: add-trailing-comma 11 | 12 | - repo: https://github.com/PyCQA/isort.git 13 | rev: 6.0.1 14 | hooks: 15 | - id: isort 16 | args: 17 | - --honor-noqa 18 | files: >- 19 | ^.*\.p(xd|y|yx)$ 20 | types: [file] 21 | 22 | - repo: https://github.com/Lucas-C/pre-commit-hooks.git 23 | rev: v1.5.5 24 | hooks: 25 | - id: remove-tabs 26 | 27 | - repo: https://github.com/python-jsonschema/check-jsonschema.git 28 | rev: 0.33.0 29 | hooks: 30 | - id: check-github-workflows 31 | files: ^\.github/workflows/[^/]+$ 32 | types: 33 | - yaml 34 | - id: check-jsonschema 35 | name: Check GitHub Workflows set timeout-minutes 36 | args: 37 | - --builtin-schema 38 | - github-workflows-require-timeout 39 | files: ^\.github/workflows/[^/]+$ 40 | types: 41 | - yaml 42 | - id: check-readthedocs 43 | 44 | - repo: https://github.com/pre-commit/pygrep-hooks.git 45 | rev: v1.10.0 46 | hooks: 47 | - id: python-check-blanket-noqa 48 | - id: python-check-mock-methods 49 | - id: python-no-eval 50 | - id: python-no-log-warn 51 | - id: rst-backticks 52 | 53 | - repo: https://github.com/pre-commit/pre-commit-hooks.git 54 | rev: v5.0.0 55 | hooks: 56 | # Side-effects: 57 | - id: trailing-whitespace 58 | - id: check-merge-conflict 59 | - id: double-quote-string-fixer 60 | - id: end-of-file-fixer 61 | - id: requirements-txt-fixer 62 | exclude: >- 63 | ^(docs/requirements|requirements-build)\.txt$ 64 | 65 | # Non-modifying checks: 66 | - id: name-tests-test 67 | files: >- 68 | ^tests/[^_].*\.py$ 69 | - id: check-added-large-files 70 | - id: check-byte-order-marker 71 | - id: check-case-conflict 72 | # disabled due to pre-commit/pre-commit-hooks#159 73 | # - id: check-docstring-first 74 | - id: check-json 75 | - id: check-symlinks 76 | - id: check-yaml 77 | - id: detect-private-key 78 | 79 | # Heavy checks: 80 | - id: check-ast 81 | exclude: >- 82 | ^docs/_samples/.*\.py$ 83 | - id: debug-statements 84 | exclude: >- 85 | ^docs/_samples/.*\.py$ 86 | 87 | - repo: https://github.com/PyCQA/pydocstyle.git 88 | rev: 6.3.0 89 | hooks: 90 | - id: pydocstyle 91 | exclude: >- 92 | ^docs/_samples/.*\.py$ 93 | 94 | - repo: https://github.com/adrienverge/yamllint.git 95 | rev: v1.37.0 96 | hooks: 97 | - id: yamllint 98 | files: \.(yaml|yml)$ 99 | types: [file, yaml] 100 | args: 101 | - --strict 102 | 103 | - repo: https://github.com/PyCQA/flake8.git 104 | rev: 7.2.0 105 | hooks: 106 | - id: flake8 107 | alias: flake8-no-wps 108 | name: flake8 WPS-excluded 109 | additional_dependencies: 110 | - flake8-2020 ~= 1.7.0 111 | - flake8-length ~= 0.3.0 112 | - flake8-logging-format ~= 0.7.5 113 | - flake8-pytest-style ~= 1.6.0 114 | exclude: >- 115 | ^docs/_samples/.*\.py$ 116 | files: >- 117 | ^.*\.p(xd|y|yx)$ 118 | language_version: python3 119 | types: 120 | - file 121 | 122 | - repo: https://github.com/PyCQA/flake8.git 123 | rev: 7.2.0 124 | hooks: 125 | - id: flake8 126 | alias: flake8-only-wps 127 | name: flake8 WPS-only 128 | args: 129 | # https://wemake-python-stylegui.de/en/latest/pages/usage/formatter.html 130 | - --format 131 | - wemake 132 | - --select 133 | - WPS 134 | additional_dependencies: 135 | - wemake-python-styleguide ~= 0.19.2 136 | exclude: >- 137 | ^docs/_samples/.*\.py$ 138 | files: >- 139 | ^.*\.p(xd|y|yx)$ 140 | language_version: python3.11 # flake8-commas doesn't work w/ Python 3.12 141 | types: 142 | - file 143 | 144 | - repo: https://github.com/MarcoGorelli/cython-lint.git 145 | rev: v0.16.6 146 | hooks: 147 | - id: cython-lint 148 | # NOTE: This linter does not have a config file so it's set up below. 149 | args: 150 | - --max-line-length 151 | - '160' # matches a similar value in the .flake8 config 152 | 153 | - repo: https://github.com/Lucas-C/pre-commit-hooks-markup.git 154 | rev: v1.0.1 155 | hooks: 156 | - id: rst-linter 157 | files: >- 158 | ^README\.rst$ 159 | 160 | - repo: local 161 | hooks: 162 | - id: changelogs-rst 163 | name: changelog filenames 164 | language: fail 165 | entry: >- 166 | Changelog files must be named 167 | ####.( 168 | bugfix 169 | | feature 170 | | deprecation 171 | | breaking 172 | | doc 173 | | packaging 174 | | contrib 175 | | misc 176 | )(.#)?(.rst)? 177 | exclude: >- 178 | (?x) 179 | ^ 180 | docs/changelog-fragments/( 181 | \.gitignore 182 | |(\d+|[0-9a-f]{8}|[0-9a-f]{7}|[0-9a-f]{40})\.( 183 | bugfix 184 | |feature 185 | |deprecation 186 | |breaking 187 | |doc 188 | |packaging 189 | |contrib 190 | |misc 191 | )(\.\d+)?(\.rst)? 192 | |README\.rst 193 | |template\.j2 194 | ) 195 | $ 196 | files: ^docs/changelog-fragments/ 197 | types: [] 198 | types_or: 199 | - file 200 | - symlink 201 | - id: changelogs-user-role 202 | name: Changelog files should use a non-broken :user:`name` role 203 | language: pygrep 204 | entry: :user:([^`]+`?|`[^`]+[\s,]) 205 | pass_filenames: true 206 | types: 207 | - file 208 | - rst 209 | 210 | # - repo: local 211 | # hooks: 212 | # - id: pylint 213 | # language: system 214 | # name: PyLint 215 | # files: \.py$ 216 | # entry: python -m pylint 217 | # args: [] 218 | 219 | ... 220 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | --- 4 | 5 | version: 2 6 | 7 | build: 8 | os: ubuntu-24.04 9 | tools: 10 | python: >- # has to be parsed as a YAML string 11 | 3.11 12 | commands: 13 | - >- 14 | PYTHONWARNINGS=error 15 | python3 -Im venv "${READTHEDOCS_VIRTUALENV_PATH}" 16 | - >- 17 | PYTHONWARNINGS=error 18 | "${READTHEDOCS_VIRTUALENV_PATH}"/bin/python -Im 19 | pip install tox 20 | - >- 21 | PYTHONWARNINGS=error 22 | "${READTHEDOCS_VIRTUALENV_PATH}"/bin/python -Im 23 | tox -e build-docs --notest -vvvvv 24 | - >- 25 | PYTHONWARNINGS=error 26 | SPHINX_BUILDER=dirhtml 27 | SPHINX_BUILD_OUTPUT_DIRECTORY="${READTHEDOCS_OUTPUT}"/html 28 | "${READTHEDOCS_VIRTUALENV_PATH}"/bin/python -Im 29 | tox -e build-docs --skip-pkg-install -q 30 | 31 | ... 32 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | rules: 6 | indentation: 7 | indent-sequences: false 8 | level: error 9 | 10 | ... 11 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | 2 | Contributors 3 | ============ 4 | 5 | Please see the following: 6 | 7 | * `Contributors listing`_ 8 | * `Community working group members`_ 9 | 10 | .. _Contributors listing: https://github.com/ansible/pylibssh/graphs/contributors 11 | .. _Community working group members: https://github.com/ansible/community/wiki/pylibssh 12 | 13 | 14 | Credits 15 | ======= 16 | 17 | Based on the good work of Jan Pazdziora (`@adelton`_). 18 | 19 | .. _`@adelton`: https://github.com/adelton 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/ansible-pylibssh.svg?logo=Python&logoColor=white 2 | :target: https://pypi.org/project/ansible-pylibssh 3 | 4 | .. image:: https://img.shields.io/badge/license-LGPL+-blue.svg?maxAge=3600 5 | :target: https://pypi.org/project/ansible-pylibssh 6 | 7 | .. image:: https://img.shields.io/pypi/pyversions/ansible-pylibssh.svg?logo=Python&logoColor=white 8 | :target: https://pypi.org/project/ansible-pylibssh 9 | 10 | .. image:: https://github.com/ansible/pylibssh/actions/workflows/ci-cd.yml/badge.svg?event=push 11 | :alt: 🧪 CI/CD @ devel 12 | :target: https://github.com/ansible/pylibssh/actions/workflows/ci-cd.yml 13 | 14 | .. image:: https://img.shields.io/codecov/c/gh/ansible/pylibssh/devel?logo=codecov&logoColor=white 15 | :target: https://codecov.io/gh/ansible/pylibssh 16 | :alt: devel branch coverage via Codecov 17 | 18 | .. image:: https://img.shields.io/badge/style-wemake-000000.svg 19 | :target: https://github.com/wemake-services/wemake-python-styleguide 20 | 21 | .. image:: https://img.shields.io/badge/Code%20of%20Conduct-Ansible-silver.svg 22 | :target: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html 23 | :alt: Ansible Code of Conduct 24 | 25 | .. DO-NOT-REMOVE-docs-badges-END 26 | 27 | pylibssh: Python bindings to client functionality of libssh specific to Ansible use case 28 | ======================================================================================== 29 | 30 | .. DO-NOT-REMOVE-docs-intro-START 31 | 32 | Nightlies @ Dumb PyPI @ GitHub Pages 33 | ------------------------------------ 34 | 35 | .. DO-NOT-REMOVE-nightlies-START 36 | 37 | We publish nightlies on tags and pushes to devel. 38 | They are hosted on a GitHub Pages based index generated 39 | by `dumb-pypi `_. 40 | 41 | The web view is @ https://ansible.github.io/pylibssh/. 42 | 43 | .. code-block:: shell-session 44 | 45 | $ pip install \ 46 | --extra-index-url=https://ansible.github.io/pylibssh/simple/ \ 47 | --pre \ 48 | ansible-pylibssh 49 | 50 | .. DO-NOT-REMOVE-nightlies-END 51 | 52 | 53 | Requirements 54 | ------------ 55 | 56 | You need Python 3.9+ 57 | 58 | pylibssh requires libssh to be installed in particular: 59 | 60 | - libssh version 0.9.0 and later. 61 | 62 | To install libssh refer to its `Downloads page 63 | `__. 64 | 65 | 66 | Building the module 67 | ------------------- 68 | 69 | In the local env, assumes there's a libssh shared library 70 | on the system, build toolchain is present and env vars 71 | are set properly: 72 | 73 | .. code-block:: shell-session 74 | 75 | $ git clone https://github.com/ansible/pylibssh.git 76 | $ cd pylibssh 77 | $ pip install tox 78 | $ tox -e build-dists 79 | 80 | ``manylinux``-compatible wheels: 81 | 82 | .. code-block:: shell-session 83 | 84 | $ git clone https://github.com/ansible/pylibssh.git 85 | $ cd pylibssh 86 | $ pip install tox 87 | $ tox -e build-dists-manylinux1-x86_64 # with Docker 88 | 89 | # or with Podman 90 | $ DOCKER_EXECUTABLE=podman tox -e build-dists-manylinux1-x86_64 91 | 92 | # to enable shell script debug mode use 93 | $ tox -e build-dists-manylinux1-x86_64 -- -e DEBUG=1 94 | 95 | Communication 96 | ------------- 97 | 98 | Join the Ansible forum: 99 | 100 | * `Get Help `_: get help or help others. Please add the appropriate tags if you start new discussions, for example the ``pylibssh`` tag. 101 | * `Posts tagged with 'pylibssh' `_: subscribe to participate in project-related conversations. 102 | * `News & Announcements `_: track project-wide announcements including social events and the `Bullhorn newsletter `_. 103 | * `Social Spaces `_: gather and interact with fellow enthusiasts. 104 | 105 | For more information about getting in touch with us, see the `Ansible communication guide `_. 106 | 107 | License 108 | ------- 109 | 110 | This library is distributed under the terms of LGPL 2 or higher, 111 | see file ``LICENSE.rst`` in this repository. 112 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RELEASE 2 | FROM quay.io/pypa/${RELEASE} 3 | ARG RELEASE 4 | ARG LIBSSH_VERSION=0.11.1 5 | MAINTAINER Python Cryptographic Authority 6 | WORKDIR /root 7 | 8 | ADD build_utils.sh /root/build_utils.sh 9 | ADD install_perl.sh /root/install_perl.sh 10 | RUN ./install_perl.sh "${RELEASE}" 11 | ADD install_libffi.sh /root/install_libffi.sh 12 | RUN ./install_libffi.sh "${RELEASE}" 13 | ADD install_openssl.sh /root/install_openssl.sh 14 | ADD openssl-version.sh /root/openssl-version.sh 15 | RUN ./install_openssl.sh "${RELEASE}" 16 | 17 | ADD install_virtualenv.sh /root/install_virtualenv.sh 18 | RUN ./install_virtualenv.sh 19 | 20 | # \pylibssh 21 | ADD install-userspace-tools.sh /root/install-userspace-tools.sh 22 | ADD activate-userspace-tools.sh /root/activate-userspace-tools.sh 23 | RUN ./install-userspace-tools.sh 24 | ADD make-static-deps-dir.sh /root/make-static-deps-dir.sh 25 | ADD get-static-deps-dir.sh /root/get-static-deps-dir.sh 26 | RUN ./make-static-deps-dir.sh 27 | ADD install_zlib.sh /root/install_zlib.sh 28 | RUN ./install_zlib.sh 29 | ADD install_libssh.sh /root/install_libssh.sh 30 | RUN ./install_libssh.sh "${LIBSSH_VERSION}" 31 | # /pylibssh 32 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/LICENSE.BSD: -------------------------------------------------------------------------------- 1 | Copyright (c) Individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of PyCA nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/README.md: -------------------------------------------------------------------------------- 1 | # manylinux images 2 | 3 | This is a container definition based on the great work of folks 4 | maintaining [PyCA/infra]. It is dual-licensed under Apache 2.0 and 5 | BSD 3-clause. The corresponding license files are present in 6 | this directory. 7 | 8 | ## Docker Containers 9 | 10 | Docker containers are rebuilt on cron by Github Actions and then 11 | uploaded to [Github Container Registry]. 12 | 13 | ## Building locally 14 | 15 | It is possible to build a container locally by running something like 16 | 17 | ```console 18 | $ podman build --build-arg RELEASE=manylinux1_x86_64 . 19 | ``` 20 | 21 | > *NOTE:* The base image is parametrised with a `--build-arg` option and> is constructed as `quay.io/pypa/${RELEASE}`. 22 | 23 | [Github Container Registry]: https://github.com/orgs/ansible/packages?ecosystem=container&visibility=all 24 | [PyCA/infra]: https://github.com/pyca/infra 25 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/activate-userspace-tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | USERSPACE_LOCAL_BIN_PATH="${HOME}/.local/bin" 5 | USERSPACE_VENV_PATH="${HOME}/.tools-venv" 6 | USERSPACE_VENV_BIN_PATH="${USERSPACE_VENV_PATH}/bin" 7 | 8 | PATH="${USERSPACE_VENV_BIN_PATH}:${USERSPACE_LOCAL_BIN_PATH}:${PATH}" 9 | 10 | function import_userspace_tools { 11 | export USERSPACE_VENV_PATH 12 | export USERSPACE_VENV_BIN_PATH 13 | export PATH 14 | } 15 | 16 | export import_userspace_tools 17 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/build_utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Helper utilities for build borrowed from https://github.com/pypa/manylinux/blob/main/docker/build_scripts/build_utils.sh 3 | 4 | 5 | function check_var { 6 | if [ -z "$1" ]; then 7 | echo "required variable not defined" 8 | exit 1 9 | fi 10 | } 11 | 12 | 13 | function fetch_source { 14 | # This is called both inside and outside the build context (e.g. in Travis) to prefetch 15 | # source tarballs, where curl exists (and works) 16 | local file=$1 17 | check_var ${file} 18 | local url=$2 19 | check_var ${url} 20 | if [ $(uname -m) = "s390x" ] || [ $(uname -m) = "ppc64le" ]; then 21 | # Expired certificate issue on these platforms 22 | # https://github.com/pypa/manylinux/issues/1203 23 | unset SSL_CERT_FILE 24 | fi 25 | if [ -f ${file} ]; then 26 | echo "${file} exists, skipping fetch" 27 | else 28 | curl -fsSL -o ${file} ${url}/${file} 29 | fi 30 | } 31 | 32 | 33 | function check_sha256sum { 34 | local fname=$1 35 | check_var ${fname} 36 | local sha256=$2 37 | check_var ${sha256} 38 | echo "${sha256} ${fname}" > "${fname}.sha256" 39 | sha256sum -c "${fname}.sha256" 40 | rm "${fname}.sha256" 41 | } 42 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/get-static-deps-dir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | STATIC_DEPS_DIR_STORAGE=.static-deps-path 5 | STATIC_DEPS_DIR=$(cat "${STATIC_DEPS_DIR_STORAGE}") 6 | 7 | function get_static_deps_dir { 8 | echo "${STATIC_DEPS_DIR}" 9 | } 10 | 11 | export get_static_deps_dir 12 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/install-userspace-tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | set -Eeuo pipefail 5 | 6 | source activate-userspace-tools.sh 7 | import_userspace_tools 8 | 9 | ARCH=$(uname -m) 10 | PYTHON_INTERPRETER=/opt/python/cp39-cp39/bin/python 11 | VIRTUALENV_PYTHON_BIN="${USERSPACE_VENV_BIN_PATH}/python" 12 | VIRTUALENV_PIP_BIN="${VIRTUALENV_PYTHON_BIN} -m pip" 13 | 14 | # NOTE: Cmake removed compatibility with `cmake < 3.5` that 15 | # NOTE: libssh 0.9.6 is set up to require. 16 | # NOTE: So this patch limits the version of `cmake` we install. 17 | # 18 | # Ref: https://github.com/eclipse-ecal/ecal/issues/2041 19 | # FIXME: Drop the restriction once libssh is bumped to v0.11 series. 20 | TOOLS_PKGS="auditwheel cmake<4 --only-binary=cmake" 21 | 22 | # Avoid creation of __pycache__/*.py[c|o] 23 | export PYTHONDONTWRITEBYTECODE=1 24 | 25 | >&2 echo 26 | >&2 echo 27 | >&2 echo ============================================================================ 28 | >&2 echo Installing build deps into a dedicated venv at "'${USERSPACE_VENV_PATH}'"... 29 | >&2 echo ============================================================================ 30 | >&2 echo 31 | "${PYTHON_INTERPRETER}" -m venv "${USERSPACE_VENV_PATH}" 32 | ${VIRTUALENV_PIP_BIN} install -U pip-with-requires-python 33 | ${VIRTUALENV_PIP_BIN} install -U setuptools wheel 34 | ${VIRTUALENV_PIP_BIN} install ${TOOLS_PKGS} 35 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/install_libffi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | unset RELEASE 5 | 6 | # Get script directory 7 | MY_DIR=$(dirname "${BASH_SOURCE[0]}") 8 | 9 | # Get build utilities 10 | source $MY_DIR/build_utils.sh 11 | 12 | LIBFFI_SHA256="bc9842a18898bfacb0ed1252c4febcc7e78fa139fd27fdc7a3e30d9d9356119b" 13 | LIBFFI_VERSION="3.4.8" 14 | 15 | fetch_source "libffi-${LIBFFI_VERSION}.tar.gz" "https://github.com/libffi/libffi/releases/download/v${LIBFFI_VERSION}/" 16 | check_sha256sum "libffi-${LIBFFI_VERSION}.tar.gz" ${LIBFFI_SHA256} 17 | tar zxf libffi-${LIBFFI_VERSION}.tar.gz 18 | 19 | pushd libffi*/ 20 | if [[ "$1" =~ '^manylinux1_.*$' ]]; then 21 | PATH=/opt/perl/bin:$PATH 22 | STACK_PROTECTOR_FLAGS="-fstack-protector --param=ssp-buffer-size=4" 23 | else 24 | STACK_PROTECTOR_FLAGS="-fstack-protector-strong" 25 | fi 26 | ./configure CFLAGS="-g -O2 $STACK_PROTECTOR_FLAGS -Wformat -Werror=format-security" 27 | make install 28 | popd 29 | rm -rf libffi* 30 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/install_libssh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | LIB_VERSION="$1" 5 | 6 | set -Eeuo pipefail 7 | 8 | unset RELEASE 9 | 10 | source get-static-deps-dir.sh 11 | 12 | LIB_NAME=libssh 13 | BUILD_DIR=$(mktemp -d "/tmp/${LIB_NAME}-${LIB_VERSION}-manylinux-build.XXXXXXXXXX") 14 | 15 | LIB_CLONE_DIR="${BUILD_DIR}/${LIB_NAME}-${LIB_VERSION}" 16 | LIB_BUILD_DIR="${LIB_CLONE_DIR}/build" 17 | 18 | STATIC_DEPS_PREFIX="$(get_static_deps_dir)" 19 | 20 | if [ -z "${LIB_VERSION}" ] 21 | then 22 | >&2 echo "Please pass libssh version as a first argument of this script (${0})" 23 | exit 1 24 | fi 25 | 26 | # NOTE: `LDFLAGS` is necessary for the linker to locate the symbols for 27 | # NOTE: things like `dlopen', `dlclose', `pthread_atfork', `dlerror', 28 | # NOTE: `dlsym', `dladdr' etc. 29 | # NOTE: Otherwise, the build failure looks as follows: 30 | # 31 | # [ 66%] Linking C executable libssh_scp 32 | # ../lib/libssh.so.4.8.5: undefined reference to `dlopen' 33 | # ../lib/libssh.so.4.8.5: undefined reference to `dlclose' 34 | # ../lib/libssh.so.4.8.5: undefined reference to `pthread_atfork' 35 | # ../lib/libssh.so.4.8.5: undefined reference to `dlerror' 36 | # ../lib/libssh.so.4.8.5: undefined reference to `dlsym' 37 | # ../lib/libssh.so.4.8.5: undefined reference to `dladdr' 38 | # collect2: error: ld returned 1 exit status 39 | # make[2]: *** [examples/libssh_scp] Error 1 40 | # make[1]: *** [examples/CMakeFiles/libssh_scp.dir/all] Error 2 41 | # make: *** [all] Error 2 42 | export LDFLAGS="-pthread -ldl" 43 | 44 | # NOTE: `PKG_CONFIG_PATH` is necessary for `cmake` to be able to locate 45 | # NOTE: C-headers files `*.h`. Otherwise, the error is: 46 | # 47 | # -- Found ZLIB: /opt/manylinux-static-deps.PPkLKziXI7/lib/libz.a (found version "1.2.11") 48 | # -- Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the system variable OPENSSL_ROOT_DIR (missing: OPENSSL_CRYPTO_LIBRARY OPENSSL_INCLUDE_DIR) 49 | # -- Could NOT find GCrypt, try to set the path to GCrypt root folder in the system variable GCRYPT_ROOT_DIR (missing: GCRYPT_INCLUDE_DIR GCRYPT_LIBRARIES) 50 | # CMake Warning (dev) at /root/.tools-venv/lib/python3.9/site-packages/cmake/data/share/cmake-3.18/Modules/FindPackageHandleStandardArgs.cmake:273 (message): 51 | # The package name passed to `find_package_handle_standard_args` (MBedTLS) 52 | # does not match the name of the calling package (MbedTLS). This can lead to 53 | # problems in calling code that expects `find_package` result variables 54 | # (e.g., `_FOUND`) to follow a certain pattern. 55 | # Call Stack (most recent call first): 56 | # cmake/Modules/FindMbedTLS.cmake:96 (find_package_handle_standard_args) 57 | # CMakeLists.txt:65 (find_package) 58 | # This warning is for project developers. Use -Wno-dev to suppress it. 59 | # 60 | # -- Could NOT find mbedTLS, try to set the path to mbedLS root folder in 61 | # the system variable MBEDTLS_ROOT_DIR (missing: MBEDTLS_INCLUDE_DIR MBEDTLS_LIBRARIES) 62 | # CMake Error at CMakeLists.txt:67 (message): 63 | # Could not find OpenSSL, GCrypt or mbedTLS 64 | # 65 | # 66 | # -- Configuring incomplete, errors occurred! 67 | # See also "/tmp/libssh-0.9.4-manylinux-build.FJUercWAg9/libssh-0.9.4/build/CMakeFiles/CMakeOutput.log". 68 | # See also "/tmp/libssh-0.9.4-manylinux-build.FJUercWAg9/libssh-0.9.4/build/CMakeFiles/CMakeError.log". 69 | export PYCA_OPENSSL_PATH=/opt/pyca/cryptography/openssl 70 | export PKG_CONFIG_PATH="${STATIC_DEPS_PREFIX}/lib64/pkgconfig:${STATIC_DEPS_PREFIX}/lib/pkgconfig:${PYCA_OPENSSL_PATH}/lib/pkgconfig" 71 | 72 | >&2 echo 73 | >&2 echo 74 | >&2 echo ================================================== 75 | >&2 echo downloading source of ${LIB_NAME} v${LIB_VERSION}: 76 | >&2 echo ================================================== 77 | >&2 echo 78 | git clone \ 79 | --depth=1 \ 80 | -b "${LIB_NAME}-${LIB_VERSION}" \ 81 | https://git.libssh.org/projects/${LIB_NAME}.git \ 82 | "${LIB_CLONE_DIR}" 83 | 84 | source activate-userspace-tools.sh 85 | import_userspace_tools 86 | 87 | mkdir -pv "${LIB_BUILD_DIR}" 88 | pushd "${LIB_BUILD_DIR}" 89 | # For some reason, libssh has to be compiled as a shared object. 90 | # If not, imports fail at runtime, with undefined symbols: 91 | # ```python-traceback 92 | # test/units/test_sftp.py:7: in 93 | # from pylibsshext.sftp import SFTP 94 | # E ImportError: /opt/python/cp27-cp27m/lib/python2.7/site-packages/pylibsshext/sftp.so: undefined symbol: sftp_get_error 95 | # ``` 96 | # Also, when compiled statically, manylinux2010 container turns dist 97 | # into manylinux1 but because of the reason above, it doesn't make sense. 98 | cmake "${LIB_CLONE_DIR}" \ 99 | -DCMAKE_INSTALL_PREFIX="${STATIC_DEPS_PREFIX}" \ 100 | -DCMAKE_BUILD_TYPE=MinSizeRel \ 101 | -DBUILD_SHARED_LIBS=ON \ 102 | -DCLIENT_TESTING=OFF \ 103 | -DSERVER_TESTING=OFF \ 104 | -DUNIT_TESTING=OFF \ 105 | -DWITH_GSSAPI=ON \ 106 | -DWITH_SERVER=OFF \ 107 | -DWITH_PCAP=OFF \ 108 | -DWITH_ZLIB=ON 109 | make 110 | make install/strip 111 | popd 112 | 113 | rm -rf "${BUILD_DIR}" 114 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/install_openssl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | unset RELEASE 5 | 6 | # Get script directory 7 | MY_DIR=$(dirname "${BASH_SOURCE[0]}") 8 | 9 | # Get build utilities 10 | source $MY_DIR/build_utils.sh 11 | source /root/openssl-version.sh 12 | 13 | fetch_source "openssl-${OPENSSL_VERSION}.tar.gz" "https://www.openssl.org/source/" 14 | check_sha256sum "openssl-${OPENSSL_VERSION}.tar.gz" ${OPENSSL_SHA256} 15 | tar zxf openssl-${OPENSSL_VERSION}.tar.gz 16 | 17 | pushd openssl-${OPENSSL_VERSION} 18 | if [[ "$1" =~ '^manylinux1_.*$' ]]; then 19 | PATH=/opt/perl/bin:$PATH 20 | fi 21 | ./config $OPENSSL_BUILD_FLAGS --prefix=/opt/pyca/cryptography/openssl --openssldir=/opt/pyca/cryptography/openssl 22 | make depend 23 | make -j4 24 | # avoid installing the docs 25 | # https://github.com/openssl/openssl/issues/6685#issuecomment-403838728 26 | make install_sw install_ssldirs 27 | popd 28 | rm -rf openssl-${OPENSSL_VERSION} 29 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/install_perl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | unset RELEASE 5 | 6 | # Get script directory 7 | MY_DIR=$(dirname "${BASH_SOURCE[0]}") 8 | 9 | # Get build utilities 10 | source $MY_DIR/build_utils.sh 11 | 12 | PERL_SHA256="e6c185c9b09bdb3f1b13f678999050c639859a7ef39c8cad418448075f5918af" 13 | PERL_VERSION="5.24.1" 14 | 15 | if [[ "$1" =~ "^manylinux1_*" ]]; then 16 | fetch_source "perl-${PERL_VERSION}.tar.gz" "https://www.cpan.org/src/5.0" 17 | check_sha256sum "perl-${PERL_VERSION}.tar.gz" ${PERL_SHA256} 18 | 19 | tar zxf perl-$PERL_VERSION.tar.gz && \ 20 | cd perl-$PERL_VERSION && \ 21 | ./Configure -des -Dprefix=/opt/perl && \ 22 | make -j && make install 23 | fi 24 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/install_virtualenv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | for python in /opt/python/*; do 5 | "$python/bin/pip" install virtualenv 6 | done 7 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/install_zlib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | unset RELEASE 5 | 6 | # Get script directory 7 | MY_DIR=$(dirname "${BASH_SOURCE[0]}") 8 | 9 | # Get build utilities 10 | source $MY_DIR/build_utils.sh 11 | source get-static-deps-dir.sh 12 | 13 | ZLIB_SHA256="b3a24de97a8fdbc835b9833169501030b8977031bcb54b3b3ac13740f846ab30" 14 | ZLIB_VERSION="1.2.13" 15 | 16 | fetch_source "zlib-${ZLIB_VERSION}.tar.gz" "https://github.com/madler/zlib/releases/download/v${ZLIB_VERSION}" 17 | check_sha256sum "zlib-${ZLIB_VERSION}.tar.gz" ${ZLIB_SHA256} 18 | tar zxf "zlib-${ZLIB_VERSION}.tar.gz" 19 | 20 | pushd "zlib-${ZLIB_VERSION}" 21 | export CFLAGS="-fPIC" 22 | STATIC_DEPS_PREFIX="$(get_static_deps_dir)" 23 | ./configure --static --prefix="${STATIC_DEPS_PREFIX}" 24 | make -j libz.a 25 | make install 26 | popd 27 | rm -rf "zlib-${ZLIB_VERSION}" 28 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/make-static-deps-dir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | STATIC_DEPS_DIR_STORAGE=.static-deps-path 5 | STATIC_DEPS_DIR="$(mktemp -d '/opt/manylinux-static-deps.XXXXXXXXXX')" 6 | 7 | mkdir -pv "${STATIC_DEPS_DIR}" 8 | echo "${STATIC_DEPS_DIR}" > "${STATIC_DEPS_DIR_STORAGE}" 9 | 10 | >&2 echo 11 | >&2 echo 12 | >&2 echo ================================================================================================== 13 | >&2 echo Created static deps path "'${STATIC_DEPS_DIR}'" and stored it to "'${STATIC_DEPS_DIR_STORAGE}'"... 14 | >&2 echo ================================================================================================== 15 | >&2 echo 16 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/manylinux_mapping.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3.9 2 | """A helper script for producing dual aliased tags.""" 3 | 4 | import platform 5 | import sys 6 | 7 | 8 | ARCH = platform.machine() 9 | 10 | 11 | TAG_ARCH_SEP = '_' 12 | 13 | 14 | ML_LEGACY_TO_MODERN_MAP = { # noqa: WPS407 15 | TAG_ARCH_SEP.join(('manylinux1', ARCH)): 'manylinux_2_5', 16 | TAG_ARCH_SEP.join(('manylinux2010', ARCH)): 'manylinux_2_12', 17 | TAG_ARCH_SEP.join(('manylinux2014', ARCH)): 'manylinux_2_17', 18 | } 19 | 20 | 21 | def to_modern_manylinux_tag(legacy_manylinux_tag): 22 | """Return a modern alias for the tag if it exists.""" 23 | try: 24 | return '_'.join(( 25 | ML_LEGACY_TO_MODERN_MAP[legacy_manylinux_tag], 26 | ARCH, 27 | )) 28 | except KeyError: 29 | return legacy_manylinux_tag 30 | 31 | 32 | def make_aliased_manylinux_tag(manylinux_tag): 33 | """Produce a dual tag if it has a modern alias.""" 34 | modern_tag = to_modern_manylinux_tag(manylinux_tag) 35 | if modern_tag != manylinux_tag: 36 | manylinux_tag = '.'.join((modern_tag, manylinux_tag)) 37 | 38 | return manylinux_tag 39 | 40 | 41 | if __name__ == '__main__': 42 | print(make_aliased_manylinux_tag(sys.argv[1])) # noqa: WPS421 43 | -------------------------------------------------------------------------------- /build-scripts/manylinux-container-image/openssl-version.sh: -------------------------------------------------------------------------------- 1 | export OPENSSL_VERSION="1.1.1k" 2 | export OPENSSL_SHA256="892a0875b9872acd04a9fde79b1f943075d5ea162415de3047c327df33fbaee5" 3 | # We need a base set of flags because on Windows using MSVC 4 | # enable-ec_nistp_64_gcc_128 doesn't work since there's no 128-bit type 5 | export OPENSSL_BUILD_FLAGS_WINDOWS="no-ssl3 no-ssl3-method no-zlib no-shared no-comp no-dynamic-engine" 6 | export OPENSSL_BUILD_FLAGS="${OPENSSL_BUILD_FLAGS_WINDOWS} enable-ec_nistp_64_gcc_128" 7 | -------------------------------------------------------------------------------- /docs/_samples/copy_files_scp.py: -------------------------------------------------------------------------------- 1 | from pylibsshext.errors import LibsshSessionException 2 | from pylibsshext.session import Session 3 | 4 | 5 | ssh = Session() 6 | 7 | HOST = 'CHANGEME' 8 | USER = 'CHANGEME' 9 | PASSWORD = 'CHANGEME' 10 | TIMEOUT = 30 11 | PORT = 22 12 | try: 13 | ssh.connect( 14 | host=HOST, 15 | user=USER, 16 | password=PASSWORD, 17 | timeout=TIMEOUT, 18 | port=PORT, 19 | ) 20 | except LibsshSessionException as ssh_exc: 21 | print(f'Failed to connect to {HOST}:{PORT} over SSH: {ssh_exc!s}') 22 | 23 | print(f'{ssh.is_connected=}') 24 | 25 | if ssh.is_connected: 26 | remote_file = '/etc/hosts' 27 | local_file = '/tmp/hosts' 28 | scp = ssh.scp() 29 | scp.get(remote_file, local_file) 30 | 31 | scp = ssh.scp() 32 | scp.put(local_file, remote_file) 33 | 34 | ssh.close() 35 | -------------------------------------------------------------------------------- /docs/_samples/copy_files_sftp.py: -------------------------------------------------------------------------------- 1 | from pylibsshext.errors import LibsshSessionException 2 | from pylibsshext.session import Session 3 | 4 | 5 | ssh = Session() 6 | 7 | HOST = 'CHANGEME' 8 | USER = 'CHANGEME' 9 | PASSWORD = 'CHANGEME' 10 | TIMEOUT = 30 11 | PORT = 22 12 | try: 13 | ssh.connect( 14 | host=HOST, 15 | user=USER, 16 | password=PASSWORD, 17 | timeout=TIMEOUT, 18 | port=PORT, 19 | ) 20 | except LibsshSessionException as ssh_exc: 21 | print(f'Failed to connect to {HOST}:{PORT} over SSH: {ssh_exc!s}') 22 | 23 | print(f'{ssh.is_connected=}') 24 | 25 | if ssh.is_connected: 26 | remote_file = '/etc/hosts' 27 | local_file = '/tmp/hosts' 28 | sftp = ssh.sftp() 29 | try: 30 | sftp.get(remote_file, local_file) 31 | finally: 32 | sftp.close() 33 | 34 | sftp = ssh.sftp() 35 | try: 36 | sftp.put(remote_file, local_file) 37 | finally: 38 | sftp.close() 39 | 40 | ssh.close() 41 | -------------------------------------------------------------------------------- /docs/_samples/get_version.py: -------------------------------------------------------------------------------- 1 | from pylibsshext import ( 2 | __full_version__, # string with both ansible-pylibssh and libssh versions 3 | ) 4 | from pylibsshext import ( 5 | __libssh_version__, # linked libssh lib version as a string 6 | ) 7 | from pylibsshext import __version__ # ansible-pylibssh version as a string 8 | from pylibsshext import __version_info__ # ansible-pylibssh version as a tuple 9 | 10 | 11 | print(f'{__full_version__=}') 12 | print(f'{__libssh_version__=}') 13 | print(f'{__version__=}') 14 | print(f'{__version_info__=}') 15 | -------------------------------------------------------------------------------- /docs/_samples/gssapi.py: -------------------------------------------------------------------------------- 1 | from pylibsshext.errors import LibsshSessionException 2 | from pylibsshext.session import Session 3 | 4 | 5 | ssh = Session() 6 | 7 | HOST = 'CHANGEME' 8 | USER = 'CHANGEME' 9 | TIMEOUT = 30 10 | PORT = 22 11 | try: 12 | ssh.connect( 13 | host=HOST, 14 | user=USER, 15 | timeout=TIMEOUT, 16 | port=PORT, 17 | # These parameters are not necessary, but can narrow down which token 18 | # should be used to connect, similar to specifying a ssh private key 19 | # gssapi_client_identity="client_principal_name", 20 | # gssapi_server_identity="server_principal_hostname", 21 | ) 22 | except LibsshSessionException as ssh_exc: 23 | print(f'Failed to connect over SSH: {ssh_exc!s}') 24 | 25 | print(f'{ssh.is_connected=}') 26 | 27 | if ssh.is_connected: 28 | chan_shell = ssh.invoke_shell() 29 | try: 30 | chan_shell.sendall(b'ls\n') 31 | data_b = chan_shell.read_bulk_response(timeout=2, retry=10) 32 | print(data_b.decode()) 33 | finally: 34 | chan_shell.close() 35 | 36 | ssh.close() 37 | -------------------------------------------------------------------------------- /docs/_samples/shell.py: -------------------------------------------------------------------------------- 1 | from pylibsshext.errors import LibsshSessionException 2 | from pylibsshext.session import Session 3 | 4 | 5 | ssh = Session() 6 | 7 | HOST = 'CHANGEME' 8 | USER = 'CHANGEME' 9 | PASSWORD = 'CHANGEME' 10 | TIMEOUT = 30 11 | PORT = 22 12 | try: 13 | ssh.connect( 14 | host=HOST, 15 | user=USER, 16 | password=PASSWORD, 17 | timeout=TIMEOUT, 18 | port=PORT, 19 | ) 20 | except LibsshSessionException as ssh_exc: 21 | print(f'Failed to connect to {HOST}:{PORT} over SSH: {ssh_exc!s}') 22 | 23 | print(f'{ssh.is_connected=}') 24 | 25 | if ssh.is_connected: 26 | ssh_channel = ssh.new_channel() 27 | try: 28 | cmd_resp = ssh_channel.exec_command(b'ls') 29 | print(f'stdout:\n{cmd_resp.stdout}\n') 30 | print(f'stderr:\n{cmd_resp.stderr}\n') 31 | print(f'return code: {cmd_resp.returncode}\n') 32 | finally: 33 | ssh_channel.close() 34 | 35 | chan_shell = ssh.invoke_shell() 36 | try: 37 | chan_shell.sendall(b'ls\n') 38 | data_b = chan_shell.read_bulk_response(timeout=2, retry=10) 39 | print(data_b.decode()) 40 | finally: 41 | chan_shell.close() 42 | 43 | ssh.close() 44 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/pylibssh/cd2970fc2a3d67ffa87d2fde45f2d033700d3c50/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/_templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansible/pylibssh/cd2970fc2a3d67ffa87d2fde45f2d033700d3c50/docs/_templates/.gitkeep -------------------------------------------------------------------------------- /docs/changelog-fragments/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !template.j2 3 | !.gitignore 4 | !README.rst 5 | !*.bugfix 6 | !*.bugfix.rst 7 | !*.bugfix.*.rst 8 | !*.breaking 9 | !*.breaking.rst 10 | !*.breaking.*.rst 11 | !*.contrib 12 | !*.contrib.rst 13 | !*.contrib.*.rst 14 | !*.deprecation 15 | !*.deprecation.rst 16 | !*.deprecation.*.rst 17 | !*.doc 18 | !*.doc.rst 19 | !*.doc.*.rst 20 | !*.feature 21 | !*.feature.rst 22 | !*.feature.*.rst 23 | !*.misc 24 | !*.misc.rst 25 | !*.misc.*.rst 26 | !*.packaging 27 | !*.packaging.rst 28 | !*.packaging.*.rst 29 | -------------------------------------------------------------------------------- /docs/changelog-fragments/1dfbf70fdfd99ae75068fdb3630790c96101a96a.contrib.rst: -------------------------------------------------------------------------------- 1 | The manylinux build scripts have been adjusted to resolve the 2 | dependency conflict between certain ``packaging`` and ``setuptools`` 3 | versions -- by :user:`webknjaz`. 4 | 5 | Previously, this was making some of the CI jobs crash with a traceback 6 | when building said wheels. 7 | -------------------------------------------------------------------------------- /docs/changelog-fragments/532.breaking.rst: -------------------------------------------------------------------------------- 1 | Dropped support for Python 3.6, 3.7 and 3.8 2 | -- by :user:`Qalthos` and :user:`webknjaz`. 3 | -------------------------------------------------------------------------------- /docs/changelog-fragments/562.breaking.rst: -------------------------------------------------------------------------------- 1 | PyPI no longer ships year-versioned manylinux wheels. One may 2 | have to update their version of pip to pick up the new ones. 3 | 4 | -- by :user:`webknjaz` 5 | -------------------------------------------------------------------------------- /docs/changelog-fragments/562.contrib.rst: -------------------------------------------------------------------------------- 1 | Manylinux wheels are no longer built using custom shell scripts. 2 | Instead, this is delegated to the ``cibuildwheel`` tool. 3 | 4 | -- by :user:`webknjaz` 5 | -------------------------------------------------------------------------------- /docs/changelog-fragments/562.packaging.rst: -------------------------------------------------------------------------------- 1 | PyPI now only ships :pep:`660`-compatible manylinux wheels 2 | -- by :user:`webknjaz`. 3 | -------------------------------------------------------------------------------- /docs/changelog-fragments/636.contrib.rst: -------------------------------------------------------------------------------- 1 | Updated the version of ``libssh`` to the latest release v0.11.1 2 | in the cached ``manylinux`` build environment container images 3 | -- by :user:`Jakuje`. 4 | -------------------------------------------------------------------------------- /docs/changelog-fragments/638.bugfix.rst: -------------------------------------------------------------------------------- 1 | Fixed reading files over SFTP that go over the pre-defined chunk size. 2 | 3 | Prior to this change, the files could end up being corrupted, ending up with the last read chunk written to the file instead of the entire payload. 4 | 5 | -- by :user:`Jakuje` 6 | -------------------------------------------------------------------------------- /docs/changelog-fragments/640.doc.rst: -------------------------------------------------------------------------------- 1 | Added a :ref:`Communication ` section to the main 2 | documentation page -- by :user:`Andersson007`. 3 | -------------------------------------------------------------------------------- /docs/changelog-fragments/646.doc.rst: -------------------------------------------------------------------------------- 1 | Fixed the argument order in the ``scp.put()`` usage example 2 | -- by :user:`kucharskim`. 3 | -------------------------------------------------------------------------------- /docs/changelog-fragments/658.bugfix.rst: -------------------------------------------------------------------------------- 1 | Repetitive calls to ``exec_channel()`` no longer crash and return reliable output -- by :user:`Jakuje`. 2 | -------------------------------------------------------------------------------- /docs/changelog-fragments/658.packaging.rst: -------------------------------------------------------------------------------- 1 | The ``pytest-forked`` dependency of build, development and test environments was removed -- by :user:`Jakuje`. 2 | -------------------------------------------------------------------------------- /docs/changelog-fragments/661.bugfix.rst: -------------------------------------------------------------------------------- 1 | Uploading large files over SCP no longer fails -- by :user:`Jakuje`. 2 | -------------------------------------------------------------------------------- /docs/changelog-fragments/664.bugfix.rst: -------------------------------------------------------------------------------- 1 | Improved performance of SFTP transfers by using larger transfer chunks -- by :user:`Jakuje`. 2 | -------------------------------------------------------------------------------- /docs/changelog-fragments/675.feature.rst: -------------------------------------------------------------------------------- 1 | The underlying ``SSH_OPTIONS_KEY_EXCHANGE`` option of ``libssh`` is 2 | now available as ``key_exchange_algorithms`` -- by :user:`NilashishC`. 3 | -------------------------------------------------------------------------------- /docs/changelog-fragments/676.contrib.rst: -------------------------------------------------------------------------------- 1 | All the uses of ``actions/upload-artifact@v3`` and 2 | ``actions/download-artifact@v3`` have been updated to use 3 | ``v4``. This also includes bumping 4 | ``re-actors/checkout-python-sdist`` to ``release/v2`` as it 5 | uses ``actions/download-artifact`` internally. 6 | 7 | -- by :user:`NilashishC` and :user:`webknjaz` 8 | -------------------------------------------------------------------------------- /docs/changelog-fragments/688.contrib.rst: -------------------------------------------------------------------------------- 1 | The CI is now configured to use 2 | :external+tox:std:ref:`tox-run---installpkg` when testing 3 | pre-built dists. This replaces the previously existing 4 | tox-level hacks in ``test-binary-dists`` and 5 | ``test-source-dists`` environments that have now been 6 | removed. 7 | 8 | -- by :user:`webknjaz` 9 | -------------------------------------------------------------------------------- /docs/changelog-fragments/692.contrib.rst: -------------------------------------------------------------------------------- 1 | The wheel building workflows have been updated to set the 2 | OCI image platform identifiers to legal values like 3 | ``linux/arm64``. 4 | 5 | -- by :user:`webknjaz` 6 | -------------------------------------------------------------------------------- /docs/changelog-fragments/692.packaging.rst: -------------------------------------------------------------------------------- 1 | The wheels are now built in cached container images with a 2 | correctly set platform identifier. 3 | 4 | -- by :user:`webknjaz` 5 | -------------------------------------------------------------------------------- /docs/changelog-fragments/706.contrib.rst: -------------------------------------------------------------------------------- 1 | The CI is now configured to always set job timeout values. 2 | This will ensure that the jobs that get stuck don't consume 3 | all 6 hours just hanging, improving responsiveness and the 4 | overall CI/CD resource usage. 5 | 6 | -- by :user:`webknjaz` 7 | -------------------------------------------------------------------------------- /docs/changelog-fragments/707.contrib.rst: -------------------------------------------------------------------------------- 1 | The linting is now configured to check schemas of the 2 | Read The Docs configuration file and the GitHub Actions 3 | CI/CD workflow files in addition to enforcing timeouts. 4 | 5 | -- by :user:`webknjaz` 6 | -------------------------------------------------------------------------------- /docs/changelog-fragments/708.deprecation.rst: -------------------------------------------------------------------------------- 1 | The project stopped being tested under Ubuntu 20.04 VM since 2 | GitHub has sunset their CI images -- by :user:`webknjaz`. 3 | -------------------------------------------------------------------------------- /docs/changelog-fragments/709.bugfix.rst: -------------------------------------------------------------------------------- 1 | Fixed crash when more operations were called after ``session.close()`` -- by :user:`Jakuje`. 2 | -------------------------------------------------------------------------------- /docs/changelog-fragments/713.contrib.rst: -------------------------------------------------------------------------------- 1 | The ``multiarch/qemu-user-static`` image got replaced with 2 | ``tonistiigi/binfmt`` because the latter is no longer 3 | maintained and the former includes the fixed version of QEMU. 4 | 5 | -- by :user:`webknjaz` 6 | -------------------------------------------------------------------------------- /docs/changelog-fragments/713.packaging.rst: -------------------------------------------------------------------------------- 1 | The ``manylinux`` build scripts now limit ``cmake`` below 2 | version 4 -- by :user:`webknjaz`. 3 | -------------------------------------------------------------------------------- /docs/changelog-fragments/714.packaging.rst: -------------------------------------------------------------------------------- 1 | Stopped skipping SCP tests in the RPM spec -- by :user:`Jakuje`. 2 | -------------------------------------------------------------------------------- /docs/changelog-fragments/715.contrib.rst: -------------------------------------------------------------------------------- 1 | Added Fedora 41 and 42 to CI configuration -- by :user:`Jakuje`. 2 | -------------------------------------------------------------------------------- /docs/changelog-fragments/716.contrib.rst: -------------------------------------------------------------------------------- 1 | Removed needless step from CI adjusting centos8 repositories -- by :user:`Jakuje`. 2 | -------------------------------------------------------------------------------- /docs/changelog-fragments/718.breaking.rst: -------------------------------------------------------------------------------- 1 | 532.breaking.rst -------------------------------------------------------------------------------- /docs/changelog-fragments/730.contrib.rst: -------------------------------------------------------------------------------- 1 | The CI/CD infrastructure no longer pre-builds custom manylinux images 2 | for building wheel targeting ``manylinux1``, ``manylinux2010`` and 3 | ``manylinux2014`` tags. 4 | 5 | -- by :user:`webknjaz` 6 | -------------------------------------------------------------------------------- /docs/changelog-fragments/731.contrib.rst: -------------------------------------------------------------------------------- 1 | The host OS is now ARM-based when building ``manylinux_*_*_aarch64`` 2 | images for CI/CD -- by :user:`webknjaz`. 3 | -------------------------------------------------------------------------------- /docs/changelog-fragments/732.contrib.rst: -------------------------------------------------------------------------------- 1 | False negative warnings reported by ``coveragepy`` when are now 2 | disabled. They are evident when ``pytest-cov`` runs with the 3 | ``pytest-xdist`` integration. ``pytest`` 8.4 gives them more 4 | visibility and out ``filterwarnings = error`` setting was turning 5 | them into errors before this change. 6 | 7 | -- by :user:`webknjaz` 8 | -------------------------------------------------------------------------------- /docs/changelog-fragments/733.contrib.rst: -------------------------------------------------------------------------------- 1 | GitHub Actions CI/CD no longer runs jobs that install source 2 | distributions into the tox environments for testing 3 | -- by :user:`webknjaz`. 4 | 5 | This is a temporary workaround for an upstream bug in tox and 6 | said jobs are non-essential. 7 | -------------------------------------------------------------------------------- /docs/changelog-fragments/734.contrib.rst: -------------------------------------------------------------------------------- 1 | Updated the pre-built ``libffi`` version to 3.4.8 in the 2 | cached ``manylinux`` build environment container images 3 | -- by :user:`Jakuje`. 4 | -------------------------------------------------------------------------------- /docs/changelog-fragments/735.packaging.rst: -------------------------------------------------------------------------------- 1 | Started bundling a copy of libssh 0.11.1 in platform-specific 2 | wheels published on PyPI -- by :user:`Jakuje`. 3 | -------------------------------------------------------------------------------- /docs/changelog-fragments/README.rst: -------------------------------------------------------------------------------- 1 | --------------------------------- 2 | Adding change notes with your PRs 3 | --------------------------------- 4 | 5 | It is very important to maintain a log for news of how 6 | updating to the new version of the software will affect 7 | end-users. This is why we enforce collection of the change 8 | fragment files in pull requests as per `Towncrier philosophy`_. 9 | 10 | The idea is that when somebody makes a change, they must record 11 | the bits that would affect end-users only including information 12 | that would be useful to them. Then, when the maintainers publish 13 | a new release, they'll automatically use these records to compose 14 | a change log for the respective version. It is important to 15 | understand that including unnecessary low-level implementation 16 | related details generates noise that is not particularly useful 17 | to the end-users most of the time. And so such details should be 18 | recorded in the Git history rather than a changelog. 19 | 20 | ----------------------------------------- 21 | Alright! So how do I add a news fragment? 22 | ----------------------------------------- 23 | 24 | To submit a change note about your PR, add a text file into the 25 | ``docs/changelog-fragments/`` folder. It should contain an 26 | explanation of what applying this PR will change in the way 27 | end-users interact with the project. One sentence is usually 28 | enough but feel free to add as many details as you feel necessary 29 | for the users to understand what it means. 30 | 31 | **Use the past tense** for the text in your fragment because, 32 | combined with others, it will be a part of the "news digest" 33 | telling the readers **what changed** in a specific version of 34 | the library *since the previous version*. You should also use 35 | *reStructuredText* syntax for highlighting code (inline or block), 36 | linking parts of the docs or external sites. 37 | However, you do not need to reference the issue or PR numbers here 38 | as *towncrier* will automatically add a reference to all of the 39 | affected issues when rendering the news file. 40 | If you wish to sign your change, feel free to add ``-- by 41 | :user:`github-username``` at the end (replace ``github-username`` 42 | with your own!). 43 | 44 | Finally, name your file following the convention that Towncrier 45 | understands: it should start with the number of an issue or a 46 | PR followed by a dot, then add a patch type, like ``feature``, 47 | ``doc``, ``contrib`` etc., and add ``.rst`` as a suffix. If you 48 | need to add more than one fragment, you may add an optional 49 | sequence number (delimited with another period) between the type 50 | and the suffix. 51 | 52 | In general the name will follow ``..rst`` pattern, 53 | where the categories are: 54 | 55 | - ``bugfix``: A bug fix for something we deemed an improper undesired 56 | behavior that got corrected in the release to match pre-agreed 57 | expectations. 58 | - ``feature``: A new behavior, public APIs. That sort of stuff. 59 | - ``deprecation``: A declaration of future API removals and breaking 60 | changes in behavior. 61 | - ``breaking``: When something public gets removed in a breaking way. 62 | Could be deprecated in an earlier release. 63 | - ``doc``: Notable updates to the documentation structure or build 64 | process. 65 | - ``packaging``: Notes for downstreams about unobvious side effects 66 | and tooling. Changes in the test invocation considerations and 67 | runtime assumptions. 68 | - ``contrib``: Stuff that affects the contributor experience. e.g. 69 | Running tests, building the docs, setting up the development 70 | environment. 71 | - ``misc``: Changes that are hard to assign to any of the above 72 | categories. 73 | 74 | A pull request may have more than one of these components, for example 75 | a code change may introduce a new feature that deprecates an old 76 | feature, in which case two fragments should be added. It is not 77 | necessary to make a separate documentation fragment for documentation 78 | changes accompanying the relevant code changes. 79 | 80 | ----------------------------------------------------------- 81 | Examples for changelog entries adding to your Pull Requests 82 | ----------------------------------------------------------- 83 | 84 | File ``docs/changelog-fragments/112.doc.rst`` (could be symlinked to 85 | ``docs/changelog-fragments/112.contrib.rst`` so it shows up in several 86 | changelog sections): 87 | 88 | .. code-block:: rst 89 | 90 | Added a ``:user:`` role to Sphinx config -- by :user:`webknjaz`. 91 | 92 | This change allows the contributors to be credited for their submitted 93 | patches in a more prominent manner. 94 | 95 | File ``docs/changelog-fragments/105.feature.rst``: 96 | 97 | .. code-block:: rst 98 | 99 | Added the support for keyboard-authentication method -- by :user:`Qalthos`. 100 | 101 | File ``docs/changelog-fragments/57.bugfix.rst``: 102 | 103 | .. code-block:: rst 104 | 105 | Fixed flaky SEGFAULTs in ``pylibsshext.channel.Channel.exec_command()`` 106 | calls -- by :user:`ganeshrn`. 107 | 108 | .. tip:: 109 | 110 | See ``pyproject.toml`` for all available categories 111 | (``tool.towncrier.type``). 112 | 113 | .. _Towncrier philosophy: 114 | https://towncrier.readthedocs.io/en/stable/#philosophy 115 | -------------------------------------------------------------------------------- /docs/changelog-fragments/ea34887831a0c6547b32cd8c6a035bb77b91e771.contrib.rst: -------------------------------------------------------------------------------- 1 | The Git archives are now immutable per the packaging recommendations. 2 | This allows downstreams safely use GitHub archive URLs when 3 | re-packaging -- by :user:`webknjaz`. 4 | -------------------------------------------------------------------------------- /docs/changelog-fragments/template.j2: -------------------------------------------------------------------------------- 1 | {# TOWNCRIER TEMPLATE #} 2 | 3 | *({{ versiondata.date }})* 4 | 5 | {% for section, _ in sections.items() %} 6 | {% set underline = underlines[0] %}{% if section %}{{section}} 7 | {{ underline * section|length }}{% set underline = underlines[1] %} 8 | 9 | {% endif %} 10 | 11 | {% if sections[section] %} 12 | {% for category, val in definitions.items() if category in sections[section]%} 13 | {{ definitions[category]['name'] }} 14 | {{ underline * definitions[category]['name']|length }} 15 | 16 | {% if definitions[category]['showcontent'] %} 17 | {% for text, change_note_refs in sections[section][category].items() %} 18 | - {{ text }} 19 | 20 | {{- '\n' * 2 -}} 21 | 22 | {#- 23 | NOTE: Replacing 'e' with 'f' is a hack that prevents Jinja's `int` 24 | NOTE: filter internal implementation from treating the input as an 25 | NOTE: infinite float when it looks like a scientific notation (with a 26 | NOTE: single 'e' char in between digits), raising an `OverflowError`, 27 | NOTE: subsequently. 'f' is still a hex letter so it won't affect the 28 | NOTE: check for whether it's a (short or long) commit hash or not. 29 | Ref: https://github.com/pallets/jinja/issues/1921 30 | -#} 31 | {%- 32 | set pr_issue_numbers = change_note_refs 33 | | map('lower') 34 | | map('replace', 'e', 'f') 35 | | map('int', default=None) 36 | | select('integer') 37 | | map('string') 38 | | list 39 | -%} 40 | {%- set arbitrary_refs = [] -%} 41 | {%- set commit_refs = [] -%} 42 | {%- with -%} 43 | {%- set commit_ref_candidates = change_note_refs | reject('in', pr_issue_numbers) -%} 44 | {%- for cf in commit_ref_candidates -%} 45 | {%- if cf | length in (7, 8, 40) and cf | int(default=None, base=16) is not none -%} 46 | {%- set _ = commit_refs.append(cf) -%} 47 | {%- else -%} 48 | {%- set _ = arbitrary_refs.append(cf) -%} 49 | {%- endif -%} 50 | {%- endfor -%} 51 | {%- endwith -%} 52 | 53 | {% if pr_issue_numbers %} 54 | *Related issues and pull requests on GitHub:* 55 | :issue:`{{ pr_issue_numbers | join('`, :issue:`') }}`. 56 | {{- '\n' * 2 -}} 57 | {%- endif -%} 58 | 59 | {% if commit_refs %} 60 | *Related commits on GitHub:* 61 | :commit:`{{ commit_refs | join('`, :commit:`') }}`. 62 | {{- '\n' * 2 -}} 63 | {%- endif -%} 64 | 65 | {% if arbitrary_refs %} 66 | *Unlinked references:* 67 | {{ arbitrary_refs | join(', ') }}. 68 | {{- '\n' * 2 -}} 69 | {%- endif -%} 70 | 71 | {% endfor %} 72 | {% else %} 73 | - {{ sections[section][category]['']|join(', ') }} 74 | 75 | {% endif %} 76 | {% if sections[section][category]|length == 0 %} 77 | No significant changes. 78 | 79 | {% else %} 80 | {% endif %} 81 | 82 | {% endfor %} 83 | {% else %} 84 | No significant changes. 85 | 86 | 87 | {% endif %} 88 | {% endfor %} 89 | ---- 90 | {{ '\n' * 2 }} 91 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Versions follow `Semantic Versioning`_ (``..``). 6 | Backward incompatible (breaking) changes will only be introduced in major 7 | versions with advance notice in the **Deprecations** section of releases. 8 | 9 | .. _Semantic Versioning: https://semver.org/ 10 | 11 | .. only:: not is_release 12 | 13 | To be included in v\ |release| (if present) 14 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 15 | 16 | .. towncrier-draft-entries:: |release| [UNRELEASED DRAFT] 17 | 18 | Released versions 19 | ^^^^^^^^^^^^^^^^^ 20 | 21 | .. include:: ../CHANGELOG.rst 22 | :start-after: .. towncrier release notes start 23 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | # Requires Python 3.8+ 3 | # Ref: https://www.sphinx-doc.org/en/master/usage/configuration.html 4 | """Configuration for the Sphinx documentation generator.""" 5 | 6 | import os 7 | from functools import partial 8 | from pathlib import Path 9 | 10 | from setuptools_scm import get_version 11 | 12 | 13 | # -- Path setup -------------------------------------------------------------- 14 | 15 | PROJECT_ROOT_DIR = Path(__file__).parents[1].resolve() 16 | IS_RELEASE_ON_RTD = ( 17 | os.getenv('READTHEDOCS', 'False') == 'True' 18 | and os.environ['READTHEDOCS_VERSION_TYPE'] == 'tag' 19 | ) 20 | if IS_RELEASE_ON_RTD: 21 | tags.add('is_release') # noqa: F821 22 | 23 | get_scm_version = partial(get_version, root=PROJECT_ROOT_DIR) 24 | 25 | # If extensions (or modules to document with autodoc) are in another directory, 26 | # add these directories to sys.path here. If the directory is relative to the 27 | # documentation root, use os.path.abspath to make it absolute. 28 | 29 | 30 | # -- Project information ----------------------------------------------------- 31 | 32 | github_url = 'https://github.com' 33 | github_repo_org = 'ansible' 34 | github_repo_name = 'pylibssh' 35 | github_repo_slug = f'{github_repo_org}/{github_repo_name}' 36 | github_repo_url = f'{github_url}/{github_repo_slug}' 37 | github_sponsors_url = f'{github_url}/sponsors' 38 | 39 | project = f'{github_repo_org}-{github_repo_name}' 40 | author = 'Ansible, Inc.' 41 | copyright = f'2020, {author}' # noqa: WPS125 42 | 43 | # The short X.Y version 44 | version = '.'.join( 45 | get_scm_version( 46 | local_scheme='no-local-version', 47 | ).split('.')[:3], 48 | ) 49 | 50 | # The full version, including alpha/beta/rc tags 51 | release = get_scm_version() 52 | 53 | rst_epilog = f""" 54 | .. |project| replace:: {project} 55 | """ # pylint: disable=invalid-name 56 | 57 | 58 | # -- General configuration --------------------------------------------------- 59 | 60 | # Ref: python-attrs/attrs#571 61 | default_role = 'any' 62 | 63 | nitpicky = True 64 | nitpick_ignore = [ 65 | ('envvar', 'PATH'), 66 | ('envvar', 'TMPDIR'), 67 | ] 68 | 69 | # Add any Sphinx extension module names here, as strings. They can be 70 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 71 | # ones. 72 | extensions = [ 73 | 'sphinx.ext.autodoc', 74 | 'sphinx.ext.autosectionlabel', # autocreate section targets for refs 75 | 'sphinx.ext.doctest', 76 | 'sphinx.ext.extlinks', 77 | 'sphinx.ext.intersphinx', 78 | 'sphinx.ext.todo', 79 | 'sphinx.ext.coverage', 80 | 'sphinx.ext.viewcode', 81 | # 'sphinxcontrib.apidoc', 82 | 'sphinxcontrib.towncrier', # provides `towncrier-draft-entries` directive 83 | 'myst_parser', # extended markdown; https://pypi.org/project/myst-parser/ 84 | ] 85 | 86 | # Add any paths that contain templates here, relative to this directory. 87 | templates_path = ['_templates'] 88 | 89 | # The language for content autogenerated by Sphinx. Refer to documentation 90 | # for a list of supported languages. 91 | # 92 | # This is also used if you do content translation via gettext catalogs. 93 | # Usually you set "language" from the command line for these cases. 94 | language = 'en' 95 | 96 | # List of patterns, relative to source directory, that match files and 97 | # directories to ignore when looking for source files. 98 | # This pattern also affects html_static_path and html_extra_path. 99 | exclude_patterns = [ 100 | 'changelog-fragments/**', # Towncrier-managed change notes 101 | ] 102 | 103 | 104 | # -- Options for HTML output ------------------------------------------------- 105 | 106 | # The theme to use for HTML and HTML Help pages. See the documentation for 107 | # a list of builtin themes. 108 | html_theme = 'sphinx_ansible_theme' 109 | 110 | html_theme_options = { 111 | 'collapse_navigation': False, 112 | 'analytics_id': '', 113 | 'style_nav_header_background': '#5bbdbf', 114 | 'style_external_links': True, 115 | 'canonical_url': 'https://ansible-pylibssh.readthedocs.io/en/latest/', 116 | 'vcs_pageview_mode': 'edit', 117 | 'navigation_depth': 3, 118 | } 119 | 120 | html_context = { 121 | 'display_github': True, 122 | 'github_user': 'ansible', 123 | 'github_repo': 'pylibssh', 124 | 'github_version': 'devel/docs/', 125 | 'current_version': version, 126 | 'latest_version': 'latest', 127 | 'available_versions': ('latest',), 128 | 'css_files': [], # https://github.com/sphinx-doc/sphinx/issues/8889 129 | } 130 | 131 | # Add any paths that contain custom static files (such as style sheets) here, 132 | # relative to this directory. They are copied after the builtin static files, 133 | # so a file named "default.css" will overwrite the builtin "default.css". 134 | html_static_path = ['_static'] 135 | 136 | master_doc = 'index' 137 | 138 | 139 | # -- Extension configuration ------------------------------------------------- 140 | 141 | # -- Options for extlinks extension --------------------------------------- 142 | extlinks = { 143 | 'issue': (f'{github_repo_url}/issues/%s', '#%s'), # noqa: WPS323 144 | 'pr': (f'{github_repo_url}/pull/%s', 'PR #%s'), # noqa: WPS323 145 | 'commit': (f'{github_repo_url}/commit/%s', '%s'), # noqa: WPS323 146 | 'gh': (f'{github_url}/%s', 'GitHub: %s'), # noqa: WPS323 147 | 'user': (f'{github_sponsors_url}/%s', '@%s'), # noqa: WPS323 148 | } 149 | 150 | # -- Options for intersphinx extension --------------------------------------- 151 | 152 | # Example configuration for intersphinx: refer to the Python standard library. 153 | intersphinx_mapping = { 154 | 'cython': ('https://cython.readthedocs.io/en/latest', None), 155 | 'packaging': ('https://packaging.python.org', None), 156 | 'pip': ('https://pip.pypa.io/en/latest', None), 157 | 'python': ('https://docs.python.org/3', None), 158 | 'python2': ('https://docs.python.org/2', None), 159 | 'tox': ('https://tox.wiki/en/latest', None), 160 | } 161 | 162 | # -- Options for todo extension ---------------------------------------------- 163 | 164 | # If true, `todo` and `todoList` produce output, else they produce nothing. 165 | todo_include_todos = True 166 | 167 | # -- Options for sphinx_tabs extension --------------------------------------- 168 | 169 | # Ref: 170 | # * https://github.com/djungelorm/sphinx-tabs/issues/26#issuecomment-422160463 171 | sphinx_tabs_valid_builders = ['linkcheck'] # prevent linkcheck warning 172 | 173 | # -- Options for linkcheck builder ------------------------------------------- 174 | 175 | linkcheck_ignore = [ 176 | r'http://localhost:\d+/', # local URLs 177 | r'https://codecov\.io/gh/ansible/pylibssh/branch/devel/graph/badge\.svg', 178 | r'https://github\.com/ansible/pylibssh/actions', # 404 if no auth 179 | 180 | # Too many links to GitHub so they cause "429 Client Error: 181 | # too many requests for url" 182 | # Ref: https://github.com/sphinx-doc/sphinx/issues/7388 183 | r'https://github\.com/ansible/pylibssh/issues', 184 | r'https://github\.com/ansible/pylibssh/pull', 185 | r'https://github\.com/ansible/ansible/issues', 186 | r'https://github\.com/ansible/ansible/pull', 187 | 188 | # Requires a more liberal 'Accept: ' HTTP request header: 189 | # Ref: https://github.com/sphinx-doc/sphinx/issues/7247 190 | r'https://github\.com/ansible/pylibssh/workflows/[^/]+/badge\.svg', 191 | ] 192 | linkcheck_workers = 25 193 | 194 | # -- Options for sphinx.ext.autosectionlabel extension ----------------------- 195 | 196 | # Ref: 197 | # https://www.sphinx-doc.org/en/master/usage/extensions/autosectionlabel.html 198 | autosectionlabel_maxdepth = 2 # mitigate Towncrier nested subtitles collision 199 | 200 | # -- Options for towncrier_draft extension ----------------------------------- 201 | 202 | towncrier_draft_autoversion_mode = 'draft' # or: 'sphinx-version', 'sphinx-release' 203 | towncrier_draft_include_empty = True 204 | towncrier_draft_working_directory = PROJECT_ROOT_DIR 205 | towncrier_draft_config_path = 'pyproject.toml' # relative to cwd 206 | -------------------------------------------------------------------------------- /docs/contributing/code_of_conduct.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../.github/CODE_OF_CONDUCT.rst 2 | -------------------------------------------------------------------------------- /docs/contributing/guidelines.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../.github/CONTRIBUTING.rst 2 | 3 | .. seealso:: 4 | 5 | :ref:`Testing Guide` 6 | Running the tests suite locally 7 | 8 | ----------------- 9 | Contributing docs 10 | ----------------- 11 | 12 | We use Sphinx_ to generate our docs website. You can trigger 13 | the process locally by executing: 14 | 15 | .. code-block:: shell-session 16 | 17 | $ tox -e build-docs 18 | 19 | It is also integrated with `Read The Docs`_ that builds and 20 | publishes each commit to the main branch and generates live 21 | docs previews for each pull request. 22 | 23 | The sources of the Sphinx_ documents use reStructuredText as a 24 | de-facto standard. But in order to make contributing docs more 25 | beginner-friendly, we've integrated `MyST parser`_ allowing us 26 | to also accept new documents written in an extended version of 27 | Markdown that supports using Sphinx directives and roles. `Read 28 | the docs `_ to learn more on how to use it. 29 | 30 | .. _MyST docs: https://myst-parser.readthedocs.io/en/latest/using/intro.html#writing-myst-in-sphinx 31 | .. _MyST parser: https://pypi.org/project/myst-parser/ 32 | .. _Read The Docs: https://readthedocs.org 33 | .. _Sphinx: https://www.sphinx-doc.org 34 | 35 | .. include:: ../changelog-fragments/README.rst 36 | -------------------------------------------------------------------------------- /docs/contributing/release_guide.rst: -------------------------------------------------------------------------------- 1 | ************* 2 | Release Guide 3 | ************* 4 | 5 | Welcome to the |project| Release Guide! 6 | 7 | This page contains information on how to release a new version 8 | of |project| using the automated Continuous Delivery pipeline. 9 | 10 | .. tip:: 11 | 12 | The intended audience for this document is maintainers 13 | and core contributors. 14 | 15 | 16 | Pre-release activities 17 | ====================== 18 | 19 | 1. Check if there are any open Pull Requests that could be 20 | desired in the upcoming release. If there are any — merge 21 | them. If some are incomplete, try to get them ready. 22 | Don't forget to review the enclosed change notes per our 23 | guidelines. 24 | 2. Visually inspect the draft section of the :ref:`Changelog` 25 | page. Make sure the content looks consistent, uses the same 26 | writing style, targets the end-users and adheres to our 27 | documented guidelines. 28 | Most of the changelog sections will typically use the past 29 | tense or another way to relay the effect of the changes for 30 | the users, since the previous release. 31 | It should not target core contributors as the information 32 | they are normally interested in is already present in the 33 | Git history. 34 | Update the changelog fragments if you see any problems with 35 | this changelog section. 36 | 3. Optionally, test the previously published nightlies, that are 37 | available through :ref:`Continuous delivery`, locally. 38 | 4. If you are satisfied with the above, inspect the changelog 39 | section categories in the draft. Presence of the breaking 40 | changes or features will hint you what version number 41 | segment to bump for the release. 42 | 43 | .. seealso:: 44 | 45 | :ref:`Adding change notes with your PRs` 46 | Writing beautiful changelogs for humans 47 | 48 | 49 | The release stage 50 | ================= 51 | 52 | 1. Open the `GitHub Actions CI/CD workflow page `_ in your web browser. 54 | 2. Click the gray button :guilabel:`Run workflow` in the blue 55 | banner at the top of the workflow runs list. 56 | 3. In the form that appears, enter the version you decided on 57 | in the preparation steps, into the mandatory field. Do not 58 | prepend a leading-``v``. Just use the raw version number as 59 | per :pep:`440`. 60 | 4. Now, click the green button :guilabel:`Run workflow`. 61 | 5. At some point, the workflow gets to the job for publishing 62 | to the "production" PyPI and pauses there. You will see a 63 | banner informing you that a deployment approval is needed. 64 | You will also get an email notification with the same 65 | information and a link to the deployment approval view. 66 | 6. While the normal PyPI upload hasn't happened yet, the 67 | TestPyPI one proceeds. This gives you a chance to optionally 68 | verify what got published there and decide if you want to 69 | abort the process. 70 | 7. Approve the deployment and wait for the workflow to complete. 71 | 8. Verify that the following things got created: 72 | 73 | - a PyPI release 74 | - a Git tag 75 | - a GitHub Releases page 76 | - a GitHub Discussions page 77 | - a GitHub pull request 78 | 79 | 9. Merge the release pull request containing the changelog 80 | update patch. Use the natural/native merge strategy. 81 | 82 | .. danger:: 83 | 84 | **Do not** use squash or rebase. The ``release/vNUMBER`` 85 | branch contains a tagged commit. That commit must become 86 | a part of the repository's default branch. Failing to 87 | follow this instruction will result in ``setuptools-scm`` 88 | getting confused and generating the intermediate commit 89 | versions incorrectly. 90 | 91 | .. tip:: 92 | 93 | If you used a YOLO mode when triggering the release 94 | automation, the branch protection rules may prevent you 95 | from being able to click the merge button. In such a case 96 | it is okay to *temporarily* deselect the :guilabel:`Do not 97 | allow bypassing the above settings` setting in the branch 98 | protection configuration, click the merge button, with an 99 | administrator override and re-check it immediately. 100 | 101 | 10. Tell everyone you released a new version of |project| :) 102 | 103 | 104 | .. _GitHub Actions CI/CD workflow: 105 | https://github.com/ansible/pylibssh/actions/workflows/ci-cd.yml 106 | -------------------------------------------------------------------------------- /docs/contributing/security.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../.github/SECURITY.rst 2 | -------------------------------------------------------------------------------- /docs/contributing/testing_guide.rst: -------------------------------------------------------------------------------- 1 | ************* 2 | Testing Guide 3 | ************* 4 | 5 | Welcome to the |project| Testing Guide! 6 | 7 | This page contains information on how to test |project| 8 | locally as well as some notes of how the automated testing and linting is implemented. 9 | 10 | 11 | Mandatory tooling 12 | ================= 13 | 14 | All build and test processes use tox_-centric workflow. So 15 | first of all, let's install it: 16 | 17 | .. code-block:: shell-session 18 | 19 | $ python -m pip install 'tox >= 3.19.0' --user 20 | 21 | .. note:: 22 | 23 | This will install tox_ in :doc:`user-global 24 | site-packages `. To make it 25 | discoverable, you may need to add :command:`export 26 | PATH="$HOME/.local/bin:$PATH"` to your :file:`~/.bashrc` 27 | or :file:`~/.zshrc`. 28 | 29 | The examples below will use the :py:mod:`python:runpy` 30 | syntax (CLI option :option:`python:-m`) to avoid the 31 | need to put scripts into the search :envvar:`PATH`. 32 | 33 | .. tip:: 34 | 35 | While the example above uses pip, alternatively you may 36 | install tox_ via your OS package manager (e.g. 37 | :program:`apt`, :program:`dnf`, :program:`emerge`, 38 | :program:`packman`, :program:`yum`, :program:`zypper` 39 | etc.). 40 | 41 | It is important to have at least `version 3.8.0 `_ because it'll allow tox_ to auto-provison a 43 | newer version of itself just for |project|. 44 | 45 | Tox_ will take care of the Python dependencies but it's up 46 | to you to make the external ecosystem dependencies available. 47 | 48 | |project|'s core dependency is libssh_. |project| links 49 | against it and so the development headers must be present 50 | on your system for build to succeed. 51 | 52 | The next external build-time dependency is `Cython 53 | ` and `using it 54 | ` requires presense of GCC_. 55 | Consult with your OS's docs to figure out how to get it onto 56 | your machine. 57 | 58 | .. _GCC: https://gcc.gnu.org 59 | .. _libssh: https://libssh.org 60 | .. _tox: https://tox.readthedocs.io 61 | .. _tox v3.8.0: 62 | https://tox.readthedocs.io/en/latest/changelog.html#v3-8-0-2019-03-27 63 | 64 | .. seealso:: 65 | 66 | :ref:`Installing |project|` 67 | Installation from source 68 | 69 | Getting the source code 70 | ======================= 71 | 72 | Once you sort out the toolchain, get |project|'s source: 73 | 74 | .. code-block:: shell-session 75 | 76 | $ git clone https://github.com/ansible/pylibssh.git ~/src/github/ansible/pylibssh 77 | $ # or, if you use SSH: 78 | $ git clone git@github.com:ansible/pylibssh.git ~/src/github/ansible/pylibssh 79 | $ cd ~/src/github/ansible/pylibssh 80 | [dir:pylibssh] $ 81 | 82 | .. attention:: 83 | 84 | All following commands assume the working dir to be the 85 | Git checkout folder (\ 86 | :file:`~/src/github/ansible/pylibssh` in the example) 87 | 88 | Running tests 89 | ============== 90 | 91 | To run tests under your current Python interpreter, run: 92 | 93 | .. code-block:: shell-session 94 | 95 | [dir:pylibssh] $ python -m tox 96 | 97 | If you want to target some other Python version, do: 98 | 99 | .. code-block:: shell-session 100 | 101 | [dir:pylibssh] $ python -m tox -e py38 102 | 103 | Continuous integration 104 | ^^^^^^^^^^^^^^^^^^^^^^ 105 | 106 | In the CI, the testing is done slightly differently. First, 107 | the Python package distributions are built with: 108 | 109 | .. code-block:: shell-session 110 | 111 | [dir:pylibssh] $ python -m tox -e build-dists 112 | 113 | Then, they are tested in a matrix of separate jobs across 114 | different OS and CPython version combinations: 115 | 116 | .. code-block:: shell-session 117 | 118 | [dir:pylibssh] $ python -m tox -e test-binary-dists 119 | 120 | Quality and sanity 121 | ^^^^^^^^^^^^^^^^^^ 122 | 123 | Additionally, there's a separate workflow that runs linting\ 124 | -related checks that can be reproduced locally as follows: 125 | 126 | .. code-block:: shell-session 127 | 128 | [dir:pylibssh] $ python -m tox -e build-docs # Sphinx docs build 129 | [dir:pylibssh] $ python -m tox -e lint # pre-commit.com tool 130 | 131 | Continuous delivery 132 | =================== 133 | 134 | Besides testing and linting, |project| also has `GitHub 135 | Actions workflows CI/CD`_ set up to publish those same 136 | Python package distributions **after** they've been tested. 137 | 138 | Commits from ``devel`` get published to 139 | https://test.pypi.org/project/ansible-pylibssh/ and tagged 140 | commits are published to 141 | https://pypi.org/project/ansible-pylibssh/. 142 | 143 | Besides, if you want to test your project against unreleased 144 | versions of |project|, you may want to go for nightlies. 145 | 146 | .. include:: ../../README.rst 147 | :start-after: DO-NOT-REMOVE-nightlies-START 148 | :end-before: DO-NOT-REMOVE-nightlies-END 149 | 150 | .. _GitHub Actions workflows CI/CD: https://github.com/features/actions 151 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to |project|'s documentation! 2 | ===================================== 3 | 4 | .. include:: ../README.rst 5 | :end-before: DO-NOT-REMOVE-docs-badges-END 6 | 7 | .. include:: ../README.rst 8 | :start-after: DO-NOT-REMOVE-docs-intro-START 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | :caption: Contents 13 | 14 | installation_guide 15 | user_guide 16 | 17 | .. toctree:: 18 | :caption: What's new 19 | 20 | changelog 21 | 22 | .. toctree:: 23 | :caption: Contributing 24 | 25 | contributing/code_of_conduct 26 | contributing/guidelines 27 | contributing/security 28 | contributing/testing_guide 29 | 30 | .. toctree:: 31 | :caption: Maintenance 32 | 33 | contributing/release_guide 34 | 35 | 36 | 37 | Indices and tables 38 | ================== 39 | 40 | * :ref:`genindex` 41 | * :ref:`modindex` 42 | * :ref:`search` 43 | -------------------------------------------------------------------------------- /docs/installation_guide.rst: -------------------------------------------------------------------------------- 1 | ******************** 2 | Installing |project| 3 | ******************** 4 | 5 | This page describes how to install |project| using 6 | :std:doc:`pip:index`. Consult with :std:doc:`pip docs 7 | ` if you are not sure whether you have it 8 | set up. 9 | 10 | .. contents:: 11 | :local: 12 | 13 | Pre-compiled binary distributions 14 | ================================= 15 | 16 | |project| contains :std:doc:`Cython `-based 17 | :std:term:`CPython C-extension modules `. Unlike :std:term:`pure-Python modules 19 | `, these must be pre-compiled 20 | before consumption. 21 | 22 | We publish :std:ref:`platform-specific wheels ` to PyPI. They are built against different arch, 24 | CPython and OS versions so in 99% of cases, you may 25 | seamlessly install |project| not needing any external 26 | dependencies on your system. 27 | 28 | It should be enough for you to just have Python 3.9+ and 29 | a recent :std:doc:`pip ` installed. 30 | 31 | .. attention:: 32 | 33 | Please make sure you have the latest version of 34 | :std:doc:`pip ` before installing |project|. 35 | 36 | If you have a version of :std:doc:`pip ` 37 | older than 8.1, it'll be unable to pick up OS-specific 38 | Python package distributions from PyPI and will try to 39 | fall back to building it from source which would require 40 | more extra dependencies to succeed. 41 | You can upgrade by following :std:doc:`pip's upgrade 42 | instructions `. 43 | 44 | To install |project|, just run: 45 | 46 | .. parsed-literal:: 47 | 48 | $ pip install --user |project| 49 | 50 | .. tip:: 51 | 52 | Avoid running :std:doc:`pip ` with 53 | :command:`sudo` as this will make global changes to the 54 | system. Since :std:doc:`pip ` does not 55 | coordinate with system package managers, it could make 56 | changes to your system that leaves it in an inconsistent 57 | or non-functioning state. This is particularly true for 58 | macOS. Installing with :std:ref:`\-\\\\-user 59 | ` is recommended unless you 60 | understand fully the implications of modifying global 61 | files on the system. 62 | 63 | Installing |project| from source distribution (PyPI) 64 | ==================================================== 65 | 66 | Installing |project| from source is a bit more complicated. 67 | First, pylibssh requires libssh to be compiled against, in 68 | particular, version 0.9.0 or newer. Please refer to `libssh 69 | Downloads page `__ for more 70 | information about installing it. Make sure that you have the 71 | development headers too. 72 | 73 | Another essential build dependency is GCC. You may already 74 | have it installed but if not, consult with your OS docs. 75 | 76 | Once you have the build prerequisites, the following command 77 | should download the tarball, build it and then install into 78 | your current env: 79 | 80 | .. parsed-literal:: 81 | 82 | $ pip install \\ 83 | --user \\ 84 | --no-binary |project| \\ 85 | |project| 86 | 87 | .. tip:: 88 | 89 | When your copy of ``libssh-dev`` is installed into a 90 | non-default directory, make sure to prepend the compiler 91 | environment variables (e.g 92 | ``CFLAGS=-I/usr/local/include``) right before the 93 | ``pip install`` command. For the macOS users who install 94 | it using Homebrew, it should be enough to set 95 | ``CFLAGS="-I $(brew --prefix)/include -I ext 96 | -L $(brew --prefix)/lib -lssh"`` 97 | 98 | Building |project| dists from the ``devel`` branch in Git 99 | ========================================================= 100 | 101 | Since our build processes are tox_-centric, let's 102 | install it first: 103 | 104 | .. code-block:: shell-session 105 | 106 | $ python -m pip install 'tox >= 3.19.0' --user 107 | 108 | .. _tox: https://tox.readthedocs.io 109 | 110 | Now, let's grab the source of |project|: 111 | 112 | .. code-block:: shell-session 113 | 114 | $ git clone https://github.com/ansible/pylibssh.git ~/src/github/ansible/pylibssh 115 | $ # or, if you use SSH: 116 | $ git clone git@github.com:ansible/pylibssh.git ~/src/github/ansible/pylibssh 117 | $ cd ~/src/github/ansible/pylibssh 118 | [dir:pylibssh] $ 119 | 120 | Finally, you can build the dists for the current env using: 121 | 122 | .. code-block:: shell-session 123 | 124 | [dir:pylibssh] $ tox -e build-dists 125 | 126 | If you want to generate the whole matrix of ``manylinux``-\ 127 | compatible wheels, use: 128 | 129 | .. code-block:: shell-session 130 | 131 | [dir:pylibssh] $ tox r -e cibuildwheel 132 | 133 | .. seealso:: 134 | 135 | :ref:`Getting Started with |project|` 136 | Examples of getting started 137 | 138 | :ref:`Continuous delivery` 139 | Using nightly builds to test your project against 140 | -------------------------------------------------------------------------------- /docs/requirements.in: -------------------------------------------------------------------------------- 1 | myst-parser >= 0.10.0 2 | setuptools_scm >= 3.5.0 3 | Sphinx >= 3 4 | sphinx-ansible-theme >= 0.3.1 5 | sphinxcontrib-apidoc >= 0.3.0 6 | sphinxcontrib-towncrier 7 | -------------------------------------------------------------------------------- /docs/user_guide.rst: -------------------------------------------------------------------------------- 1 | ****************************** 2 | Getting Started with |project| 3 | ****************************** 4 | 5 | Now that you have read the :ref:`installation guide ` and 6 | installed |project| on a your system. 7 | 8 | .. contents:: 9 | :local: 10 | 11 | 12 | .. tip:: 13 | 14 | The examples on this page use Python 3.8. If your interpreter 15 | is older, you may need to modify the syntax when copying the 16 | snippets. 17 | 18 | 19 | Checking software versions 20 | ========================== 21 | 22 | .. literalinclude:: _samples/get_version.py 23 | :language: python 24 | 25 | 26 | Creating a SSH session 27 | ====================== 28 | 29 | .. attention:: 30 | 31 | The APIs that are shown below, are low-level. You should 32 | take a great care to ensure you process any exceptions that 33 | arise and always close all the resources once they are no 34 | longer necessary. 35 | 36 | .. literalinclude:: _samples/shell.py 37 | :language: python 38 | :end-at: ssh = Session() 39 | :emphasize-lines: 5 40 | 41 | 42 | Connecting with remote SSH server 43 | ================================= 44 | 45 | .. literalinclude:: _samples/shell.py 46 | :language: python 47 | :start-at: HOST = 'CHANGEME' 48 | :end-at: print(f'{ssh.is_connected=}') 49 | :emphasize-lines: 7-13 50 | 51 | 52 | Connecting over GSSAPI 53 | ---------------------- 54 | 55 | .. attention:: 56 | 57 | This requires that your libssh is compiled with GSSAPI support 58 | enabled. 59 | 60 | Using GSSAPI, password or private key is not necessary, but client and 61 | service principals may be specified. 62 | 63 | .. literalinclude:: _samples/gssapi.py 64 | :language: python 65 | :start-at: ssh.connect( 66 | :end-before: except LibsshSessionException as ssh_exc: 67 | :dedent: 4 68 | 69 | 70 | Passing a command and reading response 71 | ====================================== 72 | 73 | .. literalinclude:: _samples/shell.py 74 | :language: python 75 | :start-at: ssh_channel = ssh.new_channel() 76 | :end-at: ssh_channel.close() 77 | :dedent: 4 78 | :emphasize-lines: 3 79 | 80 | 81 | Opening a remote shell passing command and receiving response 82 | ============================================================= 83 | 84 | .. literalinclude:: _samples/shell.py 85 | :language: python 86 | :start-at: chan_shell = ssh.invoke_shell() 87 | :end-at: chan_shell.close() 88 | :dedent: 4 89 | :emphasize-lines: 3-4 90 | 91 | 92 | Fetch file from remote host 93 | =========================== 94 | 95 | Using SCP: 96 | 97 | .. literalinclude:: _samples/copy_files_scp.py 98 | :language: python 99 | :lines: 26-29 100 | :dedent: 4 101 | :emphasize-lines: 3-4 102 | 103 | Using SFTP: 104 | 105 | .. literalinclude:: _samples/copy_files_sftp.py 106 | :language: python 107 | :lines: 26-32 108 | :dedent: 4 109 | :emphasize-lines: 3,5 110 | 111 | 112 | Copy file to remote host 113 | ======================== 114 | 115 | Using SCP: 116 | 117 | .. literalinclude:: _samples/copy_files_scp.py 118 | :language: python 119 | :lines: 26-27,31-32 120 | :dedent: 4 121 | :emphasize-lines: 3-4 122 | 123 | Using SFTP: 124 | 125 | .. literalinclude:: _samples/copy_files_sftp.py 126 | :language: python 127 | :lines: 26-27,34-38 128 | :dedent: 4 129 | :emphasize-lines: 3,5 130 | 131 | 132 | Closing SSH session 133 | =================== 134 | 135 | .. literalinclude:: _samples/shell.py 136 | :language: python 137 | :start-at: ssh.close() 138 | :dedent: 4 139 | -------------------------------------------------------------------------------- /packaging/README.md: -------------------------------------------------------------------------------- 1 | # `pep517_backend` in-tree build backend 2 | 3 | The `pep517_backend.hooks` importable exposes callables declared by PEP 517 4 | and PEP 660 and is integrated into `pyproject.toml`'s 5 | `[build-system].build-backend` through `[build-system].backend-path`. 6 | 7 | # Design considerations 8 | 9 | `__init__.py` is to remain empty, leaving `hooks.py` the only entrypoint 10 | exposing the callables. The logic is contained in private modules. This is 11 | to prevent import-time side effects. 12 | 13 | # Upstream RPM package spec 14 | 15 | This exists for smoke testing builds and is dynamically configured for 16 | targeting different operating system. It's also used by the [Packit] CI 17 | integration. 18 | 19 | [Packit]: https://packit.dev 20 | -------------------------------------------------------------------------------- /packaging/pep517_backend/__init__.py: -------------------------------------------------------------------------------- 1 | """PEP 517 build backend package pre-building Cython exts before setuptools.""" 2 | -------------------------------------------------------------------------------- /packaging/pep517_backend/__main__.py: -------------------------------------------------------------------------------- 1 | """A command-line entry-point for the build backend helpers.""" 2 | 3 | import sys 4 | 5 | from . import cli 6 | 7 | 8 | if __name__ == '__main__': 9 | sys.exit(cli.run_main_program(argv=sys.argv)) 10 | -------------------------------------------------------------------------------- /packaging/pep517_backend/_compat.py: -------------------------------------------------------------------------------- 1 | """Cross-interpreter compatibility shims.""" 2 | 3 | import os 4 | import typing as t # noqa: WPS111 5 | from contextlib import contextmanager 6 | from pathlib import Path 7 | 8 | 9 | try: 10 | from contextlib import chdir as chdir_cm # noqa: WPS433 11 | except ImportError: 12 | 13 | @contextmanager # type: ignore[no-redef] 14 | def chdir_cm(path: os.PathLike) -> t.Iterator[None]: # noqa: WPS440 15 | """Temporarily change the current directory, recovering on exit.""" 16 | original_wd = Path.cwd() 17 | os.chdir(path) 18 | try: # noqa: WPS505 19 | yield 20 | finally: 21 | os.chdir(original_wd) 22 | 23 | 24 | try: 25 | from contextlib import nullcontext as nullcontext_cm # noqa: F401, WPS433 26 | except ImportError: 27 | 28 | @contextmanager # type: ignore[no-redef] 29 | def nullcontext_cm( # noqa: WPS440 30 | enter_result: t.Any = None, # noqa: WPS318 31 | ) -> t.Iterator[t.Any]: 32 | """Emit the incoming value. 33 | 34 | A no-op context manager. 35 | """ 36 | yield enter_result 37 | 38 | 39 | try: 40 | # Python 3.11+ 41 | from tomllib import loads as load_toml_from_string # noqa: WPS433 42 | except ImportError: 43 | # before Python 3.11 44 | from tomli import loads as load_toml_from_string # noqa: WPS433, WPS440 45 | 46 | 47 | __all__ = ( # noqa: WPS410 48 | 'chdir_cm', 49 | 'load_toml_from_string', 50 | 'nullcontext_cm', 51 | ) 52 | -------------------------------------------------------------------------------- /packaging/pep517_backend/_cython_configuration.py: -------------------------------------------------------------------------------- 1 | # fmt: off 2 | 3 | # from __future__ import annotations 4 | 5 | import os 6 | from contextlib import contextmanager 7 | from pathlib import Path 8 | from sys import version_info as _python_version_tuple 9 | 10 | from expandvars import expandvars 11 | 12 | from ._compat import load_toml_from_string # noqa: WPS436 13 | from ._transformers import ( # noqa: WPS436 14 | get_cli_kwargs_from_config, get_enabled_cli_flags_from_config, 15 | ) 16 | 17 | 18 | def get_local_cython_config() -> dict: 19 | """Grab optional build dependencies from pyproject.toml config. 20 | 21 | :returns: config section from ``pyproject.toml`` 22 | :rtype: dict 23 | 24 | This basically reads entries from:: 25 | 26 | [tool.local.cythonize] 27 | # Env vars provisioned during cythonize call 28 | src = ["src/**/*.pyx"] 29 | 30 | [tool.local.cythonize.env] 31 | # Env vars provisioned during cythonize call 32 | LDFLAGS = "-lssh" 33 | 34 | [tool.local.cythonize.flags] 35 | # This section can contain the following booleans: 36 | # * annotate — generate annotated HTML page for source files 37 | # * build — build extension modules using distutils 38 | # * inplace — build extension modules in place using distutils (implies -b) 39 | # * force — force recompilation 40 | # * quiet — be less verbose during compilation 41 | # * lenient — increase Python compat by ignoring some compile time errors 42 | # * keep-going — compile as much as possible, ignore compilation failures 43 | annotate = false 44 | build = false 45 | inplace = true 46 | force = true 47 | quiet = false 48 | lenient = false 49 | keep-going = false 50 | 51 | [tool.local.cythonize.kwargs] 52 | # This section can contain args that have values: 53 | # * exclude=PATTERN exclude certain file patterns from the compilation 54 | # * parallel=N run builds in N parallel jobs (default: calculated per system) 55 | exclude = "**.py" 56 | parallel = 12 57 | 58 | [tool.local.cythonize.kwargs.directives] 59 | # This section can contain compiler directives 60 | # NAME = "VALUE" 61 | 62 | [tool.local.cythonize.kwargs.compile-time-env] 63 | # This section can contain compile time env vars 64 | # NAME = "VALUE" 65 | 66 | [tool.local.cythonize.kwargs.options] 67 | # This section can contain cythonize options 68 | # NAME = "VALUE" 69 | """ 70 | config_toml_txt = (Path.cwd().resolve() / 'pyproject.toml').read_text() 71 | config_mapping = load_toml_from_string(config_toml_txt) 72 | return config_mapping['tool']['local']['cythonize'] 73 | 74 | 75 | def make_cythonize_cli_args_from_config(config: dict) -> 'list[str]': 76 | """Compose ``cythonize`` CLI args from config.""" 77 | py_ver_arg = f'-{_python_version_tuple.major!s}' # noqa: WPS305 78 | 79 | cli_flags = get_enabled_cli_flags_from_config(config['flags']) 80 | cli_kwargs = get_cli_kwargs_from_config(config['kwargs']) 81 | 82 | return cli_flags + [py_ver_arg] + cli_kwargs + ['--'] + config['src'] 83 | 84 | 85 | @contextmanager 86 | def patched_env(env: 'dict[str, str]', cython_line_tracing_requested: bool): 87 | """Temporary set given env vars. 88 | 89 | :param env: tmp env vars to set 90 | :type env: dict 91 | 92 | :yields: None 93 | """ 94 | orig_env = os.environ.copy() 95 | expanded_env = {name: expandvars(var_val) for name, var_val in env.items()} 96 | os.environ.update(expanded_env) 97 | 98 | if cython_line_tracing_requested: 99 | os.environ['CFLAGS'] = ' '.join(( 100 | os.getenv('CFLAGS', ''), 101 | '-DCYTHON_TRACE_NOGIL=1', # Implies CYTHON_TRACE=1 102 | )).strip() 103 | try: 104 | yield 105 | finally: 106 | os.environ.clear() 107 | os.environ.update(orig_env) 108 | -------------------------------------------------------------------------------- /packaging/pep517_backend/_transformers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Data conversion helpers for the in-tree PEP 517 build backend.""" 4 | 5 | from itertools import chain 6 | from re import sub as _substitute_with_regexp 7 | 8 | 9 | def _emit_opt_pairs(opt_pair): 10 | flag, flag_value = opt_pair 11 | flag_opt = '--{name!s}'.format(name=flag) 12 | if isinstance(flag_value, dict): 13 | sub_pairs = flag_value.items() 14 | else: 15 | sub_pairs = ((flag_value,),) 16 | 17 | yield from ( 18 | '='.join(map(str, (flag_opt,) + pair)) 19 | for pair in sub_pairs 20 | ) 21 | 22 | 23 | def get_cli_kwargs_from_config(kwargs_map): 24 | """Make a list of options with values from config.""" 25 | return list(chain.from_iterable(map(_emit_opt_pairs, kwargs_map.items()))) 26 | 27 | 28 | def get_enabled_cli_flags_from_config(flags_map): 29 | """Make a list of enabled boolean flags from config.""" 30 | return [ 31 | '--{flag}'.format(flag=flag) 32 | for flag, is_enabled in flags_map.items() 33 | if is_enabled 34 | ] 35 | 36 | 37 | def sanitize_rst_roles(rst_source_text: str) -> str: # noqa: WPS210 38 | """Replace RST roles with inline highlighting.""" 39 | pep_role_regex = r"""(?x) 40 | :pep:`(?P\d+)` 41 | """ 42 | pep_substitution_pattern = ( 43 | r'`PEP \g >`__' 44 | ) 45 | 46 | user_role_regex = r"""(?x) 47 | :user:`(?P[^`]+)(?:\s+(.*))?` 48 | """ 49 | user_substitution_pattern = ( 50 | r'`@\g ' 51 | r'>`__' 52 | ) 53 | 54 | issue_role_regex = r"""(?x) 55 | :issue:`(?P[^`]+)(?:\s+(.*))?` 56 | """ 57 | issue_substitution_pattern = ( 58 | r'`#\g ' 59 | r'>`__' 60 | ) 61 | 62 | pr_role_regex = r"""(?x) 63 | :pr:`(?P[^`]+)(?:\s+(.*))?` 64 | """ 65 | pr_substitution_pattern = ( 66 | r'`PR #\g ' 67 | r'>`__' 68 | ) 69 | 70 | commit_role_regex = r"""(?x) 71 | :commit:`(?P[^`]+)(?:\s+(.*))?` 72 | """ 73 | commit_substitution_pattern = ( 74 | r'`\g ' 75 | r'>`__' 76 | ) 77 | 78 | gh_role_regex = r"""(?x) 79 | :gh:`(?P[^`<]+)(?:\s+([^`]*))?` 80 | """ 81 | gh_substitution_pattern = ( 82 | r'GitHub: ``\g``' 83 | ) 84 | 85 | meth_role_regex = r"""(?x) 86 | (?::py)?:meth:`~?(?P[^`<]+)(?:\s+([^`]*))?` 87 | """ 88 | meth_substitution_pattern = r'``\g()``' 89 | 90 | role_regex = r"""(?x) 91 | (?::\w+)?:\w+:`(?P[^`<]+)(?:\s+([^`]*))?` 92 | """ 93 | substitution_pattern = r'``\g``' 94 | 95 | project_substitution_regex = r'\|project\|' 96 | project_substitution_pattern = 'ansible-pylibssh' 97 | 98 | substitutions = ( 99 | (pep_role_regex, pep_substitution_pattern), 100 | (user_role_regex, user_substitution_pattern), 101 | (issue_role_regex, issue_substitution_pattern), 102 | (pr_role_regex, pr_substitution_pattern), 103 | (commit_role_regex, commit_substitution_pattern), 104 | (gh_role_regex, gh_substitution_pattern), 105 | (meth_role_regex, meth_substitution_pattern), 106 | (role_regex, substitution_pattern), 107 | (project_substitution_regex, project_substitution_pattern), 108 | ) 109 | 110 | rst_source_normalized_text = rst_source_text 111 | for regex, substitution in substitutions: 112 | rst_source_normalized_text = _substitute_with_regexp( 113 | regex, 114 | substitution, 115 | rst_source_normalized_text, 116 | ) 117 | 118 | return rst_source_normalized_text 119 | -------------------------------------------------------------------------------- /packaging/pep517_backend/cli.py: -------------------------------------------------------------------------------- 1 | # fmt: off 2 | 3 | """A command-line interface wrapper for calling Cython.""" 4 | 5 | # from __future__ import annotations 6 | 7 | import sys 8 | from itertools import chain 9 | from pathlib import Path 10 | 11 | from Cython.Compiler.Main import compile as _translate_cython_cli_cmd 12 | from Cython.Compiler.Main import parse_command_line as _split_cython_cli_args 13 | 14 | from ._cython_configuration import ( # noqa: WPS436 15 | get_local_cython_config as _get_local_cython_config, 16 | ) 17 | from ._cython_configuration import ( # noqa: WPS436 18 | make_cythonize_cli_args_from_config as _make_cythonize_cli_args_from_config, 19 | ) 20 | from ._cython_configuration import ( # noqa: WPS436 21 | patched_env as _patched_cython_env, 22 | ) 23 | 24 | 25 | _PROJECT_PATH = Path(__file__).parents[2] 26 | 27 | 28 | def run_main_program(argv) -> 'int | str': 29 | """Invoke ``translate-cython`` or fail.""" 30 | if len(argv) != 2: 31 | return 'This program only accepts one argument -- "translate-cython"' 32 | 33 | if argv[1] != 'translate-cython': 34 | return 'This program only implements the "translate-cython" subcommand' 35 | 36 | config = _get_local_cython_config() 37 | config['flags'] = {'keep-going': config['flags']['keep-going']} 38 | config['src'] = list( 39 | map( 40 | str, 41 | chain.from_iterable( 42 | map(_PROJECT_PATH.glob, config['src']), 43 | ), 44 | ), 45 | ) 46 | translate_cython_cli_args = _make_cythonize_cli_args_from_config(config) 47 | 48 | cython_options, cython_sources = _split_cython_cli_args( 49 | translate_cython_cli_args, 50 | ) 51 | 52 | with _patched_cython_env(config['env'], cython_line_tracing_requested=True): 53 | return _translate_cython_cli_cmd( 54 | cython_sources, 55 | cython_options, 56 | ).num_errors 57 | 58 | 59 | if __name__ == '__main__': 60 | sys.exit(run_main_program(argv=sys.argv)) 61 | -------------------------------------------------------------------------------- /packaging/pep517_backend/hooks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """PEP 517 build backend pre-building Cython exts before setuptools.""" 4 | 5 | from contextlib import suppress as _suppress 6 | 7 | # Re-exporting PEP 517 hooks 8 | # pylint: disable-next=unused-wildcard-import,wildcard-import 9 | from setuptools.build_meta import * # noqa: E501, F403, WPS347 10 | 11 | # Re-exporting PEP 517 hooks 12 | from ._backend import ( # type: ignore[assignment] # noqa: WPS436 13 | build_sdist, build_wheel, get_requires_for_build_wheel, 14 | prepare_metadata_for_build_wheel, 15 | ) 16 | 17 | 18 | with _suppress(ImportError): # Only succeeds w/ setuptools implementing PEP 660 19 | # Re-exporting PEP 660 hooks 20 | from ._backend import ( # type: ignore[assignment] # noqa: WPS433, WPS436 21 | build_editable, get_requires_for_build_editable, 22 | prepare_metadata_for_build_editable, 23 | ) 24 | -------------------------------------------------------------------------------- /packaging/rpm/ansible-pylibssh.spec: -------------------------------------------------------------------------------- 1 | %global pypi_name ansible-pylibssh 2 | 3 | # NOTE: The target version may be set dynamically via 4 | # NOTE: rpmbuild --define "upstream_version 0.2.1.dev125+g0b5bde0" 5 | %global upstream_version_fallback %(ls -t dist/%{pypi_name}-*.tar.gz 2>/dev/null | head -n 1 | sed 's#^dist\\/%{pypi_name}-\\(.*\\)\\.tar\\.gz$#\\1#') 6 | # If "upstream_version" macro is unset, use the fallback defined above: 7 | %if "%{!?upstream_version:UNSET}" == "UNSET" 8 | %global upstream_version %{upstream_version_fallback} 9 | %endif 10 | 11 | %global python_importable_name pylibsshext 12 | # RHEL or CentOS: 13 | %if 0%{?rhel} 14 | %global normalized_dist_name ansible_pylibssh 15 | %global whl_glob %{normalized_dist_name}-%{version}-cp3*-cp3*-linux_%{_arch}.whl 16 | %endif 17 | 18 | %global buildroot_site_packages "%{buildroot}%{python3_sitearch}" 19 | 20 | %if 0%{?with_debug} 21 | %global _dwz_low_mem_die_limit 0 22 | %else 23 | # RHEL or CentOS: 24 | %if 0%{?rhel} 25 | # Prevent requiring a Build ID in the compiled shared objects 26 | %global debug_package %{nil} 27 | %endif 28 | %endif 29 | 30 | Name: python-%{pypi_name} 31 | Version: %{upstream_version} 32 | Release: 1%{?dist} 33 | Summary: Python bindings for libssh client specific to Ansible use case 34 | 35 | #BuildRoot: %%{_tmppath}/%%{name}-%%{version}-%%{release}-buildroot 36 | License: LGPL-2+ 37 | URL: https://github.com/ansible/pylibssh 38 | Source0: %{pypi_source} 39 | Source1: %{pypi_source expandvars 0.7.0} 40 | # RHEL or CentOS: 41 | %if 0%{?rhel} 42 | Source2: %{pypi_source build 0.3.1.post1} 43 | Source3: %{pypi_source Cython 0.29.32} 44 | Source4: %{pypi_source packaging 20.9} 45 | Source5: %{pypi_source setuptools 56.0.0} 46 | Source6: %{pypi_source setuptools_scm 8.1.0} 47 | Source8: %{pypi_source tomli 2.0.1} 48 | Source9: %{pypi_source pep517 0.10.0} 49 | Source10: %{pypi_source pip 21.1.1} 50 | Source11: %{pypi_source pyparsing 2.4.7} 51 | # RHEL specifically, not CentOS: 52 | %if 0%{?centos} == 0 53 | Source12: %{pypi_source importlib_metadata 4.0.1} 54 | Source13: %{pypi_source zipp 3.4.1} 55 | Source14: %{pypi_source typing_extensions 4.12.2} 56 | %endif 57 | Source15: %{pypi_source pytest 6.2.4} 58 | Source16: %{pypi_source pytest-cov 2.12.1} 59 | Source18: %{pypi_source pytest-xdist 2.3.0} 60 | Source19: %{pypi_source iniconfig 1.1.1} 61 | Source20: %{pypi_source attrs 20.3.0} 62 | Source21: %{pypi_source pluggy 0.13.1} 63 | Source22: %{pypi_source py 1.10.0} 64 | Source23: %{pypi_source coverage 5.5} 65 | Source24: %{pypi_source tomli 1.2.3} 66 | %endif 67 | 68 | # Test dependencies: 69 | # keygen? 70 | BuildRequires: openssh 71 | # sshd? 72 | BuildRequires: openssh-server 73 | # ssh? 74 | BuildRequires: openssh-clients 75 | # RHEL or CentOS: 76 | %if 0%{?rhel} 77 | BuildRequires: python3dist(pytest) 78 | BuildRequires: python3dist(pytest-cov) 79 | BuildRequires: python3dist(pytest-xdist) 80 | BuildRequires: python3dist(tox) 81 | %endif 82 | 83 | # Build dependencies: 84 | BuildRequires: gcc 85 | 86 | BuildRequires: libssh-devel 87 | BuildRequires: python3-devel 88 | 89 | # RHEL or CentOS: 90 | %if 0%{?rhel} 91 | BuildRequires: python3dist(pip) 92 | BuildRequires: python3dist(wheel) 93 | # CentOS, not RHEL: 94 | %if 0%{?centos} 95 | BuildRequires: python3dist(importlib-metadata) 96 | %endif 97 | %endif 98 | # Fedora: 99 | %if 0%{?fedora} 100 | # `pyproject-rpm-macros` provides %%pyproject_buildrequires 101 | BuildRequires: pyproject-rpm-macros 102 | 103 | # `python3-pip` is used to install vendored build deps 104 | BuildRequires: python3-pip 105 | 106 | # `python3-toml` is not retrieved by %%pyproject_buildrequires for some reason 107 | BuildRequires: python3-toml 108 | %endif 109 | 110 | Requires: libssh >= 0.9.0 111 | 112 | %description 113 | $summary 114 | 115 | 116 | # Stolen from https://src.fedoraproject.org/rpms/python-pep517/blob/rawhide/f/python-pep517.spec#_25 117 | %package -n python3-%{pypi_name} 118 | Summary: %{summary} 119 | %{?python_provide:%python_provide python3-%{pypi_name}} 120 | 121 | %description -n python3-%{pypi_name} 122 | $summary 123 | 124 | %prep 125 | %autosetup -n %{pypi_name}-%{version} 126 | 127 | PYTHONPATH="$(pwd)/bin" \ 128 | %{__python3} -m pip install --no-deps -t bin %{SOURCE1} 129 | 130 | # RHEL or CentOS: 131 | %if 0%{?rhel} 132 | PYTHONPATH="$(pwd)/bin" \ 133 | %{__python3} -m pip install --no-deps -t bin %{SOURCE9} 134 | PYTHONPATH="$(pwd)/bin" \ 135 | %{__python3} -m pip install --no-deps -t bin %{SOURCE2} 136 | PYTHONPATH="$(pwd)/bin" \ 137 | %{__python3} -m pip install --no-deps -t bin %{SOURCE10} 138 | PYTHONPATH="$(pwd)/bin" \ 139 | %{__python3} -m pip install --no-deps -t bin %{SOURCE3} --install-option="--no-cython-compile" 140 | PYTHONPATH="$(pwd)/bin" \ 141 | %{__python3} -m pip install --no-deps -t bin %{SOURCE4} 142 | PYTHONPATH="$(pwd)/bin" \ 143 | %{__python3} -m pip install --no-deps -t bin %{SOURCE5} 144 | PYTHONPATH="$(pwd)/bin" \ 145 | %{__python3} -m pip install --no-deps -t bin %{SOURCE6} 146 | PYTHONPATH="$(pwd)/bin" \ 147 | %{__python3} -m pip install --no-deps -t bin %{SOURCE8} 148 | # RHEL specifically, not CentOS: 149 | %if 0%{?centos} == 0 150 | PYTHONPATH="$(pwd)/bin" \ 151 | %{__python3} -m pip install --no-deps -t bin %{SOURCE14} 152 | %endif 153 | PYTHONPATH="$(pwd)/bin" \ 154 | %{__python3} -m pip install --no-deps -t bin %{SOURCE11} 155 | # RHEL specifically, not CentOS: 156 | %if 0%{?centos} == 0 157 | PYTHONPATH="$(pwd)/bin" \ 158 | %{__python3} -m pip install --no-deps -t bin %{SOURCE12} 159 | PYTHONPATH="$(pwd)/bin" \ 160 | %{__python3} -m pip install --no-deps -t bin %{SOURCE13} 161 | %endif 162 | PYTHONPATH="$(pwd)/bin" \ 163 | %{__python3} -m pip install --no-deps -t bin %{SOURCE15} 164 | PYTHONPATH="$(pwd)/bin" \ 165 | %{__python3} -m pip install --no-deps -t bin %{SOURCE16} 166 | PYTHONPATH="$(pwd)/bin" \ 167 | %{__python3} -m pip install --no-deps -t bin %{SOURCE18} 168 | PYTHONPATH="$(pwd)/bin" \ 169 | %{__python3} -m pip install --no-deps -t bin %{SOURCE19} 170 | PYTHONPATH="$(pwd)/bin" \ 171 | %{__python3} -m pip install --no-deps -t bin %{SOURCE20} 172 | PYTHONPATH="$(pwd)/bin" \ 173 | %{__python3} -m pip install --no-deps -t bin %{SOURCE21} 174 | PYTHONPATH="$(pwd)/bin" \ 175 | %{__python3} -m pip install --no-deps -t bin %{SOURCE22} 176 | PYTHONPATH="$(pwd)/bin" \ 177 | %{__python3} -m pip install --no-deps -t bin %{SOURCE23} 178 | PYTHONPATH="$(pwd)/bin" \ 179 | %{__python3} -m pip install --no-deps -t bin %{SOURCE24} 180 | %endif 181 | 182 | # Fedora: 183 | %if 0%{?fedora} 184 | PYTHONPATH="$(pwd)/bin" \ 185 | %generate_buildrequires 186 | %pyproject_buildrequires -t 187 | %endif 188 | 189 | 190 | %build 191 | 192 | # Fedora: 193 | %if 0%{?fedora} 194 | %pyproject_wheel 195 | %endif 196 | 197 | # RHEL or CentOS: 198 | %if 0%{?rhel} 199 | PYTHONPATH="$(pwd)/bin" \ 200 | %{__python3} \ 201 | -m build \ 202 | --wheel \ 203 | --skip-dependencies \ 204 | --no-isolation \ 205 | . 206 | %endif 207 | 208 | 209 | %install 210 | 211 | # Fedora: 212 | %if 0%{?fedora} 213 | %pyproject_install 214 | %pyproject_save_files "%{python_importable_name}" 215 | %endif 216 | 217 | # RHEL or CentOS: 218 | %if 0%{?rhel} 219 | %{py3_install_wheel %{whl_glob}} 220 | # Set the installer to rpm so that pip knows not to manage this dist: 221 | sed \ 222 | -i 's/pip/rpm/' \ 223 | %{buildroot_site_packages}/%{normalized_dist_name}-%{version}.dist-info/INSTALLER 224 | %endif 225 | 226 | 227 | %check 228 | 229 | export PYTHONPATH="%{buildroot_site_packages}:${PYTHONPATH}" 230 | # Fedora: 231 | %if "%{?fedora:SET}" == "SET" 232 | %pyproject_check_import 233 | %tox -e just-pytest 234 | # CentOS or RHEL: 235 | %else 236 | export PYTHONPATH="$(pwd)/bin:${PYTHONPATH}" 237 | %{__python3} -m pytest \ 238 | --no-cov 239 | %endif 240 | 241 | 242 | %files -n python3-%{pypi_name} %{?fedora:-f} %{?fedora:%{pyproject_files}} 243 | %license LICENSE.rst 244 | %doc README.rst 245 | 246 | # RHEL or CentOS 247 | %if 0%{?rhel} 248 | # NOTE: %%{python3_sitelib} points to /lib/ while %%{python3_sitearch} 249 | # NOTE: points to /lib64/ when necessary. 250 | %{python3_sitearch}/%{python_importable_name} 251 | %{python3_sitearch}/%{normalized_dist_name}-%{version}.dist-info 252 | %endif 253 | 254 | %changelog 255 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | # `pytest-xdist`: 4 | --numprocesses=auto 5 | 6 | # Show 10 slowest invocations: 7 | --durations=10 8 | 9 | # Report all the things == -rxXs: 10 | -ra 11 | 12 | # Show values of the local vars in errors/tracebacks: 13 | --showlocals 14 | 15 | # Autocollect and invoke the doctests from all modules: 16 | # https://docs.pytest.org/en/stable/doctest.html 17 | --doctest-modules 18 | 19 | # dump the test results in junit format: 20 | --junitxml=.test-results/pytest/results.xml 21 | 22 | # Pre-load the `pytest-cov` plugin early: 23 | -p pytest_cov 24 | 25 | # `pytest-cov`: 26 | --cov 27 | --cov-config=.coveragerc 28 | --cov-context=test 29 | --cov-report=xml:.test-results/pytest/cov.xml 30 | --no-cov-on-fail 31 | 32 | # Fail on config parsing warnings: 33 | # --strict-config 34 | 35 | # Fail on non-existing markers: 36 | # * Deprecated since v6.2.0 but may be reintroduced later covering a 37 | # broader scope: 38 | # --strict 39 | # * Exists since v4.5.0 (advised to be used instead of `--strict`): 40 | --strict-markers 41 | 42 | doctest_optionflags = ALLOW_UNICODE ELLIPSIS 43 | 44 | # Marks tests with an empty parameterset as xfail(run=False) 45 | empty_parameter_set_mark = xfail 46 | 47 | faulthandler_timeout = 30 48 | 49 | filterwarnings = 50 | error 51 | ignore:Coverage disabled via --no-cov switch!:pytest.PytestWarning:pytest_cov.plugin 52 | 53 | # FIXME: drop this once `pytest-cov` is updated. 54 | # Ref: https://github.com/pytest-dev/pytest-cov/issues/557 55 | ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning 56 | 57 | junit_duration_report = call 58 | # xunit1 contains more metadata than xunit2 so it's better for CI UIs: 59 | junit_family = xunit1 60 | junit_logging = all 61 | junit_log_passing_tests = true 62 | junit_suite_name = ansible_pylibssh_test_suite 63 | 64 | # A mapping of markers to their descriptions allowed in strict mode: 65 | markers = 66 | smoke: Quick post-build self-check smoke tests 67 | 68 | minversion = 4.6.9 69 | 70 | # Optimize pytest's lookup by restricting potentially deep dir tree scan: 71 | norecursedirs = 72 | build 73 | dist 74 | docs 75 | src/pylibsshext.egg-info 76 | .cache 77 | .eggs 78 | .git 79 | .github 80 | .tox 81 | *.egg 82 | 83 | testpaths = tests/ 84 | 85 | xfail_strict = true 86 | -------------------------------------------------------------------------------- /requirements-build.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.9 3 | # To update, run: 4 | # 5 | # pip-compile --allow-unsafe --output-file=requirements-build.txt --strip-extras - 6 | # 7 | cython==3.0.12 8 | # via -r - 9 | expandvars==0.9.0 10 | # via -r - 11 | packaging==24.0 12 | # via setuptools-scm 13 | pyparsing==3.0.9 14 | # via packaging 15 | setuptools-scm==8.1.0 16 | # via -r - 17 | tomli==2.0.1 18 | # via setuptools-scm 19 | typing-extensions==4.10.0 20 | # via setuptools-scm 21 | wheel==0.37.1 22 | # via -r - 23 | 24 | # The following packages are considered to be unsafe in a requirements file: 25 | setuptools==68.2.2 26 | # via 27 | # -r - 28 | # setuptools-scm 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # wheels should be OS-specific: 3 | # their names must contain macOS/manulinux1/2010/2014/Windows identifiers 4 | universal = 0 5 | 6 | [metadata] 7 | name = ansible-pylibssh 8 | url = https://github.com/ansible/pylibssh 9 | project_urls = 10 | Bug Tracker = https://github.com/ansible/pylibssh/issues 11 | CI: GitHub Workflows = https://github.com/ansible/pylibssh/actions?query=branch:devel 12 | Code of Conduct = https://docs.ansible.com/ansible/latest/community/code_of_conduct.html 13 | Docs: RTD = https://ansible-pylibssh.rtfd.io/ 14 | Source Code = https://github.com/ansible/pylibssh 15 | description = Python bindings for libssh client specific to Ansible use case 16 | long_description = file: README.rst, CHANGELOG.rst 17 | long_description_content_type = text/x-rst 18 | author = Ansible, Inc. 19 | author_email = info+github/ansible/pylibssh@ansible.com 20 | license = LGPLv2+ 21 | license_files = 22 | LICENSE.rst 23 | classifiers = 24 | Development Status :: 2 - Pre-Alpha 25 | 26 | License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) 27 | 28 | Operating System :: MacOS 29 | Operating System :: POSIX :: Linux 30 | 31 | Programming Language :: Python :: 3 32 | Programming Language :: Python :: 3.9 33 | Programming Language :: Python :: 3.10 34 | Programming Language :: Python :: 3.11 35 | Programming Language :: Python :: 3.12 36 | Programming Language :: Cython 37 | 38 | Topic :: Software Development :: Libraries :: Python Modules 39 | Topic :: Security 40 | keywords = 41 | cython 42 | cext 43 | libssh 44 | 45 | [options] 46 | python_requires = >= 3.9 47 | # Ref: 48 | # https://setuptools.readthedocs.io/en/latest/setuptools.html#using-a-src-layout 49 | # (`src/` layout) 50 | package_dir = 51 | = src 52 | packages = find: 53 | # https://setuptools.readthedocs.io/en/latest/setuptools.html#setting-the-zip-safe-flag 54 | zip_safe = False 55 | include_package_data = True 56 | 57 | [options.packages.find] 58 | # Ref: 59 | # https://setuptools.readthedocs.io/en/latest/setuptools.html#using-a-src-layout 60 | # (`src/` layout) 61 | where = src 62 | 63 | [options.package_data] 64 | # Ref: 65 | # https://setuptools.readthedocs.io/en/latest/setuptools.html#options 66 | # (see notes for the asterisk/`*` meaning) 67 | * = 68 | # *.c 69 | *.so 70 | -------------------------------------------------------------------------------- /src/pylibsshext/.gitignore: -------------------------------------------------------------------------------- 1 | # C files generated by Cython 2 | *.c 3 | -------------------------------------------------------------------------------- /src/pylibsshext/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Python bindings for libssh.""" 4 | 5 | from ._version import ( # noqa: F401, WPS300, WPS436 6 | __full_version__, __libssh_version__, __version__, __version_info__, 7 | ) 8 | -------------------------------------------------------------------------------- /src/pylibsshext/_libssh_version.pyx: -------------------------------------------------------------------------------- 1 | from pylibsshext.includes.libssh cimport libssh_version 2 | 3 | 4 | LIBSSH_VERSION = libssh_version.decode("ascii") 5 | -------------------------------------------------------------------------------- /src/pylibsshext/_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Version definition.""" 4 | 5 | from ._libssh_version import ( # noqa: N811, WPS300, WPS436 6 | LIBSSH_VERSION as __libssh_version__, 7 | ) 8 | 9 | 10 | try: 11 | from ._scm_version import ( # noqa: WPS300, WPS433, WPS436 12 | version as __version__, 13 | ) 14 | except ImportError: 15 | from pkg_resources import get_distribution as _get_dist # noqa: WPS433 16 | __version__ = _get_dist('ansible-pylibssh').version # noqa: WPS440 17 | 18 | 19 | __full_version__ = ( 20 | ''. 21 | format(wrapper_ver=__version__, backend_ver=__libssh_version__) 22 | ) 23 | __version_info__ = tuple( 24 | (int(chunk) if chunk.isdigit() else chunk) 25 | for chunk in __version__.split('.') 26 | ) 27 | -------------------------------------------------------------------------------- /src/pylibsshext/channel.pxd: -------------------------------------------------------------------------------- 1 | # distutils: libraries = ssh 2 | # 3 | # This file is part of the pylibssh library 4 | # 5 | # This library is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU Lesser General Public 7 | # License as published by the Free Software Foundation; either 8 | # version 2.1 of the License, or (at your option) any later version. 9 | # 10 | # This library is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public 16 | # License along with this library; if not, see file LICENSE.rst in this 17 | # repository. 18 | # 19 | from pylibsshext.includes cimport callbacks, libssh 20 | 21 | 22 | cdef class Channel: 23 | cdef _session 24 | cdef libssh.ssh_channel _libssh_channel 25 | cdef libssh.ssh_session _libssh_session 26 | 27 | cdef class ChannelCallback: 28 | cdef callbacks.ssh_channel_callbacks_struct callback 29 | cdef _userdata 30 | -------------------------------------------------------------------------------- /src/pylibsshext/errors.pxd: -------------------------------------------------------------------------------- 1 | # distutils: libraries = ssh 2 | # 3 | # This file is part of the pylibssh library 4 | # 5 | # This library is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU Lesser General Public 7 | # License as published by the Free Software Foundation; either 8 | # version 2.1 of the License, or (at your option) any later version. 9 | # 10 | # This library is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public 16 | # License along with this library; if not, see file LICENSE.rst in this 17 | # repository. 18 | # 19 | from pylibsshext.includes cimport libssh 20 | 21 | 22 | cdef class LibsshException(Exception): 23 | pass 24 | 25 | cdef class LibsshSessionException(LibsshException): 26 | pass 27 | 28 | cdef class LibsshChannelException(LibsshException): 29 | pass 30 | 31 | cdef class LibsshSCPException(LibsshException): 32 | pass 33 | 34 | cdef class LibsshSFTPException(LibsshException): 35 | pass 36 | -------------------------------------------------------------------------------- /src/pylibsshext/errors.pyx: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of the pylibssh library 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, see file LICENSE.rst in this 16 | # repository. 17 | # 18 | cdef class LibsshException(Exception): 19 | def __init__(self, message=''): 20 | self.message = message 21 | super(LibsshException, self).__init__(message) 22 | 23 | def __str__(self): 24 | return self.message 25 | 26 | def __repr__(self): 27 | return self.message 28 | 29 | def _get_session_error_str(self, obj): 30 | return libssh.ssh_get_error(obj._libssh_session).decode() 31 | 32 | 33 | cdef class LibsshSessionException(LibsshException): 34 | pass 35 | 36 | 37 | cdef class LibsshChannelException(LibsshException): 38 | pass 39 | 40 | 41 | class LibsshChannelReadFailure(LibsshChannelException, ConnectionError): 42 | """Raised when there is a failure to read from a libssh channel.""" 43 | 44 | 45 | cdef class LibsshSCPException(LibsshException): 46 | pass 47 | 48 | 49 | cdef class LibsshSFTPException(LibsshException): 50 | pass 51 | -------------------------------------------------------------------------------- /src/pylibsshext/includes/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Cython interface definitions for libssh.""" 4 | -------------------------------------------------------------------------------- /src/pylibsshext/includes/callbacks.pxd: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of the pylibssh library 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, see file LICENSE.rst in this 16 | # repository. 17 | # 18 | from libc.stdint cimport uint32_t 19 | 20 | from pylibsshext.includes.libssh cimport ssh_channel, ssh_session 21 | 22 | 23 | cdef extern from "libssh/callbacks.h": 24 | 25 | void ssh_callbacks_init(void *) 26 | 27 | ctypedef int (*ssh_channel_data_callback) (ssh_session session, 28 | ssh_channel channel, 29 | void *data, 30 | uint32_t len, 31 | int is_stderr, 32 | void *userdata) 33 | ctypedef void (*ssh_channel_eof_callback) (ssh_session session, 34 | ssh_channel channel, 35 | void *userdata) 36 | 37 | ctypedef void (*ssh_channel_close_callback) (ssh_session session, 38 | ssh_channel channel, 39 | void *userdata) 40 | 41 | ctypedef void (*ssh_channel_signal_callback) (ssh_session session, 42 | ssh_channel channel, 43 | const char *signal, 44 | void *userdata) 45 | 46 | ctypedef void (*ssh_channel_exit_status_callback) (ssh_session session, 47 | ssh_channel channel, 48 | int exit_status, 49 | void *userdata) 50 | 51 | ctypedef void (*ssh_channel_exit_signal_callback) (ssh_session session, 52 | ssh_channel channel, 53 | const char *signal, 54 | int core, 55 | const char *errmsg, 56 | const char *lang, 57 | void *userdata) 58 | 59 | ctypedef int (*ssh_channel_pty_request_callback) (ssh_session session, 60 | ssh_channel channel, 61 | const char *term, 62 | int width, int height, 63 | int pxwidth, int pwheight, 64 | void *userdata) 65 | 66 | ctypedef int (*ssh_channel_shell_request_callback) (ssh_session session, 67 | ssh_channel channel, 68 | void *userdata) 69 | 70 | ctypedef void (*ssh_channel_auth_agent_req_callback) (ssh_session session, 71 | ssh_channel channel, 72 | void *userdata) 73 | 74 | ctypedef void (*ssh_channel_x11_req_callback) (ssh_session session, 75 | ssh_channel channel, 76 | int single_connection, 77 | const char *auth_protocol, 78 | const char *auth_cookie, 79 | uint32_t screen_number, 80 | void *userdata) 81 | 82 | ctypedef int (*ssh_channel_pty_window_change_callback) (ssh_session session, 83 | ssh_channel channel, 84 | int width, int height, 85 | int pxwidth, int pwheight, 86 | void *userdata) 87 | 88 | ctypedef int (*ssh_channel_exec_request_callback) (ssh_session session, 89 | ssh_channel channel, 90 | const char *command, 91 | void *userdata) 92 | 93 | ctypedef int (*ssh_channel_env_request_callback) (ssh_session session, 94 | ssh_channel channel, 95 | const char *env_name, 96 | const char *env_value, 97 | void *userdata) 98 | 99 | ctypedef int (*ssh_channel_subsystem_request_callback) (ssh_session session, 100 | ssh_channel channel, 101 | const char *subsystem, 102 | void *userdata) 103 | 104 | ctypedef int (*ssh_channel_write_wontblock_callback) (ssh_session session, 105 | ssh_channel channel, 106 | size_t bytes, 107 | void *userdata) 108 | 109 | struct ssh_channel_callbacks_struct: 110 | size_t size 111 | void *userdata 112 | ssh_channel_data_callback channel_data_function 113 | ssh_channel_eof_callback channel_eof_function 114 | ssh_channel_close_callback channel_close_function 115 | ssh_channel_signal_callback channel_signal_function 116 | ssh_channel_exit_status_callback channel_exit_status_function 117 | ssh_channel_exit_signal_callback channel_exit_signal_function 118 | ssh_channel_pty_request_callback channel_pty_request_function 119 | ssh_channel_shell_request_callback channel_shell_request_function 120 | ssh_channel_auth_agent_req_callback channel_auth_agent_req_function 121 | ssh_channel_x11_req_callback channel_x11_req_function 122 | ssh_channel_pty_window_change_callback channel_pty_window_change_function 123 | ssh_channel_exec_request_callback channel_exec_request_function 124 | ssh_channel_env_request_callback channel_env_request_function 125 | ssh_channel_subsystem_request_callback channel_subsystem_request_function 126 | ssh_channel_write_wontblock_callback channel_write_wontblock_function 127 | ctypedef ssh_channel_callbacks_struct * ssh_channel_callbacks 128 | 129 | int ssh_set_channel_callbacks(ssh_channel channel, ssh_channel_callbacks cb) 130 | -------------------------------------------------------------------------------- /src/pylibsshext/includes/sftp.pxd: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of the pylibssh library 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, see file LICENSE.rst in this 16 | # repository. 17 | # 18 | from posix.types cimport mode_t 19 | 20 | from libc cimport stdint 21 | 22 | from pylibsshext.includes.libssh cimport ssh_channel, ssh_session, ssh_string 23 | 24 | 25 | cdef extern from "libssh/sftp.h" nogil: 26 | 27 | struct sftp_session_struct: 28 | pass 29 | ctypedef sftp_session_struct * sftp_session 30 | 31 | struct sftp_file_struct: 32 | pass 33 | ctypedef sftp_file_struct * sftp_file 34 | 35 | struct sftp_attributes_struct: 36 | char *name 37 | char *longname 38 | stdint.uint32_t flags 39 | stdint.uint8_t type 40 | stdint.uint64_t size 41 | stdint.uint32_t uid 42 | stdint.uint32_t gid 43 | char *owner 44 | char *group 45 | stdint.uint32_t permissions 46 | stdint.uint64_t atime64 47 | stdint.uint32_t atime 48 | stdint.uint32_t atime_nseconds 49 | stdint.uint64_t createtime 50 | stdint.uint32_t createtime_nseconds 51 | stdint.uint64_t mtime64 52 | stdint.uint32_t mtime 53 | stdint.uint32_t mtime_nseconds 54 | ssh_string acl 55 | stdint.uint32_t extended_count 56 | ssh_string extended_type 57 | ssh_string extended_data 58 | ctypedef sftp_attributes_struct * sftp_attributes 59 | 60 | cdef int SSH_FX_OK 61 | cdef int SSH_FX_EOF 62 | cdef int SSH_FX_NO_SUCH_FILE 63 | cdef int SSH_FX_PERMISSION_DENIED 64 | cdef int SSH_FX_FAILURE 65 | cdef int SSH_FX_BAD_MESSAGE 66 | cdef int SSH_FX_NO_CONNECTION 67 | cdef int SSH_FX_CONNECTION_LOST 68 | cdef int SSH_FX_OP_UNSUPPORTED 69 | cdef int SSH_FX_INVALID_HANDLE 70 | cdef int SSH_FX_NO_SUCH_PATH 71 | cdef int SSH_FX_FILE_ALREADY_EXISTS 72 | cdef int SSH_FX_WRITE_PROTECT 73 | cdef int SSH_FX_NO_MEDIA 74 | 75 | sftp_session sftp_new(ssh_session session) 76 | int sftp_init(sftp_session sftp) 77 | void sftp_free(sftp_session sftp) 78 | 79 | sftp_file sftp_open(sftp_session session, const char *file, int accesstype, mode_t mode) 80 | int sftp_close(sftp_file file) 81 | ssize_t sftp_write(sftp_file file, const void *buf, size_t count) 82 | ssize_t sftp_read(sftp_file file, const void *buf, size_t count) 83 | int sftp_get_error(sftp_session sftp) 84 | 85 | sftp_attributes sftp_stat(sftp_session session, const char *path) 86 | 87 | 88 | cdef extern from "sys/stat.h" nogil: 89 | cdef int S_IRWXU 90 | -------------------------------------------------------------------------------- /src/pylibsshext/scp.pxd: -------------------------------------------------------------------------------- 1 | # distutils: libraries = ssh 2 | # 3 | # This file is part of the pylibssh library 4 | # 5 | # This library is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU Lesser General Public 7 | # License as published by the Free Software Foundation; either 8 | # version 2.1 of the License, or (at your option) any later version. 9 | # 10 | # This library is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public 16 | # License along with this library; if not, see file LICENSE.rst in this 17 | # repository. 18 | # 19 | from pylibsshext.includes cimport libssh 20 | from pylibsshext.session cimport Session 21 | 22 | 23 | cdef class SCP: 24 | cdef Session session 25 | cdef libssh.ssh_session _libssh_session 26 | -------------------------------------------------------------------------------- /src/pylibsshext/scp.pyx: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of the pylibssh library 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, see file LICENSE.rst in this 16 | # repository. 17 | 18 | import os 19 | 20 | from cpython.bytes cimport PyBytes_AS_STRING 21 | from cpython.mem cimport PyMem_Free, PyMem_Malloc 22 | 23 | from pylibsshext.errors cimport LibsshSCPException 24 | from pylibsshext.session cimport get_libssh_session 25 | 26 | 27 | SCP_MAX_CHUNK = 65_536 # 64kB 28 | 29 | 30 | cdef class SCP: 31 | def __cinit__(self, session): 32 | self.session = session 33 | self._libssh_session = get_libssh_session(session) 34 | 35 | def put(self, local_file, remote_file): 36 | """ 37 | Create an SCP channel and send a file to the remote host over that channel. 38 | 39 | :param local_file: The path on the local host where the file will be read from 40 | :type local_file: str 41 | 42 | :param remote_file: The path on the remote host where the file should be placed 43 | :type remote_file: str 44 | """ 45 | remote_file_b = remote_file 46 | if isinstance(remote_file_b, unicode): 47 | remote_file_b = remote_file.encode("utf-8") 48 | remote_dir_b, filename_b = os.path.split(remote_file_b) 49 | if not remote_dir_b: 50 | remote_dir_b = b"." 51 | 52 | with open(local_file, "rb") as f: 53 | file_stat = os.fstat(f.fileno()) 54 | file_size = file_stat.st_size 55 | file_mode = file_stat.st_mode & 0o777 56 | 57 | # Create the SCP session in write mode 58 | scp = libssh.ssh_scp_new(self._libssh_session, libssh.SSH_SCP_WRITE, remote_file_b) 59 | if scp is NULL: 60 | raise LibsshSCPException( 61 | "Allocating SCP session of remote file [{path!s}] for " 62 | "write failed with error [{err!s}]". 63 | format(path=remote_file, err=self._get_ssh_error_str()), 64 | ) 65 | 66 | # Initialize the SCP channel 67 | rc = libssh.ssh_scp_init(scp) 68 | if rc != libssh.SSH_OK: 69 | libssh.ssh_scp_free(scp) 70 | raise LibsshSCPException( 71 | "Initializing SCP session of remote file [{path!s}] for " 72 | "write failed with error [{err!s}]". 73 | format(path=remote_file, err=self._get_ssh_error_str()), 74 | ) 75 | 76 | try: 77 | # Read buffer 78 | read_buffer_size = min(file_size, SCP_MAX_CHUNK) 79 | 80 | # Begin to send to the file 81 | rc = libssh.ssh_scp_push_file(scp, filename_b, file_size, file_mode) 82 | if rc != libssh.SSH_OK: 83 | raise LibsshSCPException("Can't open remote file: %s" % self._get_ssh_error_str()) 84 | 85 | remaining_bytes_to_read = file_size 86 | while remaining_bytes_to_read > 0: 87 | # Read the chunk from local file 88 | read_bytes = min(remaining_bytes_to_read, read_buffer_size) 89 | read_buffer = f.read(read_bytes) 90 | 91 | # Write to the open file 92 | rc = libssh.ssh_scp_write(scp, PyBytes_AS_STRING(read_buffer), read_bytes) 93 | if rc != libssh.SSH_OK: 94 | raise LibsshSCPException("Can't write to remote file: %s" % self._get_ssh_error_str()) 95 | remaining_bytes_to_read -= read_bytes 96 | finally: 97 | libssh.ssh_scp_close(scp) 98 | libssh.ssh_scp_free(scp) 99 | 100 | return libssh.SSH_OK 101 | 102 | def get(self, remote_file, local_file): 103 | """ 104 | Create an SCP channel and retrieve a file from the remote host over that channel. 105 | 106 | :param remote_file: The path on the remote host where the file will be read from 107 | :type remote_file: str 108 | 109 | :param local_file: The path on the local host where the file should be placed 110 | :type local_file: str 111 | """ 112 | cdef char *read_buffer = NULL 113 | 114 | remote_file_b = remote_file 115 | if isinstance(remote_file_b, unicode): 116 | remote_file_b = remote_file.encode("utf-8") 117 | 118 | # Create the SCP session in read mode 119 | scp = libssh.ssh_scp_new(self._libssh_session, libssh.SSH_SCP_READ, remote_file_b) 120 | if scp is NULL: 121 | raise LibsshSCPException("Allocating SCP session of remote file [%s] for write failed with error [%s]" % (remote_file, self._get_ssh_error_str())) 122 | 123 | # Initialize the SCP channel 124 | rc = libssh.ssh_scp_init(scp) 125 | if rc != libssh.SSH_OK: 126 | libssh.ssh_scp_free(scp) 127 | raise LibsshSCPException("Initializing SCP session of remote file [%s] for write failed with error [%s]" % (remote_file, self._get_ssh_error_str())) 128 | 129 | try: 130 | # Request to pull the file 131 | rc = libssh.ssh_scp_pull_request(scp) 132 | if rc != libssh.SSH_SCP_REQUEST_NEWFILE: 133 | raise LibsshSCPException("Error receiving information about file: %s" % self._get_ssh_error_str()) 134 | 135 | size = libssh.ssh_scp_request_get_size(scp) 136 | mode = libssh.ssh_scp_request_get_permissions(scp) 137 | 138 | # cap the buffer size to reasonable number -- libssh will not return the whole data at once anyway 139 | read_buffer_size = min(size, SCP_MAX_CHUNK) 140 | read_buffer = PyMem_Malloc(read_buffer_size) 141 | if read_buffer is NULL: 142 | raise LibsshSCPException("Memory allocation error") 143 | 144 | # Indicate the transfer is ready to begin 145 | libssh.ssh_scp_accept_request(scp) 146 | if rc == libssh.SSH_ERROR: 147 | raise LibsshSCPException("Failed to start read request: %s" % self._get_ssh_error_str()) 148 | 149 | remaining_bytes_to_read = size 150 | with open(local_file, "wb") as f: 151 | while remaining_bytes_to_read > 0: 152 | requested_read_bytes = min(remaining_bytes_to_read, read_buffer_size) 153 | read_bytes = libssh.ssh_scp_read(scp, read_buffer, requested_read_bytes) 154 | if read_bytes == libssh.SSH_ERROR: 155 | raise LibsshSCPException("Error receiving file data: %s" % self._get_ssh_error_str()) 156 | 157 | py_file_bytes = read_buffer[:read_bytes] 158 | f.write(py_file_bytes) 159 | remaining_bytes_to_read -= read_bytes 160 | if mode >= 0: 161 | os.chmod(local_file, mode) 162 | 163 | # Make sure we have finished requesting files 164 | rc = libssh.ssh_scp_pull_request(scp) 165 | if rc != libssh.SSH_SCP_REQUEST_EOF: 166 | raise LibsshSCPException("Unexpected request: %s" % self._get_ssh_error_str()) 167 | 168 | finally: 169 | if read_buffer is not NULL: 170 | PyMem_Free(read_buffer) 171 | libssh.ssh_scp_close(scp) 172 | libssh.ssh_scp_free(scp) 173 | 174 | return libssh.SSH_OK 175 | 176 | def _get_ssh_error_str(self): 177 | return libssh.ssh_get_error(self._libssh_session) 178 | -------------------------------------------------------------------------------- /src/pylibsshext/session.pxd: -------------------------------------------------------------------------------- 1 | # distutils: libraries = ssh 2 | # 3 | # This file is part of the pylibssh library 4 | # 5 | # This library is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU Lesser General Public 7 | # License as published by the Free Software Foundation; either 8 | # version 2.1 of the License, or (at your option) any later version. 9 | # 10 | # This library is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public 16 | # License along with this library; if not, see file LICENSE.rst in this 17 | # repository. 18 | # 19 | from pylibsshext.includes cimport libssh 20 | 21 | 22 | cdef class Session: 23 | cdef libssh.ssh_session _libssh_session 24 | cdef _opts 25 | cdef _policy 26 | cdef _hash_py 27 | cdef _fingerprint_py 28 | cdef _keytype_py 29 | cdef _channel_callbacks 30 | 31 | cdef libssh.ssh_session get_libssh_session(Session session) 32 | -------------------------------------------------------------------------------- /src/pylibsshext/sftp.pxd: -------------------------------------------------------------------------------- 1 | # distutils: libraries = ssh 2 | # 3 | # This file is part of the pylibssh library 4 | # 5 | # This library is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU Lesser General Public 7 | # License as published by the Free Software Foundation; either 8 | # version 2.1 of the License, or (at your option) any later version. 9 | # 10 | # This library is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public 16 | # License along with this library; if not, see file LICENSE.rst in this 17 | # repository. 18 | # 19 | from pylibsshext.includes cimport libssh, sftp 20 | from pylibsshext.session cimport Session 21 | 22 | 23 | cdef class SFTP: 24 | cdef Session session 25 | cdef sftp.sftp_session _libssh_sftp_session 26 | -------------------------------------------------------------------------------- /src/pylibsshext/sftp.pyx: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of the pylibssh library 3 | # 4 | # This library is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # This library is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with this library; if not, see file LICENSE.rst in this 16 | # repository. 17 | 18 | from posix.fcntl cimport O_CREAT, O_RDONLY, O_TRUNC, O_WRONLY 19 | 20 | from cpython.bytes cimport PyBytes_AS_STRING 21 | from cpython.mem cimport PyMem_Free, PyMem_Malloc 22 | 23 | from pylibsshext.errors cimport LibsshSFTPException 24 | from pylibsshext.session cimport get_libssh_session 25 | 26 | 27 | SFTP_MAX_CHUNK = 32_768 # 32kB 28 | 29 | 30 | MSG_MAP = { 31 | sftp.SSH_FX_OK: "No error", 32 | sftp.SSH_FX_EOF: "End-of-file encountered", 33 | sftp.SSH_FX_NO_SUCH_FILE: "File doesn't exist", 34 | sftp.SSH_FX_PERMISSION_DENIED: "Permission denied", 35 | sftp.SSH_FX_FAILURE: "Generic failure", 36 | sftp.SSH_FX_BAD_MESSAGE: "Garbage received from server", 37 | sftp.SSH_FX_NO_CONNECTION: "No connection has been set up", 38 | sftp.SSH_FX_CONNECTION_LOST: "There was a connection, but we lost it", 39 | sftp.SSH_FX_OP_UNSUPPORTED: "Operation not supported by the server", 40 | sftp.SSH_FX_INVALID_HANDLE: "Invalid file handle", 41 | sftp.SSH_FX_NO_SUCH_PATH: "No such file or directory path exists", 42 | sftp.SSH_FX_FILE_ALREADY_EXISTS: "An attempt to create an already existing file or directory has been made", 43 | sftp.SSH_FX_WRITE_PROTECT: "We are trying to write on a write-protected filesystem", 44 | sftp.SSH_FX_NO_MEDIA: "No media in remote drive" 45 | } 46 | cdef class SFTP: 47 | def __cinit__(self, session): 48 | self.session = session 49 | self._libssh_sftp_session = sftp.sftp_new(get_libssh_session(session)) 50 | if self._libssh_sftp_session is NULL: 51 | raise LibsshSFTPException("Failed to create new session") 52 | if sftp.sftp_init(self._libssh_sftp_session) != libssh.SSH_OK: 53 | raise LibsshSFTPException("Error initializing SFTP session") 54 | 55 | def __dealloc__(self): 56 | if self._libssh_sftp_session is not NULL: 57 | sftp.sftp_free(self._libssh_sftp_session) 58 | self._libssh_sftp_session = NULL 59 | 60 | def put(self, local_file, remote_file): 61 | cdef sftp.sftp_file rf 62 | with open(local_file, "rb") as f: 63 | remote_file_b = remote_file 64 | if isinstance(remote_file_b, unicode): 65 | remote_file_b = remote_file.encode("utf-8") 66 | 67 | rf = sftp.sftp_open(self._libssh_sftp_session, remote_file_b, O_WRONLY | O_CREAT | O_TRUNC, sftp.S_IRWXU) 68 | if rf is NULL: 69 | raise LibsshSFTPException("Opening remote file [%s] for write failed with error [%s]" % (remote_file, self._get_sftp_error_str())) 70 | buffer = f.read(SFTP_MAX_CHUNK) 71 | 72 | while buffer != b"": 73 | length = len(buffer) 74 | written = sftp.sftp_write(rf, PyBytes_AS_STRING(buffer), length) 75 | if written != length: 76 | sftp.sftp_close(rf) 77 | raise LibsshSFTPException( 78 | "Writing to remote file [%s] failed with error [%s]" % ( 79 | remote_file, 80 | self._get_sftp_error_str(), 81 | ) 82 | ) 83 | buffer = f.read(SFTP_MAX_CHUNK) 84 | sftp.sftp_close(rf) 85 | 86 | def get(self, remote_file, local_file): 87 | cdef sftp.sftp_file rf 88 | cdef char *read_buffer = NULL 89 | cdef sftp.sftp_attributes attrs 90 | 91 | remote_file_b = remote_file 92 | if isinstance(remote_file_b, unicode): 93 | remote_file_b = remote_file.encode("utf-8") 94 | 95 | attrs = sftp.sftp_stat(self._libssh_sftp_session, remote_file_b) 96 | if attrs is NULL: 97 | raise LibsshSFTPException("Failed to stat the remote file [%s]. Error: [%s]" 98 | % (remote_file, self._get_sftp_error_str())) 99 | file_size = attrs.size 100 | 101 | rf = sftp.sftp_open(self._libssh_sftp_session, remote_file_b, O_RDONLY, sftp.S_IRWXU) 102 | if rf is NULL: 103 | raise LibsshSFTPException("Opening remote file [%s] for read failed with error [%s]" % (remote_file, self._get_sftp_error_str())) 104 | 105 | try: 106 | with open(local_file, 'wb') as f: 107 | buffer_size = min(SFTP_MAX_CHUNK, file_size) 108 | read_buffer = PyMem_Malloc(buffer_size) 109 | if read_buffer is NULL: 110 | raise LibsshSFTPException("Memory allocation error") 111 | 112 | while True: 113 | file_data = sftp.sftp_read(rf, read_buffer, sizeof(char) * buffer_size) 114 | if file_data == 0: 115 | break 116 | elif file_data < 0: 117 | sftp.sftp_close(rf) 118 | raise LibsshSFTPException("Reading data from remote file [%s] failed with error [%s]" 119 | % (remote_file, self._get_sftp_error_str())) 120 | 121 | bytes_written = f.write(read_buffer[:file_data]) 122 | if bytes_written and file_data != bytes_written: 123 | sftp.sftp_close(rf) 124 | raise LibsshSFTPException("Number of bytes [%s] read from remote file [%s]" 125 | " does not match number of bytes [%s] written to local file [%s]" 126 | " due to error [%s]" 127 | % (file_data, remote_file, bytes_written, local_file, self._get_sftp_error_str())) 128 | finally: 129 | if read_buffer is not NULL: 130 | PyMem_Free(read_buffer) 131 | sftp.sftp_close(rf) 132 | 133 | def close(self): 134 | if self._libssh_sftp_session is not NULL: 135 | sftp.sftp_free(self._libssh_sftp_session) 136 | self._libssh_sftp_session = NULL 137 | 138 | def _get_sftp_error_str(self): 139 | error = sftp.sftp_get_error(self._libssh_sftp_session) 140 | if error in MSG_MAP and error != sftp.SSH_FX_FAILURE: 141 | return MSG_MAP[error] 142 | return "Generic failure: %s" % self.session._get_session_error_str() 143 | -------------------------------------------------------------------------------- /tests/_service_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Test util helpers.""" 4 | 5 | import getpass 6 | import subprocess 7 | import sys 8 | import time 9 | 10 | 11 | IS_MACOS = sys.platform == 'darwin' 12 | _MACOS_RECONNECT_ATTEMPT_DELAY = 0.06 13 | _LINUX_RECONNECT_ATTEMPT_DELAY = 0.002 14 | _DEFAULT_RECONNECT_ATTEMPT_DELAY = ( 15 | _MACOS_RECONNECT_ATTEMPT_DELAY 16 | if IS_MACOS 17 | else _LINUX_RECONNECT_ATTEMPT_DELAY 18 | ) 19 | 20 | 21 | def wait_for_svc_ready_state( 22 | host, 23 | port, 24 | clientkey_path, 25 | max_conn_attempts=40, 26 | reconnect_attempt_delay=_DEFAULT_RECONNECT_ATTEMPT_DELAY, 27 | ): 28 | """Verify that the serivce is up and running. 29 | 30 | :param host: Hostname. 31 | :type host: str 32 | 33 | :param port: Port. 34 | :type port: int 35 | 36 | :param clientkey_path: Path to the client private key. 37 | :type clientkey_path: pathlib.Path 38 | 39 | :param max_conn_attempts: Number of tries when connecting. 40 | :type max_conn_attempts: int 41 | 42 | :param reconnect_attempt_delay: Time to sleep between retries. 43 | :type reconnect_attempt_delay: float 44 | 45 | # noqa: DAR401 46 | """ 47 | cmd = [ # noqa: WPS317 48 | '/usr/bin/ssh', 49 | '-l', getpass.getuser(), 50 | '-i', str(clientkey_path), 51 | '-p', str(port), 52 | '-o', 'UserKnownHostsFile=/dev/null', 53 | '-o', 'StrictHostKeyChecking=no', 54 | host, 55 | '--', 'exit 0', 56 | ] 57 | 58 | attempts = 0 59 | rc = -1 60 | while attempts < max_conn_attempts and rc != 0: 61 | check_result = subprocess.run(cmd) 62 | rc = check_result.returncode 63 | if rc != 0: 64 | time.sleep(reconnect_attempt_delay) 65 | 66 | if rc != 0: 67 | raise TimeoutError('Timed out waiting for a successful connection') 68 | 69 | 70 | def ensure_ssh_session_connected( # noqa: WPS317 71 | ssh_session, sshd_addr, ssh_clientkey_path, # noqa: WPS318 72 | ): 73 | """Attempt connecting to the SSH server until successful. 74 | 75 | :param ssh_session: SSH session object. 76 | :type ssh_session: pylibsshext.session.Session 77 | 78 | :param sshd_addr: Hostname and port tuple. 79 | :type sshd_addr: tuple[str, int] 80 | 81 | :param ssh_clientkey_path: Hostname and port tuple. 82 | :type ssh_clientkey_path: pathlib.Path 83 | """ 84 | hostname, port = sshd_addr 85 | ssh_session.connect( 86 | host=hostname, 87 | port=port, 88 | user=getpass.getuser(), 89 | private_key=ssh_clientkey_path.read_bytes(), 90 | host_key_checking=False, 91 | look_for_keys=False, 92 | ) 93 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # pylint: disable=redefined-outer-name 3 | 4 | """Pytest plugins and fixtures configuration.""" 5 | 6 | import logging 7 | import shutil 8 | import socket 9 | import subprocess 10 | from functools import partial 11 | 12 | import pytest 13 | from _service_utils import ( # noqa: WPS436 14 | ensure_ssh_session_connected, wait_for_svc_ready_state, 15 | ) 16 | 17 | from pylibsshext.session import Session 18 | 19 | 20 | _DIR_PRIV_RW_OWNER = 0o700 21 | _FILE_PRIV_RW_OWNER = 0o600 22 | 23 | 24 | @pytest.fixture 25 | def free_port_num(): 26 | """Detect a free port number using a temporary ephemeral port. 27 | 28 | :returns: An unoccupied port number. 29 | :rtype: int 30 | 31 | # noqa: 401 32 | """ 33 | # NOTE: It should work most of the time except for the races. 34 | # This technique is suboptimal because we temporary occupy the port 35 | # and then close the socket. So there's a small time slot between 36 | # us closing the port and sshd trying to listen to it when some 37 | # other process can intercept it. Ideally, sshd should rely on an 38 | # ephemeral port but it doesn't support Port=0. I tried to work 39 | # around this by emulating socket activation mode but 40 | # unsuccessfully so far. 41 | sock = socket.socket(socket.AF_INET) 42 | 43 | try: 44 | sock.bind(('127.0.0.1', 0)) 45 | except socket.error: 46 | sock.close() 47 | raise 48 | 49 | try: # noqa: WPS501 50 | return sock.getsockname()[1] 51 | finally: 52 | sock.close() 53 | 54 | 55 | @pytest.fixture 56 | def sshd_path(tmp_path): 57 | """Create a tmp dir for sshd. 58 | 59 | :return: Temporary SSH dir path. 60 | :rtype: pathlib.Path 61 | 62 | # noqa: DAR101 63 | """ 64 | path = tmp_path / 'sshd' 65 | path.mkdir() 66 | path.chmod(_DIR_PRIV_RW_OWNER) 67 | return path 68 | 69 | 70 | @pytest.fixture 71 | def sshd_hostkey_path(sshd_path): 72 | """Generate a keypair for SSHD. 73 | 74 | :return: Private key path for SSHD server. 75 | :rtype: pathlib.Path 76 | 77 | # noqa: DAR101 78 | """ 79 | path = sshd_path / 'ssh_host_rsa_key' 80 | keygen_cmd = 'ssh-keygen', '-N', '', '-f', str(path) 81 | subprocess.check_call(keygen_cmd) 82 | path.chmod(_FILE_PRIV_RW_OWNER) 83 | return path 84 | 85 | 86 | @pytest.fixture 87 | def ssh_clientkey_path(sshd_path): 88 | """Generate an SSH keypair. 89 | 90 | :return: Private SSH key path. 91 | :rtype: pathlib.Path 92 | 93 | # noqa: DAR101 94 | """ 95 | path = sshd_path / 'ssh_client_rsa_key' 96 | keygen_cmd = ( # noqa: WPS317 97 | 'ssh-keygen', 98 | '-t', 'rsa', 99 | '-b', '8192', 100 | '-C', 'ansible-pylibssh integration tests key', 101 | '-N', '', 102 | '-f', str(path), 103 | ) 104 | subprocess.check_call(keygen_cmd) 105 | path.chmod(_FILE_PRIV_RW_OWNER) 106 | return path 107 | 108 | 109 | @pytest.fixture 110 | def ssh_client_session(ssh_session_connect): 111 | """Authenticate against SSHD with a private SSH key. 112 | 113 | :yields: Pre-authenticated SSH session. 114 | :ytype: pylibsshext.session.Session 115 | 116 | # noqa: DAR101 117 | """ 118 | ssh_session = Session() 119 | # TODO Adjust when #597 will be merged 120 | ssh_session.set_log_level(logging.CRITICAL) 121 | ssh_session_connect(ssh_session) 122 | try: # noqa: WPS501 123 | yield ssh_session 124 | finally: 125 | ssh_session.close() 126 | del ssh_session # noqa: WPS420 127 | 128 | 129 | @pytest.fixture 130 | def ssh_session_connect(sshd_addr, ssh_clientkey_path): 131 | """ 132 | Authenticate existing session object against SSHD with a private SSH key. 133 | 134 | It returns a function that takes session as parameter. 135 | 136 | :returns: Function that will connect the session. 137 | :rtype: Callback 138 | """ 139 | return partial( 140 | ensure_ssh_session_connected, 141 | sshd_addr=sshd_addr, 142 | ssh_clientkey_path=ssh_clientkey_path, 143 | ) 144 | 145 | 146 | @pytest.fixture 147 | def ssh_authorized_keys_path(sshd_path, ssh_clientkey_path): 148 | """Populate authorized_keys. 149 | 150 | :return: `authorized_keys` file path. 151 | :rtype: pathlib.Path 152 | 153 | # noqa: DAR101 154 | """ 155 | path = sshd_path / 'authorized_keys' 156 | public_key_path = ssh_clientkey_path.with_suffix('.pub') 157 | shutil.copyfile(str(public_key_path), str(path)) 158 | path.chmod(_FILE_PRIV_RW_OWNER) 159 | return path 160 | 161 | 162 | @pytest.fixture 163 | def sshd_addr(free_port_num, ssh_authorized_keys_path, sshd_hostkey_path, sshd_path, ssh_clientkey_path): 164 | """Spawn an instance of sshd on a free port. 165 | 166 | :raises RuntimeError: If spawning SSHD failed. 167 | 168 | :yields: SSHD host/port address. 169 | :ytype: tuple 170 | 171 | # noqa: DAR101 172 | """ 173 | hostname = '127.0.0.1' 174 | opt = '-o' 175 | cmd = ( # noqa: WPS317 176 | '/usr/sbin/sshd', 177 | '-D', 178 | '-f', '/dev/null', 179 | '-E', '/dev/stderr', 180 | opt, 'LogLevel=DEBUG3', 181 | opt, 'HostKey={key!s}'.format(key=sshd_hostkey_path), 182 | opt, 'PidFile={pid!s}'.format(pid=sshd_path / 'sshd.pid'), 183 | 184 | # NOTE: 'UsePAM no' is not supported on Fedora. 185 | # Ref: https://bugzilla.redhat.com/show_bug.cgi?id=770756#c1 186 | opt, 'UsePAM=yes', 187 | opt, 'PasswordAuthentication=no', 188 | opt, 'ChallengeResponseAuthentication=no', 189 | opt, 'GSSAPIAuthentication=no', 190 | 191 | opt, 'StrictModes=no', 192 | opt, 'PermitEmptyPasswords=yes', 193 | opt, 'PermitRootLogin=yes', 194 | opt, 'HostbasedAuthentication=no', 195 | opt, 'IgnoreUserKnownHosts=yes', 196 | opt, 'Port={port:d}'.format(port=free_port_num), # port before addr 197 | opt, 'ListenAddress={host!s}'.format(host=hostname), # addr after port 198 | opt, 'AuthorizedKeysFile={auth_keys!s}'.format(auth_keys=ssh_authorized_keys_path), 199 | opt, 'AcceptEnv=LANG LC_*', 200 | opt, 'Subsystem=sftp internal-sftp', 201 | ) 202 | proc = subprocess.Popen(cmd) 203 | 204 | wait_for_svc_ready_state(hostname, free_port_num, ssh_clientkey_path) 205 | 206 | if proc.returncode: 207 | raise RuntimeError('sshd boom 💣') 208 | try: # noqa: WPS501 209 | yield hostname, free_port_num 210 | finally: 211 | proc.terminate() 212 | proc.wait() 213 | -------------------------------------------------------------------------------- /tests/integration/sshd_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Sanity tests for sshd-related helpers.""" 4 | 5 | MAX_PORT_NUMBER = 65535 6 | 7 | 8 | def test_sshd_addr_fixture_port(sshd_addr, ssh_client_session): 9 | """Smoke-test sshd_addr fixture. 10 | 11 | # noqa: DAR101 12 | """ 13 | _host, port = sshd_addr 14 | assert 0 < port <= MAX_PORT_NUMBER 15 | -------------------------------------------------------------------------------- /tests/unit/channel_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Tests suite for channel.""" 4 | 5 | import gc 6 | import signal 7 | import time 8 | 9 | import pytest 10 | 11 | from pylibsshext.session import Session 12 | 13 | 14 | COMMAND_TIMEOUT = 30 15 | POLL_EXIT_CODE_TIMEOUT = 5 16 | POLL_TIMEOUT = 5000 17 | 18 | 19 | @pytest.fixture 20 | def ssh_channel(ssh_client_session): 21 | """Initialize a channel and tear it down after testing. 22 | 23 | :param ssh_client_session: A pre-authenticated SSH session. 24 | :type ssh_client_session: pylibsshext.session.Session 25 | 26 | :yields: A libssh channel instance. 27 | :ytype: pylibsshext.channel.Channel 28 | """ 29 | chan = ssh_client_session.new_channel() 30 | try: # noqa: WPS501 31 | yield chan 32 | finally: 33 | chan.close() 34 | 35 | 36 | def exec_second_command(ssh_channel): 37 | """Check the standard output of ``exec_command()`` as a string.""" 38 | u_cmd = ssh_channel.exec_command('echo -n Hello Again') 39 | assert u_cmd.returncode == 0 40 | assert u_cmd.stderr.decode() == '' # noqa: WPS302 41 | assert u_cmd.stdout.decode() == u'Hello Again' # noqa: WPS302 42 | 43 | 44 | def test_exec_command(ssh_channel): 45 | """Test getting the output of a remotely executed command.""" 46 | u_cmd = ssh_channel.exec_command('echo -n Hello World') 47 | assert u_cmd.returncode == 0 48 | assert u_cmd.stderr.decode() == '' 49 | assert u_cmd.stdout.decode() == u'Hello World' # noqa: WPS302 50 | # Test that repeated calls to exec_command do not segfault. 51 | 52 | # NOTE: Call `exec_command()` once again from another function to 53 | # NOTE: force it to happen in another place of the call stack, 54 | # NOTE: making sure that the context is different from one in this 55 | # NOTE: this test function. The resulting call stack will end up 56 | # NOTE: being more random. 57 | exec_second_command(ssh_channel) 58 | 59 | 60 | def test_exec_command_stderr(ssh_channel): 61 | """Test getting the stderr of a remotely executed command.""" 62 | u_cmd = ssh_channel.exec_command('echo -n Hello World 1>&2') 63 | assert u_cmd.returncode == 0 64 | assert u_cmd.stderr.decode() == u'Hello World' # noqa: WPS302 65 | assert u_cmd.stdout.decode() == '' 66 | 67 | 68 | def test_double_close(ssh_channel): 69 | """Test that closing the channel multiple times doesn't explode.""" 70 | for _ in range(3): # noqa: WPS122 71 | ssh_channel.close() 72 | 73 | 74 | def test_channel_exit_status(ssh_channel): 75 | """Test retrieving a channel exit status upon close.""" 76 | ssh_channel.close() 77 | assert ssh_channel.get_channel_exit_status() == -1 78 | 79 | 80 | def test_read_bulk_response(ssh_client_session): 81 | """Test getting the output of a remotely executed command.""" 82 | ssh_shell = ssh_client_session.invoke_shell() 83 | ssh_shell.sendall(b'echo -n Hello World') 84 | response = b'' 85 | timeout = 2 86 | while b'Hello World' not in response: 87 | response += ssh_shell.read_bulk_response() 88 | time.sleep(timeout) 89 | timeout += 2 90 | if timeout == COMMAND_TIMEOUT: 91 | break 92 | 93 | assert b'Hello World' in response # noqa: WPS302 94 | 95 | 96 | def test_request_exec(ssh_channel): 97 | """Test direct call to request_exec.""" 98 | ssh_channel.request_exec('exit 1') 99 | 100 | rc = -1 101 | while rc == -1: 102 | ssh_channel.poll(timeout=POLL_EXIT_CODE_TIMEOUT) 103 | rc = ssh_channel.get_channel_exit_status() 104 | assert rc == 1 105 | 106 | 107 | def test_send_eof(ssh_channel): 108 | """Test send_eof correctly terminates input stream.""" 109 | ssh_channel.request_exec('cat') 110 | ssh_channel.send_eof() 111 | 112 | rc = -1 113 | while rc == -1: 114 | ssh_channel.poll(timeout=POLL_EXIT_CODE_TIMEOUT) 115 | rc = ssh_channel.get_channel_exit_status() 116 | assert rc == 0 117 | 118 | 119 | def test_send_signal(ssh_channel): 120 | """Test send_signal correctly forwards signal to the process.""" 121 | ssh_channel.request_exec('bash -c \'trap "exit 1" SIGUSR1; echo ready; sleep 5; exit 0\'') 122 | 123 | # Wait until the process is ready to receive signal 124 | output = '' 125 | while not output.startswith('ready'): 126 | ssh_channel.poll(timeout=POLL_TIMEOUT) 127 | output += ssh_channel.recv().decode('utf-8') 128 | 129 | # Send SIGUSR1 130 | ssh_channel.send_signal(signal.SIGUSR1) 131 | 132 | rc = -1 133 | while rc == -1: 134 | ssh_channel.poll(timeout=POLL_EXIT_CODE_TIMEOUT) 135 | rc = ssh_channel.get_channel_exit_status() 136 | 137 | assert rc == 1 138 | 139 | 140 | def test_recv_eof(ssh_channel): 141 | """ 142 | Test that reading EOF does not raise error. 143 | 144 | SystemError: Negative size passed to PyBytes_FromStringAndSize 145 | """ 146 | ssh_channel.request_exec('exit 0') 147 | ssh_channel.poll(timeout=POLL_TIMEOUT) 148 | assert ssh_channel.is_eof 149 | ssh_channel.recv() 150 | 151 | 152 | def test_is_eof(ssh_channel): 153 | """Test that EOF-state is correctly obtained with is_eof.""" 154 | ssh_channel.request_exec('exit 0') 155 | ssh_channel.poll(timeout=POLL_TIMEOUT) 156 | assert ssh_channel.is_eof 157 | 158 | 159 | def test_destructor(ssh_session_connect): 160 | """ 161 | Garbage collector can destroy session before channel. 162 | 163 | Test that this event does not cause a segfault in channels destructor. 164 | """ 165 | def _do_not_crash(): # noqa: WPS430 # required to create a garbage-collection scope 166 | ssh_session = Session() 167 | ssh_session_connect(ssh_session) 168 | ssh_channel = ssh_session.new_channel() # noqa: F841 # setting a non-accessed var is needed for testing GC 169 | 170 | # Without fix, garbage collector first deletes session and we segfault 171 | # in channel destructor when trying to access low-level C session object. 172 | gc.disable() 173 | try: # noqa: WPS229, WPS501 # we need to reenable gc if anything happens 174 | gc.collect() 175 | _do_not_crash() 176 | gc.collect(0) # the test will segfault without the fix 177 | finally: 178 | gc.enable() 179 | -------------------------------------------------------------------------------- /tests/unit/scp_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Tests suite for scp.""" 4 | 5 | import os 6 | import random 7 | import string 8 | 9 | import pytest 10 | 11 | from pylibsshext.errors import LibsshSCPException 12 | from pylibsshext.scp import SCP_MAX_CHUNK 13 | 14 | 15 | @pytest.fixture 16 | def ssh_scp(ssh_client_session): 17 | """Initialize an SCP session and destroy it after testing.""" 18 | scp = ssh_client_session.scp() 19 | try: # noqa: WPS501 20 | yield scp 21 | finally: 22 | del scp # noqa: WPS420 23 | 24 | 25 | @pytest.fixture( 26 | params=(32, SCP_MAX_CHUNK + 1), 27 | ids=('small-payload', 'large-payload'), 28 | ) 29 | def transmit_payload(request: pytest.FixtureRequest): 30 | """Generate a binary test payload. 31 | 32 | The choice 32 is arbitrary small value. 33 | 34 | The choice SCP_CHUNK_SIZE + 1 (64kB + 1B) is meant to be 1B larger than the chunk 35 | size used in :file:`scp.pyx` to make sure we excercise at least two rounds of 36 | reading/writing. 37 | """ 38 | payload_len = request.param 39 | random_bytes = [ord(random.choice(string.printable)) for _ in range(payload_len)] 40 | return bytes(random_bytes) 41 | 42 | 43 | @pytest.fixture 44 | def file_paths_pair(tmp_path, transmit_payload): 45 | """Populate a source file and make a destination path.""" 46 | src_path = tmp_path / 'src-file.txt' 47 | dst_path = tmp_path / 'dst-file.txt' 48 | src_path.write_bytes(transmit_payload) 49 | return src_path, dst_path 50 | 51 | 52 | @pytest.fixture 53 | def src_path(file_paths_pair): 54 | """Return a data source path.""" 55 | return file_paths_pair[0] 56 | 57 | 58 | @pytest.fixture 59 | def dst_path(file_paths_pair): 60 | """Return a data destination path.""" 61 | path = file_paths_pair[1] 62 | assert not path.exists() 63 | return path 64 | 65 | 66 | def test_put(dst_path, src_path, ssh_scp, transmit_payload): 67 | """Check that SCP file transfer works.""" 68 | ssh_scp.put(str(src_path), str(dst_path)) 69 | assert dst_path.read_bytes() == transmit_payload 70 | 71 | 72 | def test_get(dst_path, src_path, ssh_scp, transmit_payload): 73 | """Check that SCP file download works.""" 74 | ssh_scp.get(str(src_path), str(dst_path)) 75 | assert dst_path.read_bytes() == transmit_payload 76 | 77 | 78 | @pytest.fixture 79 | def path_to_non_existent_src_file(tmp_path): 80 | """Return a remote path that does not exist.""" 81 | path = tmp_path / 'non-existing.txt' 82 | assert not path.exists() 83 | return path 84 | 85 | 86 | def test_copy_from_non_existent_remote_path(path_to_non_existent_src_file, ssh_scp): 87 | """Check that SCP file download raises exception if the remote file is missing.""" 88 | error_msg = '^Error receiving information about file:' 89 | with pytest.raises(LibsshSCPException, match=error_msg): 90 | ssh_scp.get(str(path_to_non_existent_src_file), os.devnull) 91 | 92 | 93 | @pytest.fixture 94 | def pre_existing_file_path(tmp_path): 95 | """Return local path for a pre-populated file.""" 96 | path = tmp_path / 'pre-existing-file.txt' 97 | path.write_bytes(b'whatever') 98 | return path 99 | 100 | 101 | def test_get_existing_local(pre_existing_file_path, src_path, ssh_scp, transmit_payload): 102 | """Check that SCP file download works and overwrites local file if it exists.""" 103 | ssh_scp.get(str(src_path), str(pre_existing_file_path)) 104 | assert pre_existing_file_path.read_bytes() == transmit_payload 105 | -------------------------------------------------------------------------------- /tests/unit/session_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Tests suite for session.""" 4 | 5 | import pytest 6 | 7 | from pylibsshext.errors import LibsshSessionException 8 | from pylibsshext.session import Session 9 | 10 | 11 | def test_make_session(): 12 | """Smoke-test Session instance creation.""" 13 | assert Session() 14 | 15 | 16 | def test_make_session_close_connect(): 17 | """Make sure the session is usable after call to close().""" 18 | session = Session() 19 | session.close() 20 | error_msg = '^ssh connect failed: Hostname required$' 21 | with pytest.raises(LibsshSessionException, match=error_msg): 22 | session.connect() 23 | 24 | 25 | def test_session_connection_refused(free_port_num): 26 | """Test that connecting to a missing service raises an error.""" 27 | error_msg = '^ssh connect failed: Connection refused$' 28 | ssh_session = Session() 29 | with pytest.raises(LibsshSessionException, match=error_msg): 30 | ssh_session.connect(host='127.0.0.1', port=free_port_num) 31 | -------------------------------------------------------------------------------- /tests/unit/sftp_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Tests suite for sftp.""" 4 | 5 | import random 6 | import string 7 | import uuid 8 | 9 | import pytest 10 | 11 | from pylibsshext.sftp import SFTP_MAX_CHUNK 12 | 13 | 14 | @pytest.fixture 15 | def sftp_session(ssh_client_session): 16 | """Initialize an SFTP session and destroy it after testing.""" 17 | sftp_sess = ssh_client_session.sftp() 18 | try: # noqa: WPS501 19 | yield sftp_sess 20 | finally: 21 | sftp_sess.close() 22 | del sftp_sess # noqa: WPS420 23 | 24 | 25 | @pytest.fixture( 26 | params=(32, SFTP_MAX_CHUNK + 1), 27 | ids=('small-payload', 'large-payload'), 28 | ) 29 | def transmit_payload(request: pytest.FixtureRequest) -> bytes: 30 | """Generate binary test payloads of assorted sizes. 31 | 32 | The choice 32 is arbitrary small value. 33 | 34 | The choice SFTP_MAX_CHUNK + 1 (32kB + 1B) is meant to be 1B larger than the chunk 35 | size used in :file:`sftp.pyx` to make sure we excercise at least two rounds of 36 | reading/writing. 37 | """ 38 | payload_len = request.param 39 | random_bytes = [ord(random.choice(string.printable)) for _ in range(payload_len)] 40 | return bytes(random_bytes) 41 | 42 | 43 | @pytest.fixture 44 | def file_paths_pair(tmp_path, transmit_payload): 45 | """Populate a source file and make a destination path.""" 46 | src_path = tmp_path / 'src-file.txt' 47 | dst_path = tmp_path / 'dst-file.txt' 48 | src_path.write_bytes(transmit_payload) 49 | return src_path, dst_path 50 | 51 | 52 | @pytest.fixture 53 | def src_path(file_paths_pair): 54 | """Return a data source path.""" 55 | return file_paths_pair[0] 56 | 57 | 58 | @pytest.fixture 59 | def dst_path(file_paths_pair): 60 | """Return a data destination path.""" 61 | path = file_paths_pair[1] 62 | assert not path.exists() 63 | return path 64 | 65 | 66 | @pytest.fixture 67 | def other_payload(): 68 | """Generate a binary test payload.""" 69 | uuid_name = uuid.uuid4() 70 | return 'Original content: {name!s}'.format(name=uuid_name).encode() 71 | 72 | 73 | @pytest.fixture 74 | def pre_existing_dst_path(dst_path, other_payload): 75 | """Return a data destination path.""" 76 | dst_path.write_bytes(other_payload) 77 | assert dst_path.exists() 78 | return dst_path 79 | 80 | 81 | def test_make_sftp(sftp_session): 82 | """Smoke-test SFTP instance creation.""" 83 | assert sftp_session 84 | 85 | 86 | def test_put(dst_path, src_path, sftp_session, transmit_payload): 87 | """Check that SFTP file transfer works.""" 88 | sftp_session.put(str(src_path), str(dst_path)) 89 | assert dst_path.read_bytes() == transmit_payload 90 | 91 | 92 | def test_get(dst_path, src_path, sftp_session, transmit_payload): 93 | """Check that SFTP file download works.""" 94 | sftp_session.get(str(src_path), str(dst_path)) 95 | assert dst_path.read_bytes() == transmit_payload 96 | 97 | 98 | def test_get_existing(pre_existing_dst_path, src_path, sftp_session, transmit_payload): 99 | """Check that SFTP file download works when target file exists.""" 100 | sftp_session.get(str(src_path), str(pre_existing_dst_path)) 101 | assert pre_existing_dst_path.read_bytes() == transmit_payload 102 | 103 | 104 | def test_put_existing(pre_existing_dst_path, src_path, sftp_session, transmit_payload): 105 | """Check that SFTP file upload works when target file exists.""" 106 | sftp_session.put(str(src_path), str(pre_existing_dst_path)) 107 | assert pre_existing_dst_path.read_bytes() == transmit_payload 108 | -------------------------------------------------------------------------------- /tests/unit/version_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Tests for the version info representation.""" 4 | 5 | import pytest 6 | 7 | from pylibsshext import ( 8 | __full_version__, __libssh_version__, __version__, __version_info__, 9 | ) 10 | 11 | 12 | pytestmark = pytest.mark.smoke 13 | 14 | 15 | def test_dunder_version(): 16 | """Check that the version string has at least 3 parts.""" 17 | assert __version__.count('.') >= 2 18 | 19 | 20 | def test_dunder_version_info(): 21 | """Check that the version info tuple looks legitimate.""" 22 | assert isinstance(__version_info__, tuple) 23 | assert len(__version_info__) >= 3 24 | assert all( 25 | isinstance(digit, int) 26 | for digit in __version_info__[:2] 27 | ) 28 | 29 | 30 | def test_dunder_full_version(): 31 | """Check that the full version mentions the wrapper and the lib.""" 32 | assert __version__ in __full_version__ 33 | assert __libssh_version__ in __full_version__ 34 | 35 | 36 | def test_dunder_libssh_version(): 37 | """Check that libssh version looks valid.""" 38 | assert __libssh_version__.count('.') == 2 39 | --------------------------------------------------------------------------------