Compare commits

...

666 Commits

Author SHA1 Message Date
Alex Willmer 8ed467911b
Merge pull request #1433 from moreati/prepare-0.3.39
Prepare 0.3.39
1 day ago
Alex Willmer 49c6b93009 Begin 0.3.40dev 1 day ago
Alex Willmer 11d01e7109 Prepare v0.3.39 1 day ago
Alex Willmer f2498689df
Merge pull request #1432 from moreati/issue485-fileservice
mitogen: Add explicit Message encoding, send FileService data as raw binary
1 day ago
Alex Willmer 1402328e3e mitogen: Send FileService content as ENC_BIN
This should speed up file transfer greatly on Python 3.x, because it avoids
the compatibility shim for byte strings in Pickle protocol 2.
2 days ago
Alex Willmer 2de7bf58a6 mitogen: Add explicit binary Message encoding, marked using ENC_BIN 2 days ago
Alex Willmer b25b42e028 mitogen: Explicitly mark pickled messages, re-uses magic header field 2 days ago
Alex Willmer 52b8b767e5
Merge pull request #1431 from moreati/issue1430-resourcereader
mitogen: Pickle GET_REQUEST as textual strings (2.x: unicode, 3.x: str)
2 days ago
Alex Willmer e3499a643c mitogen: Pickle GET_REQUEST as textual strings (2.x: unicode, 3.x: str)
Pickle handles encoding of unicode itself, there's no need to manually do it
and when using Pickle protocol 2 byte strings are inefficient going between
Python 2.x and 3.x.
2 days ago
Alex Willmer 8b1740b245
Merge pull request #1427 from moreati/prepare-0.3.38
Prepare 0.3.38
5 days ago
Alex Willmer a2ad876474 Begin 0.3.39dev 5 days ago
Alex Willmer af0eab071e Prepare v0.3.38 5 days ago
Alex Willmer dff14efeef
Merge pull request #1425 from moreati/benchmarks-cleanup
Benchmarks cleanup
6 days ago
Alex Willmer 9d70e83221 mitogen: Consolidate `range` & `xrange` polyfills -> `mitogen.core.range` 6 days ago
Alex Willmer c9236b2360 tests: Parameterize and format output of large message benchmark 6 days ago
Alex Willmer 2a172a3ad7 tests: Parameterize throughput benchmark 6 days ago
Alex Willmer 088a640b6e tests: Standardise output of connection benchmarks 6 days ago
Alex Willmer 0d7a549182 tests: Parameterize connection benchmarks 6 days ago
Alex Willmer f4d942c197 tests: Group and unify naming of connection benchmarks 6 days ago
Alex Willmer 75b9c7255b
Merge pull request #1418 from moreati/msg-repr
mitogen: Format Message src & dst as `<context>:<handle>`
2 weeks ago
Alex Willmer 2403d4570b mitogen: Format Message src & dst of as <context>:<handle>
The notation is inspired by host:port notation in a URL. In Mitogen context id
and handles fill roughly the same role as IP and port in TCP or UDP.
2 weeks ago
Alex Willmer ba6e612ec4 mitogen: Fix docstring typo 2 weeks ago
Alex Willmer 3badddc908
Merge pull request #1415 from moreati/import-diet
mitogen: Put fallbacks & polyfills with `if sys.version_info` blocks
2 weeks ago
Alex Willmer 4d372cb160 mitogen: Last few `except ImportError` -> `if sys.version_info ...` 2 weeks ago
Alex Willmer 0447950acd mitogen: Replace uses mitogen.core.{PY24,PY3} with sys.version_info
Helps static analysis by type checkers, LSPs, etc.
2 weeks ago
Alex Willmer 52ec693ef7 mitogen: Provide mitogen.core exception fallbacks based on sys.version_info
Simplify work of static type checkers, LSPs, etc.
2 weeks ago
Alex Willmer 08a3f271f3 mitogen: Provide mitogen.core.{all,any} based on sys.version_info
Simplify work of static type checkers, LSPs, etc.
2 weeks ago
Alex Willmer 9bede962b3 mitogen: Provide mitogen.core.threading* based on sys.version_info
Simplify work of static type checkers, LSPs, etc.
2 weeks ago
Alex Willmer 55bd5fd7a4 mitogen: Provide mitogen.core.next based on sys.version_info
Simplify work of static type checkers, LSPs, etc.
2 weeks ago
Alex Willmer d1ee8c788f mitogen: Configure mitogen.core.str_partition etc. based on sys.version_info
Simplify work of static type checkers, LSPs, etc.
2 weeks ago
Alex Willmer 67264ed174 mitogen: Configure mitogen.core.now based on sys.version_info
Simplify work of static type checkers, LSPs, etc.
2 weeks ago
Alex Willmer 94b2f5d8d6 mitogen: Guard importlib imports with sys.version_info
Reduce number of doomed mitogen.core.Importer GET_MODULE requests from e.g.
Python 2.x contexts. Simplify work of static type checkers, LSPs, etc.
2 weeks ago
Alex Willmer cd58b7eac1 mitogen: Avoid import of linecache on Python >= 2.5
It's only needed for a workaround on Python 2.4.
2 weeks ago
Alex Willmer 384d37f630 mitogen: Use built-in _codecs, eliminate direct import of encodings package
The package will still get imported indirectly by _something_, but every
little helps.
2 weeks ago
Alex Willmer 5011263bf0 mitogen: Use built-in _codecs module to encode latin1
Streamlines initilialzation slightly. Next commit will do the same for utf-8.
2 weeks ago
Alex Willmer c6d6ea8432
Merge pull request #1411 from moreati/pre-0.3.37
Prepare 0.3.37
3 weeks ago
Alex Willmer 1c5a1a3d72 Begin 0.3.38dev 3 weeks ago
Alex Willmer abe3671c7b Prepare v0.3.37 3 weeks ago
Alex Willmer 34fddee719
Merge pull request #1410 from moreati/issue1407
mitogen: Fix AttributeError in mitogen.profiler
3 weeks ago
Alex Willmer 0e2da7deb5 mitogen: Fix AttributeError in mitogen.profiler 3 weeks ago
Alex Willmer 6c81a1b550
Merge pull request #1403 from moreati/issue1398-poc
mitogen: Add initial importlib ResourceReader support
3 weeks ago
Alex Willmer b7eddf2cdb mitogen: Add initial support for importlib ResourceReader
The new classes are modelled closely on their existing Module* counterparts.
For now I've duplicated the code, once it's bedded in I may refactor it. I
didn't replicate the FORWARD_MODULE plumbing, it didn't seem to be necessary
and may be dead code.
3 weeks ago
Alex Willmer 73f60a3123
Merge pull request #1399 from palfrey/ssl-discovery-with-builtin
tests: Handle builtin _ssl module on Linux
4 weeks ago
Tom Parker-Shemilt 4251991c3a tests: Handle builtin _ssl module on Linux 1 month ago
Alex Willmer 492bd2fa1f
Merge pull request #1396 from moreati/prepare-v0.3.36
Prepare v0.3.36
1 month ago
Alex Willmer 1d62a51810 Begin 0.3.37dev 1 month ago
Alex Willmer 4111224161 Prepare v0.3.36 1 month ago
Alex Willmer 4952c5635c
Merge pull request #1389 from mhartmay/possible-fix
mitogen: first_stage: Break the while loop in case of EOF
1 month ago
Marc Hartmayer 8807cd53be mitogen: first_stage: Break the while loop in case of EOF
The current implementation can cause an infinite loop, leading to a process that
hangs and consumes 100% CPU. This occurs because the EOF condition is not
handled properly, resulting in repeated select(...) and read(...) calls.

The fix is to properly handle the EOF condition and break out of the loop when
it occurs.

-SSH command size: 822
+SSH command size: 838
 Preamble (mitogen.core + econtext) size: 18226 (17.80KiB)

-mitogen.parent        99062  96.7KiB  51235 50.0KiB 51.7%  12936 12.6KiB 13.1%
+mitogen.parent        99240  96.9KiB  51244 50.0KiB 51.6%  12956 12.7KiB 13.1%

Fixes: https://github.com/mitogen-hq/mitogen/issues/1348
Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
1 month ago
Marc Hartmayer f5195edf08 first_stage_test: Add more tests
+ test_non_blocking_stdin
 + test_blocking_stdin
 + test_premature_eof
 + test_broker_connect_eof_error
 + test_broker_connect_timeout_because_blocking_read(self):

Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
1 month ago
Marc Hartmayer f7ca6af62d first_stage_test: Open /dev/zero in binary mode
Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
1 month ago
Marc Hartmayer 0ab5b425d8 first_stage_test: Refactor the test
Use testlib.subprocess instead of subprocess and make the test description a
docstring that can be used by the test runner.

Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
1 month ago
Marc Hartmayer 2d9e90acf9 parent_test: Refactor `wait_for_child`
Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
1 month ago
Marc Hartmayer fdaf09c4d6 mitogen/parent: Fix typo
Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
1 month ago
Alex Willmer 2398df1159
Merge pull request #1393 from moreati/firststage-linecomments
mitogen: Line comments in first stage
2 months ago
Alex Willmer 606a21fb27 mitogen: Add first line comments to _first_stage() 2 months ago
Alex Willmer 733f4bca81 mitogen: Allow line comments in first stage, strip them. 2 months ago
Alex Willmer 9c71a158e7
Merge pull request #1390 from mhartmay/fix-hanging-tests
testlib: Fix hanging tests
2 months ago
Marc Hartmayer 0a559ec8d8 testlib: Fix hanging tests
When I run

$ MITOGEN_LOG_LEVEL=debug SKIP_ANSIBLE=1 ./run_tests -v -k first_stage_test.CommandLineTest

in a interactive Shell (with a tty), it ends in a hanging process as the
`have_python2` and `have_python3` ends up ends up in an interactive Python
shell. Therefore check the Python version instead.

Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
2 months ago
Alex Willmer 7be79d05e9
Merge pull request #1013 from moreati/issue1011-blacklist-msg
mitogen: Clarify blacklisted import error message
2 months ago
Alex Willmer ccaaf4b7fe mitogen: Clarify blacklisted ModuleNotFoundError message
Previous phrasing was misleading - it implied a given module was explicitly on
the blacklist, even if it was due to a restrictive whitelist and the blacklist
was empty.

Arguably the blacklist/whitelist semantics are themselves misleading. A
redesign is tempting.
2 months ago
Alex Willmer 073fc48afc tests: Remove BlacklistTest stubs (covered by ImporterBlacklistTest) 2 months ago
Alex Willmer e0de4d3b8e
Merge pull request #1386 from moreati/issue1237
Tidy ups
2 months ago
Alex Willmer 64a581b2ac tests: Add Ubuntu 16.04 to image_prep inventory
I missed this when committing what built 2025.02 iamges
2 months ago
Alex Willmer 823d1d8b47 docs: Document Ansible 13 (ansible-core 2.20) support 2 months ago
Alex Willmer 9b46882478 ansible_mitogen: Remove a use of ansible.module_utils.six 2 months ago
Alex Willmer b105877f4d mitogen: Re-declare Python 2.4 compatibility
With CentOS 5 now covered by the Mitogen unit tests I'm content to
reverse/clarify 104865e866
2 months ago
Alex Willmer fb9efb24ca
Merge pull request #1383 from moreati/prepare-v0.3.35
Prepare v0.3.35
2 months ago
Alex Willmer 9ce6a43329 Begin 0.3.36dev 2 months ago
Alex Willmer 4af6a75278 Prepare v0.3.35 2 months ago
Alex Willmer be3d496110
Merge pull request #1382 from moreati/issue1225-interpreter-python-fallback
ansible_mitogen: Use INTERPRETER_PYTHON_FALLBACK as python candidates
2 months ago
Alex Willmer 699a8ebfb5 ansible_mitogen: Use INTERPRETER_PYTHON_FALLBACK as python candidates
This shouldn't change the interpreter ultimately chosen by Ansible. It should
only improve the hit rate of performing interpreter discovery, particular in
cases where only pythonX.Y is present on the target.

Interpreter discovery may take longer or shorter, depending on the Ansible
version and the interpreters present on the target.
2 months ago
Alex Willmer abb77e77e1
Merge pull request #1374 from moreati/prepare-v0.3.34
Prepare v0.3.34
2 months ago
Alex Willmer 19938ec05a Begin 0.3.35.dev 2 months ago
Alex Willmer 8e6a93dd0f Prepare v0.3.34 2 months ago
Alex Willmer 682faf85fc
Merge pull request #1126 from moreati/issue1124
Ansible: Avoid sending `__main__` and its dependencies from controller to targets
2 months ago
Alex Willmer 83b6cdb616 ansible_mitogen: Speedup startup by not sending `__main__`
On my laptop his reduces the time to execute `ansible -mping ...` by approx
300 ms with `strategy=mitogen_linear`.

Until this commit Mitogen was unnecessarily sending large chunks of Ansible
from the controller to targets, due to `__main__` being identified as a
related module of `ansible.module_utils.basic`, and resolving to something
within `ansible.cli...`.

On Ansible target hosts executing any Ansible Module `__main__` is imported by
`ansible.module_utils.basic` as part of Ansible's module delivery mechanism.
When `mitogen.master.ModuleResponder` (on the controller) processes the
request for `ansible.module_utils.basic` from the target, it scans
`ansible.module_utils.basic` for related imports and finds `__main__`. However
`__main__` on the controller is not the same module as `__main__` on the
target. On the controller it is a module in `ansible.cli...` that implements
one of the ansible commands (e.g. `ansible`, `ansible-playbook`).
2 months ago
Alex Willmer f191f050bf mitogen: Log why a module is sent or not sent by ModuleResponder
This should not change the logic
2 months ago
Alex Willmer f556ec12b1
Merge pull request #1373 from moreati/issue1118-new-os-releases
CI: Expand test coverage of OS releases
2 months ago
Alex Willmer a208daa461 CI: Add OS release coverage: Ubuntu 22.04, Ubuntu 24.04 2 months ago
Alex Willmer 14e8334705 CI: Add OS release coverage: Debian 12 2 months ago
Alex Willmer 1fe55f1c67 CI: Add OS release coverage: CentOS 5
Only the Mitogen unit tests will run against CentOS 5, providing atleast some
Python 2.4test coverage. There is no version of Ansible that supports Python
2.4 that is also supported by Mitogen 0.3.

The SSH key exchange argument is to persuade newer SSH clients to talk with
such an old SSH server.

See https://www.openssh.org/legacy.html
2 months ago
Alex Willmer e0103eb66c CI: Add OS release coverage: AlmaLinux 9 2 months ago
Alex Willmer e044893a88 tests: Variabalize virtualenv creation in isssue 152 regression test
Prep for AlamaLinux 9 introduction
2 months ago
Alex Willmer 1cbd1777bc tests: Check Mitogen+Ansible discovered interpreter fresh Ansible result
Previously this test used a manually compiled list of results, which is
fragile and an ongoing maintenance burden. New method should 'just work' and
be more transparent.

This technique might be more widely applicable in the test suite.
2 months ago
Alex Willmer 0bafbd501c tests: Remove unused distros_* Tox factors 2 months ago
Alex Willmer f0a83168bf
Merge pull request #1370 from moreati/ci-macos15
CI: bump macOS 13 runner -> macOS 15
2 months ago
Alex Willmer 15b2619fb2 CI: Bump deprecated macOS 13 runner to macOS 15 2 months ago
Alex Willmer 5c9abeda94
Merge pull request #1372 from moreati/issue1118-ci-use-2025.02-images
CI: Use 2025.02 test images
2 months ago
Alex Willmer 006d497c25 CI: Show details of failed ci_lib.run_batches() commands 2 months ago
Alex Willmer 9609437262 CI: Use 2025.02 test images, keeping same OS releases
centos8-test:2025.02 no longer has a /usr/bin/python installed, so use
centos8-py3 target which sets `ansible_python_interpreter=/usr/bin/python3` in
the templated inventory.

Ansible <= 9 (ansible-core <= 2.6) now discover the interpreter as
/usr/bin/python3 on debian11-test:2025.02, as opposed to
/usr/bin/python3.9 on debian11-test:2021. I'm don't know the exact
cause. From manual tests the change in observed behaviour appears to be common to
vanilla Ansible (strategy=linear) and Mitogen flavour
(strategy=mitogen_linear).

```console
(ans9) ➜  mitogen git:(4efb7158) ✗ ANSIBLE_STRATEGY=mitogen_linear ANSIBLE_STRATEGY_PLUGINS=ansible_mitogen/plugins/strategy ans9/bin/ansible -e ansible_python_interpreter=auto -mping d11.lan
d11.lan | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
(ans9) ➜  mitogen git:(4efb7158) ✗ ans9/bin/ansible -e ansible_python_interpreter=auto -mping d11.lan
d11.lan | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
```

Update some tests which assume `/usr/bin/python` exists or that `env python`
will resolve successfully.
2 months ago
Alex Willmer 7eabcc61c1 tests: Only test doas on targets with doas binary installed 2 months ago
Alex Willmer 017de4c8e1
Merge pull request #1254 from moreati/issue1118-update-containers
CI: Build newer test images
2 months ago
Alex Willmer 7996a03a37 ci: Bootstrap Debian like containers with python-apt or python3-apt
The Ansible apt module requires it
2 months ago
Alex Willmer 5b6b076c4e ci: Avoid ansible_virtualization_type to check for docker targets
It's not consistant across Ansible versions, particular the oldest ones. This
may have contributed to older test images containing usernames from the host
OS that they were built on (e.g. dmw, alex).
2 months ago
Alex Willmer 4ecafc564d ci: Use command: true as noop handler
meta: noop failed on older Ansibles (e.g. 2.3)
2 months ago
Alex Willmer 56dce28906 ci: Dont show arguments in task name during image prep
A bit to noisy for my taste
2 months ago
Alex Willmer ff973775ce ci: Push new container images to GitHub Container Registry 2 months ago
Alex Willmer 5ffdbb5999 ci: Add Alma 9, Debian 12, Ubuntu 22.04, & Ubuntu 24.04 to image prep 2 months ago
Alex Willmer 01e24f9ddf ci: Use highest supported Ansible version during image prep
It was necessary to split setup.yml because there is no common subset of
supported include/import keywords across Ansible 2.3 - 2.11. The yaml stdout
callback is unavailabe in Ansible 2.3.
2 months ago
Alex Willmer 22e7046cf6 ci: Run image-prep as fast as possible
Mitogen maintainer(s) got better laptops in the last decaade or so.
2 months ago
Alex Willmer cc8a39864d ci: Only install default Python 3.x during image prep
Newer images will shortly be generated, so these higher Python versions aren't
needed anymore.
2 months ago
Alex Willmer 40fbfe58fc ci: Install doas package during image prep, delete vendored doas
Debian 11 is the earliest Debian release with such a package. Ubuntu first
included it in 22.04 CentOS doesn't have it.
2 months ago
Alex Willmer b353980699 ci: Tighten Ansible error checking during image prep 2 months ago
Alex Willmer 3fe9b9bd87 ci: Install setfacl for vanilla Ansible unprivileged become 2 months ago
Alex Willmer cfbb7f884e ci: Add playbook to configure container host for image prep 2 months ago
Alex Willmer a1b5d4941e ci: Use upstream base images for image prep
This eliminates use of third-party *-vault images and performs repository
config during image prep.

The Apache httpd proxy is necessary because https://vault.centos.org now only
accepts TLS 1.x connections, and CentOS 5 can only do upto SSL 3.0. It is
developed to run on Debian 11.
2 months ago
Alex Willmer e32c90a63e ci: Factor out package installation role 2 months ago
Alex Willmer a143787c02 ci: Handle custom package repositories in bootstrap role 2 months ago
Alex Willmer bcc726d3b7 ci: Handle dnf packages in bootstrap role 2 months ago
Alex Willmer 780f8af1a4 ci: Factor out image prep bootstrap as a role
Promoting the script to a full template will fix some whitespace errors later.
2 months ago
Alex Willmer d1c4217db0 ci: Wait for fresh image prep containers to start 2 months ago
Alex Willmer 09b972e96e ci: Fix ansible-lint complaints in image prep playbooks 2 months ago
Alex Willmer 509c572682
Merge pull request #1368 from moreati/prepare-v0.3.33
Prepare v0.3.33
2 months ago
Alex Willmer c9eb6e54e2 Begin 0.3.34dev 2 months ago
Alex Willmer 4a442f503e Prepare v0.3.33 2 months ago
Alex Willmer e52132c89b
Merge pull request #1367 from moreati/ansible13
Test/fix Ansible 13 (ansible-core 2.20) support
2 months ago
Alex Willmer f966b3e5c6 CI: Remove lingering stdout_callback=yaml in macOS jobs
Support removed in Ansible 13 (ansible-core 2.20).
refs #1285, #1291
2 months ago
Alex Willmer 7c9c38325d ansible_mitogen: Ansible 13 (ansible-core 2.20) support 2 months ago
Alex Willmer 5da56f577c CI: Use non-rc Python 3.14 releases
Left over from Python 3.14 support work.
2 months ago
Alex Willmer 734047e1cc CI: Remove Ansible 11 (ansible-core 2.18) strategy=linear jobs
I believe I kep them as a reference during Ansible 12 fixups. No longer needed
and they slow down CI runs.
2 months ago
Alex Willmer 8b29846990
Merge pull request #1365 from moreati/prepare-v0.3.32
Prepare v0.3.32
2 months ago
Alex Willmer 61a7fa1fee Begin 0.3.33dev 2 months ago
Alex Willmer 28ea4780db Prepare v0.3.32 2 months ago
Alex Willmer 36f7cee2d1
Merge pull request #1363 from moreati/issue1362-issue-templates
Convert bug issue templates to an issue form
2 months ago
Alex Willmer 9a2e600317 chore: Convert bug template to a form 2 months ago
Alex Willmer 2cc507a6de chore: Remove Mitogen 0.2 issue template
No longer used
2 months ago
Alex Willmer db63dd1def
Merge pull request #1359 from moreati/issue1260
CI: Remove integration of retired lgtm.com
3 months ago
Alex Willmer cab024a6fc CI: Remove integration of retired lgtm.com
Company was aquired by Github in 2019. Service was switched off in Dec 2022,
replaced by GitHub code scanning. Fixes #1260

See
- https://github.blog/news-insights/product-news/the-next-step-for-lgtm-com-github-code-scanning/
3 months ago
Alex Willmer aea028f175
Merge pull request #1357 from moreati/issue1218
ansible_mitogen: Remove maximum Ansible version check
3 months ago
Alex Willmer df890459c5 ansible_mitogen: Remove maximum Ansible version check
fixes #1218
3 months ago
Alex Willmer 847f34c17d
Merge pull request #1243 from moreati/boot-cmd--argv
mitogen: Pass first stage, preamble length, and context name in argv
3 months ago
Alex Willmer 83c5ab1900 mitogen: Send first stage parameters as argv (796 bytes -> 822)
Benefit: The base64 lump is now static for a given Mitogen version, and the
variable parts are more visible. This will make debugging, auditting, and
allow-listing a bit easier.
Potential benefit: generate the base64 once, at build time or startup. Rather
than once per connection.
Cost: Bootstrap command is 26 bytes longer.

```
➜  mitogen git:(boot-cmd--argv) ✗ ./preamble_size.py
SSH command size: 822
Preamble (mitogen.core + econtext) size: 18230 (17.80KiB)

                        Original           Minimized           Compressed
mitogen.core         152237 148.7KiB  68453 66.8KiB 45.0%  18130 17.7KiB 11.9%
mitogen.parent        98746  96.4KiB  51215 50.0KiB 51.9%  12922 12.6KiB 13.1%
mitogen.fork           8445   8.2KiB   4139  4.0KiB 49.0%   1652  1.6KiB 19.6%
mitogen.ssh           10847  10.6KiB   6913  6.8KiB 63.7%   2102  2.1KiB 19.4%
mitogen.sudo          12089  11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB 18.6%
mitogen.select        12325  12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB  7.8%
mitogen.service       41581  40.6KiB  22398 21.9KiB 53.9%   5847  5.7KiB 14.1%
mitogen.fakessh       15753  15.4KiB   8135  7.9KiB 51.6%   2672  2.6KiB 17.0%
mitogen.master        52891  51.7KiB  27586 26.9KiB 52.2%   7129  7.0KiB 13.5%
```
3 months ago
Alex Willmer 3b7a75dfaf mitogen: Send first stage as argv (786 bytes -> 796 bytes)
This saves one layer of quoting/quote escaping in the bootstrap command and a
string interpolation per connection. The cost is an increasing the bootstrap
command by 10 bytes. I like the tradeoff. I could be convinced to revert it.

```console
➜  mitogen git:(boot-cmd--argv) ✗ ./preamble_size.py
SSH command size: 796
Preamble (mitogen.core + econtext) size: 18230 (17.80KiB)

                        Original           Minimized           Compressed
mitogen.core         152237 148.7KiB  68453 66.8KiB 45.0%  18130 17.7KiB 11.9%
mitogen.parent        99181  96.9KiB  51384 50.2KiB 51.8%  12956 12.7KiB 13.1%
mitogen.fork           8445   8.2KiB   4139  4.0KiB 49.0%   1652  1.6KiB 19.6%
mitogen.ssh           10847  10.6KiB   6913  6.8KiB 63.7%   2102  2.1KiB 19.4%
mitogen.sudo          12089  11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB 18.6%
mitogen.select        12325  12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB  7.8%
mitogen.service       41581  40.6KiB  22398 21.9KiB 53.9%   5847  5.7KiB 14.1%
mitogen.fakessh       15753  15.4KiB   8135  7.9KiB 51.6%   2672  2.6KiB 17.0%
mitogen.master        52891  51.7KiB  27586 26.9KiB 52.2%   7129  7.0KiB 13.5%
```
3 months ago
Alex Willmer 191abd492a mitogen: Compress first stage without header or checksum (790 bytes -> 786)
```console
➜  mitogen git:(boot-cmd--argv) ✗ ./preamble_size.py
SSH command size: 786
Preamble (mitogen.core + econtext) size: 18230 (17.80KiB)

                        Original           Minimized           Compressed
mitogen.core         152237 148.7KiB  68453 66.8KiB 45.0%  18130 17.7KiB 11.9%
mitogen.parent        99166  96.8KiB  51375 50.2KiB 51.8%  12957 12.7KiB 13.1%
mitogen.fork           8445   8.2KiB   4139  4.0KiB 49.0%   1652  1.6KiB 19.6%
mitogen.ssh           10847  10.6KiB   6913  6.8KiB 63.7%   2102  2.1KiB 19.4%
mitogen.sudo          12089  11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB 18.6%
mitogen.select        12325  12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB  7.8%
mitogen.service       41581  40.6KiB  22398 21.9KiB 53.9%   5847  5.7KiB 14.1%
mitogen.fakessh       15753  15.4KiB   8135  7.9KiB 51.6%   2672  2.6KiB 17.0%
mitogen.master        52891  51.7KiB  27586 26.9KiB 52.2%   7129  7.0KiB 13.5%
```

Confirmed Python 2.4 supports this use of zlib.compressobj, despite lack of
mention in https://docs.python.org/2.4/lib/module-zlib.html

```pycon
Python 2.4.6 (#2, Apr 29 2018, 11:16:24)
[GCC 7.3.0] on linux4
Type "help", "copyright", "credits" or "license" for more information.
>>> import zlib
>>> c=zlib.compressobj(zlib.Z_BEST_COMPRESSION,zlib.DEFLATED,-zlib.MAX_WBITS)
>>> c.compress('qwertyuiop') + c.flush()
'+,O-*\xa9,\xcd\xcc/\x00\x00'
```
3 months ago
Alex Willmer 408946adbe mitogen: Golf 8 bytes from bootstrap first stage (798 -> 790)
Before
```
SSH command size: 798
Preamble (mitogen.core + econtext) size: 18230 (17.80KiB)

                        Original           Minimized           Compressed
mitogen.core         152237 148.7KiB  68453 66.8KiB 45.0%  18130 17.7KiB 11.9%
mitogen.parent        99020  96.7KiB  51247 50.0KiB 51.8%  12910 12.6KiB 13.0%
mitogen.fork           8445   8.2KiB   4139  4.0KiB 49.0%   1652  1.6KiB 19.6%
mitogen.ssh           10847  10.6KiB   6913  6.8KiB 63.7%   2102  2.1KiB 19.4%
mitogen.sudo          12089  11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB 18.6%
mitogen.select        12325  12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB  7.8%
mitogen.service       41581  40.6KiB  22398 21.9KiB 53.9%   5847  5.7KiB 14.1%
mitogen.fakessh       15753  15.4KiB   8135  7.9KiB 51.6%   2672  2.6KiB 17.0%
mitogen.master        52891  51.7KiB  27586 26.9KiB 52.2%   7129  7.0KiB 13.5%
```

After
```
SSH command size: 790
Preamble (mitogen.core + econtext) size: 18230 (17.80KiB)

                        Original           Minimized           Compressed
mitogen.core         152237 148.7KiB  68453 66.8KiB 45.0%  18130 17.7KiB 11.9%
mitogen.parent        99020  96.7KiB  51247 50.0KiB 51.8%  12903 12.6KiB 13.0%
mitogen.fork           8445   8.2KiB   4139  4.0KiB 49.0%   1652  1.6KiB 19.6%
mitogen.ssh           10847  10.6KiB   6913  6.8KiB 63.7%   2102  2.1KiB 19.4%
mitogen.sudo          12089  11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB 18.6%
mitogen.select        12325  12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB  7.8%
mitogen.service       41581  40.6KiB  22398 21.9KiB 53.9%   5847  5.7KiB 14.1%
mitogen.fakessh       15753  15.4KiB   8135  7.9KiB 51.6%   2672  2.6KiB 17.0%
mitogen.master        52891  51.7KiB  27586 26.9KiB 52.2%   7129  7.0KiB 13.5%
```
3 months ago
Alex Willmer fdb5c62532
Merge pull request #1353 from moreati/prepare-v0.3.31
Prepare v0.3.31
3 months ago
Alex Willmer e4e82f53a1 Begin 0.3.32dev 3 months ago
Alex Willmer 77b7a31949 Prepare v0.3.31 3 months ago
Alex Willmer 69a5cdce1b
Merge pull request #1352 from moreati/issue1350
ansible_mitogen: Fix ModuleNotFoundError: No module named 'ansible_mitogen'
3 months ago
Alex Willmer 85069b28cd ansible_mitogen: Fix ModuleNotFoundError: No module named 'ansible_mitogen'
Loading the ansible_mitogen Ansible plugins apparently doesn't follow the same
rules as importing a Python module. So sys.path manipulations in __init__.py
weren't fired when Ansible tried to load the plugins from a /custom/path that
wasn't already on sys.path.

This wasn't picked up by the test because CI always installs Mitogen as a
Python package (in a virtual env).

This reverses 6145508312.
3 months ago
Alex Willmer 2305446ab8
Merge pull request #1346 from moreati/prepare-v0.3.30
Prepare v0.3.30
3 months ago
Alex Willmer c72acfd966 Begin v0.3.31.dev 3 months ago
Alex Willmer 1e90ff25ee Prepare v0.3.30 3 months ago
Alex Willmer 48243724a0
Merge pull request #1341 from mhartmay/logforwarder-fix
master: Fix LogForwarder in case an own LogRecordFactory is used
3 months ago
Marc Hartmayer 24745183ed master: Fix LogForwarder in case an own LogRecordFactory is used
Since Python 3.2 the log record factory can be changed by using
`logging.setLogRecordFactory` [1]. Therefore use `logging.makeLogRecord` as
recommended in the documentation:

"LogRecord instances are created automatically by the Logger every time
something is logged, and can be created manually via makeLogRecord() (for
example, from a pickled event received over the wire)." [2]

This fixes the test case
`log_handler_test.LogRecordFactoryTest.test_logrecordfactory`.

[1] https://docs.python.org/3/library/logging.html#logging.setLogRecordFactory
[2] https://docs.python.org/3/library/logging.html#logrecord-objects

Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
3 months ago
Marc Hartmayer dad28e8b4a tests: Add a test case that verifies behavior when the log record factory is modified
The test currently fails with the following error:

  $ PYTHONPATH=$(pwd)/tests:$PYTHONPATH python3 -m unittest -v log_handler_test
  ...
  test_logrecordfactory (log_handler_test.LogRecordFactoryTest.test_logrecordfactory) ... --- Logging error ---
  Traceback (most recent call last):
    File "/usr/lib/python3.12/logging/__init__.py", line 464, in format
      return self._format(record)
             ^^^^^^^^^^^^^^^^^^^^
    File "/usr/lib/python3.12/logging/__init__.py", line 460, in _format
      return self._fmt % values
             ~~~~~~~~~~^~~~~~~~
  KeyError: 'custom_attribute'

  During handling of the above exception, another exception occurred:

  Traceback (most recent call last):
    File "/usr/lib/python3.12/logging/__init__.py", line 1160, in emit
      msg = self.format(record)
            ^^^^^^^^^^^^^^^^^^^
    File "/usr/lib/python3.12/logging/__init__.py", line 999, in format
      return fmt.format(record)
             ^^^^^^^^^^^^^^^^^^
    File "/usr/lib/python3.12/logging/__init__.py", line 999, in format
      return fmt.format(record)
             ^^^^^^^^^^^^^^^^^^
    File "/usr/lib/python3.12/logging/__init__.py", line 706, in format
      s = self.formatMessage(record)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/usr/lib/python3.12/logging/__init__.py", line 675, in formatMessage
      return self._style.format(record)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/usr/lib/python3.12/logging/__init__.py", line 466, in format
      raise ValueError('Formatting field not found in record: %s' % e)
  ValueError: Formatting field not found in record: 'custom_attribute'

Signed-off-by: Marc Hartmayer <mhartmay@linux.ibm.com>
3 months ago
Alex Willmer 3c648f7df8
Merge pull request #1345 from moreati/oi-mate-wheres-your-loicense
Fix and formalise license metadata
3 months ago
Alex Willmer 01baec8347 Declare license as SPDX identifier in metadata
Fixes warning seen during packaging operations

```
➜  mitogen git:(master) ✗ uv build --sdist
Building source distribution...
...
!!

        ********************************************************************************
        Please consider removing the following classifiers in favor of a SPDX license expression:

        License :: OSI Approved :: BSD License

        See https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license for details.
        ********************************************************************************

!!
  self._finalize_license_expression()
running egg_info
...
```
3 months ago
Alex Willmer 85f0c33dc5 Correct mitogen.imports.* licenses 3 months ago
Alex Willmer 8f66aa5fcd
Merge pull request #1248 from moreati/sys.path
ansible_mitogen: De-duplicate sys.path manipulations by Ansible plugins
3 months ago
Alex Willmer 6145508312 ansible_mitogen: De-duplicate sys.path manipulations by Ansible plugins 3 months ago
Alex Willmer 9701424be0
Merge pull request #1266 from moreati/imports-cleanup
Imports cleanup
3 months ago
Alex Willmer 7d5f63ccbf Cleanup unused and missing imports 3 months ago
Alex Willmer 3338a651a6
Merge pull request #1339 from moreati/prepare-v0.3.29
Prepare v0.3.29
4 months ago
Alex Willmer 6071fb58c9 Begin 0.3.30dev 4 months ago
Alex Willmer e670bf0ebd Prepare v0.3.29 4 months ago
Alex Willmer f6451bf795
Merge pull request #1287 from moreati/issue1242-py3.14
Python 3.14 support
4 months ago
Alex Willmer 090952a987 Python 3.14 support 4 months ago
Alex Willmer d27275ad46 ci: Set global max failed logins on macOS 4 months ago
Alex Willmer 1b00ca2581 tests: Bump dependency versions 4 months ago
Alex Willmer f4f646a00a
Merge pull request #1337 from moreati/prepare-v0.3.28
Prepare v0.3.28
4 months ago
Alex Willmer b03c1f3d87 Begin 0.3.29dev 4 months ago
Alex Willmer 9f9b37d1ad Prepare v0.3.28 4 months ago
Alex Willmer f6902dd05d
Merge pull request #1336 from Nihlus/freeipa-fixes
Add FreeIPA client modules to the always-fork list
4 months ago
Alex Willmer 2736f38c4b docs: Changelog for FreeIPA client modules -> ALWAYS_FORK_MODULES 4 months ago
Jarl Gullberg 59d5d74abd
Add FreeIPA client modules to the always-fork list. 5 months ago
Alex Willmer 36569792bc
Merge pull request #1307 from moreati/issue1306-investigate
mitogen: Fix non-blocking IO errors in first stage of bootstrap
5 months ago
Alex Willmer 85d6046f2f mitogen: Fix non-blocking IO errors in first stage of bootstrap
When /etc/sudoers has log_output (or similar) enabled the process spawned by
`ctx.sudo()` via `mitogen.parent.Connection.start_child()` receives a stdin
that is in non-blocking mode. The immediate symptom is that `os.openfd(0,
...).read(n)` sometimes returns `None`, causing the first stage to raise an
unhandled TypeError.

The fix (for now) is to use `select.select()` in a while loop to read stdin.
This increases the command size slightly, but I think it's a reasonable
tradeoff until/unless the cause is more fully understood.

All CI tests are now run with sudoers log_output enabled, in order to catch
regressions. `first_stage_test.CommandLineTest` has been amended, because it
relied on implementation details of the bootstrap process that are no longer
true.

Before
```
SSH command size: 755
Preamble (mitogen.core + econtext) size: 18227 (17.80KiB)

                        Original           Minimized           Compressed
mitogen.core         152218 148.7KiB  68437 66.8KiB 45.0%  18124 17.7KiB 11.9%
mitogen.parent        98853  96.5KiB  51103 49.9KiB 51.7%  12881 12.6KiB 13.0%
mitogen.fork           8445   8.2KiB   4139  4.0KiB 49.0%   1652  1.6KiB 19.6%
mitogen.ssh           10827  10.6KiB   6893  6.7KiB 63.7%   2099  2.0KiB 19.4%
mitogen.sudo          12089  11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB 18.6%
mitogen.select        12325  12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB  7.8%
mitogen.service       41581  40.6KiB  22398 21.9KiB 53.9%   5847  5.7KiB 14.1%
mitogen.fakessh       15767  15.4KiB   8149  8.0KiB 51.7%   2676  2.6KiB 17.0%
mitogen.master        55317  54.0KiB  28846 28.2KiB 52.1%   7528  7.4KiB 13.6%
```

After
```
SSH command size: 798
Preamble (mitogen.core + econtext) size: 18227 (17.80KiB)

                        Original           Minimized           Compressed
mitogen.core         152218 148.7KiB  68437 66.8KiB 45.0%  18124 17.7KiB 11.9%
mitogen.parent        98944  96.6KiB  51180 50.0KiB 51.7%  12910 12.6KiB 13.0%
mitogen.fork           8445   8.2KiB   4139  4.0KiB 49.0%   1652  1.6KiB 19.6%
mitogen.ssh           10827  10.6KiB   6893  6.7KiB 63.7%   2099  2.0KiB 19.4%
mitogen.sudo          12089  11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB 18.6%
mitogen.select        12325  12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB  7.8%
mitogen.service       41581  40.6KiB  22398 21.9KiB 53.9%   5847  5.7KiB 14.1%
mitogen.fakessh       15767  15.4KiB   8149  8.0KiB 51.7%   2676  2.6KiB 17.0%
mitogen.master        55317  54.0KiB  28846 28.2KiB 52.1%   7528  7.4KiB 13.6%
```
5 months ago
Alex Willmer c508bfb58b tests: Check stdio is blocking in sudo contexts
refs #712
5 months ago
Alex Willmer 76f6eb741d tests: Count bytes written in stdio_test.StdIOTest
This is mainly for peace of mind. With all this non-blocking IO investigation
I'm getting a bit paranoid wrt file objects.

refs #712
5 months ago
Alex Willmer 3dfaf83ce7 preamble_size: Fix variability of command & preamble(?) size
Previously the command size could very depanding on the current username, hostname, and process pid.

Before
```
SSH command size: 759
Preamble (mitogen.core + econtext) size: 18227 (17.80KiB)
...
```

After
SSH command size: 755
Preamble (mitogen.core + econtext) size: 18227 (17.80KiB)
...
```
5 months ago
Alex Willmer 936b08dd08 preamble_size: Include mitogen.core and clarify bootstrap size
After:
SSH command size: 759
Preamble (mitogen.core + econtext) size: 18227 (17.80KiB)

                        Original           Minimized           Compressed
mitogen.core         152218 148.7KiB  68437 66.8KiB 45.0%  18124 17.7KiB 11.9%
mitogen.parent        98853  96.5KiB  51103 49.9KiB 51.7%  12881 12.6KiB 13.0%
mitogen.fork           8445   8.2KiB   4139  4.0KiB 49.0%   1652  1.6KiB 19.6%
mitogen.ssh           10827  10.6KiB   6893  6.7KiB 63.7%   2099  2.0KiB 19.4%
mitogen.sudo          12089  11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB 18.6%
mitogen.select        12325  12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB  7.8%
mitogen.service       41581  40.6KiB  22398 21.9KiB 53.9%   5847  5.7KiB 14.1%
mitogen.fakessh       15767  15.4KiB   8149  8.0KiB 51.7%   2676  2.6KiB 17.0%
mitogen.master        55317  54.0KiB  28846 28.2KiB 52.1%   7528  7.4KiB 13.6%
5 months ago
Alex Willmer 30d8a38a3b preamble_size: Consolidate table formatting, align columns better
Before
./preamble_size.py
SSH command size: 759
Bootstrap (mitogen.core) size: 18227 (17.80KiB)

                              Original          Minimized           Compressed
mitogen.parent            98853 96.5KiB  51103 49.9KiB 51.7%  12881 12.6KiB 13.0%
mitogen.fork               8445  8.2KiB   4139  4.0KiB 49.0%   1652  1.6KiB 19.6%
mitogen.ssh               10827 10.6KiB   6893  6.7KiB 63.7%   2099  2.0KiB 19.4%
mitogen.sudo              12089 11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB 18.6%
mitogen.select            12325 12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB 7.8%
mitogen.service           41581 40.6KiB  22398 21.9KiB 53.9%   5847  5.7KiB 14.1%
mitogen.fakessh           15767 15.4KiB   8149  8.0KiB 51.7%   2676  2.6KiB 17.0%
mitogen.master            55317 54.0KiB  28846 28.2KiB 52.1%   7528  7.4KiB 13.6%

After:
SSH command size: 759
Bootstrap (mitogen.core) size: 18227 (17.80KiB)

                        Original           Minimized           Compressed
mitogen.parent        98853  96.5KiB  51103 49.9KiB 51.7%  12881 12.6KiB 13.0%
mitogen.fork           8445   8.2KiB   4139  4.0KiB 49.0%   1652  1.6KiB 19.6%
mitogen.ssh           10827  10.6KiB   6893  6.7KiB 63.7%   2099  2.0KiB 19.4%
mitogen.sudo          12089  11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB 18.6%
mitogen.select        12325  12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB  7.8%
mitogen.service       41581  40.6KiB  22398 21.9KiB 53.9%   5847  5.7KiB 14.1%
mitogen.fakessh       15767  15.4KiB   8149  8.0KiB 51.7%   2676  2.6KiB 17.0%
mitogen.master        55317  54.0KiB  28846 28.2KiB 52.1%   7528  7.4KiB 13.6%
5 months ago
Alex Willmer e4e2c6caaf CI: Move sudo test users defaults into /etc/sudoers.d
Prep for reusing it in non-Ansible tests
5 months ago
Alex Willmer 5abdde1117 CI: Report sudo version on Ansible targets 5 months ago
Alex Willmer dc7fae973b CI: Fix ci_lib and test_lib have_<cmd>() when <cmd> exits abnormally
We were not raising CalledProcessError when exit status != 0.
5 months ago
Alex Willmer 885c6de65e
Merge pull request #1331 from moreati/prep-v0.3.27
Prep v0.3.27
5 months ago
Alex Willmer 07d1078010 Begin v0.3.28dev 5 months ago
Alex Willmer 154331e455 Prepare v0.3.27 5 months ago
Alex Willmer b8d3f86b12
Merge pull request #1328 from moreati/issue1325-scan_code_imports-refactor
mitogen: Refactor `mitogen.master.scan_code_imports()` -> `mitogen.imports.codeobj_imports()`
5 months ago
Alex Willmer 0e5f47f145 mitogen: Refactor scan_code_imports() as mitogen.imports.codeobj_imports()
This replaces `mitogen.master.scan_code_imports()` with
`mitogen.imports.codeobj_imports()`. The Python 3.x implementation now uses
`str.find()`, relying on Python >= 3.6 "widecode" format. Behaviour and
semantics should be unchanged. Now implementations are approx
- 1.5 x faster on Python 2.x
- 2 - 3 x faster on Python 3.x

Before
```console
$ ./tests/bench/scan_code
scan_code_imports python2.7  100 loops, best of 3: 3.19 msec per loop
scan_code_imports python3.9  500 loops, best of 5: 685 usec per loop
scan_code_imports python3.10  500 loops, best of 5: 727 usec per loop
scan_code_imports python3.11  500 loops, best of 5: 601 usec per loop
scan_code_imports python3.12  500 loops, best of 5: 609 usec per loop
scan_code_imports python3.13  500 loops, best of 5: 586 usec per loop
```

After
```console
codeobj_imports python2.7  1000 loops, best of 3: 1.98 msec per loop
codeobj_imports python3.9  1000 loops, best of 5: 302 usec per loop
codeobj_imports python3.10  1000 loops, best of 5: 297 usec per loop
codeobj_imports python3.11  1000 loops, best of 5: 243 usec per loop
codeobj_imports python3.12  1000 loops, best of 5: 278 usec per loop
codeobj_imports python3.13  1000 loops, best of 5: 259 usec per loop
```
```console
$ uname -a
Darwin kintha 24.6.0 Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:29 PDT
2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6000 arm64
```
5 months ago
Alex Willmer 3093d0bb2d tests: Add scan_code_imports benchmark
```console
$ ./tests/bench/scan_code
scan_code_imports python2.7  100 loops, best of 3: 3.19 msec per loop
scan_code_imports python3.9  500 loops, best of 5: 685 usec per loop
scan_code_imports python3.10  500 loops, best of 5: 727 usec per loop
scan_code_imports python3.11  500 loops, best of 5: 601 usec per loop
scan_code_imports python3.12  500 loops, best of 5: 609 usec per loop
scan_code_imports python3.13  500 loops, best of 5: 586 usec per loop
```
5 months ago
Alex Willmer 2fd88298ae tests: Improve master_test.ScanCodeImportsTest coverage
This covers existing behaviours of `mitogen.master.scan_code_imports()` some
of which are relied on, some not, but regardless weren't tested. Notably
- Explicit relative imports return level > 0
- Imports inside `class` and `def` are excluded
- Imports inside other blocks are included
- Python 3.x prunes impossible if/else branches (previously unknown)

It also
- Decouples the test results from the implementation details of the unit test.
- Fixes a missing import
- Fixes at least one Python 2.4 incompatibility (use of with block)
5 months ago
Alex Willmer 1386529493
Merge pull request #1330 from moreati/gha-workflow-cleanup
CI: Refactor and de-duplicate Github Actions workflow
5 months ago
Alex Willmer 618eccc0f3 CI: Set macOS failed logins limit of mitogen test users to 1000
refs #1315
5 months ago
Alex Willmer 9e3377c0a8 CI: Combine build deps & tooling steps 5 months ago
Alex Willmer e3241912f7 CI: Factor out .ci/show_python_versions 5 months ago
Alex Willmer 9b6fc117f9 CI: Remove unused python_version vars from Ubuntu 22.04 jobs 5 months ago
Alex Willmer 4cad51a629
Merge pull request #1320 from moreati/prepare-v0.3.26
Prepare v0.3.26
6 months ago
Alex Willmer 7fb7567809 Begin v0.3.27dev 6 months ago
Alex Willmer 5908936f8c Prepare v0.3.26 6 months ago
Alex Willmer 64feda250e
Merge pull request #1300 from moreati/issue712-stdout-non-blocking
stdio EAGAIN investigation
6 months ago
Alex Willmer 17bee70dc2 mitogen: Fix BlockingIOError & EAGAIN in subprocess stdio
Mitogen was leaving the stdout and stderr of subprocesses in non-blocking
mode. When Python code ran in the remote process created by Mitogen calls such
as `print(long_string)` or `os.stout.write(bigger_than_the_buffer)` sometimes
raised `BlockingIOError`, or similar.

This change
- Removes code in `mitogen.core.Side` that set blocking/non-blocking mode
- Adds blocking/non-blocking control to `os.mitogen.pipe()` and a new
  function `mitogen.core.socketpair()`
- Replaces `mitogen.core.set_block` and `mitogen.core.set_nonblock`
  with `mitogen.core.set_blocking`, mirroring `os.set_blocking`
- Updates call sites as appropriate
- Adds tests for new functions and arguments
- Adds a regression test for subprocess stdio blocking/non-blocking

fixes #712
6 months ago
Alex Willmer 4529a217e8
Merge pull request #1301 from moreati/repear-delay-simplify
mitogen: Simplify `mitogen.parent.Reaper._calc_delay()` calculation
6 months ago
Alex Willmer fde2dda87e mitogen: Simplify `mitogen.parent.Reaper._calc_delay()` calculation 6 months ago
Alex Willmer 65db935c57
Merge pull request #1311 from moreati/issue1309
ansible_mitogen: Fix `become_method=doas`, add tests
6 months ago
Alex Willmer 868d77a402 ansible_mitogen: Fix become_method=doas, add tests 6 months ago
Alex Willmer d6e74ad663
Merge pull request #1319 from moreati/ci-job-names
CI: Abbreviate Github Actions job names
6 months ago
Alex Willmer 32f6d0c358 CI: Abbreviate Github Actions job names
This is to prevent job names being truncated in the Github Actions web UI. So
it is obvious at a glance which jobs have failed. Previously one had to click
into the details to know which job was which, leading to confusion and wasted
time.

This also

- removes braced ranges in `testenv.setenv`. They appear not to be supported
  by tox (see https://github.com/tox-dev/tox/issues/3571)
- fixes the env var `DEFAULT_STDOUT_CALLBACK` -> `ANSIBLE_STDOUT_CALLBACK`

as a result of these test output format was previously not as intended for
some Ansible versions.
6 months ago
Alex Willmer 53ab2b2a4d
Merge pull request #1313 from moreati/prep-0.3.25
Prep 0.3.25
6 months ago
Alex Willmer 7f84874755 Begin 0.3.26dev 6 months ago
Alex Willmer dfafa1430e Prepare v0.3.25 6 months ago
Alex Willmer d240a78af3
Merge pull request #1308 from moreati/prep-v0.3.25b1
Prepare v0.3.25b1
6 months ago
Alex Willmer fa22aa1c8e Resume 0.3.25dev 6 months ago
Alex Willmer 2ae35c8a15 Prepare v0.3.25b1 6 months ago
Alex Willmer 058787ff83
Merge pull request #1304 from moreati/issue1303
CI: Switch to archived Debian 10 (buster) apt repository
7 months ago
Alex Willmer 573303ac73 CI: Switch to archived Debian 10 (buster) apt repository
The Debian project recently removed this EOL version from the live mirrors.
7 months ago
Alex Willmer bb18d9e77a
Merge pull request #1298 from feteu/patch-1
Update missing host key checking argument in docs/ansible_detailed.rst
7 months ago
Fabio Greco d818b201b3
Update missing host key checking argument in docs/ansible_detailed.rst
The arguments ansible_ssh_host_key_checking and ansible_host_key_checking were missing in the documentation, while being introduced with the commit "5749845324"
7 months ago
Alex Willmer 2ff904951c
Merge pull request #1296 from moreati/prep-0.3.25a3
Prep 0.3.25a3, again
7 months ago
Alex Willmer d4adce5d7e Resume 0.3.25dev 7 months ago
Alex Willmer 8ccaa48d2a Correct v0.3.25a3 __version__ tuple 7 months ago
Alex Willmer d6b5ea0787
Merge pull request #1295 from moreati/prep-0.3.25a3
Prepare 0.3.25a3
7 months ago
Alex Willmer 8090943031 Resume 0.3.25dev 7 months ago
Alex Willmer 945933854a Prepare v0.3.25a3 7 months ago
Alex Willmer 72cfc785db
Merge pull request #1294 from moreati/issue1281
ansible_mitogen: Support ANSIBLE_SSH_VERBOSITY with Ansible >= 12
7 months ago
Alex Willmer c1296b5d75 ansible_mitogen: Support ANSIBLE_SSH_VERBOSITY with Ansible >= 12
In vanilla Ansible >= 12 (ansible-core 2.19)
- ssh connection plugin `verbosity` controls `ssh [-v[v[v]]]`
- config option `DEFAULT_VERBOSITY` controls whether that output is displayed

In vanilla Ansible <= 11 (ansible-core <= 2.18)
- `DEFAULT_VERBOSITY` controls both `ssh` verbosity & display verbositty

As of this change
- Mitogen + Ansible >= 12 behaviour matches vanilla Ansible >= 12.
- Mitogen + Ansible <= 11 behaviour remains unchanged
  - `DEFAULT_VERBOSITY` only controls display verbosity.
- Mitogen + Ansible respect the Ansible variable `mitogen_ssh_debug_level`

I've chosen not to retroactively replicate the old vanilla Ansible behaviour
in Mitogen + Ansible <= 11 cases. I'm pretty sure it was an oversight,
rather than a design choice, but Ansible+Mitogen with `ANSIBLE_VERBOSITY=3`
is already very verbose.

fixes #1282

See
- https://docs.ansible.com/ansible/latest/reference_appendices/config.html#default-verbosity
- https://docs.ansible.com/ansible/devel/collections/ansible/builtin/ssh_connection.html#parameter-verbosity
7 months ago
Alex Willmer 4dc286371f
Merge pull request #1289 from moreati/issue1275-ssh_askpass
CI: Test SSH password authentication without sshpass command
7 months ago
Alex Willmer 55b0ece0e7 CI: Test SSH password authentication without sshpass command
Ansible 12 (ansible-core 2.19) has gained support for specifying an SSH
password, without requiring `sshpass`. It specifies the environment variable
`SSH_ASKPASS` such that `ansible` itself is called.

Mitogen is already able to support this. This change provides test coverage of
the new feature by not installing `sshpass` on macOS runners. when Ansible 12
is under test. Ubuntu runners come with `sshpass` pre-installed.

Required Ansible is also bumped to the latest pre-releases, for relevant
fixes.
7 months ago
Alex Willmer 3cba11a126 CI: Fix ansible_version comparison with ansible-core 2.19.0rc1
Note that tests/ansible/integration/ssh/templated_by_play_taskvar.yml was
previously erroniously being skipped with ansible-core 2.19.0a<N> and
2.19.0b<N>.

fixes #1293
refs #1175
7 months ago
Alex Willmer 98ae5e2c32
Merge pull request #1291 from moreati/issue1285-ci-callback-format
CI: replace `stdout=yaml` with `result_format=yaml` for Ansible >= 6 tests
7 months ago
Alex Willmer f330c2b158 CI: replace stdout=yaml with result_format=yaml for Ansible >= 6 tests
Ansible >= 12 (ansible-core >= 2.19) deprecates `stdout_callback=yaml`,
superceded by `callback_result_format=yaml`. There is a change in behaviour:
`callback_result_format` applies to output of both `ansible-playbook` _and_
`ansible`.

Tests that run `ansible` in a subprocess are now explicitly configured to use
json (even if they don't inspect that output yet) for more assert-able output
across all versions of Ansible.
7 months ago
Alex Willmer 8ddc181403
Merge pull request #1290 from moreati/prep-0.3.25a2
Prep 0.3.25a2
7 months ago
Alex Willmer 239ee673da Continue 0.3.25dev 7 months ago
Alex Willmer be4a214820 Prep v0.3.25a2 7 months ago
Alex Willmer 08f0eca1c2
Merge pull request #1286 from webknjaz/patch-1
Keep compatibility with `setuptools` tagging wheels with `py2.py3`
7 months ago
🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) d481a035c3 Keep comatibility with `setuptools` tagging wheels with `py2.py3`
Modern versions of `setuptools` emit a warning when the `universal = 1` option of `bdist_wheel` is used. This warning will turn into an error on Aug 30, 2025.

The only function of `universal = 1` is assigning the dual `py2.py3` tag to the wheels. It does not perform any content or metadata compatibility validation that might be related to this.

It is possible to keep producing same-tagged wheels by setting the non-deprecated `python_tag` option instead, which is what this PR does.

Fixes #1283

Ref https://github.com/pypa/setuptools/pull/4939
7 months ago
Alex Willmer 432ee9a844
Merge pull request #1280 from moreati/issue1274-json-in-the-dumps
ansible_mitogen: Replace use of deprecated `jsonify()`
7 months ago
Alex Willmer 32b346371d CI: Reject Ansible 12.0a6, avoids bug in yaml callback plugin
Avoids an error when the yaml stdout callback is enabled

> module 'ansible._internal._yaml._dumper' has no attribute 'SafeRepresenter'

refs #1284
7 months ago
Alex Willmer 41bee1a693 CI: Ansible wheels and development releases notes 7 months ago
Alex Willmer 2598941384 tests: Add Debian 11/bullseye security archive signing key
Tests that install packages are failing due to repos/packages that are signed
with this key.

```console
$ wget https://ftp-master.debian.org/keys/archive-key-11-security.asc
--2025-06-17 14:36:04--  https://ftp-master.debian.org/keys/archive-key-11-security.asc
Resolving ftp-master.debian.org (ftp-master.debian.org)... 192.91.235.231
Connecting to ftp-master.debian.org (ftp-master.debian.org)|192.91.235.231|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 11873 (12K) [application/pgp-keys]
Saving to: ‘archive-key-11-security.asc’

archive-key-11-security.asc 100%[=================>]  11.59K  --.-KB/s    in 0.002s

2025-06-17 14:36:05 (6.64 MB/s) - ‘archive-key-11-security.asc’ saved [11873/11873]

$ sha256sum archive-key-11-security.asc
716e79393c724d14ecba8be46e99ecbe1b689f67ceff3cb3cab28f6e69e8b8b8  archive-key-11-security.asc
$ cp archive-key-11-security.asc \
     ~/src/mitogen/tests/image_prep/roles/package_manager/files/debian-archive-bullseye-security-automatic.asc
```
8 months ago
Alex Willmer 667dd4237a ansible_mitogen: Replace use of `ansible.parsing.utils.jsonify.jsonify()`
The function is Ansible >= 12 (ansible-core >= 2.19). See #1274 for analysis
of `json.dumps()` vs `jsonify()` differences. This change is a middle ground
between full backward compatibility and using `json.dumps()` unadorned.

- if `data` is `None`, then it will still be transferred as `{}` on older
  versions of Ansible, but 'null' in newer releases. Cases where 'null'
  caused a problem are suspected/reported, but no reproducers are available.
- `ensure_ascii=True` will be still be tried, with fallback. I believe this
  is only relevant on Python 2.x.
- `sort_keys=True` will no longer be used.
- No indentation/pretty printing will be applied, this remains unchanged

fixes #1274
8 months ago
Alex Willmer 0d6aa99370
Merge pull request #1279 from moreati/issue1272-strategic-notice
docs: Mention Ansible strategy plugin deprecation in changelog
8 months ago
Alex Willmer 5f33849311 docs: Mention Ansible strategy plugin deprecation in changelog
fixes #1272
8 months ago
Alex Willmer 0e6d795db0
Merge pull request #1277 from moreati/docs-version-stable-only
docs: Fix website download link when there is a pre-release
8 months ago
Alex Willmer 5ec61c90a6 docs: Fix website download link when there is a pre-release
The previous regex was incorrectly matching a prefix (e.g. 1.2.3) of a
pre-release (e.g. 1.2.3a1, 1.2.3rc1).

fixes #1276
8 months ago
Alex Willmer d4a19ae9d3
Merge pull request #1267 from moreati/ansible-lint-ignore
chore: Ignore locally installed collections, dummy modules, etc
8 months ago
Alex Willmer d224a61556 chore: Ignore locally installed collections, dummy modules, etc
With mock_modules ansible-lint creates files here, e.g.

```console
✗ find .ansible
.ansible
.ansible/roles
.ansible/modules
.ansible/modules/test_echo_module.py
.ansible/modules/custom_python_leaky_class_vars.py
.ansible/modules/custom_python_new_style_module.py
...
```
8 months ago
Alex Willmer 837111c6d5
Merge pull request #1271 from moreati/prep-0.3.25a1
Prepare v0.3.25a1
8 months ago
Alex Willmer dd4b5755ef Continue 0.3.25dev 8 months ago
Alex Willmer bd774b7489 Prepare v0.3.25a1 8 months ago
Alex Willmer 208e37abef packaging: Handle pre-release __version__ tuples 8 months ago
Alex Willmer 9dee94641f
Merge pull request #1264 from moreati/ansible-2.19
Ansible 2.19 (review/sandbox)
8 months ago
Alex Willmer 0187418697 ansible_mitogen: alpha datatag handling & CI for Ansible 12 (ansible-core 2.19)
refs #1258
8 months ago
Alex Willmer e5a56a833c docs: Add Ansible 12 to support table 8 months ago
Alex Willmer 8ed4e9d41c
Merge pull request #1259 from stefanor/ansible-2.19
Ansible 2.19 alpha support
8 months ago
Stefano Rivera 8abafe8c8e Changelog entry for ansible 12 support 8 months ago
Lee Garrett 375cdbe5cf Work around missing internal method
ansible 2.19 has removed the _strip_unsafe internal method, so work
around this for now.
8 months ago
Lee Garrett d3413372b4 Allow running with ansible-core 2.19 8 months ago
Stefano Rivera 54ad3341f1 Support tox tests on ansible 12 alphas 8 months ago
Alex Willmer b382855aa2
Merge pull request #1269 from moreati/prep-0.3.24
Prep v0.3.24
8 months ago
Alex Willmer 65a81121c5 Begin 0.3.25dev 8 months ago
Alex Willmer 229fd67e97 Prepare v0.3.24 8 months ago
Alex Willmer 07028c190b
Merge pull request #1268 from moreati/stdio-no-no-no
mitogen: Only close stdio file descriptors that were open at process startup
8 months ago
Alex Willmer 566a4c1e3c mitogen: log child file objects rather than descriptors
The repr() of file objects is more self descriptive, and includes the fd.
8 months ago
Alex Willmer 6cec613daa mitogen: Only close stdio file descriptors that were open at process startup
File descriptors 0, 1, and 2 are usually stdin, stdout, stderr; but not
always. If a process is started without one of these then the first descriptor
allocated by the process opening a file or socket will be allocated an fd <=
STDERR_FILENO. This isn't common, but it does occur, e.g. Windows GUI apps
started without being connected to a console, controller side plugins run
under Ansible 12 (ansible-core 2.19).

In such cases the corresponding sys attribute (e.g. sys.stderr) will be None.

refs #1258

See also
- https://docs.python.org/3/library/sys.html#sys.__stdin__
- https://docs.ansible.com/ansible/devel/porting_guides/porting_guide_12.html#porting-guide-for-v12-0-0a1
- https://github.com/ansible/ansible/pull/82770
- https://github.com/python/typeshed/issues/11778
- https://gist.github.com/moreati/034fef45f73d809d9411a8a63eca34d6
8 months ago
Alex Willmer 49fea37879 mitogen: Use pty.STD*_FILENO constants
Makes it more obvious and easier to find where stdin, stdout, and stderr file
descriptors are being interacted with.
8 months ago
Alex Willmer 8faae13d7e mitogen: Improve readability of stderr initialisation in create_child()
There should be no behaviour change.
8 months ago
Alex Willmer ab63864ace CI: Fix indentation 8 months ago
Alex Willmer e19397ee59 CI: Timeout Linux jobs after 25 minutes
Some tests have been seen deadlocked. They continued running for an hour+,
until the default Github timeout. Linux timeout higher than macOS because the
Linux jobs run more tests.
8 months ago
Alex Willmer a00263d963 CI: Fix cosmetic typos in get_with_context() regression test 8 months ago
Alex Willmer 68f17420e2 CI: Name Ansible stack construction plays
Simplifies matching a failed test to the play/task.
8 months ago
Alex Willmer c31d902dc8 CI: Skip tests that require Mitogen when Ansible strategy is linear
The Van_* GitHub Actions jobs (corresponding to Tox factor strategy_linear,
environment variable ANSIBLE_STRATEGY=linear) were failing inside Mitogen
modules, which they should not touch. The jobs are intended as a cross
validation of the test suite, they should only fail if Ansible itself has a
problem.
8 months ago
Alex Willmer 4c41bf02f1 CI: Specify ANSIBLE_STRATEGY in tasks that run ansible or ansible-playbook
This makes the behaviour more consistent across jobs that run with
`mitogen_linear` or plain `linear`.
8 months ago
Alex Willmer a9048f0f7d CI: Use Ansible finished test (`result.finished` -> `result is finished`)
Required by Ansible 12 (ansible-core-2.19).

refs #1298
8 months ago
Alex Willmer 491d438427 CI: Add is_macos_controller Ansible variable for become_unpriv_available
This eliminates the need for ansible_facts to be gathered before
become_unpriv_available can be referenced.
8 months ago
Alex Willmer dcff603267
Merge pull request #1261 from moreati/prep-0.3.23
Prepare 0.3.23
9 months ago
Alex Willmer c146682e2e Begin 0.3.24dev 9 months ago
Alex Willmer 8e25944c94 Prepare v0.3.23 9 months ago
Alex Willmer 4a75648774 packaging: Fix InvalidVersion in release versions
fixes #1263
9 months ago
Alex Willmer fa28024810
Merge pull request #1257 from moreati/issue1256
CI: Upgrade Linux runner to Ubuntu 22.04 & 24.04
9 months ago
Alex Willmer 27b4b77bba CI: Upgrade Github jobs from Ubuntu 20.04 to 22.04 & 24.04
Python 2.7 (distro package) and 3.6 (pyenv managed) jobs run on Ubuntu 22.04.
More recent Pythons (distro or Github provided) run on 24.04.

fixes #1256

Ansible tasks that run locally (e.g. `connection: local`, `delegate_to:
localhost`) must now specify their `ansible_python_interpreter`, typically as
`{{ ansible_playbook_python }}`; otherwise the system Python on the controller
(e.g. `/usr/bin/python`) is likely to be used and this is often outside the
version range supported by the Ansible verison under test. If this occurs then
the symptom is often a failure to import a builtin from
`ansible.module_utils.six.moves`, e.g.

```
fatal: [target-centos6-1]: FAILED! => changed=true
  cmd:
  - ansible
  - -m
  - shell
  - -c
  - local
  - -a
  - whoami
  - -i
  - /tmp/mitogen_ci_ansibled3llejls/hosts
  - test-targets
  delta: '0:00:02.076385'
  end: '2025-04-17 17:27:02.561500'
  msg: non-zero return code
  rc: 8
  start: '2025-04-17 17:27:00.485115'
  stderr: |-
  stderr_lines: <omitted>
  stdout: |-
    An exception occurred during task execution. To see the full traceback,
    use -vvv. The error was:     from ansible.module_utils.six.moves import
    map, reduce, shlex_quote
```
9 months ago
Alex Willmer 1f737568b2 ci: Remove vestigial Python 2.7 support in macOS Github jobs 9 months ago
Alex Willmer 9b17f2839c ci: Label Github jobs by OS/distribution & Tox environment 9 months ago
Alex Willmer b130cd9f90
Merge pull request #1252 from moreati/issue1118-update-containers
CI: Mark and use 2021 test containers
11 months ago
Alex Willmer fdbd1a8c9b ci: Configure package managers using a role
This allows code sharing between integration tests and test image prep.
11 months ago
Alex Willmer 1e12edbf95 ci: Use file module to set mitogen__readonly_homedir permissions 11 months ago
Alex Willmer f2e0e552ac ci: Fix sshd configuration during image prep
This will allow image preparation using Ansible versions that
- predate ansible_facts.*
- predate loop keyword
- predate collections
11 months ago
Alex Willmer 20e23b5bd9 ci: Name all image prep plays 11 months ago
Alex Willmer 8e58c4a759 ci: Decouple image prep from Ansible controller reporting
This will allow image preparation using Ansible versions that predate
import_playbook.
11 months ago
Alex Willmer 67ececc804 ci: Use GitHub Container Registery images tagged 2021
Previously (and implicitly) used "latest". The tag 2021 is new today, the
image contents have not changed since they were generated in 2021.

They have moved container registry twice since 2021
- #791 Docker -> Amazon Elastic Container Registry (public.ecr.aws/n5z0e8q)
- #1128 Amazon ECR -> GitHub Container Registry (ghcr.io/mitogen-hq)

This commit also removes the last references to ECR.
11 months ago
Alex Willmer 0388cd5c0f
Merge pull request #1247 from moreati/issue-1118-ci_lib
CI: ci_lib cleanup
12 months ago
Alex Willmer a376daa04d CI: Consolidate directory path constants 12 months ago
Alex Willmer f659213159 CI: Don't share temporary directory between test groupings
Each grouping gets an independant dir, e.g.
 - ansible -> /tmp/mitogen_ci_ansible
 - debops -> /tmp/mitogen_ci_debops

Importing ci_lib no longer creates a temporary directory as a side effect.
12 months ago
Alex Willmer 620bc3a944 CI: Don't copy SSH private key to temporary dir
Running tests aren't using the copy & it wasn't being cleaned up.
12 months ago
Alex Willmer 2095342245 CI: Remove unused ci_lib.have_*() functions 12 months ago
Alex Willmer 2c7eda1dc1 CI: Fix NameError in ci_lib._have_cmd() 12 months ago
Alex Willmer 0b216c815a CI: Consolidate `apt-get install`s 12 months ago
Alex Willmer 26507481b1
Merge pull request #1246 from moreati/issue1118
CI: Test user creation tidy up
12 months ago
Alex Willmer 78b440104e CI: Validate sudoers file 12 months ago
Alex Willmer c92df356e6 CI: Consolidate sudoers config tasks 12 months ago
Alex Willmer 11d2d70fd8 CI: Use native Ansible support to hide macOS users 12 months ago
Alex Willmer 5283e6756b CI: Statically specify test usernames and group names
This makes it easier to grep for a username and to discover how the user was
create. Hence it should be easier to understand/debug tests.
12 months ago
Alex Willmer 913090ea7e
Merge pull request #1245 from moreati/issue1238
packaging: Avoid ast module, requires Python = 2.6
12 months ago
Alex Willmer a0d3858ff9 packaging: Avoid ast module, requires Python = 2.6
fixes #1238, refs #1236
12 months ago
Alex Willmer 77d87cd3cd
Merge pull request #1244 from moreati/issue1121
mitogen: cfmakeraw() cleanups
12 months ago
Alex Willmer 927fb172d8 mitogen: Log skipped termios attributes
refs #1121
12 months ago
Alex Willmer 5f42da36f3 mitogen: Deduplicate cfmakeraw() flags
refs #1121
12 months ago
Alex Willmer a3768a0443
Merge pull request #1240 from moreati/prep-v0.3.22
Prepare v0.3.22
12 months ago
Alex Willmer 657e40b982 Begin 0.3.23dev 12 months ago
Alex Willmer ae703b97a7 Prepare v0.3.22 12 months ago
Alex Willmer 8580da903c
Merge pull request #1235 from moreati/issue1234
ansible_mitogen: Fix TypeError in set_file_owner()
1 year ago
Alex Willmer 9b91a1a529 ansible_mitogen: Fix TypeError in set_file_owner()
fixes #1234
1 year ago
Sergey Putko 186404829d
ansible_mitogen: Fix dnf module by patching include of dnf.cli (#1230)
* fix dnf module import

* add changelog
1 year ago
Alex Willmer 45472c6edc
Merge pull request #1227 from moreati/rename-tc-via-tests
tests: Name transport_config tests that involve mitogen_via
1 year ago
Alex Willmer f82c72f539 tests: Name transport_config tests that involve mitogen_via
This should make it much easier to find a (failed) test, based on test output.
1 year ago
Alex Willmer fca7578cdf
Merge pull request #1213 from moreati/spring-clean-2025
Spring clean 2025
1 year ago
Alex Willmer e97d20c9d1 ansible_mitogen: Return stderr_lines from _low_level_execute_command()
Vanilla Ansible has returned stderr since v1.9 or earlier, stderr_lines was
added in v2.6.0 (https://github.com/ansible/ansible/pull/40079).
1 year ago
Alex Willmer 51c7b789f7 ansible_mitogen: Decouple possible_pythons order & error handling
'python' could now be tried earlier, or not at all.
1 year ago
Alex Willmer 356be2e65f ansible_mitogen: Remove unneeded internal _run_cmd() 1 year ago
Alex Willmer 1b8b2c8b1a ansible_mitogen: Rename Mitogen interpreter discovery attributes
This makes their nature and ownership/responsibility much more explicit.
1 year ago
Alex Willmer d3da3ff769 ansible_mitogen: Don't redeclare interpreter discovery attributes
Duplicated effort on Ansible 2.10, and a potential source of future error
1 year ago
Alex Willmer 9342186b22 tests: Fix unclosed file in fd_check script 1 year ago
Alex Willmer 67219c309a mitogen: Fix unclosed file in first stage 1 year ago
Alex Willmer 6fcb7aae96 mitogen: Replace uses of deprecated `pkgutil.find_loader()`
fixes #1111
1 year ago
Alex Willmer 90779fe846 ci: Enable Python warnings 1 year ago
Alex Willmer 6698f4bcd9 tests: Remove unused tasks fragment 1 year ago
Alex Willmer e564944c5b tests: Stricter playbook and inventory parsing 1 year ago
Alex Willmer 3f068e7c83 ci: Remove unneeded aws command entries
CI container imagesa re now hosted by GitHub Container Register. There is
nothing that needs to run `aws`.

fixes #1036
1 year ago
Alex Willmer 0953a931c0
Merge pull request #1223 from moreati/prep-v0.3.21
Prepare v0.3.21
1 year ago
Alex Willmer 5c76941d1e Begin 0.3.22dev 1 year ago
Alex Willmer 8b7354cb3a Prepare v0.3.21 1 year ago
Alex Willmer 53b6bf0292 docs: Add changelog, credits for connection_loader__get fixes
refs #1215
1 year ago
Alex Willmer c39c47501a
Merge pull request #1215 from Nihlus/fix-connection-loader
ansible_mitogen: Fix usage of connection_loader__get.
1 year ago
Jarl Gullberg 211079f130
Add regression tests for the new connection logic.
Co-authored-by: Alex Willmer <alex@moreati.org.uk>
Co-authored-by: Mark Farrell <mark.a.farrell@team.telstra.com>
1 year ago
Jarl Gullberg 8ebeaffaad
Disallow connection redirection of sub-connections if the top-level connection in the play is not a Mitogen connection. 1 year ago
Jarl Gullberg d0afae7806
Fix usage of connection_loader__get by wrapping the correct upstream functions. 1 year ago
Alex Willmer 69a61732cd
Merge pull request #1217 from Nihlus/fork-freeipa-modules
ansible_mitogen: Add all ansible_freeipa modules to the always-fork list.
1 year ago
Jarl Gullberg 0d1c84e727
Update changelog. 1 year ago
Jarl Gullberg 7330d68e19
Add all ansible_freeipa modules to the always-fork list. 1 year ago
Alex Willmer 20e92a63ff
Merge pull request #1212 from moreati/issue1209
docs: Attempt to configure Netlify build of mitogen.networkgenomics.com
1 year ago
Alex Willmer f032762012 docs: Attempt to configure Netlify build of mitogen.networkgenomics.com
This site has an unknown build config. Specifically I do not know the value of

- Package directory
- Base directory

these are the locations that Netlify looks for netlify.toml. I've already
tried docs/netlify.toml (#1210), unsuccessfully. This attempt is on the basis
that Package directory == '/', or less likely that Base directory == '/'.

refs #1209
See
- https://docs.netlify.com/configure-builds/monorepos/#use-a-netlify-configuration-file
- https://docs.netlify.com/configure-builds/file-based-configuration/
1 year ago
Alex Willmer 5ef8db92b6
Merge pull request #1211 from moreati/issue1209
docs: Read release version from changelog
1 year ago
Alex Willmer 1195e39b55 docs: Read release version from changelog
fixes #1157
1 year ago
Alex Willmer 69b6552f3a
Merge pull request #1210 from moreati/issue1209
docs: Fix "No module named 'imghdr'" when building website
1 year ago
Alex Willmer 882dc0ca06 docs: Fix "No module named 'imghdr'" when building website
Between Mitogen 0.3.19 & 0.3.20 Netlify changed their default Python to 3.13.
This broke our deployment of https://mitogen.networkgenomics.com/. The
previous default was Python 3.8, based on a recent successful build of
https://spiffy-croissant-696d93.netlify.app.
1 year ago
Alex Willmer 34d9df101f
Merge pull request #1207 from moreati/prep-0.3.20
Prepare v0.3.20
1 year ago
Alex Willmer 3ee6a0ff93 Begin v0.3.21dev 1 year ago
Alex Willmer f1f8ad11ee Prepare v0.3.20 1 year ago
Alex Willmer 11a6ccfe71
Merge pull request #1206 from moreati/issue1083-python_path2
ansible_mitogen: Respect interpreter_python and ANSIBLE_PYTHON_INTERPRETER
1 year ago
Alex Willmer 945e360363 ansible_mitogen: Respect interpreter_python and ANSIBLE_PYTHON_INTERPRETER
This adapts PR #740 by @extmind (afe0026890),
which augmented the call to `Connection.get_task_var()` with
`C.config.get_config_value('INTERPRETER_PYTHON'` as a default. Instead this
*replaces* the call to `Connection.get_task_var()`. The aim is greater
simplicity by disentangling templating of a configured interpreter path and
discovery of an interpreter when none is configured. I think this also reduces
the number of times `Connection._get_task_vars()` is called, so reducing the
number of times we do the ugly stack frame inspection.

I've also added test cases.

Co-authored-by: Lars Beckers <lars@extmind.de>
1 year ago
Alex Willmer e8005ece3a
Merge pull request #1201 from moreati/issue1083-timeout
ansible_mitogen: Templated connection timeout
1 year ago
Alex Willmer 5e6d7bf4fb ansible_mitogen: Templated connection timeout
Ansible >= 4 (ansible-core >= 2.11) the SSH plugin has a `timeout` option and
with variable `ansible_ssh_timeout`, but not a `ansible_timeout` variable.
The local plugin has no such option or variable(s). However `ansible_timeout`
is backfilled for all conection plugins, by legacy mechanisms that populate
the play context attribute:
- `ansible.constants.COMMON_CONNECTION_VARS`
- `ansible.constants.MAGIC_VARIABLE_MAPPING`

The `timeout` keyword is for task completion timeout, not connection timeout.
1 year ago
Alex Willmer 6900e88dfd ansible_mitogen: Fix templated python interpreter with `meta: reset_connection`
refs #1079
1 year ago
Alex Willmer 941da31735 build: Reduce macOS job timeout to mitigate BlockingIOError
This will mitigate the impact of #1185 a little, which causes the job to
continue running without making progress, until it hits this timeout.
Successful jobs are typically completing in 8 - 12 minutes.

refs #1185
1 year ago
Alex Willmer d033f7b057 ansible_mitogen: Restore dummy objects in Connection.reset()
The previous commit (53b4881628 in PR 1200) was
not intended to change these values, but some WIP slipped through. This
partially reverts that commit so the two changes (moving the monkey patch,
making the monkey patch more capable) exist in distinct commits.
1 year ago
Alex Willmer 7a828e7510
Merge pull request #1200 from moreati/issue1079-templated-python-interpreter
ansible_mitogen: Fix timeout in wait_for_connection with templated ansible_python_interpreter
1 year ago
Alex Willmer 53b4881628 ansible_mitogen: Fix wait_for_connection + templated ansible_python_interpreter
This tightens up our monkey patching `Connection._action` so it's only applied
during `meta: reset_connection` & promptly removed. This fixes "'int' object
has no attribute 'template'" when `ansible.plugins.action.wait_for_connection`
or other code calls `ansible.plugins.connection.ConnectionBase.reset()`.

This could also have switched to `templar=templar` on the temporary action,
rather than `templar=0`, but it's not strictly necessary to fix this bug. I
anticipate other changes doing so soon, to improve interpreter discovery &
templated python interpreter path support.
1 year ago
Alex Willmer 288b0051d2
Merge pull request #1198 from moreati/prep-v0.3.19
Prepare v0.3.19
1 year ago
Alex Willmer ffb1709630 Begin v0.3.20dev 1 year ago
Alex Willmer 24ba734ff9 Prepare v0.3.19 1 year ago
Alex Willmer d68368e549
Merge pull request #1177 from moreati/issue1129
Ansible 11 (ansible-core 2.18)
1 year ago
Alex Willmer 0b99169f42 Support Ansible 11 (ansible-core 2.18) 1 year ago
Alex Willmer b36806ab7f
Merge pull request #1194 from moreati/prepare-v0.3.18
Prepare v0.3.18
1 year ago
Alex Willmer 1cd7ea18d3 Begin v0.3.19dev 1 year ago
Alex Willmer d85d9a25ee Prepare v0.3.18 1 year ago
Alex Willmer 4a2ba525a7
Merge pull request #1193 from moreati/issue1083-become
ansible_mitogen: Templated become flag
1 year ago
Alex Willmer dd41ddf89b ansible_mitogen: Templated become flag
The code change to support this was already made in transport_config.py, as
part of templated become_user support (commit bf6607e27e, PR #1148). This
commit adds tests to confirm the functionality.
1 year ago
Alex Willmer 8e64459bbf
Merge pull request #1192 from moreati/issue-1083-become_method
ansible_mitogen: Templated become method
1 year ago
Alex Willmer e120cd2cae ansible_mitogen: Templated become method 1 year ago
Alex Willmer 61c8267605
Merge pull request #1190 from moreati/test-port-keyword
Prepare v0.3.17 and some washup tests
1 year ago
Alex Willmer d2db3c3840 Begin v0.3.18dev 1 year ago
Alex Willmer 6cf6f69751 Prepare v0.3.17 1 year ago
Alex Willmer 905b87b71a tests: Test templated ansible_host_key_checking provided by task vars
missed by #1184
1 year ago
Alex Willmer 5ae5bb94ac docs: Changelog entry for templated ansible_host 1 year ago
Alex Willmer 6da2c6a80f
Merge pull request #1189 from moreati/issue1083-host
ansible_mitogen: Templated target host
1 year ago
Alex Willmer f50a61f981 ansible_mitogen: Templated host option (e.g. ansible_host, ansible_ssh_host)
A twist - for the connection option "host" the corresponding legacy
PlayContext attribute is PlayContext.remote_addr. This may be the only case
where a connection option name and the PlayContext attribute name differ.
1 year ago
Alex Willmer 6d9f2e12d9 tests: Switch remaining tt_targets_inventory group vars to host vars
This is ground work for adding/testing templated hostnames and python
interpreters. The extreme wideness will hopefully be temporary, e.g. by
switching to YAML inventories. The INI inventory plugin doesn't support
multiline host entries.

> 640 K(olumns) should be enough for anyone
> -- Apocryphal, not Bill Gates
1 year ago
Alex Willmer 0d09174031
Merge pull request #1184 from moreati/issue1083-host_key_checking
ansible_mitogen: Templated SSH host key checking
1 year ago
Alex Willmer 3a1b5ec620 CI: Increase sshd MaxAuthRetries to 50 on macOS runners
refs #1186
1 year ago
Alex Willmer 8cfcb66cda CI: Refactor sshd configuration into a role
Prep for applying it to macOS 13 GitHub runners.

refs #1186
1 year ago
Alex Willmer 9e0dad2a1a ansible_mitogen: Templated SSH host key checking
refs #1083
1 year ago
Alex Willmer 9189c01c16
Merge pull request #1181 from moreati/issue1083-private_key_file
ansible_mitogen: Templated SSH private key file
1 year ago
Alex Willmer c7df5c97c1 ansible_mitogen: Templated SSH private key file 1 year ago
Alex Willmer 5895ccadd2
Merge pull request #1183 from moreati/issue1182
CI: Fix incorrect u=r,g=r,o=rw file permissions on mitogen__has_sudo_pubkey.key
1 year ago
Alex Willmer 43cc937bc6 CI: Fix incorrect u=r,g=r,o=rw file permissions on mitogen__has_sudo_pubkey.key
The wrong base was used when calculating the mode. So the file became world
readable and writable on a CI runner, until
ansible/integration/ssh/variables.yml happened to correct it near the end of
the integration tests.

I believe this was the only instance.

```console
mitogen git:(issue1182) ✗ ag --python 'int\(.+7\)' . .ci | wc -l
       0
```

fixes #1182
1 year ago
Alex Willmer a35b208acd
Merge pull request #1179 from moreati/prep-v0.3.16
Prepare v0.3.16
1 year ago
Alex Willmer 757527635d Begin v0.3.17dev 1 year ago
Alex Willmer d28dd09e23 Prepare v0.3.16 1 year ago
Alex Willmer df8f11d731
Merge pull request #1176 from moreati/issue1133
CI: Migrate to from macOS 12 to 13 test runners
1 year ago
Alex Willmer 06df62c8b8 CI: Migrated macOS 12 runners to macOS 13, due to EOL.
macOS Python 2.7 jobs have been removed because the macOS 13 image doesn't
include CPython 2.7.
1 year ago
Alex Willmer 88e7c568d2
Merge pull request #1175 from moreati/issue1083-ssh_executable
ansible_mitogen: Templated ssh executable
1 year ago
Alex Willmer 833e2845e9 ansible_mitogen: Templated ssh executable, templated reset_connection fix
Adding a the tt-ssh-executable test target uncovered an Ansible bug during
`meta: reset_connection` tasks. So this commit includes a workaround for
affected versions of Ansible.
1 year ago
Alex Willmer 89244703ff
Merge pull request #1174 from moreati/issue1083-become_flags
ansible_mitogen: Template become command arguments (become_flags)
1 year ago
Alex Willmer 66ea10d577 ansible_mitogen: Template become command arguments (become_flags)
Uses the same fallback for (mitogen_sudo et al) as become_exe (see #1173).

The new `Spec.become_flags()` is not yet explicitly tested. Note that it
returns a string (matching the Ansible option of the same name), whereas
`Spec.sudo_args()` returns a list.

refs #1083
1 year ago
Alex Willmer 04f7b7a282
Merge pull request #1172 from moreati/issue1083-become_exe
ansible_mitogen: Support templated become_exe option
1 year ago
Alex Willmer ec9b3e5c5d ansible_mitogen: Support templated become_exe option
Some ansible_mitogen connection plugins look more like become plugins (e.g.
mitogen_sudo) & use become plugin options. For now there's special handling in
PlayContextSpec._become_option(). Further design/discussion can go in #1173.

Refs #1087.
1 year ago
Alex Willmer 06a82d3944
Merge pull request #1170 from moreati/prep-v0.3.15
Prep v0.3.15
1 year ago
Alex Willmer 26c4c33ad3 Begin 0.3.16dev 1 year ago
Alex Willmer 7634e2c469 Prepare v0.3.15 1 year ago
Alex Willmer 0526f8e167
Merge pull request #1169 from moreati/issue1083-become_pass
ansible_mitogen: Support templated become passwords
1 year ago
Alex Willmer 7e5b064139 ansible_mitogen: Support templated become passwords 1 year ago
Alex Willmer 21e002af2d
Merge pull request #1168 from moreati/issue1083-become_pass
tests: Re-enable become/sudo tests, fix them on macOS runners
1 year ago
Alex Willmer 8a34b925a4 tests: Re-enable become/sudo tests, fix them on macOS runners
The tasks in tests/imageprep/_user_accounts.yml that create users did not
specify a primary group for those users - this left the decision to Ansible's
user module, and/or the underlying OS. In Ansible 9+ (ansible-core 2.16+ the
user module defaults to primary group "staff." Earlier don't supply a default,
which releases probably results in a primary group nameed "None" (due to
stringifying the Python singleton of the same name), or whatever the macOS
Directory Services has for no data/NULL.

The invalid GID 4294967295 (MAX_UINT32 == 2**32-1) in the sudo error probably
enters the mix via something similar to sudo CVE-2019-14287.

Fixes #692

See
- https://github.com/ansible/ansible/pull/79999
- https://github.com/ansible/ansible/commit/c69c83c962f987c78af98da0746527df
- https://www.sudo.ws/security/advisories/minus_1_uid/

> Bruce Wayne : [confused]  Am I meant to understand any of that?
> Lucius Fox : Not at all, I just wanted you to know how hard it was.
> -- Batman Begins
1 year ago
Alex Willmer 257d602a11
Merge pull request #1167 from moreati/issue905
ansible_mitogen: Template `ssh_args`, `ssh_common_args`, `ssh_extra_args`
1 year ago
Alex Willmer cdfaf31ebc ansible_mitogen: Template ssh_*_args connection options
This expands support to setting them in Play scoped variables. Task scoped
variables are also very likely to work, but untested for now.

refs #905
1 year ago
Alex Willmer a1d079acd7
Merge pull request #1163 from moreati/prep-v0.3.14
Prep v0.3.14
1 year ago
Alex Willmer d35ca3e4af Begin 0.3.15.dev 1 year ago
Alex Willmer c4ca015266 Prepare v0.3.14 1 year ago
Alex Willmer a07489dbd4
Merge pull request #1148 from mordekasg/#1083
ansible_mitogen: Support templated `become_user`
1 year ago
Alex Willmer bf6607e27e ansible_mitogen: Support templated become_user
This reads the become username from the `become_user` attribute of the play
context, to the `"become_user"` option of the loaded become plugin. This has
been supported by vanilla Ansible since Ansible 2.10 (ansible-base 2.10).

To support this I've also switched from using the `play_context.become` (a
bool), to `connection.become` (an instance of the appropriate) become plugin.

New tests have been added, modelled on those for templated connection
parameters (see #1147, #1153, #1159).

See
- 480b106d65

refs #1083

Co-authored-by: mordek <m.pirog@bonasoft.pl>
1 year ago
Alex Willmer 3b2b03bd97
Merge pull request #1150 from moreati/local-options
Add and test templated local connection parameters
1 year ago
Alex Willmer e9bddf0c03 CI: Use templated ansible_user for localhost Ansible tests
refs #1022, #1116
1 year ago
Alex Willmer f384fc33d0
Merge pull request #1159 from moreati/test-distro-specs
ci: Consolidate Mitogen jobs
1 year ago
Alex Willmer 28e08ef94c ci: Reduce number of Jobs by parameterizing Mitogen Docker SSH tests
This reduces the number of jobs from 48 to 24. The Mitogen part of the test
suite has been parameterized on the Linux container targets to be run against.
Both the Ansible tests & Mitogen tests now use the same source of truth to
control which targets to use: environment variable MITOGEN_TEST_DISTRO_SPECS.
This replaces the two mutually exclusive env vars DISTRO and DISTROS. I've
also removed vestgial traces of an unused env var MITOGEN_TEST_DISTRO.

Parameterization adapted from
https://eli.thegreenplace.net/2014/04/02/dynamically-generating-python-test-cases

refs #1058, #1059
1 year ago
Alex Willmer 9859e44ee8 tests: Standardise on DockerizedSshDaemon.host & .port 1 year ago
Alex Willmer c45b13bee3
Merge pull request #1154 from moreati/test-port-keyword
tests: templated remote_user keyword with delegate_to
1 year ago
Alex Willmer 5e816be12c tests: Templated connection keywords with delegated_to 1 year ago
Alex Willmer 825a84a0d1
Merge pull request #1153 from moreati/issue1040
tests: Templated "remote_user" provided as Ansible playbook keyword
1 year ago
Alex Willmer 5d6a185242 tests: Templated "remote_user" provided as Ansible playbook keyword
The password is provided as a variable because there is no corresponding
keyword. I get the impression that keywords are considered a legacy mechanism,
so most (new) options are only overridable by variables.

The port is proved as a variable for now, to test remote_name in isolation.
1 year ago
Alex Willmer 24e39b241f
Merge pull request #1151 from moreati/prep-0.3.13
Prepare 0.3.13
1 year ago
Alex Willmer 47e25eb8c5 Begin 0.3.14 development 1 year ago
Alex Willmer 8dec038941 Prepare v0.3.13 1 year ago
Alex Willmer b91407a779 docs: Correct v0.3.12 version in changelog
fixes #1149
1 year ago
Alex Willmer 11fe832a79
Merge pull request #1075 from moreati/issue1073
Python 3.13 support
1 year ago
Alex Willmer 62b75f7750 docs: shields.io badges for PyPI version & supported Python versions 1 year ago
Alex Willmer 9cdd51cf5b Declare Python 3.13 support
No code changes needed, that I could find.
1 year ago
Alex Willmer e2c112d2fe
Merge pull request #1146 from stefanor/python3.13
Remove get_password_hash, unused
1 year ago
Stefano Rivera 34d441fb87 Remove get_password_hash, unused
spwd is removed in Python 3.13. But fortunately, this function itself is
never used.

Part of: #1073
1 year ago
Alex Willmer e3b16d6d13
Merge pull request #1145 from moreati/issue978
Ansible: templated SSH port
1 year ago
Alex Willmer 77a01ff8d6 ansible_mitogen: Support templated SSH port
fixes #978
1 year ago
Alex Willmer fb76f2eeea
Merge pull request #1147 from jmkeyes/template-ssh-user-and-port
ansible_mitogen: Handle templated ansible_ssh_user.
1 year ago
Alex Willmer 14cb8be7e5 ansible_mitogen: Test templated connection user (e.g. ansible_user) 1 year ago
Joshua K 2c4316fa16
Fix rST whitespace error in changelog entry.
Co-authored-by: Alex Willmer <alex@moreati.org.uk>
1 year ago
Joshua M. Keyes 6053e1b5cf ansible_mitogen: Handle templated ansible_ssh_user. 1 year ago
Alex Willmer 45ab5344d5
Merge pull request #1144 from moreati/washup
Washup
1 year ago
Alex Willmer c395b13184 CI: Remove Azure DevOps environment variable handling
refs #1138
1 year ago
Alex Willmer 8bf4eb2ce9 CI: Remove awcli from local tooling, add missing python*{-dev,-venv} variants 1 year ago
Alex Willmer 0e9c890637 tests: Remove unused physical_hosts variable 1 year ago
Alex Willmer 90ba0a74eb ansible_mitogen: Remove unused imports 1 year ago
Alex Willmer 1773c9aba6 trivia: Fix trailing whitespace 1 year ago
Alex Willmer 9f0566b522 docs: Changelog entry for migration to GitHub Actions
refs #1138
1 year ago
Alex Willmer 69edac7b98
Merge pull request #1142 from moreati/azure-no-more
CI: Remove Azure DevOps pipelines (replaced by GitHub Actions)
1 year ago
Alex Willmer 8362d61462 CI: Remove Azure DevOps pipelines (replaced by GitHub Actions)
fixes #1138
1 year ago
Alex Willmer 6e4336ce0e
Merge pull request #1140 from moreati/prep-0.3.12
Prep 0.3.12
1 year ago
Alex Willmer 61b800781b Begin v0.3.13 development 1 year ago
Alex Willmer 298d28a650 Prep v0.3.12 1 year ago
Alex Willmer 3f288f934a docs: Correct 0.3.11 release month
Reports of Mitogen's time machine will have been greatly exaggerated.
1 year ago
Alex Willmer 8f7ec88a9f
Merge pull request #1139 from moreati/github-action-all-greens
CI: Add re-actors/alls-green GitHub Actions job
1 year ago
Alex Willmer b05b2c8c8e CI: Add re-actors/alls-green GitHub Actions job
This will allow a single job to be required in the GitHub branch protection
web UI; regardless of which jobs are added to or removed from the matrix of
platform specific, Ansible specific jobs.
1 year ago
Alex Willmer c9f2d905a0
Merge pull request #1137 from moreati/test-targets-disabled
tests: Ignore inventory files of inactive tests & benchmarks
1 year ago
Alex Willmer 3504bea3bb tests: Ignore inventory files of inactive tests & benchmarks
These targets are not used by any active tests, and the large numbers of hosts
multiply the size of the taskvars disctionary in memory to many (10s) MiB.

refs #1058
1 year ago
Alex Willmer de3c6dcdc9
Merge pull request #1136 from moreati/fail_msg
tests: Improve Ansible fail_msg formatting
1 year ago
Alex Willmer 6accc87da1 tests: Improve Ansible fail_msg formatting
By switching to block style (`|`) with clip (no `-` or `+`) the failure
messages don't require quoting and gain a single trailing newline. This causes
Ansible to print them as block style, when using the yaml stdout callback
plugin. As a result the values have one less layer of quoting and quote
escaping, making them much easier to read.
1 year ago
Alex Willmer 17d3f39e44
Merge pull request #1114 from moreati/ansible_ssh_password
`ansible_ssh_password` support
1 year ago
Alex Willmer 551690ee1d ansible_mitogen: Handle templated connection passwords and ansible_ssh_password
This switches `ansible_mitogen.transport_config.PlayContextSpec.password()` to
Ansible's plugin option framework. As a result
- The relatively recent `ansible_ssh_password` variable is now respected.
- The SSH connection password can be templated and specified as a play
  variable. Task variables will probably also work, but testing was blocked
  by #1132.

There is a chance this change will cause a regression in another connection
plugin (e.g. mitogen_docker), but nothing turned up in the test suite.
I intend ot migrate other connection configuration to
`ansible_mitogen.transport_config.PlayContextSpec._connect_option()`, the next
candidate is the remote port.

fixes #1106
1 year ago
Alex Willmer 3bdd3e237a tests: Coverage of support for ansible_ssh_password variable 1 year ago
michael.dsilva d0993e9918 allow ansible_ssh_password as it is documented as valid in current ansible documentation
Co-authored-by: Alex Willmer <alex@moreati.org.uk>
1 year ago
Alex Willmer ab2a921744
Merge pull request #1130 from moreati/prep-0.3.11
Prep v0.3.11
1 year ago
Alex Willmer 809d169d36 Begin v0.3.12dev 1 year ago
Alex Willmer c63dc0e080 Prepare v0.3.11 1 year ago
Alex Willmer a51909ea79
Merge pull request #1128 from moreati/github-actions
CI: Begin migration from Azure DevOps to GitHub actions
1 year ago
Alex Willmer 4f60d01f09 CI: Enable GitHub Actions testing workflow
This replicate the existing Azure DevOps workflow, and adds a couple of new
jobs (Python 2.7 on macOS, Python + vanilla Ansible on Linux).

The GitHub Actions use container images hosted on GitHub Container Registry
(GHCR, ghcr.io/mitogen-hq). These images have been copied straight from the
existing Amazon Elastic Cloud Registry (AWS ECR, public.ecr.aws/n5z0e8q9).

A short period of parallel running is planned. Then a second PR will remove
the Azure DevOps workflow.
1 year ago
Alex Willmer 27214517a7 tests: Use a subprocess to check discovered python == running
This replaces the use of `os.path.realpath()` which gave incorrect results on
macOS - depending on the exact Python build, Python version, macOS version,
installation method, and phase of the moon.

realpath information kept around to aid debugging.
1 year ago
Alex Willmer c6c8bfb690 tests: Skip vanilla Ansible on Linux unpriviliged -> unprivileged become
CI containers lack the necessary `setfacl` command. This has not previously
been noticed because no vanilla Ansible jobs were being run on Linux, only on
macOS.

refs #1118
1 year ago
Alex Willmer 8b92e09655 ci: Extract container registry location into variables
Preperation for migrating from Azure DevOps with Amazon Elastic Container
Registry (AWS ECR), to GitHub Actions with GitHub Container Registry (GHCR).

DebOps tests are not currently being run, the updates to .ci/debops*.py are
best effort only.
1 year ago
Alex Willmer b926795973 ci: Move container registry authentication to an Azure Devops step
This aims to
- Reduce duplication
- Seperate CI specific setup from test setup
- Prepare for migration from Azure DevOps to GitHub Actions
1 year ago
Alex Willmer 2e2dfb147e
Merge pull request #1127 from moreati/import-cleanups
Consolidate backward compatibility imports and polyfills
1 year ago
Alex Willmer 0e7eefbc70 tests: Remove unused import 1 year ago
Alex Willmer 34088a8b7f ansible_mitogen: Consolidate Python 2 & 3 compatibility
Rough guidelines, in decending preference:
- Use mitogen.core if possible
- Use ansible.module_utils.six if possible
- Embed a getattr() or try/except

viewkeys() et al can't be brought into mitogen.core because that package still
targets Python 2.4. dict.viewkeys() were introduced in Python 2.7.
1 year ago
Alex Willmer 0a908d76da ansible_mitogen: Remove fallback imports for Ansible < 2.10 1 year ago
Alex Willmer b1fd6038bf ansible_mitogen: Remove Python 2.4 and 2.5 backward compatibility fallbacks
Because ansible_mitogen >= 0.3 supports Ansible >= 2.10 and Ansible 2.10
requires supports Python >= 2.7 on controllers and Python >= 2.6 on targets
these are dead weight.

See
- https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix
- tox.ini
1 year ago
Alex Willmer c6cf08ab39 mitogen: Consolidate back compatibility fallbacks and polyfills in mitogen.core
This saves some bytes on the wire ad simplifies reasoning about the code.
1 year ago
Alex Willmer ce6297b0e9 Begin v0.3.11 1 year ago
Alex Willmer cea2e7b98d Prepare v0.3.10 1 year ago
Gaige B Paulsen 2ba1b2b3f8
Fix: termios.error: (22, 'Invalid argument') during `become` on Solaris/Illumos/SmartOS (#1089)
This fixes compatibility with Solaris/Illumos/SmartOS, addressing an issue that shows up most frequently with become. The issue was mostly due to differences in how the TTY driver is handled and the pty driver not supporting echo on both sides of the pipe (as designed, from a Solaris point of view).

Fixes #950

Co-authored-by: Alex Willmer <alex@moreati.org.uk>
1 year ago
Alex Willmer b8b15580af
Merge pull request #1119 from moreati/ci-resourcewarnings
CI: Reliability, eliminate a race condition and some resource leaks
1 year ago
Alex Willmer d032c591c2 tests: Retry container process check during teardown
I'm about 75% sure the check is an unavoidable race condition, see
https://github.com/mitogen-hq/mitogen/issues/694#issuecomment-2338001694. If
it occurs again, then reopen the issue.

Fixes #694
1 year ago
Alex Willmer 315204271e tests: Don't suppress output while testing unix Listener
It's not noisy, and it has been hiding an error I wasn't aware of.
1 year ago
Alex Willmer a3192d2beb mitogen: close mitogen.unix.Listener socket in error conditions
To avoid ResourceWarning
1 year ago
Alex Willmer 598de81143 mitogen: Fix subprocess ResourceWarning
Python 3.x emits `ResourceWarning`s if certains resources aren't correctly
closed. Due to the way Mitogen has been terminating child processes this has
been occurring.

```
test_dev_tty_open_succeeds
(create_child_test.TtyCreateChildTest.test_dev_tty_open_succeeds) ...
/opt/hostedtoolcache/Python/3.12.5/x64/lib/python3.12/subprocess.py:1127:
ResourceWarning: subprocess 3313 is still running
  _warn("subprocess %s is still running" % self.pid,
ResourceWarning: Enable tracemalloc to get the object allocation traceback
ok
```

During garbage collection subprocess.Popen() objects emit
ResourceWarning("subprocess 123 is still running")
if proc.returncode hasn't been set. Typically calling proc.wait() does so,
once the sub-process has exited. Calling os.waitpid(proc.pid, 0) also waits
for the sub-process to exit, but it doesn't update proc.returncode, so the
ResourceWarning is still emitted.

This change exposes `subprocess.Popen` methods on
`mitogen.parent.PopenProcess`, so that the returncode can be set.

See https://gist.github.com/moreati/b8d157ff82cb15234bece4033accc5e5
1 year ago
Alex Willmer 7238403392 tests: Add missing logging import 1 year ago
Alex Willmer 7c92b8ef2b tests: Shutdown contexts on completion
This should terminate any child processes and connections.
1 year ago
Alex Willmer 7d9eebdb9a tests: Close file object in six_brokenpkg 1 year ago
Alex Willmer 16c602aaa2
Merge pull request #1117 from moreati/macos-no-py311
ci: Drop macOS Python 3.12 + Ansible 9 tests
1 year ago
Alex Willmer 4b4bfdc0f3 ci: Drop macOS Python 3.12 + Ansible 9 tests
They were meant to be replaced by Python 3.12 + ANsible 10, not supplemented.
1 year ago
Alex Willmer a8c0a414b8
Merge pull request #1071 from moreati/ansible-unpin
tests: Unpin versions of Ansible 2.10, 3, & 4
1 year ago
Alex Willmer f76ccbf8ed tests: Unpin versions of Ansible 2.10, 3, & 4 1 year ago
Alex Willmer fc7b7eaba1
Merge pull request #1115 from moreati/pr956
Initial support for templated `ansible_ssh_args`,   `ansible_ssh_common_args`, and `ansible_ssh_extra_args`
1 year ago
Alex Willmer 79ed797bad tests: Test templating of ansible_ssh_common_args et al
refs #905
1 year ago
Alex Willmer f3097b5743 ci: Template Ansible test-targets inventory with Jinja2 1 year ago
root be288ad398 patch #509 : ansible_ssh_common_args issues 1 year ago
Alex Willmer 979241e171
Merge pull request #1113 from moreati/ssh-password-tests
tests: Simplify Ansible ssh password tests, test priority
1 year ago
Alex Willmer 46c9f772d8 tests: Simplify Ansible ssh password tests, test priority
This
- Removes the indirection of calling ansible in a sub-shell
- Includes vanilla Ansible, which was previously skipped
- Tests whether ansible_ssh_pass overrides ansible_password, as it should

As a one off I've the new tests against vanilla Ansible 2.10 through Ansible
10, to confirm the baseline priorities have remained unchanged all releases
currently supported by Mitogen 0.3.x.
1 year ago
Alex Willmer 289a78040d
Merge pull request #1110 from bbc/unsafe-large-copy
Fix AnsibleUnsafeText when copying files larger than SMALL_FILE_LIMIT
1 year ago
Alex Willmer 5af6534a70 tests: Test AnsibleUnsafeText when copying files larger SMALL_FILE_LIMIT
The bug was fixed in a previous commit by Jonathan Rosser. This adds testing.
The bug is only triggered when the copy module is used inside a `with_items:`
loop and the destination filename has an extension. A `loop:` loop is not
sufficient.

refs #1110
1 year ago
Jonathan Rosser 0bd3c6cba5 Fix AnsibleUnsafeText when copying files larger than SMALL_FILE_LIMIT
Small files are carried in-band in the communication between
controller and remote, with larger files being copied by falling back
to a more traditional ansible put_file mechanism. This large
file code path was missed in b822f20.
1 year ago
Alex Willmer ce1accedbc tests: Refactor Ansible copy integration tests to be loop driven
This is in anticipation of #1110, which only exhibits inside a with_items:
loop. For this refactor `loop:` is used, to confirm the refactored tests are
still correct. A subsequent commit will change them to with_items.

The content of the files and their SHA1 checksums are unchanged.
1 year ago
Alex Willmer 84a4fcdf00
Merge pull request #1087 from bbc/unsafe-chmod
ansible_mitogen: Handle unsafe paths in _remote_chmod
1 year ago
Alex Willmer 5ab872f289 ansible_mitogen: Add regression test for ActionModuleMixin._remote_chmod()
Adapted from Jonathon's reproducer in #1087.
1 year ago
Jonathan Rosser 06617f8231 ansible_mitogen: Handle unsafe paths in _remote_chmod
This is missing from b822f20007
1 year ago
Alex Willmer c95d41128f
Merge pull request #1101 from moreati/prep-0.3.9
Prepare v0.3.9
1 year ago
Alex Willmer d15051b187 Begin v0.3.10dev 1 year ago
Alex Willmer 6fbad3ae7d Prepare v0.3.9 1 year ago
Alex Willmer c1c33297ac
Merge pull request #1098 from moreati/docs-untrack
docs: Remove dead references to Piwik and mailing list
1 year ago
Alex Willmer c948e6668a docs: Remove email form from website
https://networkgenomics.com is no longer served, so the form submission would
fail.
1 year ago
Alex Willmer 2edcb31996 docs: Remove piwik analytics hooks from website
https://networkgenomics.com is no longer served, so the javascript and other
attempts to beacon or redirect result in HTTP errors.
1 year ago
Alex Willmer 022f0c4b5f
Merge pull request #1095 from moreati/ansible-2.17
Ansible 10 support
1 year ago
Alex Willmer 357fe38766 Ansible 10 (ansible-core 2.17) support
Notably
- Python 2.7 and 3.6 are no longer supported by Ansible on targets
- The yum module has been removed, and redirected to dnf
- _INTERPRETER_PYTHON_DISTRO_MAP has been neutered. Interpreter discovery
  always favours specific `python3.<x>` interpreters in decending version
  order, then generic `python3` or `python`.
- Add the ability for an action plugin to call self._execute_module(*,
  ignore_unknown_opts=True) to execute a module with options that may not be
  supported for the version being called.

https://docs.ansible.com/ansible/devel/porting_guides/porting_guide_10.html
https://github.com/ansible-community/ansible-build-data/blob/main/10/CHANGELOG-v10.md
https://github.com/ansible/ansible/blob/stable-2.17/changelogs/CHANGELOG-v2.17.rst

fixes #1074, refs #1082

Co-authored-by: Claude Becker <becker@phys.ethz.ch>
1 year ago
Alex Willmer 85b1b4070a tests: Respect configured or detected Python more often
Relying on the virtualenv default or hardcoding "python" results in a Python
2.x virtualenv on some targets (e.g. debian10-test). This caused a failure
when testing with Ansible >= 10 (ansible-core >= 2.17), which have dropped
Python 2.x support.

refs #1074
1 year ago
Alex Willmer 863f923f14 tests: Bypass interpreter discovery on non-existant connection delegation targets
By setting ansible_python_interpreter for these fictious hosts we avoid
Ansible trying and failing to connect to them in a attempt to populate
ansible_facts.discovered_interpreter_python. This speeds up these tests by
avoiding a timeout.

It is also a necessary pre-requisite for Ansible 10 (ansible-core 2.17). In
that release no hardcoded fallback is used, failure to determine a valid
Python interpreter is a fatal error.

refs #1074
1 year ago
Alex Willmer 40695f413b ansible_mitogen: Respect ansible_facts.discovered_interpreter_python more
fixes #1097
1 year ago
Alex Willmer 9185805bf2 ansible_mitogen: cast ansible_python_interpreter value
This was the last remaining use of `mitogen.utils.cast()`. I missed it in
#1046.
1 year ago
Alex Willmer 8613f685ab tests: Skip AWS ECR login outside of CI jobs
To avoid rate limiting errors, CI (currently Azure Devops) logs into the
container registry (currently AWS ECR). Outside CI this is unnnecessary and
makes it harder to run the tests, because very few people have access to a
suitable AWS secret token.

Following this change `aws ecr-public get-login-password` will only be run if
the environment variable $TF_BUILD==True. This is set by Azure Pipelines
jobs. If the CI platform is changed then another indicator should be used.

https://adamj.eu/tech/2020/03/09/detect-if-your-tests-are-running-on-ci/
1 year ago
Alex Willmer fe26b70902
Merge pull request #1092 from moreati/prep-v0.3.8
Prepare v0.3.8
2 years ago
Alex Willmer 62cde17150 Start v0.3.9 development 2 years ago
Alex Willmer e334b50d9d Prepare v0.3.8 2 years ago
Alex Willmer 8bec30d97c
Merge pull request #1091 from moreati/issue1090
ci: Summer 2024 test fixups
2 years ago
Alex Willmer fe435bb7d0 CI: Workaround "No module named 'setuptools.command.test'"
Pip 72 was released yesterday (2024-07-28), dropping `setup.py test` support.
hdrhistogram 0.6.1 requires it to install.

For now constrain Pip to earlier releases, so our tests can be run.

refs #1090
2 years ago
Alex Willmer 924dbd6f0c CI: Migrate macOS integration tests to macOS 12, drop Python 2.7 jobs
macOS 11 is not longer an available runner on Azure Devops. The minimum is now
macOS 12. This runner does not have Python 2.7 installed, so running them
would require a custom install - which I'm declaring too much effort for too
little gain.

refs #1090
2 years ago
Alex Willmer f5a8840668 CI: Use archived RPMs on CentOS 8
CentOS 8 has reached EOL. Packages are no longer mirrored or maintained. A
historic snapshot of the packages is kept on vault.centos.org.

refs #1088, #1090
2 years ago
Alex Willmer 23d9d0bc82
Merge pull request #1060 from moreati/issue1059
Speed up test suite
2 years ago
Alex Willmer 7079a07a15 tests: Fix duplicate local task executions
integration/ssh/timeouts.yml is noteworthy. It was an accidental N**2 in time
-  executing num hosts * num hosts tasks.
2 years ago
Alex Willmer 65c8a42c13 tests: Use same verbosity when re-executing Ansible inside a playbook 2 years ago
Alex Willmer 05d98e5b49 tests: Speed up ssh timeout tests 2 years ago
Alex Willmer 0ce9ffc464
Merge pull request #1067 from philfry/host_key_checking
Fix add_hosts when ansible_host_key_checking is passed to the new host
2 years ago
Alex Willmer 60f868290d
tests: Remove --limit when running Ansible localhost CI
Some tests were being incorrectly excluded. Including those that use
`add_host`.
refs #1066, #1069
2 years ago
Alex Willmer d2eefc06aa
tests: Add regression for add_host with host_key_checking
refs #1066
2 years ago
Philippe Kueck ec05e542b4
Fix 'ansible_host_key_checking' and 'ansible_ssh_host_key_checking' for
adding new hosts to the inventory using 'add_hosts'

Co-authored-by: Alex Willmer <alex@moreati.org.uk>
2 years ago
Alex Willmer 0f34e2505b
Merge pull request #1065 from moreati/issue957
ansible_mitogen: Fix "filedescriptor out of range in select()" in WorkerProcess
2 years ago
Alex Willmer 4996ec2f09 ansible_mitogen: Fix "filedescriptor out of range in select()" in WorkerProcess
`mitogen.parent.POLLER_LIGHTWEIGHT` will normally be `PollPoller`, falling
back to `EpollPoller`, `KqueuePoller`, or `Poller`.

Fixes #957

Co-authored-by: Luca Berruti <nadirio@gmail.com>
Co-authored-by: Philippe Kueck <bqobccy6ejnq2bqvmebqiwqha4cs4@protected32.unixadm.org>
2 years ago
Alex Willmer efdd82d1ab mitogen: Streamline Poller classes and Latch.poller_class selection
This
- Clarifies and corrects docstrings and comments based on investigation for #957
- Removes unused `Poller*._repr` attributes
- Eliminates some uses of `getattr()`
- Introduces `mitogen.parent.POLLERS` & `mitogen.parent.POLLER_LIGHTWEIGHT`

Preamble size change
```
@@ -1,7 +1,7 @@
 SSH command size: 759
-Bootstrap (mitogen.core) size: 17862 (17.44KiB)
+Bootstrap (mitogen.core) size: 17934 (17.51KiB)

                               Original          Minimized           Compressed
-mitogen.parent            98171 95.9KiB  50569 49.4KiB 51.5%  12742 12.4KiB 13.0%
+mitogen.parent            96979 94.7KiB  49844 48.7KiB 51.4%  12697 12.4KiB 13.1%
 mitogen.fork               8436  8.2KiB   4130  4.0KiB 49.0%   1648  1.6KiB 19.5%
 mitogen.ssh               10892 10.6KiB   6952  6.8KiB 63.8%   2113  2.1KiB 19.4%
```
2 years ago
Alex Willmer bb9c51b3e9
Merge pull request #1007 from moreati/ask-become-pass
Fix --ask-become-pass
2 years ago
Alex Willmer 8c93973f98 tests: Use Android portal to check get_url
Should have higher uptime, and make us less of a burden. Refs #1058
2 years ago
Alex Willmer d5e9186289 ansible_mitogen: Fix --ask-become-pass, add test coverage
Previously f1503874de fixed the priority of
ansible_become_pass over ansible_become_password, but broke --ask-become-pass.
Fixes #952.
2 years ago
Alex Willmer c4cf0d5ba2 ci: Use profile_tasks callback as rough benchmark of Ansible tests 2 years ago
Alex Willmer e1b2f38c8e tox: Add python2 & python3 to adhoc install hint 2 years ago
Alex Willmer 37ebce7e6e Begin 0.3.8dev 2 years ago
Alex Willmer a3644963c4 Prepare v0.3.7 2 years ago
Alex Willmer cca651da1f ansible_mitogen: Ansible 9 (ansible-core 2.16) support 2 years ago
Alex Willmer 45c42d386a tests: Replace uses of ``include:``, unify skipping of mitogen only tests
The tag mitogen_only is only informational for now. It may be possible to use
it with ANSIBLE_SKIP_TAGS in the future.
2 years ago
Alex Willmer fa1d21747f ansible_mitogen: Declare Ansible 8 (ansible-core 2.15) support
refs #1021
2 years ago
Alex Willmer 2333b9aced ci: Exclude docs-master branch 2 years ago
Alexandre Detiste fe54b0ac3f prefer newer unittest.mock from the standad library 2 years ago
Alexandre Detiste 58235e3675 add Python3 compatibility 2 years ago
Alex Willmer 933477fcbe Begin 0.3.7dev 2 years ago
Alex Willmer 5d789faee5 Prepare 0.3.6 2 years ago
Alex Willmer b822f20007 ansible_mitogen: Handle AnsibleUnsafeText et al in Ansible >= 7
Follwing fixes in Ansible 7-9 for CVE-2023-5764 cating `AnsibleUnsafeBytes` &
`AnsibleUnsafeText` to `bytes()` or `str()` requires special handling. The
handling is Ansible specific, so it shouldn't go in the mitogen package but
rather the ansible_mitogen package.

`ansible_mitogen.utils.unsafe.cast()` is most like `mitogen.utils.cast()`.
During development it began as `ansible_mitogen.utils.unsafe.unwrap_var()`,
closer to an inverse of `ansible.utils.unsafe_procy.wrap_var()`. Future
enhancements may move in this direction.

refs #977, refs #1046

See also
- https://github.com/advisories/GHSA-7j69-qfc3-2fq9
- https://github.com/ansible/ansible/pull/82293
- https://github.com/mitogen-hq/mitogen/wiki/AnsibleUnsafe-notes
2 years ago
Alex Willmer 813f253d6b ansible_mitogen: Make ansible_mitogens.utils a package
Prep work for ansible_mitogen.utils.unsafe
2 years ago
Alex Willmer d7979c3597 mitogen: Raise TypeError on `mitogen.utils.cast(custom_str)` failures
If casting a string fails then raise a TypeError. This is potentially an API
breaking change; chosen as the lesser evil vs. allowing silent errors.

`cast()` relies on `bytes(obj)` & `str(obj)` returning the respective
supertype. That's no longer the case for `AnsibleUnsafeBytes` &
`AnsibleUnsafeText`; since fixes/mitigations for  CVE-2023-5764.

fixes #1046, refs #977

See also
- https://github.com/advisories/GHSA-7j69-qfc3-2fq9
- https://github.com/ansible/ansible/pull/82293
2 years ago
Orion Poplawski dfc3c7d516 ansible_mitogen: Add Ansible 7 support
Co-authored-by: Orion Poplawski <orion@nwra.com>
2 years ago
Alex Willmer 21e874e60c
Merge pull request #1047 from mitogen-hq/changlog-pep451
docs: Correct PEP 451 hyperlink
2 years ago
Alex Willmer 50efa53f8f
docs: Correct PEP 451 hyperlink 2 years ago
Alex Willmer a9d32a7708
Merge pull request #1043 from moreati/rel-0.3.5
Prepare 0.3.5 release, start 0.3.6 development
2 years ago
Alex Willmer fc24b3f25e Start v0.3.6 development 2 years ago
Alex Willmer e97ab2f597 Prepare v0.3.5 2 years ago
Alex Willmer a210c37f70
Merge pull request #1032 from moreati/docs-download-url
Python 3.12 support
2 years ago
Alex Willmer 123efa7510 mitogen: Support Python 3.12
Most of the necessary changes were made in recent PEP 451 commits. This bumps
the CI jobs, and declares the support. Test dependendancies are bumped to
latest supportted/available versions.

refs #1033
2 years ago
Alex Willmer fe8a3a71fc ansible_mitogen: Remove use of distutils, which was removed in Python 3.12 2 years ago
Alex Willmer 92c00d913e tests: Skip "discovered python matches invoked" on macOS 11/Python 2.7/Vanilla 2 years ago
Alex Willmer 5ad3d14ceb mitogen: Support PEP 451 ModuleSpec API, required for Python 3.12
importlib.machinery.ModuleSpec and find_spec() were introduced in Python 3.4
under PEP 451. They replace the find_module() API of PEP 302, which was
deprecated from Python 3.4. They were removed in Python 3.12 along with the
imp module.

This change adds support for the PEP 451 APIs. Mitogen should no longer import
imp on Python versions that support ModuleSpec. Tests have been added to cover
the new APIs.

CI jobs have been added to cover Python 3.x on macOS.

Refs #1033
Co-authored-by: Witold Baryluk <witold.baryluk@gmail.com>
2 years ago
Alex Willmer 3a31a7d886 mitogen: Workaround CPython importlib PermissionError when cwd is unreadable
On macOS when using a become plugin as an unprivileged user, to another
unprivileged user it is likely that the current working directory can't be
read. In this case os.cwd() raises PermissionError.

On versions of Python currently in the wild (March 2024, CPython <= 3.13) if
any non-builtin or non-frozen module (e.g. zlib, base64) is imported then
`importlib._bootstrap_external.PathFinder._path_importer_cache()` attempts to
call os.cwd() without catching PermissionError.

The previous comment about needing an extra .encode() appears to be wrong,
atleast for Python 3.x >= 3.6.

Command size increased by 54 bytes, bootstrap by 804 bytes. Changed from
codecs module to binascii & zlib because they're extensions, and importing
them triggers fewer supporting imports (e.g. encodings module).

Before

```
✗ ./preamble_size.py
SSH command size: 705
Bootstrap (mitogen.core) size: 17078 (16.68KiB)

                              Original          Minimized           Compressed
mitogen.parent            97884 95.6KiB  50515 49.3KiB 51.6%  12727 12.4KiB
13.0%
mitogen.fork               8436  8.2KiB   4130  4.0KiB 49.0%   1648  1.6KiB
19.5%
mitogen.ssh               10892 10.6KiB   6952  6.8KiB 63.8%   2113  2.1KiB
19.4%
mitogen.sudo              12089 11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB
18.6%
mitogen.select            12325 12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB
7.8%
mitogen.service           41699 40.7KiB  22477 22.0KiB 53.9%   5885  5.7KiB
14.1%
mitogen.fakessh           15577 15.2KiB   7989  7.8KiB 51.3%   2623  2.6KiB
16.8%
mitogen.master            51398 50.2KiB  25715 25.1KiB 50.0%   6886  6.7KiB
13.4%
```

After

```
✗ ./preamble_size.py
SSH command size: 759
Bootstrap (mitogen.core) size: 17882 (17.46KiB)

                              Original          Minimized           Compressed
mitogen.parent            98173 95.9KiB  50571 49.4KiB 51.5%  12747 12.4KiB
13.0%
mitogen.fork               8436  8.2KiB   4130  4.0KiB 49.0%   1648  1.6KiB
19.5%
mitogen.ssh               10892 10.6KiB   6952  6.8KiB 63.8%   2113  2.1KiB
19.4%
mitogen.sudo              12089 11.8KiB   5924  5.8KiB 49.0%   2249  2.2KiB
18.6%
mitogen.select            12325 12.0KiB   2929  2.9KiB 23.8%    964  0.9KiB
7.8%
mitogen.service           41699 40.7KiB  22477 22.0KiB 53.9%   5885  5.7KiB
14.1%
mitogen.fakessh           15577 15.2KiB   7989  7.8KiB 51.3%   2623  2.6KiB
16.8%
mitogen.master            56116 54.8KiB  29427 28.7KiB 52.4%   7627  7.4KiB
13.6%
```

Fixes #885
Refs https://github.com/python/cpython/issues/115911
2 years ago
Alex Willmer 1031551dd9 tests: Clarify transport config tests optimisation & correct value
The ini inventory parser doesn't support comments after a value, so the value
parsed was "python3000  # Not expected to exist".
2 years ago
Alex Willmer 2973d90670 tests: Enable su tests under vanilla Ansible >= 2.11
cwd_show was useful when debugging these tests, worth keeping around.
2 years ago
Alex Willmer e2f4d9275c tests: Fix ansible_python_interpreter & discovered_interpreter_python tests on macOS
Should account for fiddling in mitogen.parent.Connection._first_stage() and
symlinks. I won't be surprised if it breaks again soon and often.
2 years ago
Alex Willmer c2ad52e54e tests: Fix tests using get_url across Python versions
Using https:// requires certificate store management and additional parameter
passing that changed across Ansible and Python versions. Using http:// allows
the same tests to be used across wider spans of Python version on the
controller, and Python verison on the targets.

Python 3.12 on a target + get_uri needs Ansible >= 8 (ansible-core >= 2.15).
Python 3.12 removed deprecated httplib.HTTPSConnection() arguments.
https://github.com/ansible/ansible/pull/80751
2 years ago
Alex Willmer a6a5c5bb97 tests: Clarify status/purpose of Python 2.x era Ansible Module workaround 2 years ago
Alex Willmer 2839954559 tests: Account for /tmp symlink in virtualenv test on macOS 2 years ago
Alex Willmer adfd4e17f3 tests: Declare inventory file types to Visual Studio Code and Vim
Works with the VS Code modeline extension. Enables syntax highlighting.
2 years ago
Alex Willmer 591152bef0 tests: Avoid intermittant 2 hour timeout in new style Ansible module tests
This has been lurking for years, raising it's head at unpredictable times.
This change doesn't fix it, but it should make it a lot less mysterious.
2 years ago
Alex Willmer a6c89751f9 tests: Cleanup ansible-lint errors & warnings in user creation playbook
Task " Install slow profile for one account" removed because it duplicates
earlier work.
2 years ago
Alex Willmer 8b574f234d tests: Report Ansible controller parameters before image prep & user creation 2 years ago
Alex Willmer bde7f062b9 tests: Fix Ansible module shebangs
With https://github.com/ansible/ansible/pull/76677 Ansible
fixed shebang substitution for Ansible modules and tightened
up what shebang is allowed.

Changing these fixes the tests using them with vanilla Ansible.

https://docs.ansible.com/ansible/latest/dev_guide/testing/sanity/shebang.html
2 years ago
Alex Willmer 9a9dd66ba0 Ignore Ansible retry files 2 years ago
Alex Willmer fc3e788cb4 non functional: Add comments about imp module removal in Python 3.12 2 years ago
Alex Willmer f9a6748154 ci: Fix Python 2.7 builds on macOS 11
With current macOS 11 runner images (20231216.1) the `python` on `$PATH` is
Python 3.12 and setuptools isn't installed by default. E.g.

```
python -mtox -e "py27-mode_localhost-ansible4"
========================== Starting Command Output ===========================
/bin/bash --noprofile --norc
/Users/runner/work/_temp/93a29c4c-f606-45e4-8dbd-a4a5f51b8730.sh
GLOB sdist-make: /Users/runner/work/1/s/setup.py
ERROR: invocation failed (exit code 1), logfile:
/Users/runner/work/1/s/.tox/log/GLOB-0.log
================================== log start
===================================
Traceback (most recent call last):
  File "/Users/runner/work/1/s/setup.py", line 32, in <module>
    from setuptools import find_packages, setup
ModuleNotFoundError: No module named 'setuptools'
```

Installing setuptools under Python 3.12 chooses package versions incompatible
with Python 2.7. Additionally Mitogen isn't yet compatible with Python 3.12
(#1033), so tests that call a local context with `python` fail.
2 years ago
Alex Willmer b7188c1cad docs: Decouple website download version from package version
This prevents unreleased versions appearing on the website (e.g. 0.3.5.dev0),
but introduces the risk of forgetting to update the website after a release.
A better fix requires deeper design/workflow thought.

refs #1028
2 years ago
Alex Willmer e580258071 docs: Bypass networkgenomics.com/try/ -> PyPI redirect
refs #1028
2 years ago
Alex Willmer 798032b979
Merge pull request #1027 from moreati/pyver-token
ci: Authenticate UsePythonVersion requests to Github
2 years ago
Alex Willmer 3f105d5169 ci: Authenticate UsePythonVersion requests to Github
This should address the warning in Azure Pipelines

> You should provide GitHub token if you want to download a python release.
> Otherwise you may hit the GitHub anonymous download limit.

The token is provided from a secret variable in the pipeline.
2 years ago
Alex Willmer d839cbfaf2
Merge pull request #1026 from moreati/netlify
docs: Fix generating static website on Netlify
2 years ago
Alex Willmer 63457b4866 docs: Update external URLs (e.g. dw/mitogen -> mitogen-hq/mitogen)
Found with sphinx-build -b linkcheck. Not all flagged URLs have been changed,
e.g. Ansible plugins, deleted Github users.
2 years ago
Alex Willmer 6aa4fd3573 docs: Fix generation of static website
Bare minimum syntax errors and requirements constraints to work with Netlify
hosting.
2 years ago
Alex Willmer 85f9261c9a
Merge pull request #1019 from moreati/pr987
Add Python 3.11 support
3 years ago
Nerijus Baliūnas 4089e875a9 Add Python 3.11 support
Co-authored-by: Alex Willmer <alex@moreati.org.uk>
3 years ago
Alex Willmer 57b5be3589
Merge pull request #1016 from moreati/pr987
Prepare for Python 3.11
3 years ago
Alex Willmer 49c54386b3 tests: Only use subprocess32 package on Python 2.x
This is how the package documentation recommends and it's less likely to
interfere with new features in stdlib subprocess module.
3 years ago
Alex Willmer 98d110ed16 tests: Bump Django ModuleFinder test cases
Preperation for Python 3.11 support
3 years ago
Alex Willmer 6258365df6 tests: Handle square bracket IPv6 in `docker port` output
Fixes
```
======================================================================
ERROR: setUpClass (ssh_test.BannerTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/vsts/work/1/s/tests/testlib.py", line 625, in setUpClass
    cls.dockerized_ssh = DockerizedSshDaemon(**daemon_args)
  File "/home/vsts/work/1/s/tests/testlib.py", line 553, in __init__
    self.start_container()
  File "/home/vsts/work/1/s/tests/testlib.py", line 533, in start_container
    self._get_container_port()
  File "/home/vsts/work/1/s/tests/testlib.py", line 510, in _get_container_port
    self.port = int(bport)
ValueError: invalid literal for int() with base 10: ':]:32770'
```
3 years ago
Alex Willmer 88f15a8169
Merge pull request #1006 from moreati/test-on-22.04
tests: Support Ubuntu 22.04 as test suite runner (controller)
3 years ago
Alex Willmer 270c3a25de tests: Support Ubuntu 22.04 as test suite runner (controller)
To do so the test suite allows a weak cryptographic alogorithm (SHA1) to be
used, principally for CentOS 6 targets. This can be removed if/when support
for older (legacy) targets is dropped.

Only the test suite enables this known weak alogorithm. Mitogen as-shipped
doesn't enable or disable algorithms.
3 years ago
Alex Willmer ec212a10d8
Merge pull request #1002 from moreati/prep-0.3.4
Prepare 0.3.4
3 years ago
Alex Willmer 455fd2bcdf Bump version 3 years ago
Alex Willmer f18f5165cd Prep for v0.3.4 3 years ago
Alex Willmer 5602445709
Merge pull request #1001 from moreati/ci-fixup
CI: Fix tests on Linux, Ansible tests targetting Debian 9 & 11
3 years ago
Alex Willmer 19b79f7ab5 CI: Fix tests on Linux, Ansible tests targetting Debian 9 & 11
Without Ubuntu 20.04 virtualenv package being installed pip was installing a
version of virtualenv that couldn't create the Tox environment for Python 2.7.

> Successfully installed distlib-0.3.6 filelock-3.12.2 platformdirs-3.8.0
> pluggy-1.2.0 py-1.11.0 tomli-2.0.1 tox-3.28.0 virtualenv-20.23.1
> Finishing: Install tooling
> ...
> py27-mode_mitogen-distro_centos6 create: /home/vsts/work/1/s/.tox/py27-
> mode_mitogen-distro_centos6
> ERROR: invocation failed (exit code 1), logfile: /home/vsts/work/1/s/.tox/
> py27-mode_mitogen-distro_centos6/log/py27-mode_mitogen-distro_centos6-0.log
> ================================== log start
> ===================================
> RuntimeError: failed to query /usr/bin/python2.7 with code 1 err:
> '  File "/home/vsts/.local/lib/python3.8/site-packages/virtualenv/discovery/
> py_info.py", line 24\n    return list(OrderedDict.fromkeys(["",
> *os.environ.get("PATHEXT", "").lower().split(os.pathsep)]))\n
> ^\nSyntaxError: invalid syntax\n'
3 years ago
Alex Willmer 330375be32
Merge pull request #980 from InsanePrawn/transport_lxc_fix
ansible_mitogen: correct typo in MitogenViaSpec.mitogen_lxc_path()
3 years ago
InsanePrawn 317a2abd57 ansible_mitogen: correct typo in MitogenViaSpec.mitogen_lxc_path()
self.host_vars -> self._host_vars
3 years ago
Alex Willmer d488b5ebb3
Merge pull request #965 from moreati/evict-simplejson
Remove vendored mitogen.compat.simplejson
3 years ago
Alex Willmer 8151577b75 CI: Limit to Tox < 4.0 to avoid plugin incompatibility
I'm abandoning tox-factor because having any [tox] requires = ... causes tox
3.x to create an isolated virtualenv for running tox itself. Since Tox 4.x was
released that virtualenv gets it, which is incompatible with the tox-factor
plugin.
e.g.
```
Traceback (most recent call last):
File
"/Users/runner/work/1/s/.tox/.tox/lib/python3.10/site-packages/tox_factor/compat.py",
line 2, in <module>
    from tox.config.parallel import ENV_VAR_KEY_PUBLIC as TOX_PARALLEL_ENV
ModuleNotFoundError: No module named 'tox.config.parallel'
```
3 years ago
Alex Willmer 21cb4a3472 CI: Remove faulthandler fallback requirement
faulthandler is a stdlib module in Python 3.3+. For a long time a PyPI package
of the same name was available for earlier Python releases. That package has
since been removed from PyPI, and the source respoitory archived. So we should
not rely on it.
fixes #983 refs #970
3 years ago
Alex Willmer 1871f2a9b1 Remove vendored mitogen.compat.simplejson
Python 2.6 added json to the stdlib. We no longer support Python <= 2.7 in
Mitogen 0.3.x, so this fallback is unneeded complexity. Fixes #659
3 years ago
Alex Willmer a47b9f3631
Merge pull request #969 from moreati/transport_config_python_undiscover
tests: Speed up transport config tests by avoiding interpreter discovery
3 years ago
Alex Willmer 392c41d160 ci: Ignore WALinuxAgent processes in Azure Piplines CI runner 3 years ago
Alex Willmer 0af2ce8c30 Remove ansible_mitogen Connection.close() workaround
Refs #925 #969

I'm not 100% confident that merely removing this is the full fix,
without substituting something else. I am sure keeping it would be
the greater of two evils. __del__() should be avoided on general
principal, and it's associated with multiple intermittant CI
failures, plus multiple user reported issues.
3 years ago
Alex Willmer 03acf40315 tests: Speed up transport config tests by avoiding interpreter discovery
Reduced execution time of tests/ansible/integration/transport_config/all.yml
from 11 minutes to 49 seconds.
3 years ago
Alex Willmer 572636a9d3
Merge pull request #964 from moreati/test-tweaks
Test tweaks
3 years ago
Alex Willmer a3a10cb32e tests: Upgrade coverage dependency 3 years ago
Alex Willmer 7d79c56cb6 tests: Clarify skipped Poller test reasons 3 years ago
Alex Willmer edd2868ef6 tests: Don't rely on facts when setting become
They won't be available if the play is first, and hence no facts have been
gather in previous play(s), e.g. due to --start-at-task
3 years ago
Alex Willmer 1ed932e8d5 tests: Eliminate MITOGEN_INVENTORY_FILE
Replaced with ansible_inventory_sources.
3 years ago
Alex Willmer 900760e913 tests: Increase Ansible timeout to reduce false positives
Was failing on my dsktop PC, with a spinning rust HDD
3 years ago
Alex Willmer 526422b74b tests: Name tasks
For easier grep, and easier identification in task_profiler summaries.
3 years ago
Alex Willmer 2e8bf73877 tests: Print filename of a failed task (Ansible >= 2.11) 3 years ago
Alex Willmer 99fe9d48e6 tests: Print task durations 3 years ago
Alex Willmer 4a5d52961f
Merge pull request #959 from adone/mitogen_ssh_options_doc
add SSH args into options documentation
3 years ago
Artem Nistratov 00dab14111 add SSH args into options documentation 3 years ago
Alex Willmer 39dfd2dfe8 ci: Upgrade VM Images to macOS 11 and Ubuntu 20.04 3 years ago
Alex Willmer 8cda5f5537
Merge pull request #933 from moreati/ansible6
Ansible 6 support
4 years ago
Alex Willmer f1503874de ansible_mitogen: Correct ansible_become_pass/ansible_become_password precendence
Until Ansible 2.9 it looks like ansible_become_password had higher priority.
From Ansible 2.10 ansible_become_pass has higher priority [1]. Mitogen was not
respecting this.

I may need to rework this further, instatiating the become plugin may have
slowed down execution.

[1] Based on testing with

```
[ubuntus]
become-pass-pass ansible_become_pass=1234
become-pass-password ansible_become_password=1234
become-pass-both ansible_become_password=wrong ansible_become_pass=1234

[ubuntus:vars]
ansible_host=ubuntu2004.local
ansible_user=ubuntu
```
```
- hosts: ubuntus
  gather_facts: false
  become: true
  tasks:
    - ping:
```
4 years ago
Alex Willmer ad4b686836 master.PkgutilMethod: Skip module loaders that raise ValueError
e.g. in Ansible 6, ansible-core 2.13

```
[mux  2717] 23:39:11.342416 D mitogen: PkgutilMethod(): _AnsibleCollectionLoader(path=None).get_file_name('ansible.plugins') failed: ValueError('_AnsibleCollectionLoader(path=None) cannot find files for ansible.plugins, only ansible_collections.ansible.builtin.plugins')
```
4 years ago
Alex Willmer e8ad12e881 Ansible 6 support
fixes #929
4 years ago
Alex Willmer 195b400087 ci: Drop Ansible 3 tests to free up CI capacity 4 years ago
Alex Willmer db114d3bb2 ci: Bump Ansible releases used in tests 4 years ago
Alex Willmer 63e39c1ac5 ci: Remove traces of Ansible < 2.10 (not supported in 0.3.x) 4 years ago
Alex Willmer e36bbde9ac tests: Replace uses of assertTrue() with specific methods 4 years ago
Alex Willmer eb4a7e0ad5 tests: cleanup subprocess file handles in create_child_test 4 years ago
Alex Willmer 64819ecb5f tests: Regression test for #776 (package/yum/dnf module called twice) 4 years ago
Alex Willmer 24c845379a tests: Remove redundant regression tags
The tag is applied by the playbook that imports this one.
4 years ago
Alex Willmer db0ffae352 tests: Enable stricter error handling, fix resulting failures 4 years ago
Alex Willmer c32577295a tests: Check and/or suppress stderr of subprocesses, reduce shell=True uses 4 years ago
Alex Willmer 216e7c9150 tests: Correct Ansible targets 4 years ago
Alex Willmer 8e79488768 tests: Mark or avoid sudo tasks on localhost 4 years ago
Alex Willmer f070767dad tests: Use meaningful play names 4 years ago
SAADY Yousef c1e72b8225 Fix typo changelog.rst 4 years ago
David Mehren a30a743ce7 Add ansible.builtin.dnf to ALWAYS_FORK_MODULES
The new fully qualified name of the DNF module needs to also be added to the list.

Fixes #832
4 years ago
Alex Willmer d71fb672e8 Begin v0.3.4.dev0 4 years ago
Alex Willmer c0d3deeac5 Prepare v0.3.3 4 years ago
Felix Stupp b1e67cc7df tests/ansible/README: Replace reference with actual link
- working for GitHub and similar Markdown engines
4 years ago
Alex Willmer 89c0cc94d1
Merge pull request #923 from moreati/issue915
Fix [DEPRECATION WARNING]: The '_remote_checksum()' method is deprecated.
4 years ago
Alex Willmer 25ea6dde02 ansible_mitogen: Allow mitogen_fetch to bypass slurp module
This reapplies an earlier change, when this plugin was first introduced to
Mitogen. The plugin was updated to fix

[DEPRECATION WARNING]: The '_remote_checksum()' method is deprecated.

I've elected to short-circuit the if statemtn logic, rather than
deleting/unindenting, to make the code delta much smaller. This should make it
easier to maintain/update.

Fixes #915
4 years ago
Alex Willmer 0ff9c6e579 ansible_mitogen: Replace fetch action plug from upstream
From
be0cdc0ea2/lib/ansible/plugins/action/fetch.py
4 years ago
Alex Willmer 11a61acb32
Merge pull request #922 from moreati/functools.wraps
mitogen.utils: Preserve docstring of functions decorated @with_router
4 years ago
Alex Willmer e101cc4f44 mitogen.utils: Preserve docstring of functions decorated @with_router
Co-authored-by: Rezart Qelibari <gast-kontakt+mitogen@astzweig.de>

Replaces #837
Fixes #836
4 years ago
Alex Willmer a743e831c6
Merge pull request #921 from moreati/import-cleanups
Cleanup imports in mitogen, ansible_mitogen, & tests
4 years ago
Alex Willmer 31b3a4eb4a ansible_mitogen: Standardise __future__ imports to match Ansible
Some modules additionally enable unicode_literals (which Ansible doesn't do).
I've chosen not to change that, for now.
4 years ago
Alex Willmer 3dbb0e28ce tests: List leaked file descriptors 4 years ago
Alex Willmer 109feec6d5 Fix lints found by flake8 4 years ago
Alex Willmer 18c89de5a9 Remove unused module imports 4 years ago
Alex Willmer 566d75d82f
Merge pull request #920 from moreati/unittest-deprecations
Add Ansible podman connection support
4 years ago
Alex Willmer db9e52ce8e tests: Run containers on macOS with podman, instead of Docker 4 years ago
Alex Willmer 96e20a09d6 ansible_mitogen: Add podman connection plugin 4 years ago
Alex Willmer 0417d4d73a Replace os.system() with subprocess.check_call()
Non-zero return codes should raise an exception, not pass silently.
4 years ago
Alex Willmer 1287d58a54 Use with open(): ... to ensure file objects get closed 4 years ago
Alex Willmer 65809a6f0f mitogen: Handle Python 3.10 threading depreactions 4 years ago
Alex Willmer caa20be43e tests: Use TestCase.assertEqual()
assertEquals() is deperecated in unittest
4 years ago
Alex Willmer c4f1cc150d
Merge pull request #918 from moreati/python3.10
Python 3.10 support
4 years ago
Alex Willmer a8317c2393 tests: Remove unittest2, use stdlib unittest
unittest2 is incomplatible with Python 3.10
4 years ago
Alex Willmer 2a95d039ab Python 3.10 support 4 years ago
Alex Willmer af03b9a9b3
Merge pull request #917 from moreati/cleanups
Test and build improvements
4 years ago
Alex Willmer 104865e866 build: Remove declared support for Python<2.7
Master and the 0.3.x branch have never supported these versions, but we didn't
update the metadata.
4 years ago
Alex Willmer ccca77bcc0 tests: Fix old Ansible dependencies installed by Tox 4 years ago
Alex Willmer 63543b3b83 tests: Skip heavy & sudo Ansible tests by default
We don't wish to modify someone's local OS, or rely on them having sudo (with
or without password).
4 years ago
Alex Willmer c87976af40 tests: Fix lingering Python 2 isms 4 years ago
Alex Willmer c9318a26f6 tests: Suppress pip version warnings 4 years ago
Alex Willmer 491458673b tests: Manage ANSIBLE_STRATEGY with Tox 4 years ago
Alex Willmer 5805e30232 tests: Remove unused imports 4 years ago
Alex Willmer a167f164e4 tests.parent_test: Don't assume interpreter is in /usr/bin 4 years ago
Alex Willmer 7c4982ebee
Merge pull request #913 from willmerae/issue-906-minimal
master.ParentEnumerationMethod: Require matching pkg.__name__
4 years ago
Alex Willmer d2ca8a9423 master.ParentEnumerationMethod: Require matching pkg.__name__
Co-authored-by: Stefano Rivera <stefano@rivera.za.net>

When the requested module (e.g. ansible.module_utils.distro)
- is provided by another module *e.g. distro)
- that itself was a package (e.g. distro 1.7.0)

At runtime
- ansible/module_utils/distro/__init__.py executes
- if https://pypi.org/project/distro/ is present, it's loaded as
ansible.module_utils.distro
- otherwise ansible/module_utils/distro/_distro.py is loaded

ParentEnumerationMethod would wrongly use whatever was in
sys.modules['ansible.module_utils.distro]. Instead we should ascend to
the first parent that has fullname == sys.modules[fullname].__name__.
Then descend to the appropriate .py file on disk.

This bug didn't show up before because until distro 1.7.0 (Feb 2022) the
top-level distro module was a module (distro.py) not a package
(distro/__init__.py)

fixes #906
4 years ago
Alex Willmer 47699e15aa master.SysModulesMethod: log rejection reasons 4 years ago
Alex Willmer 0fa0a93f55 master.PkgutilMethod: log rejection reasons 4 years ago
Alex Willmer 60c4ae5599
Add notes on imports and importlib 4 years ago
Alex Willmer 5b8f7dd1be
Start v0.3.3 development 4 years ago
Alex Willmer e8c3fe7881
Fix Trove classifier, bump version
fixes #891

(cherry picked from commit 1a84184838)
4 years ago

@ -2,7 +2,7 @@
# `.ci`
This directory contains scripts for Continuous Integration platforms. Currently
Azure Pipelines, but they will also happily run on any Debian-like machine.
GitHub Actions, but ideally they will also run on any Debian-like machine.
The scripts are usually split into `_install` and `_test` steps. The `_install`
step will damage your machine, the `_test` step will just run the tests the way
@ -28,15 +28,15 @@ for doing `setup.py install` while pulling a Docker container, for example.
### Environment Variables
* `TARGET_COUNT`: number of targets for `debops_` run. Defaults to 2.
* `DISTRO`: the `mitogen_` tests need a target Docker container distro. This
name comes from the Docker Hub `mitogen` user, i.e. `mitogen/$DISTRO-test`
* `DISTROS`: the `ansible_` tests can run against multiple targets
simultaneously, which speeds things up. This is a space-separated list of
DISTRO names, but additionally, supports:
* `MITOGEN_TEST_DISTRO_SPECS`: a space delimited list of distro specs to run
the tests against. (e.g. `centos6 ubuntu2004-py3*4`). Each spec determines
the Linux distribution, target Python interepreter & number of instances.
Only distributions with a pre-built Linux container image can be used.
* `debian-py3`: when generating Ansible inventory file, set
`ansible_python_interpreter` to `python3`, i.e. run a test where the
target interpreter is Python 3.
* `debian*16`: generate 16 Docker containers running Debian. Also works
with -py3.
* `MITOGEN_TEST_IMAGE_TEMPLATE`: specifies the Linux container image name,
and hence the container registry used for test targets.

@ -1,11 +0,0 @@
#!/usr/bin/env python
import ci_lib
batches = [
[
'aws ecr-public get-login-password | docker login --username AWS --password-stdin public.ecr.aws',
]
]
ci_lib.run_batches(batches)

@ -1,17 +1,19 @@
#!/usr/bin/env python
# Run tests/ansible/all.yml under Ansible and Ansible-Mitogen
import collections
import glob
import os
import signal
import sys
import jinja2
import ci_lib
from ci_lib import run
TESTS_DIR = os.path.join(ci_lib.GIT_ROOT, 'tests/ansible')
HOSTS_DIR = os.path.join(ci_lib.TMP, 'hosts')
TMP = ci_lib.TempDir(prefix='mitogen_ci_ansible')
TMP_HOSTS_DIR = os.path.join(TMP.path, 'hosts')
def pause_if_interactive():
@ -32,46 +34,50 @@ ci_lib.check_stray_processes(interesting)
with ci_lib.Fold('docker_setup'):
containers = ci_lib.make_containers()
containers = ci_lib.container_specs(ci_lib.DISTRO_SPECS.split())
ci_lib.start_containers(containers)
with ci_lib.Fold('job_setup'):
os.chdir(TESTS_DIR)
os.chmod('../data/docker/mitogen__has_sudo_pubkey.key', int('0600', 7))
os.chmod(ci_lib.TESTS_SSH_PRIVATE_KEY_FILE, int('0600', 8))
os.chdir(ci_lib.ANSIBLE_TESTS_DIR)
run("mkdir %s", HOSTS_DIR)
for path in glob.glob(TESTS_DIR + '/hosts/*'):
os.mkdir(TMP_HOSTS_DIR)
for path in glob.glob(os.path.join(ci_lib.ANSIBLE_TESTS_HOSTS_DIR, '*')):
if not path.endswith('default.hosts'):
run("ln -s %s %s", path, HOSTS_DIR)
os.symlink(path, os.path.join(TMP_HOSTS_DIR, os.path.basename(path)))
distros = collections.defaultdict(list)
families = collections.defaultdict(list)
for container in containers:
distros[container['distro']].append(container['name'])
families[container['family']].append(container['name'])
jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(
searchpath=ci_lib.ANSIBLE_TESTS_TEMPLATES_DIR,
),
lstrip_blocks=True, # Remove spaces and tabs from before a block
trim_blocks=True, # Remove first newline after a block
)
inventory_template = jinja_env.get_template('test-targets.j2')
inventory_path = os.path.join(TMP_HOSTS_DIR, 'test-targets.ini')
inventory_path = os.path.join(HOSTS_DIR, 'target')
with open(inventory_path, 'w') as fp:
fp.write('[test-targets]\n')
fp.writelines(
"%(name)s "
"ansible_host=%(hostname)s "
"ansible_port=%(port)s "
"ansible_python_interpreter=%(python_path)s "
"ansible_user=mitogen__has_sudo_nopw "
"ansible_password=has_sudo_nopw_password"
"\n"
% container
for container in containers
)
fp.write(inventory_template.render(
containers=containers,
distros=distros,
families=families,
))
ci_lib.dump_file(inventory_path)
if not ci_lib.exists_in_path('sshpass'):
run("sudo apt-get update")
run("sudo apt-get install -y sshpass")
with ci_lib.Fold('ansible'):
playbook = os.environ.get('PLAYBOOK', 'all.yml')
try:
run('./run_ansible_playbook.py %s -i "%s" %s',
playbook, HOSTS_DIR, ' '.join(sys.argv[1:]))
ci_lib.run('./run_ansible_playbook.py %s -i "%s" %s',
playbook, TMP_HOSTS_DIR, ' '.join(sys.argv[1:]),
)
except:
pause_if_interactive()
raise

@ -1,22 +0,0 @@
parameters:
name: ''
pool: ''
sign: false
steps:
- task: UsePythonVersion@0
displayName: Install python
inputs:
versionSpec: '$(python.version)'
condition: ne(variables['python.version'], '')
- script: python -mpip install tox
displayName: Install tooling
- script: python -mtox -e "$(tox.env)"
displayName: "Run tests"
env:
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
AWS_DEFAULT_REGION: $(AWS_DEFAULT_REGION)

@ -1,261 +0,0 @@
# Python package
# Create and test a Python package on multiple Python versions.
# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/python
# User defined variables are also injected as environment variables
# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables#environment-variables
#variables:
#ANSIBLE_VERBOSITY: 3
jobs:
- job: Mac1015
# vanilla Ansible is really slow
timeoutInMinutes: 120
steps:
- template: azure-pipelines-steps.yml
pool:
# https://github.com/actions/virtual-environments/blob/main/images/macos/macos-10.15-Readme.md
vmImage: macOS-10.15
strategy:
matrix:
Mito_27:
python.version: '2.7'
tox.env: py27-mode_mitogen
Mito_36:
python.version: '3.6'
tox.env: py36-mode_mitogen
Mito_39:
python.version: '3.9'
tox.env: py39-mode_mitogen
# TODO: test python3, python3 tests are broken
Loc_27_210:
python.version: '2.7'
tox.env: py27-mode_localhost-ansible2.10
Loc_27_3:
python.version: '2.7'
tox.env: py27-mode_localhost-ansible3
Loc_27_4:
python.version: '2.7'
tox.env: py27-mode_localhost-ansible4
# NOTE: this hangs when ran in Ubuntu 18.04
Van_27_210:
python.version: '2.7'
tox.env: py27-mode_localhost-ansible2.10
STRATEGY: linear
ANSIBLE_SKIP_TAGS: resource_intensive
Van_27_3:
python.version: '2.7'
tox.env: py27-mode_localhost-ansible3
STRATEGY: linear
ANSIBLE_SKIP_TAGS: resource_intensive
Van_27_4:
python.version: '2.7'
tox.env: py27-mode_localhost-ansible4
STRATEGY: linear
ANSIBLE_SKIP_TAGS: resource_intensive
- job: Mac11
# vanilla Ansible is really slow
timeoutInMinutes: 120
steps:
- template: azure-pipelines-steps.yml
pool:
# https://github.com/actions/virtual-environments/blob/main/images/macos/
vmImage: macOS-11
strategy:
matrix:
Mito_27:
tox.env: py27-mode_mitogen
Mito_37:
python.version: '3.7'
tox.env: py37-mode_mitogen
Mito_39:
python.version: '3.9'
tox.env: py39-mode_mitogen
# TODO: test python3, python3 tests are broken
Loc_27_210:
tox.env: py27-mode_localhost-ansible2.10
Loc_27_3:
tox.env: py27-mode_localhost-ansible3
Loc_27_4:
tox.env: py27-mode_localhost-ansible4
# NOTE: this hangs when ran in Ubuntu 18.04
Van_27_210:
tox.env: py27-mode_localhost-ansible2.10
STRATEGY: linear
ANSIBLE_SKIP_TAGS: resource_intensive
Van_27_3:
tox.env: py27-mode_localhost-ansible3
STRATEGY: linear
ANSIBLE_SKIP_TAGS: resource_intensive
Van_27_4:
tox.env: py27-mode_localhost-ansible4
STRATEGY: linear
ANSIBLE_SKIP_TAGS: resource_intensive
- job: Linux
pool:
# https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu1804-README.md
vmImage: "Ubuntu 18.04"
steps:
- template: azure-pipelines-steps.yml
strategy:
matrix:
Mito_27_centos6:
python.version: '2.7'
tox.env: py27-mode_mitogen-distro_centos6
Mito_27_centos7:
python.version: '2.7'
tox.env: py27-mode_mitogen-distro_centos7
Mito_27_centos8:
python.version: '2.7'
tox.env: py27-mode_mitogen-distro_centos8
Mito_27_debian9:
python.version: '2.7'
tox.env: py27-mode_mitogen-distro_debian9
Mito_27_debian10:
python.version: '2.7'
tox.env: py27-mode_mitogen-distro_debian10
Mito_27_debian11:
python.version: '2.7'
tox.env: py27-mode_mitogen-distro_debian11
Mito_27_ubuntu1604:
python.version: '2.7'
tox.env: py27-mode_mitogen-distro_ubuntu1604
Mito_27_ubuntu1804:
python.version: '2.7'
tox.env: py27-mode_mitogen-distro_ubuntu1804
Mito_27_ubuntu2004:
python.version: '2.7'
tox.env: py27-mode_mitogen-distro_ubuntu2004
Mito_36_centos6:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_centos6
Mito_36_centos7:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_centos7
Mito_36_centos8:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_centos8
Mito_36_debian9:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_debian9
Mito_36_debian10:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_debian10
Mito_36_debian11:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_debian11
Mito_36_ubuntu1604:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_ubuntu1604
Mito_36_ubuntu1804:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_ubuntu1804
Mito_36_ubuntu2004:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_ubuntu2004
Mito_39_centos6:
python.version: '3.9'
tox.env: py39-mode_mitogen-distro_centos6
Mito_39_centos7:
python.version: '3.9'
tox.env: py39-mode_mitogen-distro_centos7
Mito_39_centos8:
python.version: '3.9'
tox.env: py39-mode_mitogen-distro_centos8
Mito_39_debian9:
python.version: '3.9'
tox.env: py39-mode_mitogen-distro_debian9
Mito_39_debian10:
python.version: '3.9'
tox.env: py39-mode_mitogen-distro_debian10
Mito_39_debian11:
python.version: '3.9'
tox.env: py39-mode_mitogen-distro_debian11
Mito_39_ubuntu1604:
python.version: '3.9'
tox.env: py39-mode_mitogen-distro_ubuntu1604
Mito_39_ubuntu1804:
python.version: '3.9'
tox.env: py39-mode_mitogen-distro_ubuntu1804
Mito_39_ubuntu2004:
python.version: '3.9'
tox.env: py39-mode_mitogen-distro_ubuntu2004
#DebOps_2460_27_27:
#python.version: '2.7'
#MODE: debops_common
#VER: 2.4.6.0
#DebOps_262_36_27:
#python.version: '3.6'
#MODE: debops_common
#VER: 2.6.2
#Ansible_2460_26:
#python.version: '2.7'
#MODE: ansible
#VER: 2.4.6.0
#Ansible_262_26:
#python.version: '2.7'
#MODE: ansible
#VER: 2.6.2
#Ansible_2460_36:
#python.version: '3.6'
#MODE: ansible
#VER: 2.4.6.0
#Ansible_262_36:
#python.version: '3.6'
#MODE: ansible
#VER: 2.6.2
#Vanilla_262_27:
#python.version: '2.7'
#MODE: ansible
#VER: 2.6.2
#DISTROS: debian
#STRATEGY: linear
Ans_27_210:
python.version: '2.7'
tox.env: py27-mode_ansible-ansible2.10
Ans_27_3:
python.version: '2.7'
tox.env: py27-mode_ansible-ansible3
Ans_27_4:
python.version: '2.7'
tox.env: py27-mode_ansible-ansible4
Ans_36_210:
python.version: '3.6'
tox.env: py36-mode_ansible-ansible2.10
Ans_36_3:
python.version: '3.6'
tox.env: py36-mode_ansible-ansible3
Ans_36_4:
python.version: '3.6'
tox.env: py36-mode_ansible-ansible4
Ans_39_210:
python.version: '3.9'
tox.env: py39-mode_ansible-ansible2.10
Ans_39_3:
python.version: '3.9'
tox.env: py39-mode_ansible-ansible3
Ans_39_4:
python.version: '3.9'
tox.env: py39-mode_ansible-ansible4
Ans_39_5:
python.version: '3.9'
tox.env: py39-mode_ansible-ansible5

@ -0,0 +1,13 @@
# shellcheck shell=bash
# Tox environment name -> Python executable name (e.g. py312-m_mtg -> python3.12)
toxenv-python() {
local pattern='^py([23])([0-9]{1,2}).*'
if [[ $1 =~ $pattern ]]; then
echo "python${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
return
else
echo "${FUNCNAME[0]}: $1: environment name not recognised" >&2
return 1
fi
}

@ -1,15 +1,20 @@
from __future__ import absolute_import
from __future__ import print_function
import atexit
import errno
import os
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
if sys.version_info < (3, 0):
import subprocess32 as subprocess
else:
import subprocess
try:
import urlparse
except ImportError:
@ -22,6 +27,25 @@ os.chdir(
)
)
GIT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
ANSIBLE_TESTS_DIR = os.path.join(GIT_ROOT, 'tests/ansible')
ANSIBLE_TESTS_HOSTS_DIR = os.path.join(GIT_ROOT, 'tests/ansible/hosts')
ANSIBLE_TESTS_TEMPLATES_DIR = os.path.join(GIT_ROOT, 'tests/ansible/templates')
DISTRO_SPECS = os.environ.get(
'MITOGEN_TEST_DISTRO_SPECS',
'alma9-py3 centos5 centos8-py3 debian9 debian12-py3 ubuntu1604 ubuntu2404-py3',
)
IMAGE_PREP_DIR = os.path.join(GIT_ROOT, 'tests/image_prep')
IMAGE_TEMPLATE = os.environ.get(
'MITOGEN_TEST_IMAGE_TEMPLATE',
'ghcr.io/mitogen-hq/%(distro)s-test:2025.02',
)
SUDOERS_DEFAULTS_SRC = './tests/image_prep/files/sudoers_defaults'
SUDOERS_DEFAULTS_DEST = '/etc/sudoers.d/mitogen_test_defaults'
TESTS_SSH_PRIVATE_KEY_FILE = os.path.join(GIT_ROOT, 'tests/data/docker/mitogen__has_sudo_pubkey.key')
_print = print
def print(*args, **kwargs):
file = kwargs.get('file', sys.stdout)
@ -31,40 +55,24 @@ def print(*args, **kwargs):
file.flush()
#
# check_output() monkeypatch cutpasted from testlib.py
#
def subprocess__check_output(*popenargs, **kwargs):
# Missing from 2.6.
process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
output, _ = process.communicate()
retcode = process.poll()
if retcode:
cmd = kwargs.get("args")
if cmd is None:
cmd = popenargs[0]
raise subprocess.CalledProcessError(retcode, cmd)
return output
if not hasattr(subprocess, 'check_output'):
subprocess.check_output = subprocess__check_output
# ------------------
def have_apt():
proc = subprocess.Popen('apt --help >/dev/null 2>/dev/null', shell=True)
return proc.wait() == 0
def have_brew():
proc = subprocess.Popen('brew help >/dev/null 2>/dev/null', shell=True)
return proc.wait() == 0
def _have_cmd(args):
# Code duplicated in testlib.py
try:
subprocess.run(
args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True,
)
except OSError as exc:
if exc.errno == errno.ENOENT:
return False
raise
except subprocess.CalledProcessError:
return False
return True
def have_docker():
proc = subprocess.Popen('docker info >/dev/null 2>/dev/null', shell=True)
return proc.wait() == 0
return _have_cmd(['docker', 'info'])
def _argv(s, *args):
@ -148,7 +156,15 @@ def run_batches(batches):
subprocess.Popen(combine(batch), shell=True)
for batch in batches
]
assert [proc.wait() for proc in procs] == [0] * len(procs)
for proc in procs:
proc.wait()
if proc.returncode:
print(
'proc: pid=%i rc=%i args=%r'
% (proc.pid, proc.returncode, proc.args),
file=sys.stderr, flush=True,
)
assert [proc.returncode for proc in procs] == [0] * len(procs)
def get_output(s, *args, **kwargs):
@ -179,8 +195,8 @@ def exists_in_path(progname):
class TempDir(object):
def __init__(self):
self.path = tempfile.mkdtemp(prefix='mitogen_ci_lib')
def __init__(self, prefix='mitogen_ci_lib'):
self.path = tempfile.mkdtemp(prefix=prefix)
atexit.register(self.destroy)
def destroy(self, rmtree=shutil.rmtree):
@ -193,28 +209,6 @@ class Fold(object):
def __exit__(self, _1, _2, _3): pass
os.environ.setdefault('ANSIBLE_STRATEGY',
os.environ.get('STRATEGY', 'mitogen_linear'))
GIT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
# Used only when MODE=mitogen
DISTRO = os.environ.get('DISTRO', 'debian9')
# Used only when MODE=ansible
DISTROS = os.environ.get('DISTROS', 'centos6 centos8 debian9 debian11 ubuntu1604 ubuntu2004').split()
TARGET_COUNT = int(os.environ.get('TARGET_COUNT', '2'))
BASE_PORT = 2200
TMP = TempDir().path
# We copy this out of the way to avoid random stuff modifying perms in the Git
# tree (like git pull).
src_key_file = os.path.join(GIT_ROOT,
'tests/data/docker/mitogen__has_sudo_pubkey.key')
key_file = os.path.join(TMP,
'mitogen__has_sudo_pubkey.key')
shutil.copyfile(src_key_file, key_file)
os.chmod(key_file, int('0600', 8))
os.environ['PYTHONDONTWRITEBYTECODE'] = 'x'
os.environ['PYTHONPATH'] = '%s:%s' % (
os.environ.get('PYTHONPATH', ''),
@ -224,6 +218,7 @@ os.environ['PYTHONPATH'] = '%s:%s' % (
def get_docker_hostname():
"""Return the hostname where the docker daemon is running.
"""
# Duplicated in testlib
url = os.environ.get('DOCKER_HOST')
if url in (None, 'http+docker://localunixsocket'):
return 'localhost'
@ -232,64 +227,65 @@ def get_docker_hostname():
return parsed.netloc.partition(':')[0]
def image_for_distro(distro):
"""Return the container image name or path for a test distro name.
The returned value is suitable for use with `docker pull`.
>>> image_for_distro('centos5')
'public.ecr.aws/n5z0e8q9/centos5-test'
>>> image_for_distro('centos5-something_custom')
'public.ecr.aws/n5z0e8q9/centos5-test'
"""
return 'public.ecr.aws/n5z0e8q9/%s-test' % (distro.partition('-')[0],)
def make_containers(name_prefix='', port_offset=0):
def container_specs(
distros,
base_port=2200,
image_template=IMAGE_TEMPLATE,
name_template='target-%(distro)s-%(index)d',
):
"""
>>> import pprint
>>> BASE_PORT=2200; DISTROS=['debian', 'centos6']
>>> pprint.pprint(make_containers())
[{'distro': 'debian',
>>> pprint.pprint(container_specs(['debian11-py3', 'centos6']))
[{'distro': 'debian11',
'family': 'debian',
'hostname': 'localhost',
'image': 'public.ecr.aws/n5z0e8q9/debian-test',
'name': 'target-debian-1',
'image': 'ghcr.io/mitogen-hq/debian11-test:2025.02',
'index': 1,
'name': 'target-debian11-1',
'port': 2201,
'python_path': '/usr/bin/python'},
'python_path': '/usr/bin/python3'},
{'distro': 'centos6',
'family': 'centos',
'hostname': 'localhost',
'image': 'public.ecr.aws/n5z0e8q9/centos6-test',
'image': 'ghcr.io/mitogen-hq/centos6-test:2025.02',
'index': 2,
'name': 'target-centos6-2',
'port': 2202,
'python_path': '/usr/bin/python'}]
"""
docker_hostname = get_docker_hostname()
firstbit = lambda s: (s+'-').split('-')[0]
secondbit = lambda s: (s+'-').split('-')[1]
# Code duplicated in testlib.py, both should be updated together
distro_pattern = re.compile(r'''
(?P<distro>(?P<family>[a-z]+)[0-9]+)
(?:-(?P<py>py3))?
(?:\*(?P<count>[0-9]+))?
''',
re.VERBOSE,
)
i = 1
lst = []
for distro in DISTROS:
distro, star, count = distro.partition('*')
if star:
count = int(count)
for distro in distros:
# Code duplicated in testlib.py, both should be updated together
d = distro_pattern.match(distro).groupdict(default=None)
if d.pop('py') == 'py3':
python_path = '/usr/bin/python3'
else:
count = 1
python_path = '/usr/bin/python'
count = int(d.pop('count') or '1', 10)
for x in range(count):
lst.append({
"distro": firstbit(distro),
"image": image_for_distro(distro),
"name": name_prefix + ("target-%s-%s" % (distro, i)),
d['index'] = i
d.update({
'image': image_template % d,
'name': name_template % d,
"hostname": docker_hostname,
"port": BASE_PORT + i + port_offset,
"python_path": (
'/usr/bin/python3'
if secondbit(distro) == 'py3'
else '/usr/bin/python'
)
'port': base_port + i,
"python_path": python_path,
})
lst.append(d)
i += 1
return lst
@ -313,18 +309,24 @@ def proc_is_docker(pid):
def get_interesting_procs(container_name=None):
"""
Return a list of (pid, line) tuples for processes considered interesting.
"""
args = ['ps', 'ax', '-oppid=', '-opid=', '-ocomm=', '-ocommand=']
if container_name is not None:
args = ['docker', 'exec', container_name] + args
out = []
for line in subprocess__check_output(args).decode().splitlines():
for line in subprocess.check_output(args).decode().splitlines():
ppid, pid, comm, rest = line.split(None, 3)
if (
(
any(comm.startswith(s) for s in INTERESTING_COMMS) or
'mitogen:' in rest
) and
(
'WALinuxAgent' not in rest
) and
(
container_name is not None or
(not proc_is_docker(pid))

@ -2,15 +2,9 @@
import ci_lib
# Naturally DebOps only supports Debian.
ci_lib.DISTROS = ['debian']
ci_lib.run_batches([
[
'pip install -qqq "debops[ansible]==2.1.2"',
],
[
'aws ecr-public get-login-password | docker login --username AWS --password-stdin public.ecr.aws',
'python -m pip --no-python-version-warning --disable-pip-version-check "debops[ansible]==2.1.2"',
],
])

@ -6,21 +6,24 @@ import sys
import ci_lib
# DebOps only supports Debian.
ci_lib.DISTROS = ['debian'] * ci_lib.TARGET_COUNT
project_dir = os.path.join(ci_lib.TMP, 'project')
TMP = ci_lib.TempDir(prefix='mitogen_ci_debops')
project_dir = os.path.join(TMP.path, 'project')
vars_path = 'ansible/inventory/group_vars/debops_all_hosts.yml'
inventory_path = 'ansible/inventory/hosts'
docker_hostname = ci_lib.get_docker_hostname()
with ci_lib.Fold('docker_setup'):
containers = ci_lib.make_containers(port_offset=500, name_prefix='debops-')
containers = ci_lib.container_specs(
['debian*2'],
base_port=2700,
name_template='debops-target-%(distro)s-%(index)d',
)
ci_lib.start_containers(containers)
with ci_lib.Fold('job_setup'):
os.chmod(ci_lib.TESTS_SSH_PRIVATE_KEY_FILE, int('0600', 8))
ci_lib.run('debops-init %s', project_dir)
os.chdir(project_dir)
@ -44,7 +47,7 @@ with ci_lib.Fold('job_setup'):
"\n"
# Speed up slow DH generation.
"dhparam__bits: ['128', '64']\n"
% (ci_lib.key_file,)
% (ci_lib.TESTS_SSH_PRIVATE_KEY_FILE,)
)
with open(inventory_path, 'a') as fp:

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
VERSION="$1"
curl \
--fail \
--location \
--no-progress-meter \
--remote-name \
"https://downloads.sourceforge.net/project/sshpass/sshpass/${VERSION}/sshpass-${VERSION}.tar.gz"
tar xvf "sshpass-${VERSION}.tar.gz"
cd "sshpass-${VERSION}"
./configure
sudo make install

@ -1,8 +0,0 @@
#!/usr/bin/env python
import ci_lib
batches = [
]
ci_lib.run_batches(batches)

@ -1,17 +1,13 @@
#!/usr/bin/env python
# Run tests/ansible/all.yml under Ansible and Ansible-Mitogen
from __future__ import print_function
import os
import subprocess
import sys
import ci_lib
from ci_lib import run
TESTS_DIR = os.path.join(ci_lib.GIT_ROOT, 'tests/ansible')
IMAGE_PREP_DIR = os.path.join(ci_lib.GIT_ROOT, 'tests/image_prep')
HOSTS_DIR = os.path.join(TESTS_DIR, 'hosts')
KEY_PATH = os.path.join(TESTS_DIR, '../data/docker/mitogen__has_sudo_pubkey.key')
with ci_lib.Fold('unit_tests'):
@ -20,41 +16,54 @@ with ci_lib.Fold('unit_tests'):
with ci_lib.Fold('job_setup'):
os.chmod(KEY_PATH, int('0600', 8))
# NOTE: sshpass v1.06 causes errors so pegging to 1.05 -> "msg": "Error when changing password","out": "passwd: DS error: eDSAuthFailed\n",
# there's a checksum error with "brew install http://git.io/sshpass.rb" though, so installing manually
if not ci_lib.exists_in_path('sshpass'):
os.system("curl -O -L https://sourceforge.net/projects/sshpass/files/sshpass/1.05/sshpass-1.05.tar.gz && \
tar xvf sshpass-1.05.tar.gz && \
cd sshpass-1.05 && \
./configure && \
sudo make install")
os.chmod(ci_lib.TESTS_SSH_PRIVATE_KEY_FILE, int('0600', 8))
with ci_lib.Fold('machine_prep'):
# generate a new ssh key for localhost ssh
if not os.path.exists(os.path.expanduser("~/.ssh/id_rsa")):
os.system("ssh-keygen -P '' -m pem -f ~/.ssh/id_rsa")
os.system("cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys")
subprocess.check_call("ssh-keygen -P '' -m pem -f ~/.ssh/id_rsa", shell=True)
subprocess.check_call("cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys", shell=True)
os.chmod(os.path.expanduser('~/.ssh'), int('0700', 8))
os.chmod(os.path.expanduser('~/.ssh/authorized_keys'), int('0600', 8))
# also generate it for the sudo user
if os.system("sudo [ -f /var/root/.ssh/id_rsa ]") != 0:
os.system("sudo ssh-keygen -P '' -m pem -f /var/root/.ssh/id_rsa")
os.system("sudo cat /var/root/.ssh/id_rsa.pub | sudo tee -a /var/root/.ssh/authorized_keys")
if os.system("sudo [ -f ~root/.ssh/id_rsa ]") != 0:
subprocess.check_call("sudo ssh-keygen -P '' -m pem -f ~root/.ssh/id_rsa", shell=True)
subprocess.check_call("sudo cat ~root/.ssh/id_rsa.pub | sudo tee -a ~root/.ssh/authorized_keys", shell=True)
subprocess.check_call('sudo chmod 700 ~root/.ssh', shell=True)
subprocess.check_call('sudo chmod 600 ~root/.ssh/authorized_keys', shell=True)
os.chmod(os.path.expanduser('~/.ssh'), int('0700', 8))
os.chmod(os.path.expanduser('~/.ssh/authorized_keys'), int('0600', 8))
# run chmod through sudo since it's owned by root
os.system('sudo chmod 700 /var/root/.ssh')
os.system('sudo chmod 600 /var/root/.ssh/authorized_keys')
os.chdir(ci_lib.IMAGE_PREP_DIR)
ci_lib.run("ansible-playbook -c local -i localhost, macos_localhost.yml")
if os.path.expanduser('~mitogen__user1') == '~mitogen__user1':
os.chdir(IMAGE_PREP_DIR)
run("ansible-playbook -c local -i localhost, _user_accounts.yml")
os.chdir(ci_lib.IMAGE_PREP_DIR)
ci_lib.run("ansible-playbook -c local -i localhost, _user_accounts.yml")
cmd = ';'.join([
'from __future__ import print_function',
'import os, sys',
'print(sys.executable, os.path.realpath(sys.executable))',
])
for interpreter in ['/usr/bin/python', '/usr/bin/python2', '/usr/bin/python2.7']:
print(interpreter)
try:
subprocess.call([interpreter, '-c', cmd])
except OSError as exc:
print(exc)
print(interpreter, 'with PYTHON_LAUNCHED_FROM_WRAPPER=1')
environ = os.environ.copy()
environ['PYTHON_LAUNCHED_FROM_WRAPPER'] = '1'
try:
subprocess.call([interpreter, '-c', cmd], env=environ)
except OSError as exc:
print(exc)
with ci_lib.Fold('ansible'):
os.chdir(TESTS_DIR)
os.chdir(ci_lib.ANSIBLE_TESTS_DIR)
playbook = os.environ.get('PLAYBOOK', 'all.yml')
run('./run_ansible_playbook.py %s -l target %s',
ci_lib.run('./run_ansible_playbook.py %s %s',
playbook, ' '.join(sys.argv[1:]))

@ -1,14 +0,0 @@
#!/usr/bin/env python
import ci_lib
batches = [
]
if ci_lib.have_docker():
batches.append([
'aws ecr-public get-login-password | docker login --username AWS --password-stdin public.ecr.aws',
])
ci_lib.run_batches(batches)

@ -3,9 +3,6 @@
import ci_lib
batches = [
[
'aws ecr-public get-login-password | docker login --username AWS --password-stdin public.ecr.aws',
],
[
'curl https://dw.github.io/mitogen/binaries/ubuntu-python-2.4.6.tar.bz2 | sudo tar -C / -jxv',
]

@ -8,8 +8,6 @@ import ci_lib
os.environ.update({
'NOCOVERAGE': '1',
'UNIT2': '/usr/local/python2.4.6/bin/unit2',
'MITOGEN_TEST_DISTRO': ci_lib.DISTRO,
'MITOGEN_LOG_LEVEL': 'debug',
'SKIP_ANSIBLE': '1',
})

@ -2,11 +2,11 @@
# Run the Mitogen tests.
import os
import subprocess
import ci_lib
os.environ.update({
'MITOGEN_TEST_DISTRO': ci_lib.DISTRO,
'MITOGEN_LOG_LEVEL': 'debug',
'SKIP_ANSIBLE': '1',
})
@ -14,6 +14,14 @@ os.environ.update({
if not ci_lib.have_docker():
os.environ['SKIP_DOCKER_TESTS'] = '1'
subprocess.check_call(
"umask 0022; sudo cp '%s' '%s'"
% (ci_lib.SUDOERS_DEFAULTS_SRC, ci_lib.SUDOERS_DEFAULTS_DEST),
shell=True,
)
subprocess.check_call(['sudo', 'visudo', '-cf', ci_lib.SUDOERS_DEFAULTS_DEST])
subprocess.check_call(['sudo', '-l'])
interesting = ci_lib.get_interesting_procs()
ci_lib.run('./run_tests -v')
ci_lib.check_stray_processes(interesting)

@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail
INDENT=" "
POSSIBLE_PYTHONS=(
python
python2
python3
/usr/bin/python
/usr/bin/python2
/usr/bin/python3
# GitHub macOS 12 images: python2.7 is installed, but not on $PATH
/Library/Frameworks/Python.framework/Versions/2.7/bin/python2.7
)
for p in "${POSSIBLE_PYTHONS[@]}"; do
echo "$p"
if [[ ${p:0:1} == "/" && -e $p ]]; then
:
elif type "$p" > /dev/null 2>&1; then
type "$p" 2>&1 | sed -e "s/^/${INDENT}type: /"
else
echo "${INDENT}Not present"
echo
continue
fi
$p -c "import sys; print('${INDENT}version: %d.%d.%d' % sys.version_info[:3])"
# macOS builders lack a realpath command
$p -c "import os.path; print('${INDENT}realpath: %s' % os.path.realpath('$(type -p "$p")'))"
$p -c "import sys; print('${INDENT}sys.executable: %s' % sys.executable)"
echo
done

@ -1,33 +0,0 @@
---
name: Mitogen 0.2.x bug report
about: Report a bug in Mitogen 0.2.x (for Ansible 2.5, 2.6, 2.7, 2.8, or 2.9)
title: ''
labels: affects-0.2, bug
assignees: ''
---
Please drag-drop large logs as text file attachments.
Feel free to write an issue in your preferred format, however if in doubt, use
the following checklist as a guide for what to include.
* Which version of Ansible are you running?
* Is your version of Ansible patched in any way?
* Are you running with any custom modules, or `module_utils` loaded?
* Have you tried the latest master version from Git?
* Do you have some idea of what the underlying problem may be?
https://mitogen.networkgenomics.com/ansible_detailed.html#common-problems has
instructions to help figure out the likely cause and how to gather relevant
logs.
* Mention your host and target OS and versions
* Mention your host and target Python versions
* If reporting a performance issue, mention the number of targets and a rough
description of your workload (lots of copies, lots of tiny file edits, etc.)
* If reporting a crash or hang in Ansible, please rerun with -vvv and include
200 lines of output around the point of the error, along with a full copy of
any traceback or error text in the log. Beware "-vvv" may include secret
data! Edit as necessary before posting.
* If reporting any kind of problem with Ansible, please include the Ansible
version along with output of "ansible-config dump --only-changed".

@ -1,33 +0,0 @@
---
name: Mitogen 0.3.x bug report
about: Report a bug in Mitogen 0.3.x (for Ansible 2.10.x)
title: ''
labels: affects-0.3, bug
assignees: ''
---
Please drag-drop large logs as text file attachments.
Feel free to write an issue in your preferred format, however if in doubt, use
the following checklist as a guide for what to include.
* Which version of Ansible are you running?
* Is your version of Ansible patched in any way?
* Are you running with any custom modules, or `module_utils` loaded?
* Have you tried the latest master version from Git?
* Do you have some idea of what the underlying problem may be?
https://mitogen.networkgenomics.com/ansible_detailed.html#common-problems has
instructions to help figure out the likely cause and how to gather relevant
logs.
* Mention your host and target OS and versions
* Mention your host and target Python versions
* If reporting a performance issue, mention the number of targets and a rough
description of your workload (lots of copies, lots of tiny file edits, etc.)
* If reporting a crash or hang in Ansible, please rerun with -vvv and include
200 lines of output around the point of the error, along with a full copy of
any traceback or error text in the log. Beware "-vvv" may include secret
data! Edit as necessary before posting.
* If reporting any kind of problem with Ansible, please include the Ansible
version along with output of "ansible-config dump --only-changed".

@ -0,0 +1,62 @@
name: Bug report
description: Report a bug in Mitogen 0.3.x (for Ansible 2.10 and above)
labels:
- affects-0.3
type: bug
body:
- type: textarea
attributes:
label: Description
description: >
When does the problem occur?
What happens after?
How is this different?
Did it previously behave as expected?
placeholder: |
When I do X, Y happens, but I was expecting Z because ...
Before version 1.2.3 it worked as expected.
validations:
required: true
- type: input
attributes:
label: Mitogen version
placeholder: 0.3.31, 0.3.3-9+deb12u1
validations:
required: true
- type: input
attributes:
label: Ansible version (if applicable)
placeholder: 2.18.11
- type: textarea
attributes:
label: OS and environment
description: >
What operating system version(s), Python version(s), etc. are you using?
placeholder: |
Controller (master): Debian 13, Python 3.14
Targets (slaves): Ubuntu 20.04/Python 2.7, RHEL 10, ...
- type: textarea
attributes:
label: Steps to reproduce
description: >
Instructions, code, or playbook(s) recreate the beahviour
value: |
Steps:
1. Set config `foo = 42` in somefile.cfg
2. Run the following Python or Playbook with `cmd --option bar ...`
```
Code or playbook here
```
- type: textarea
attributes:
label: Anything else
description: >
Include any other details you think might be relevant or helpful.
Examples might include logs, unusual settings, environment variables, ...

@ -0,0 +1,211 @@
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
name: Tests
# env:
# ANSIBLE_VERBOSITY: 3
# MITOGEN_LOG_LEVEL: DEBUG
on:
pull_request:
push:
branches-ignore:
- docs-master
# https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners
# https://github.com/actions/runner-images/blob/main/README.md#software-and-image-support
jobs:
u2204:
name: u2204 ${{ matrix.tox_env }}
# https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2204-Readme.md
runs-on: ubuntu-22.04
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
include:
- tox_env: py27-m_ans-ans2.10
- tox_env: py27-m_ans-ans4
- tox_env: py36-m_ans-ans2.10
- tox_env: py36-m_ans-ans4
- tox_env: py27-m_mtg
- tox_env: py36-m_mtg
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: .ci/show_python_versions
- name: Install deps
id: install-deps
run: |
set -o errexit -o nounset -o pipefail
source .ci/bash_functions
PYTHON="$(toxenv-python '${{ matrix.tox_env }}')"
sudo apt-get update
if [[ $PYTHON == "python2.7" ]]; then
sudo apt install -y python2-dev sshpass virtualenv
curl "https://bootstrap.pypa.io/pip/2.7/get-pip.py" --output "get-pip.py"
"$PYTHON" get-pip.py --user --no-python-version-warning
# Avoid Python 2.x pip masking system pip
rm -f ~/.local/bin/{easy_install,pip,wheel}
elif [[ $PYTHON == "python3.6" ]]; then
sudo apt install -y gcc-10 make libbz2-dev liblzma-dev libreadline-dev libsqlite3-dev libssl-dev sshpass virtualenv zlib1g-dev
curl --fail --silent --show-error --location https://pyenv.run | bash
CC=gcc-10 ~/.pyenv/bin/pyenv install --force 3.6
PYTHON="$HOME/.pyenv/versions/3.6.15/bin/python3.6"
fi
"$PYTHON" -m pip install -r "tests/requirements-tox.txt"
echo "python=$PYTHON" >> $GITHUB_OUTPUT
- name: Run tests
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -o errexit -o nounset -o pipefail
PYTHON="${{ steps.install-deps.outputs.python }}"
"$PYTHON" -m tox -e "${{ matrix.tox_env }}"
u2404:
name: u2404 ${{ matrix.tox_env }}
# https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md
runs-on: ubuntu-24.04
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
include:
- tox_env: py311-m_ans-ans2.10
python_version: '3.11'
- tox_env: py311-m_ans-ans3
python_version: '3.11'
- tox_env: py311-m_ans-ans4
python_version: '3.11'
- tox_env: py311-m_ans-ans5
python_version: '3.11'
- tox_env: py313-m_ans-ans6
python_version: '3.13'
- tox_env: py313-m_ans-ans7
python_version: '3.13'
- tox_env: py313-m_ans-ans8
python_version: '3.13'
- tox_env: py314-m_ans-ans9
python_version: '3.14'
- tox_env: py314-m_ans-ans10
python_version: '3.14'
- tox_env: py314-m_ans-ans11
python_version: '3.14'
- tox_env: py314-m_ans-ans12
python_version: '3.14'
- tox_env: py314-m_ans-ans13
python_version: '3.14'
- tox_env: py314-m_ans-ans13-s_lin
python_version: '3.14'
- tox_env: py314-m_mtg
python_version: '3.14'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python_version }}
if: ${{ matrix.python_version }}
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: .ci/show_python_versions
- name: Install deps
id: install-deps
run: |
set -o errexit -o nounset -o pipefail
source .ci/bash_functions
PYTHON="$(toxenv-python '${{ matrix.tox_env }}')"
sudo apt-get update
sudo apt-get install -y sshpass virtualenv
"$PYTHON" -m pip install -r "tests/requirements-tox.txt"
echo "python=$PYTHON" >> $GITHUB_OUTPUT
- name: Run tests
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -o errexit -o nounset -o pipefail
PYTHON="${{ steps.install-deps.outputs.python }}"
"$PYTHON" -m tox -e "${{ matrix.tox_env }}"
macos:
name: macos ${{ matrix.tox_env }}
# https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md
runs-on: macos-15
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- tox_env: py314-m_lcl-ans13
python_version: '3.14'
- tox_env: py314-m_lcl-ans13-s_lin
python_version: '3.14'
- tox_env: py314-m_mtg
python_version: '3.14'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python_version }}
if: ${{ matrix.python_version }}
- run: .ci/show_python_versions
- run: .ci/install_sshpass ${{ matrix.sshpass_version }}
if: ${{ matrix.sshpass_version }}
- name: Install deps
id: install-deps
run: |
set -o errexit -o nounset -o pipefail
source .ci/bash_functions
PYTHON="$(toxenv-python '${{ matrix.tox_env }}')"
"$PYTHON" -m pip install -r "tests/requirements-tox.txt"
echo "python=$PYTHON" >> $GITHUB_OUTPUT
- name: Run tests
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -o errexit -o nounset -o pipefail
PYTHON="${{ steps.install-deps.outputs.python }}"
"$PYTHON" -m tox -e "${{ matrix.tox_env }}"
# https://github.com/marketplace/actions/alls-green
check:
if: always()
needs:
- u2204
- u2404
- macos
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

3
.gitignore vendored

@ -1,3 +1,4 @@
.ansible/
.coverage
.tox
.venv
@ -6,11 +7,13 @@ venvs/**
*.pyc
*.pyd
*.pyo
*.retry
MANIFEST
build/
dist/
extra/
tests/ansible/.*.pid
tests/image_prep/logs
docs/_build/
htmlcov/
*.egg-info

@ -1,10 +0,0 @@
path_classifiers:
library:
- "mitogen/compat"
- "ansible_mitogen/compat"
queries:
# Mitogen 2.4 compatibility trips this query everywhere, so just disable it
- exclude: py/unreachable-statement
- exclude: py/should-use-with
# mitogen.core.b() trips this query everywhere, so just disable it
- exclude: py/import-and-import-from

@ -1,9 +1,9 @@
# Mitogen
[![PyPI - Version](https://img.shields.io/pypi/v/mitogen)](https://pypi.org/project/mitogen/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mitogen)](https://pypi.org/project/mitogen/)
[![Build Status](https://img.shields.io/github/actions/workflow/status/mitogen-hq/mitogen/tests.yml?branch=master)](https://github.com/mitogen-hq/mitogen/actions?query=branch%3Amaster)
<a href="https://mitogen.networkgenomics.com/">Please see the documentation</a>.
![](https://i.imgur.com/eBM6LhJ.gif)
[![Total alerts](https://img.shields.io/lgtm/alerts/g/mitogen-hq/mitogen.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/mitogen-hq/mitogen/alerts/)
[![Build Status](https://dev.azure.com/mitogen-hq/mitogen/_apis/build/status/mitogen-hq.mitogen?branchName=master)](https://dev.azure.com/mitogen-hq/mitogen/_build/latest?definitionId=1&branchName=master)

@ -73,7 +73,9 @@ necessarily involves preventing the scheduler from making load balancing
decisions.
"""
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import ctypes
import logging
import mmap
@ -81,7 +83,6 @@ import multiprocessing
import os
import struct
import mitogen.core
import mitogen.parent
@ -263,7 +264,7 @@ class LinuxPolicy(FixedPolicy):
for x in range(16):
chunks.append(struct.pack('<Q', mask & shiftmask))
mask >>= 64
return mitogen.core.b('').join(chunks)
return b''.join(chunks)
def _get_thread_ids(self):
try:

@ -1,318 +0,0 @@
r"""JSON (JavaScript Object Notation) <http://json.org> is a subset of
JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data
interchange format.
:mod:`simplejson` exposes an API familiar to users of the standard library
:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained
version of the :mod:`json` library contained in Python 2.6, but maintains
compatibility with Python 2.4 and Python 2.5 and (currently) has
significant performance advantages, even without using the optional C
extension for speedups.
Encoding basic Python object hierarchies::
>>> import simplejson as json
>>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
'["foo", {"bar": ["baz", null, 1.0, 2]}]'
>>> print json.dumps("\"foo\bar")
"\"foo\bar"
>>> print json.dumps(u'\u1234')
"\u1234"
>>> print json.dumps('\\')
"\\"
>>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True)
{"a": 0, "b": 0, "c": 0}
>>> from StringIO import StringIO
>>> io = StringIO()
>>> json.dump(['streaming API'], io)
>>> io.getvalue()
'["streaming API"]'
Compact encoding::
>>> import simplejson as json
>>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':'))
'[1,2,3,{"4":5,"6":7}]'
Pretty printing::
>>> import simplejson as json
>>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=4)
>>> print '\n'.join([l.rstrip() for l in s.splitlines()])
{
"4": 5,
"6": 7
}
Decoding JSON::
>>> import simplejson as json
>>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}]
>>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj
True
>>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar'
True
>>> from StringIO import StringIO
>>> io = StringIO('["streaming API"]')
>>> json.load(io)[0] == 'streaming API'
True
Specializing JSON object decoding::
>>> import simplejson as json
>>> def as_complex(dct):
... if '__complex__' in dct:
... return complex(dct['real'], dct['imag'])
... return dct
...
>>> json.loads('{"__complex__": true, "real": 1, "imag": 2}',
... object_hook=as_complex)
(1+2j)
>>> import decimal
>>> json.loads('1.1', parse_float=decimal.Decimal) == decimal.Decimal('1.1')
True
Specializing JSON object encoding::
>>> import simplejson as json
>>> def encode_complex(obj):
... if isinstance(obj, complex):
... return [obj.real, obj.imag]
... raise TypeError(repr(o) + " is not JSON serializable")
...
>>> json.dumps(2 + 1j, default=encode_complex)
'[2.0, 1.0]'
>>> json.JSONEncoder(default=encode_complex).encode(2 + 1j)
'[2.0, 1.0]'
>>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j))
'[2.0, 1.0]'
Using simplejson.tool from the shell to validate and pretty-print::
$ echo '{"json":"obj"}' | python -m simplejson.tool
{
"json": "obj"
}
$ echo '{ 1.2:3.4}' | python -m simplejson.tool
Expecting property name: line 1 column 2 (char 2)
"""
__version__ = '2.0.9'
__all__ = [
'dump', 'dumps', 'load', 'loads',
'JSONDecoder', 'JSONEncoder',
]
__author__ = 'Bob Ippolito <bob@redivi.com>'
from decoder import JSONDecoder
from encoder import JSONEncoder
_default_encoder = JSONEncoder(
skipkeys=False,
ensure_ascii=True,
check_circular=True,
allow_nan=True,
indent=None,
separators=None,
encoding='utf-8',
default=None,
)
def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
allow_nan=True, cls=None, indent=None, separators=None,
encoding='utf-8', default=None, **kw):
"""Serialize ``obj`` as a JSON formatted stream to ``fp`` (a
``.write()``-supporting file-like object).
If ``skipkeys`` is true then ``dict`` keys that are not basic types
(``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``)
will be skipped instead of raising a ``TypeError``.
If ``ensure_ascii`` is false, then the some chunks written to ``fp``
may be ``unicode`` instances, subject to normal Python ``str`` to
``unicode`` coercion rules. Unless ``fp.write()`` explicitly
understands ``unicode`` (as in ``codecs.getwriter()``) this is likely
to cause an error.
If ``check_circular`` is false, then the circular reference check
for container types will be skipped and a circular reference will
result in an ``OverflowError`` (or worse).
If ``allow_nan`` is false, then it will be a ``ValueError`` to
serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``)
in strict compliance of the JSON specification, instead of using the
JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
If ``indent`` is a non-negative integer, then JSON array elements and object
members will be pretty-printed with that indent level. An indent level
of 0 will only insert newlines. ``None`` is the most compact representation.
If ``separators`` is an ``(item_separator, dict_separator)`` tuple
then it will be used instead of the default ``(', ', ': ')`` separators.
``(',', ':')`` is the most compact JSON representation.
``encoding`` is the character encoding for str instances, default is UTF-8.
``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.
To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the
``.default()`` method to serialize additional types), specify it with
the ``cls`` kwarg.
"""
# cached encoder
if (not skipkeys and ensure_ascii and
check_circular and allow_nan and
cls is None and indent is None and separators is None and
encoding == 'utf-8' and default is None and not kw):
iterable = _default_encoder.iterencode(obj)
else:
if cls is None:
cls = JSONEncoder
iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii,
check_circular=check_circular, allow_nan=allow_nan, indent=indent,
separators=separators, encoding=encoding,
default=default, **kw).iterencode(obj)
# could accelerate with writelines in some versions of Python, at
# a debuggability cost
for chunk in iterable:
fp.write(chunk)
def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
allow_nan=True, cls=None, indent=None, separators=None,
encoding='utf-8', default=None, **kw):
"""Serialize ``obj`` to a JSON formatted ``str``.
If ``skipkeys`` is false then ``dict`` keys that are not basic types
(``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``)
will be skipped instead of raising a ``TypeError``.
If ``ensure_ascii`` is false, then the return value will be a
``unicode`` instance subject to normal Python ``str`` to ``unicode``
coercion rules instead of being escaped to an ASCII ``str``.
If ``check_circular`` is false, then the circular reference check
for container types will be skipped and a circular reference will
result in an ``OverflowError`` (or worse).
If ``allow_nan`` is false, then it will be a ``ValueError`` to
serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in
strict compliance of the JSON specification, instead of using the
JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
If ``indent`` is a non-negative integer, then JSON array elements and
object members will be pretty-printed with that indent level. An indent
level of 0 will only insert newlines. ``None`` is the most compact
representation.
If ``separators`` is an ``(item_separator, dict_separator)`` tuple
then it will be used instead of the default ``(', ', ': ')`` separators.
``(',', ':')`` is the most compact JSON representation.
``encoding`` is the character encoding for str instances, default is UTF-8.
``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.
To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the
``.default()`` method to serialize additional types), specify it with
the ``cls`` kwarg.
"""
# cached encoder
if (not skipkeys and ensure_ascii and
check_circular and allow_nan and
cls is None and indent is None and separators is None and
encoding == 'utf-8' and default is None and not kw):
return _default_encoder.encode(obj)
if cls is None:
cls = JSONEncoder
return cls(
skipkeys=skipkeys, ensure_ascii=ensure_ascii,
check_circular=check_circular, allow_nan=allow_nan, indent=indent,
separators=separators, encoding=encoding, default=default,
**kw).encode(obj)
_default_decoder = JSONDecoder(encoding=None, object_hook=None)
def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None,
parse_int=None, parse_constant=None, **kw):
"""Deserialize ``fp`` (a ``.read()``-supporting file-like object containing
a JSON document) to a Python object.
If the contents of ``fp`` is encoded with an ASCII based encoding other
than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must
be specified. Encodings that are not ASCII based (such as UCS-2) are
not allowed, and should be wrapped with
``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode``
object and passed to ``loads()``
``object_hook`` is an optional function that will be called with the
result of any object literal decode (a ``dict``). The return value of
``object_hook`` will be used instead of the ``dict``. This feature
can be used to implement custom decoders (e.g. JSON-RPC class hinting).
To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
kwarg.
"""
return loads(fp.read(),
encoding=encoding, cls=cls, object_hook=object_hook,
parse_float=parse_float, parse_int=parse_int,
parse_constant=parse_constant, **kw)
def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None,
parse_int=None, parse_constant=None, **kw):
"""Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON
document) to a Python object.
If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding
other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name
must be specified. Encodings that are not ASCII based (such as UCS-2)
are not allowed and should be decoded to ``unicode`` first.
``object_hook`` is an optional function that will be called with the
result of any object literal decode (a ``dict``). The return value of
``object_hook`` will be used instead of the ``dict``. This feature
can be used to implement custom decoders (e.g. JSON-RPC class hinting).
``parse_float``, if specified, will be called with the string
of every JSON float to be decoded. By default this is equivalent to
float(num_str). This can be used to use another datatype or parser
for JSON floats (e.g. decimal.Decimal).
``parse_int``, if specified, will be called with the string
of every JSON int to be decoded. By default this is equivalent to
int(num_str). This can be used to use another datatype or parser
for JSON integers (e.g. float).
``parse_constant``, if specified, will be called with one of the
following strings: -Infinity, Infinity, NaN, null, true, false.
This can be used to raise an exception if invalid JSON numbers
are encountered.
To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
kwarg.
"""
if (cls is None and encoding is None and object_hook is None and
parse_int is None and parse_float is None and
parse_constant is None and not kw):
return _default_decoder.decode(s)
if cls is None:
cls = JSONDecoder
if object_hook is not None:
kw['object_hook'] = object_hook
if parse_float is not None:
kw['parse_float'] = parse_float
if parse_int is not None:
kw['parse_int'] = parse_int
if parse_constant is not None:
kw['parse_constant'] = parse_constant
return cls(encoding=encoding, **kw).decode(s)

@ -1,354 +0,0 @@
"""Implementation of JSONDecoder
"""
import re
import sys
import struct
from simplejson.scanner import make_scanner
try:
from simplejson._speedups import scanstring as c_scanstring
except ImportError:
c_scanstring = None
__all__ = ['JSONDecoder']
FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
def _floatconstants():
_BYTES = '7FF80000000000007FF0000000000000'.decode('hex')
if sys.byteorder != 'big':
_BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1]
nan, inf = struct.unpack('dd', _BYTES)
return nan, inf, -inf
NaN, PosInf, NegInf = _floatconstants()
def linecol(doc, pos):
lineno = doc.count('\n', 0, pos) + 1
if lineno == 1:
colno = pos
else:
colno = pos - doc.rindex('\n', 0, pos)
return lineno, colno
def errmsg(msg, doc, pos, end=None):
# Note that this function is called from _speedups
lineno, colno = linecol(doc, pos)
if end is None:
#fmt = '{0}: line {1} column {2} (char {3})'
#return fmt.format(msg, lineno, colno, pos)
fmt = '%s: line %d column %d (char %d)'
return fmt % (msg, lineno, colno, pos)
endlineno, endcolno = linecol(doc, end)
#fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})'
#return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end)
fmt = '%s: line %d column %d - line %d column %d (char %d - %d)'
return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end)
_CONSTANTS = {
'-Infinity': NegInf,
'Infinity': PosInf,
'NaN': NaN,
}
STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS)
BACKSLASH = {
'"': u'"', '\\': u'\\', '/': u'/',
'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t',
}
DEFAULT_ENCODING = "utf-8"
def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match):
"""Scan the string s for a JSON string. End is the index of the
character in s after the quote that started the JSON string.
Unescapes all valid JSON string escape sequences and raises ValueError
on attempt to decode an invalid string. If strict is False then literal
control characters are allowed in the string.
Returns a tuple of the decoded string and the index of the character in s
after the end quote."""
if encoding is None:
encoding = DEFAULT_ENCODING
chunks = []
_append = chunks.append
begin = end - 1
while 1:
chunk = _m(s, end)
if chunk is None:
raise ValueError(
errmsg("Unterminated string starting at", s, begin))
end = chunk.end()
content, terminator = chunk.groups()
# Content is contains zero or more unescaped string characters
if content:
if not isinstance(content, unicode):
content = unicode(content, encoding)
_append(content)
# Terminator is the end of string, a literal control character,
# or a backslash denoting that an escape sequence follows
if terminator == '"':
break
elif terminator != '\\':
if strict:
msg = "Invalid control character %r at" % (terminator,)
#msg = "Invalid control character {0!r} at".format(terminator)
raise ValueError(errmsg(msg, s, end))
else:
_append(terminator)
continue
try:
esc = s[end]
except IndexError:
raise ValueError(
errmsg("Unterminated string starting at", s, begin))
# If not a unicode escape sequence, must be in the lookup table
if esc != 'u':
try:
char = _b[esc]
except KeyError:
msg = "Invalid \\escape: " + repr(esc)
raise ValueError(errmsg(msg, s, end))
end += 1
else:
# Unicode escape sequence
esc = s[end + 1:end + 5]
next_end = end + 5
if len(esc) != 4:
msg = "Invalid \\uXXXX escape"
raise ValueError(errmsg(msg, s, end))
uni = int(esc, 16)
# Check for surrogate pair on UCS-4 systems
if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535:
msg = "Invalid \\uXXXX\\uXXXX surrogate pair"
if not s[end + 5:end + 7] == '\\u':
raise ValueError(errmsg(msg, s, end))
esc2 = s[end + 7:end + 11]
if len(esc2) != 4:
raise ValueError(errmsg(msg, s, end))
uni2 = int(esc2, 16)
uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00))
next_end += 6
char = unichr(uni)
end = next_end
# Append the unescaped character
_append(char)
return u''.join(chunks), end
# Use speedup if available
scanstring = c_scanstring or py_scanstring
WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)
WHITESPACE_STR = ' \t\n\r'
def JSONObject((s, end), encoding, strict, scan_once, object_hook, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
pairs = {}
# Use a slice to prevent IndexError from being raised, the following
# check will raise a more specific ValueError if the string is empty
nextchar = s[end:end + 1]
# Normally we expect nextchar == '"'
if nextchar != '"':
if nextchar in _ws:
end = _w(s, end).end()
nextchar = s[end:end + 1]
# Trivial empty object
if nextchar == '}':
return pairs, end + 1
elif nextchar != '"':
raise ValueError(errmsg("Expecting property name", s, end))
end += 1
while True:
key, end = scanstring(s, end, encoding, strict)
# To skip some function call overhead we optimize the fast paths where
# the JSON key separator is ": " or just ":".
if s[end:end + 1] != ':':
end = _w(s, end).end()
if s[end:end + 1] != ':':
raise ValueError(errmsg("Expecting : delimiter", s, end))
end += 1
try:
if s[end] in _ws:
end += 1
if s[end] in _ws:
end = _w(s, end + 1).end()
except IndexError:
pass
try:
value, end = scan_once(s, end)
except StopIteration:
raise ValueError(errmsg("Expecting object", s, end))
pairs[key] = value
try:
nextchar = s[end]
if nextchar in _ws:
end = _w(s, end + 1).end()
nextchar = s[end]
except IndexError:
nextchar = ''
end += 1
if nextchar == '}':
break
elif nextchar != ',':
raise ValueError(errmsg("Expecting , delimiter", s, end - 1))
try:
nextchar = s[end]
if nextchar in _ws:
end += 1
nextchar = s[end]
if nextchar in _ws:
end = _w(s, end + 1).end()
nextchar = s[end]
except IndexError:
nextchar = ''
end += 1
if nextchar != '"':
raise ValueError(errmsg("Expecting property name", s, end - 1))
if object_hook is not None:
pairs = object_hook(pairs)
return pairs, end
def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
values = []
nextchar = s[end:end + 1]
if nextchar in _ws:
end = _w(s, end + 1).end()
nextchar = s[end:end + 1]
# Look-ahead for trivial empty array
if nextchar == ']':
return values, end + 1
_append = values.append
while True:
try:
value, end = scan_once(s, end)
except StopIteration:
raise ValueError(errmsg("Expecting object", s, end))
_append(value)
nextchar = s[end:end + 1]
if nextchar in _ws:
end = _w(s, end + 1).end()
nextchar = s[end:end + 1]
end += 1
if nextchar == ']':
break
elif nextchar != ',':
raise ValueError(errmsg("Expecting , delimiter", s, end))
try:
if s[end] in _ws:
end += 1
if s[end] in _ws:
end = _w(s, end + 1).end()
except IndexError:
pass
return values, end
class JSONDecoder(object):
"""Simple JSON <http://json.org> decoder
Performs the following translations in decoding by default:
+---------------+-------------------+
| JSON | Python |
+===============+===================+
| object | dict |
+---------------+-------------------+
| array | list |
+---------------+-------------------+
| string | unicode |
+---------------+-------------------+
| number (int) | int, long |
+---------------+-------------------+
| number (real) | float |
+---------------+-------------------+
| true | True |
+---------------+-------------------+
| false | False |
+---------------+-------------------+
| null | None |
+---------------+-------------------+
It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as
their corresponding ``float`` values, which is outside the JSON spec.
"""
def __init__(self, encoding=None, object_hook=None, parse_float=None,
parse_int=None, parse_constant=None, strict=True):
"""``encoding`` determines the encoding used to interpret any ``str``
objects decoded by this instance (utf-8 by default). It has no
effect when decoding ``unicode`` objects.
Note that currently only encodings that are a superset of ASCII work,
strings of other encodings should be passed in as ``unicode``.
``object_hook``, if specified, will be called with the result
of every JSON object decoded and its return value will be used in
place of the given ``dict``. This can be used to provide custom
deserializations (e.g. to support JSON-RPC class hinting).
``parse_float``, if specified, will be called with the string
of every JSON float to be decoded. By default this is equivalent to
float(num_str). This can be used to use another datatype or parser
for JSON floats (e.g. decimal.Decimal).
``parse_int``, if specified, will be called with the string
of every JSON int to be decoded. By default this is equivalent to
int(num_str). This can be used to use another datatype or parser
for JSON integers (e.g. float).
``parse_constant``, if specified, will be called with one of the
following strings: -Infinity, Infinity, NaN.
This can be used to raise an exception if invalid JSON numbers
are encountered.
"""
self.encoding = encoding
self.object_hook = object_hook
self.parse_float = parse_float or float
self.parse_int = parse_int or int
self.parse_constant = parse_constant or _CONSTANTS.__getitem__
self.strict = strict
self.parse_object = JSONObject
self.parse_array = JSONArray
self.parse_string = scanstring
self.scan_once = make_scanner(self)
def decode(self, s, _w=WHITESPACE.match):
"""Return the Python representation of ``s`` (a ``str`` or ``unicode``
instance containing a JSON document)
"""
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
end = _w(s, end).end()
if end != len(s):
raise ValueError(errmsg("Extra data", s, end, len(s)))
return obj
def raw_decode(self, s, idx=0):
"""Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning
with a JSON document) and return a 2-tuple of the Python
representation and the index in ``s`` where the document ended.
This can be used to decode a JSON document from a string that may
have extraneous data at the end.
"""
try:
obj, end = self.scan_once(s, idx)
except StopIteration:
raise ValueError("No JSON object could be decoded")
return obj, end

@ -1,440 +0,0 @@
"""Implementation of JSONEncoder
"""
import re
try:
from simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii
except ImportError:
c_encode_basestring_ascii = None
try:
from simplejson._speedups import make_encoder as c_make_encoder
except ImportError:
c_make_encoder = None
ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]')
ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
HAS_UTF8 = re.compile(r'[\x80-\xff]')
ESCAPE_DCT = {
'\\': '\\\\',
'"': '\\"',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
}
for i in range(0x20):
#ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i))
ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,))
# Assume this produces an infinity on all machines (probably not guaranteed)
INFINITY = float('1e66666')
FLOAT_REPR = repr
def encode_basestring(s):
"""Return a JSON representation of a Python string
"""
def replace(match):
return ESCAPE_DCT[match.group(0)]
return '"' + ESCAPE.sub(replace, s) + '"'
def py_encode_basestring_ascii(s):
"""Return an ASCII-only JSON representation of a Python string
"""
if isinstance(s, str) and HAS_UTF8.search(s) is not None:
s = s.decode('utf-8')
def replace(match):
s = match.group(0)
try:
return ESCAPE_DCT[s]
except KeyError:
n = ord(s)
if n < 0x10000:
#return '\\u{0:04x}'.format(n)
return '\\u%04x' % (n,)
else:
# surrogate pair
n -= 0x10000
s1 = 0xd800 | ((n >> 10) & 0x3ff)
s2 = 0xdc00 | (n & 0x3ff)
#return '\\u{0:04x}\\u{1:04x}'.format(s1, s2)
return '\\u%04x\\u%04x' % (s1, s2)
return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"'
encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii
class JSONEncoder(object):
"""Extensible JSON <http://json.org> encoder for Python data structures.
Supports the following objects and types by default:
+-------------------+---------------+
| Python | JSON |
+===================+===============+
| dict | object |
+-------------------+---------------+
| list, tuple | array |
+-------------------+---------------+
| str, unicode | string |
+-------------------+---------------+
| int, long, float | number |
+-------------------+---------------+
| True | true |
+-------------------+---------------+
| False | false |
+-------------------+---------------+
| None | null |
+-------------------+---------------+
To extend this to recognize other objects, subclass and implement a
``.default()`` method with another method that returns a serializable
object for ``o`` if possible, otherwise it should call the superclass
implementation (to raise ``TypeError``).
"""
item_separator = ', '
key_separator = ': '
def __init__(self, skipkeys=False, ensure_ascii=True,
check_circular=True, allow_nan=True, sort_keys=False,
indent=None, separators=None, encoding='utf-8', default=None):
"""Constructor for JSONEncoder, with sensible defaults.
If skipkeys is false, then it is a TypeError to attempt
encoding of keys that are not str, int, long, float or None. If
skipkeys is True, such items are simply skipped.
If ensure_ascii is true, the output is guaranteed to be str
objects with all incoming unicode characters escaped. If
ensure_ascii is false, the output will be unicode object.
If check_circular is true, then lists, dicts, and custom encoded
objects will be checked for circular references during encoding to
prevent an infinite recursion (which would cause an OverflowError).
Otherwise, no such check takes place.
If allow_nan is true, then NaN, Infinity, and -Infinity will be
encoded as such. This behavior is not JSON specification compliant,
but is consistent with most JavaScript based encoders and decoders.
Otherwise, it will be a ValueError to encode such floats.
If sort_keys is true, then the output of dictionaries will be
sorted by key; this is useful for regression tests to ensure
that JSON serializations can be compared on a day-to-day basis.
If indent is a non-negative integer, then JSON array
elements and object members will be pretty-printed with that
indent level. An indent level of 0 will only insert newlines.
None is the most compact representation.
If specified, separators should be a (item_separator, key_separator)
tuple. The default is (', ', ': '). To get the most compact JSON
representation you should specify (',', ':') to eliminate whitespace.
If specified, default is a function that gets called for objects
that can't otherwise be serialized. It should return a JSON encodable
version of the object or raise a ``TypeError``.
If encoding is not None, then all input strings will be
transformed into unicode using that encoding prior to JSON-encoding.
The default is UTF-8.
"""
self.skipkeys = skipkeys
self.ensure_ascii = ensure_ascii
self.check_circular = check_circular
self.allow_nan = allow_nan
self.sort_keys = sort_keys
self.indent = indent
if separators is not None:
self.item_separator, self.key_separator = separators
if default is not None:
self.default = default
self.encoding = encoding
def default(self, o):
"""Implement this method in a subclass such that it returns
a serializable object for ``o``, or calls the base implementation
(to raise a ``TypeError``).
For example, to support arbitrary iterators, you could
implement default like this::
def default(self, o):
try:
iterable = iter(o)
except TypeError:
pass
else:
return list(iterable)
return JSONEncoder.default(self, o)
"""
raise TypeError(repr(o) + " is not JSON serializable")
def encode(self, o):
"""Return a JSON string representation of a Python data structure.
>>> JSONEncoder().encode({"foo": ["bar", "baz"]})
'{"foo": ["bar", "baz"]}'
"""
# This is for extremely simple cases and benchmarks.
if isinstance(o, basestring):
if isinstance(o, str):
_encoding = self.encoding
if (_encoding is not None
and not (_encoding == 'utf-8')):
o = o.decode(_encoding)
if self.ensure_ascii:
return encode_basestring_ascii(o)
else:
return encode_basestring(o)
# This doesn't pass the iterator directly to ''.join() because the
# exceptions aren't as detailed. The list call should be roughly
# equivalent to the PySequence_Fast that ''.join() would do.
chunks = self.iterencode(o, _one_shot=True)
if not isinstance(chunks, (list, tuple)):
chunks = list(chunks)
return ''.join(chunks)
def iterencode(self, o, _one_shot=False):
"""Encode the given object and yield each string
representation as available.
For example::
for chunk in JSONEncoder().iterencode(bigobject):
mysocket.write(chunk)
"""
if self.check_circular:
markers = {}
else:
markers = None
if self.ensure_ascii:
_encoder = encode_basestring_ascii
else:
_encoder = encode_basestring
if self.encoding != 'utf-8':
def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding):
if isinstance(o, str):
o = o.decode(_encoding)
return _orig_encoder(o)
def floatstr(o, allow_nan=self.allow_nan, _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY):
# Check for specials. Note that this type of test is processor- and/or
# platform-specific, so do tests which don't depend on the internals.
if o != o:
text = 'NaN'
elif o == _inf:
text = 'Infinity'
elif o == _neginf:
text = '-Infinity'
else:
return _repr(o)
if not allow_nan:
raise ValueError(
"Out of range float values are not JSON compliant: " +
repr(o))
return text
if _one_shot and c_make_encoder is not None and not self.indent and not self.sort_keys:
_iterencode = c_make_encoder(
markers, self.default, _encoder, self.indent,
self.key_separator, self.item_separator, self.sort_keys,
self.skipkeys, self.allow_nan)
else:
_iterencode = _make_iterencode(
markers, self.default, _encoder, self.indent, floatstr,
self.key_separator, self.item_separator, self.sort_keys,
self.skipkeys, _one_shot)
return _iterencode(o, 0)
def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot,
## HACK: hand-optimized bytecode; turn globals into locals
False=False,
True=True,
ValueError=ValueError,
basestring=basestring,
dict=dict,
float=float,
id=id,
int=int,
isinstance=isinstance,
list=list,
long=long,
str=str,
tuple=tuple,
):
def _iterencode_list(lst, _current_indent_level):
if not lst:
yield '[]'
return
if markers is not None:
markerid = id(lst)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = lst
buf = '['
if _indent is not None:
_current_indent_level += 1
newline_indent = '\n' + (' ' * (_indent * _current_indent_level))
separator = _item_separator + newline_indent
buf += newline_indent
else:
newline_indent = None
separator = _item_separator
first = True
for value in lst:
if first:
first = False
else:
buf = separator
if isinstance(value, basestring):
yield buf + _encoder(value)
elif value is None:
yield buf + 'null'
elif value is True:
yield buf + 'true'
elif value is False:
yield buf + 'false'
elif isinstance(value, (int, long)):
yield buf + str(value)
elif isinstance(value, float):
yield buf + _floatstr(value)
else:
yield buf
if isinstance(value, (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, dict):
chunks = _iterencode_dict(value, _current_indent_level)
else:
chunks = _iterencode(value, _current_indent_level)
for chunk in chunks:
yield chunk
if newline_indent is not None:
_current_indent_level -= 1
yield '\n' + (' ' * (_indent * _current_indent_level))
yield ']'
if markers is not None:
del markers[markerid]
def _iterencode_dict(dct, _current_indent_level):
if not dct:
yield '{}'
return
if markers is not None:
markerid = id(dct)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = dct
yield '{'
if _indent is not None:
_current_indent_level += 1
newline_indent = '\n' + (' ' * (_indent * _current_indent_level))
item_separator = _item_separator + newline_indent
yield newline_indent
else:
newline_indent = None
item_separator = _item_separator
first = True
if _sort_keys:
items = dct.items()
items.sort(key=lambda kv: kv[0])
else:
items = dct.iteritems()
for key, value in items:
if isinstance(key, basestring):
pass
# JavaScript is weakly typed for these, so it makes sense to
# also allow them. Many encoders seem to do something like this.
elif isinstance(key, float):
key = _floatstr(key)
elif key is True:
key = 'true'
elif key is False:
key = 'false'
elif key is None:
key = 'null'
elif isinstance(key, (int, long)):
key = str(key)
elif _skipkeys:
continue
else:
raise TypeError("key " + repr(key) + " is not a string")
if first:
first = False
else:
yield item_separator
yield _encoder(key)
yield _key_separator
if isinstance(value, basestring):
yield _encoder(value)
elif value is None:
yield 'null'
elif value is True:
yield 'true'
elif value is False:
yield 'false'
elif isinstance(value, (int, long)):
yield str(value)
elif isinstance(value, float):
yield _floatstr(value)
else:
if isinstance(value, (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, dict):
chunks = _iterencode_dict(value, _current_indent_level)
else:
chunks = _iterencode(value, _current_indent_level)
for chunk in chunks:
yield chunk
if newline_indent is not None:
_current_indent_level -= 1
yield '\n' + (' ' * (_indent * _current_indent_level))
yield '}'
if markers is not None:
del markers[markerid]
def _iterencode(o, _current_indent_level):
if isinstance(o, basestring):
yield _encoder(o)
elif o is None:
yield 'null'
elif o is True:
yield 'true'
elif o is False:
yield 'false'
elif isinstance(o, (int, long)):
yield str(o)
elif isinstance(o, float):
yield _floatstr(o)
elif isinstance(o, (list, tuple)):
for chunk in _iterencode_list(o, _current_indent_level):
yield chunk
elif isinstance(o, dict):
for chunk in _iterencode_dict(o, _current_indent_level):
yield chunk
else:
if markers is not None:
markerid = id(o)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = o
o = _default(o)
for chunk in _iterencode(o, _current_indent_level):
yield chunk
if markers is not None:
del markers[markerid]
return _iterencode

@ -1,65 +0,0 @@
"""JSON token scanner
"""
import re
try:
from simplejson._speedups import make_scanner as c_make_scanner
except ImportError:
c_make_scanner = None
__all__ = ['make_scanner']
NUMBER_RE = re.compile(
r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?',
(re.VERBOSE | re.MULTILINE | re.DOTALL))
def py_make_scanner(context):
parse_object = context.parse_object
parse_array = context.parse_array
parse_string = context.parse_string
match_number = NUMBER_RE.match
encoding = context.encoding
strict = context.strict
parse_float = context.parse_float
parse_int = context.parse_int
parse_constant = context.parse_constant
object_hook = context.object_hook
def _scan_once(string, idx):
try:
nextchar = string[idx]
except IndexError:
raise StopIteration
if nextchar == '"':
return parse_string(string, idx + 1, encoding, strict)
elif nextchar == '{':
return parse_object((string, idx + 1), encoding, strict, _scan_once, object_hook)
elif nextchar == '[':
return parse_array((string, idx + 1), _scan_once)
elif nextchar == 'n' and string[idx:idx + 4] == 'null':
return None, idx + 4
elif nextchar == 't' and string[idx:idx + 4] == 'true':
return True, idx + 4
elif nextchar == 'f' and string[idx:idx + 5] == 'false':
return False, idx + 5
m = match_number(string, idx)
if m is not None:
integer, frac, exp = m.groups()
if frac or exp:
res = parse_float(integer + (frac or '') + (exp or ''))
else:
res = parse_int(integer)
return res, m.end()
elif nextchar == 'N' and string[idx:idx + 3] == 'NaN':
return parse_constant('NaN'), idx + 3
elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity':
return parse_constant('Infinity'), idx + 8
elif nextchar == '-' and string[idx:idx + 9] == '-Infinity':
return parse_constant('-Infinity'), idx + 9
else:
raise StopIteration
return _scan_once
make_scanner = c_make_scanner or py_make_scanner

@ -26,8 +26,9 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__metaclass__ = type
import errno
import logging
@ -40,18 +41,16 @@ import time
import ansible.constants as C
import ansible.errors
import ansible.plugins.connection
import ansible.utils.shlex
import mitogen.core
import mitogen.fork
import mitogen.utils
import mitogen.parent
import mitogen.service
import ansible_mitogen.mixins
import ansible_mitogen.parsing
import ansible_mitogen.process
import ansible_mitogen.services
import ansible_mitogen.target
import ansible_mitogen.transport_config
import ansible_mitogen.utils.unsafe
LOG = logging.getLogger(__name__)
@ -120,7 +119,7 @@ def _connect_ssh(spec):
"""
Return ContextService arguments for an SSH connection.
"""
if C.HOST_KEY_CHECKING:
if spec.host_key_checking():
check_host_keys = 'enforce'
else:
check_host_keys = 'ignore'
@ -146,9 +145,9 @@ def _connect_ssh(spec):
'identity_file': private_key_file,
'identities_only': False,
'ssh_path': spec.ssh_executable(),
'connect_timeout': spec.ansible_ssh_timeout(),
'connect_timeout': spec.timeout(),
'ssh_args': spec.ssh_args(),
'ssh_debug_level': spec.mitogen_ssh_debug_level(),
'ssh_debug_level': spec.verbosity(),
'remote_name': get_remote_name(spec),
'keepalive_count': (
spec.mitogen_ssh_keepalive_count() or 10
@ -159,6 +158,7 @@ def _connect_ssh(spec):
}
}
def _connect_buildah(spec):
"""
Return ContextService arguments for a Buildah connection.
@ -169,11 +169,12 @@ def _connect_buildah(spec):
'username': spec.remote_user(),
'container': spec.remote_addr(),
'python_path': spec.python_path(),
'connect_timeout': spec.ansible_ssh_timeout() or spec.timeout(),
'connect_timeout': spec.timeout(),
'remote_name': get_remote_name(spec),
}
}
def _connect_docker(spec):
"""
Return ContextService arguments for a Docker connection.
@ -184,7 +185,7 @@ def _connect_docker(spec):
'username': spec.remote_user(),
'container': spec.remote_addr(),
'python_path': spec.python_path(rediscover_python=True),
'connect_timeout': spec.ansible_ssh_timeout() or spec.timeout(),
'connect_timeout': spec.timeout(),
'remote_name': get_remote_name(spec),
}
}
@ -199,7 +200,7 @@ def _connect_kubectl(spec):
'kwargs': {
'pod': spec.remote_addr(),
'python_path': spec.python_path(),
'connect_timeout': spec.ansible_ssh_timeout() or spec.timeout(),
'connect_timeout': spec.timeout(),
'kubectl_path': spec.mitogen_kubectl_path(),
'kubectl_args': spec.extra_args(),
'remote_name': get_remote_name(spec),
@ -217,7 +218,7 @@ def _connect_jail(spec):
'username': spec.remote_user(),
'container': spec.remote_addr(),
'python_path': spec.python_path(),
'connect_timeout': spec.ansible_ssh_timeout() or spec.timeout(),
'connect_timeout': spec.timeout(),
'remote_name': get_remote_name(spec),
}
}
@ -233,7 +234,7 @@ def _connect_lxc(spec):
'container': spec.remote_addr(),
'python_path': spec.python_path(),
'lxc_attach_path': spec.mitogen_lxc_attach_path(),
'connect_timeout': spec.ansible_ssh_timeout() or spec.timeout(),
'connect_timeout': spec.timeout(),
'remote_name': get_remote_name(spec),
}
}
@ -249,7 +250,7 @@ def _connect_lxd(spec):
'container': spec.remote_addr(),
'python_path': spec.python_path(),
'lxc_path': spec.mitogen_lxc_path(),
'connect_timeout': spec.ansible_ssh_timeout() or spec.timeout(),
'connect_timeout': spec.timeout(),
'remote_name': get_remote_name(spec),
}
}
@ -262,6 +263,22 @@ def _connect_machinectl(spec):
return _connect_setns(spec, kind='machinectl')
def _connect_podman(spec):
"""
Return ContextService arguments for a Docker connection.
"""
return {
'method': 'podman',
'kwargs': {
'username': spec.remote_user(),
'container': spec.remote_addr(),
'python_path': spec.python_path(rediscover_python=True),
'connect_timeout': spec.timeout(),
'remote_name': get_remote_name(spec),
}
}
def _connect_setns(spec, kind=None):
"""
Return ContextService arguments for a mitogen_setns connection.
@ -392,6 +409,7 @@ def _connect_mitogen_doas(spec):
#: generating ContextService keyword arguments matching a connection
#: specification.
CONNECTION_METHOD = {
# Ansible connection plugins
'buildah': _connect_buildah,
'docker': _connect_docker,
'kubectl': _connect_kubectl,
@ -400,12 +418,18 @@ CONNECTION_METHOD = {
'lxc': _connect_lxc,
'lxd': _connect_lxd,
'machinectl': _connect_machinectl,
'podman': _connect_podman,
'setns': _connect_setns,
'ssh': _connect_ssh,
'smart': _connect_ssh, # issue #548.
# Ansible become plugins
'community.general.doas': _connect_doas,
'su': _connect_su,
'sudo': _connect_sudo,
'doas': _connect_doas,
# Mitogen specific methods
'mitogen_su': _connect_mitogen_su,
'mitogen_sudo': _connect_mitogen_sudo,
'mitogen_doas': _connect_mitogen_doas,
@ -469,6 +493,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
login_context = None
#: Only sudo, su, and doas are supported for now.
# Ansible ConnectionBase attribute, removed in Ansible >= 2.8
become_methods = ['sudo', 'su', 'doas']
#: Dict containing init_child() return value as recorded at startup by
@ -506,15 +531,6 @@ class Connection(ansible.plugins.connection.ConnectionBase):
# set by `_get_task_vars()` for interpreter discovery
_action = None
def __del__(self):
"""
Ansible cannot be trusted to always call close() e.g. the synchronize
action constructs a local connection like this. So provide a destructor
in the hopes of catching these cases.
"""
# https://github.com/dw/mitogen/issues/140
self.close()
def on_action_run(self, task_vars, delegate_to_hostname, loader_basedir):
"""
Invoked by ActionModuleMixin to indicate a new task is about to start
@ -669,6 +685,9 @@ class Connection(ansible.plugins.connection.ConnectionBase):
@property
def connected(self):
"""
Ansible connection plugin property. Used by ansible-connection command.
"""
return self.context is not None
def _spec_from_via(self, proxied_inventory_name, via_spec):
@ -754,7 +773,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
C.BECOME_ALLOW_SAME_USER):
stack += (CONNECTION_METHOD[spec.become_method()](spec),)
return stack
return ansible_mitogen.utils.unsafe.cast(stack)
def _build_stack(self):
"""
@ -787,7 +806,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
call_context=self.binding.get_service_context(),
service_name='ansible_mitogen.services.ContextService',
method_name='get',
stack=mitogen.utils.cast(list(stack)),
stack=ansible_mitogen.utils.unsafe.cast(list(stack)),
)
except mitogen.core.CallError:
LOG.warning('Connection failed; stack configuration was:\n%s',
@ -801,7 +820,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self.context = dct['context']
self.chain = CallChain(self, self.context, pipelined=True)
if self._play_context.become:
if self.become:
self.login_context = dct['via']
else:
self.login_context = self.context
@ -827,14 +846,18 @@ class Connection(ansible.plugins.connection.ConnectionBase):
the _connect_*() service calls defined above to cause the master
process to establish the real connection on our behalf, or return a
reference to the existing one.
Ansible connection plugin method.
"""
# In some Ansible connection plugins this method returns self.
# However nothing I've found uses it, it's not even assigned.
if self.connected:
return
inventory_name, stack = self._build_stack()
worker_model = ansible_mitogen.process.get_worker_model()
self.binding = worker_model.get_binding(
mitogen.utils.cast(inventory_name)
ansible_mitogen.utils.unsafe.cast(inventory_name)
)
self._connect_stack(stack)
@ -865,12 +888,37 @@ class Connection(ansible.plugins.connection.ConnectionBase):
Arrange for the mitogen.master.Router running in the worker to
gracefully shut down, and wait for shutdown to complete. Safe to call
multiple times.
Ansible connection plugin method.
"""
self._put_connection()
if self.binding:
self.binding.close()
self.binding = None
def _mitogen_var_options(self, templar):
# Workaround for https://github.com/ansible/ansible/issues/84238
var_names = C.config.get_plugin_vars('connection', self._load_name)
variables = templar.available_variables
var_options = {
var_name: templar.template(variables[var_name])
for var_name in var_names
if var_name in variables
}
if self.allow_extras:
extras_var_prefix = 'ansible_%s_' % self.extras_prefix
var_options['_extras'] = {
var_name: templar.template(variables[var_name])
for var_name in variables
if var_name not in var_options
and var_name.startswith(extras_var_prefix)
}
else:
var_options['_extras'] = {}
return var_options
reset_compat_msg = (
'Mitogen only supports "reset_connection" on Ansible 2.5.6 or later'
)
@ -881,6 +929,8 @@ class Connection(ansible.plugins.connection.ConnectionBase):
any local state we hold for the connection, returns the Connection to
the 'disconnected' state, and informs ContextService the connection is
bad somehow, and should be shut down and discarded.
Ansible connection plugin method.
"""
if self._play_context.remote_addr is None:
# <2.5.6 incorrectly populate PlayContext for reset_connection
@ -889,23 +939,44 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self.reset_compat_msg
)
# Strategy's _execute_meta doesn't have an action obj but we'll need one for
# running interpreter_discovery
# will create a new temporary action obj for this purpose
self._action = ansible_mitogen.mixins.ActionModuleMixin(
task=0,
connection=self,
play_context=self._play_context,
loader=0,
templar=0,
shared_loader_obj=0
)
# Handle templated connection variables during `meta: reset_connection`.
# Many bugs/implementation details of Mitogen & Ansible collide here.
# See #1079, #1096, #1132, ansible/ansible#84238, ...
try:
task, templar = self._play_context.vars.pop(
'_mitogen.smuggled.reset_connection',
)
except KeyError:
self._action_monkey_patched_by_mitogen = False
else:
# LOG.info('%r.reset(): remote_addr=%r', self, self._play_context.remote_addr)
# ansible.plugins.strategy.StrategyBase._execute_meta() doesn't
# have an action object, which we need for interpreter_discovery.
# Create a temporary action object for this purpose.
self._action = ansible_mitogen.mixins.ActionModuleMixin(
task=task,
connection=self,
play_context=self._play_context,
loader=templar._loader,
templar=templar,
shared_loader_obj=0,
)
self._action_monkey_patched_by_mitogen = True
# Workaround for https://github.com/ansible/ansible/issues/84238
self.set_options(
task_keys=task.dump_attrs(),
var_options=self._mitogen_var_options(templar),
)
del task
del templar
# Clear out state in case we were ever connected.
self.close()
inventory_name, stack = self._build_stack()
if self._play_context.become:
if self.become:
stack = stack[:-1]
worker_model = ansible_mitogen.process.get_worker_model()
@ -915,11 +986,16 @@ class Connection(ansible.plugins.connection.ConnectionBase):
call_context=binding.get_service_context(),
service_name='ansible_mitogen.services.ContextService',
method_name='reset',
stack=mitogen.utils.cast(list(stack)),
stack=ansible_mitogen.utils.unsafe.cast(list(stack)),
)
finally:
binding.close()
# Cleanup any monkey patching we did for `meta: reset_connection`
if self._action_monkey_patched_by_mitogen:
del self._action
del self._action_monkey_patched_by_mitogen
# Compatibility with Ansible 2.4 wait_for_connection plug-in.
_reset = reset
@ -987,12 +1063,14 @@ class Connection(ansible.plugins.connection.ConnectionBase):
Data to supply on ``stdin`` of the process.
:returns:
(return code, stdout bytes, stderr bytes)
Ansible connection plugin method.
"""
emulate_tty = (not in_data and sudoable)
rc, stdout, stderr = self.get_chain().call(
ansible_mitogen.target.exec_command,
cmd=mitogen.utils.cast(cmd),
in_data=mitogen.utils.cast(in_data),
cmd=ansible_mitogen.utils.unsafe.cast(cmd),
in_data=ansible_mitogen.utils.unsafe.cast(in_data),
chdir=mitogen_chdir or self.get_default_cwd(),
emulate_tty=emulate_tty,
)
@ -1012,12 +1090,14 @@ class Connection(ansible.plugins.connection.ConnectionBase):
Remote filesystem path to read.
:param str out_path:
Local filesystem path to write.
Ansible connection plugin method.
"""
self._connect()
ansible_mitogen.target.transfer_file(
context=self.context,
# in_path may be AnsibleUnicode
in_path=mitogen.utils.cast(in_path),
in_path=ansible_mitogen.utils.unsafe.cast(in_path),
out_path=out_path
)
@ -1035,7 +1115,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
"""
self.get_chain().call_no_reply(
ansible_mitogen.target.write_path,
mitogen.utils.cast(out_path),
ansible_mitogen.utils.unsafe.cast(out_path),
mitogen.core.Blob(data),
mode=mode,
utimes=utimes,
@ -1061,6 +1141,8 @@ class Connection(ansible.plugins.connection.ConnectionBase):
Local filesystem path to read.
:param str out_path:
Remote filesystem path to write.
Ansible connection plugin method.
"""
try:
st = os.stat(in_path)
@ -1081,7 +1163,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
s = fp.read(self.SMALL_FILE_LIMIT + 1)
finally:
fp.close()
except OSError:
except OSError as e:
self._throw_io_error(e, in_path)
raise
@ -1095,7 +1177,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
call_context=self.binding.get_service_context(),
service_name='mitogen.service.FileService',
method_name='register',
path=mitogen.utils.cast(in_path)
path=ansible_mitogen.utils.unsafe.cast(in_path)
)
# For now this must remain synchronous, as the action plug-in may have
@ -1105,6 +1187,6 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self.get_chain().call(
ansible_mitogen.target.transfer_file,
context=self.binding.get_child_service_context(),
in_path=in_path,
out_path=out_path
in_path=ansible_mitogen.utils.unsafe.cast(in_path),
out_path=ansible_mitogen.utils.unsafe.cast(out_path)
)

@ -30,12 +30,16 @@
Stable names for PluginLoader instances across Ansible versions.
"""
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import ansible.errors
import ansible_mitogen.utils
__all__ = [
'action_loader',
'become_loader',
'connection_loader',
'module_loader',
'module_utils_loader',
@ -45,18 +49,6 @@ __all__ = [
ANSIBLE_VERSION_MIN = (2, 10)
ANSIBLE_VERSION_MAX = (2, 12)
NEW_VERSION_MSG = (
"Your Ansible version (%s) is too recent. The most recent version\n"
"supported by Mitogen for Ansible is %s.x. Please check the Mitogen\n"
"release notes to see if a new version is available, otherwise\n"
"subscribe to the corresponding GitHub issue to be notified when\n"
"support becomes available.\n"
"\n"
" https://mitogen.rtfd.io/en/latest/changelog.html\n"
" https://github.com/mitogen-hq/mitogen/issues/\n"
)
OLD_VERSION_MSG = (
"Your version of Ansible (%s) is too old. The oldest version supported by "
"Mitogen for Ansible is %s."
@ -74,11 +66,6 @@ def assert_supported_release():
OLD_VERSION_MSG % (v, ANSIBLE_VERSION_MIN)
)
if v[:2] > ANSIBLE_VERSION_MAX:
raise ansible.errors.AnsibleError(
NEW_VERSION_MSG % (v, ANSIBLE_VERSION_MAX)
)
# this is the first file our strategy plugins import, so we need to check this here
# in prior Ansible versions, connection_loader.get_with_context didn't exist, so if a user
@ -87,6 +74,7 @@ assert_supported_release()
from ansible.plugins.loader import action_loader
from ansible.plugins.loader import become_loader
from ansible.plugins.loader import connection_loader
from ansible.plugins.loader import module_loader
from ansible.plugins.loader import module_utils_loader
@ -94,5 +82,5 @@ from ansible.plugins.loader import shell_loader
from ansible.plugins.loader import strategy_loader
# These are original, unwrapped implementations
action_loader__get = action_loader.get
connection_loader__get = connection_loader.get_with_context
action_loader__get_with_context = action_loader.get_with_context
connection_loader__get_with_context = connection_loader.get_with_context

@ -26,19 +26,18 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import logging
import os
import mitogen.core
import ansible.utils.display
import mitogen.utils
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
display = ansible.utils.display.Display()
#: The process name set via :func:`set_process_name`.
_process_name = None

@ -26,52 +26,32 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
import logging
import os
import pwd
import random
import traceback
try:
from shlex import quote as shlex_quote
except ImportError:
from pipes import quote as shlex_quote
from ansible.module_utils._text import to_bytes
from ansible.parsing.utils.jsonify import jsonify
import ansible
import ansible.constants
import ansible.plugins
import ansible.plugins.action
import ansible.utils.unsafe_proxy
import ansible.vars.clean
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible.module_utils.six.moves import shlex_quote
import mitogen.core
import mitogen.select
import mitogen.utils
import ansible_mitogen.connection
import ansible_mitogen.planner
import ansible_mitogen.target
import ansible_mitogen.utils
from ansible.module_utils._text import to_text
try:
from ansible.utils.unsafe_proxy import wrap_var
except ImportError:
from ansible.vars.unsafe_proxy import wrap_var
try:
# ansible 2.8 moved remove_internal_keys to the clean module
from ansible.vars.clean import remove_internal_keys
except ImportError:
try:
from ansible.vars.manager import remove_internal_keys
except ImportError:
# ansible 2.3.3 has remove_internal_keys as a protected func on the action class
# we'll fallback to calling self._remove_internal_keys in this case
remove_internal_keys = lambda a: "Not found"
import ansible_mitogen.utils.unsafe
LOG = logging.getLogger(__name__)
@ -123,13 +103,10 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
# required for python interpreter discovery
connection.templar = self._templar
self._finding_python_interpreter = False
self._rediscovered_python = False
# redeclaring interpreter discovery vars here in case running ansible < 2.8.0
self._discovered_interpreter_key = None
self._discovered_interpreter = False
self._discovery_deprecation_warnings = []
self._discovery_warnings = []
self._mitogen_discovering_interpreter = False
self._mitogen_interpreter_candidate = None
self._mitogen_rediscovered_interpreter = False
def run(self, tmp=None, task_vars=None):
"""
@ -185,7 +162,7 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
LOG.debug('_remote_file_exists(%r)', path)
return self._connection.get_chain().call(
ansible_mitogen.target.file_exists,
mitogen.utils.cast(path)
ansible_mitogen.utils.unsafe.cast(path)
)
def _configure_module(self, module_name, module_args, task_vars=None):
@ -242,8 +219,13 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
Used by the base _execute_module(), and in <2.4 also by the template
action module, and probably others.
"""
if data is None and ansible_mitogen.utils.ansible_version[:2] <= (2, 18):
data = '{}'
if isinstance(data, dict):
data = jsonify(data)
try:
data = json.dumps(data, ensure_ascii=False)
except UnicodeDecodeError:
data = json.dumps(data)
if not isinstance(data, bytes):
data = to_bytes(data, errors='surrogate_or_strict')
@ -278,7 +260,9 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
paths, mode, sudoable)
return self.fake_shell(lambda: mitogen.select.Select.all(
self._connection.get_chain().call_async(
ansible_mitogen.target.set_file_mode, path, mode
ansible_mitogen.target.set_file_mode,
ansible_mitogen.utils.unsafe.cast(path),
mode,
)
for path in paths
))
@ -312,7 +296,7 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
if not path.startswith('~'):
# /home/foo -> /home/foo
return path
if sudoable or not self._play_context.become:
if sudoable or not self._connection.become:
if path == '~':
# ~ -> /home/dmw
return self._connection.homedir
@ -322,7 +306,7 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
# ~root/.ansible -> /root/.ansible
return self._connection.get_chain(use_login=(not sudoable)).call(
os.path.expanduser,
mitogen.utils.cast(path),
ansible_mitogen.utils.unsafe.cast(path),
)
def get_task_timeout_secs(self):
@ -355,7 +339,9 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
def _execute_module(self, module_name=None, module_args=None, tmp=None,
task_vars=None, persist_files=False,
delete_remote_tmp=True, wrap_async=False):
delete_remote_tmp=True, wrap_async=False,
ignore_unknown_opts=False,
):
"""
Collect up a module's execution environment then use it to invoke
target.run_module() or helpers.run_module_async() in the target
@ -368,7 +354,13 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
if task_vars is None:
task_vars = {}
self._update_module_args(module_name, module_args, task_vars)
if ansible_mitogen.utils.ansible_version[:2] >= (2, 17):
self._update_module_args(
module_name, module_args, task_vars,
ignore_unknown_opts=ignore_unknown_opts,
)
else:
self._update_module_args(module_name, module_args, task_vars)
env = {}
self._compute_environment_string(env)
self._set_temp_file_args(module_args, wrap_async)
@ -385,11 +377,11 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
ansible_mitogen.planner.Invocation(
action=self,
connection=self._connection,
module_name=mitogen.core.to_text(module_name),
module_args=mitogen.utils.cast(module_args),
module_name=ansible_mitogen.utils.unsafe.cast(mitogen.core.to_text(module_name)),
module_args=ansible_mitogen.utils.unsafe.cast(module_args),
task_vars=task_vars,
templar=self._templar,
env=mitogen.utils.cast(env),
env=ansible_mitogen.utils.unsafe.cast(env),
wrap_async=wrap_async,
timeout_secs=self.get_task_timeout_secs(),
)
@ -401,10 +393,7 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
self._remove_tmp_path(tmp)
# prevents things like discovered_interpreter_* or ansible_discovered_interpreter_* from being set
# handle ansible 2.3.3 that has remove_internal_keys in a different place
check = remove_internal_keys(result)
if check == 'Not found':
self._remove_internal_keys(result)
ansible.vars.clean.remove_internal_keys(result)
# taken from _execute_module of ansible 2.8.6
# propagate interpreter discovery results back to the controller
@ -415,20 +404,22 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
# only cache discovered_interpreter if we're not running a rediscovery
# rediscovery happens in places like docker connections that could have different
# python interpreters than the main host
if not self._rediscovered_python:
if not self._mitogen_rediscovered_interpreter:
result['ansible_facts'][self._discovered_interpreter_key] = self._discovered_interpreter
if self._discovery_warnings:
discovery_warnings = getattr(self, '_discovery_warnings', [])
if discovery_warnings:
if result.get('warnings') is None:
result['warnings'] = []
result['warnings'].extend(self._discovery_warnings)
result['warnings'].extend(discovery_warnings)
if self._discovery_deprecation_warnings:
discovery_deprecation_warnings = getattr(self, '_discovery_deprecation_warnings', [])
if discovery_deprecation_warnings:
if result.get('deprecations') is None:
result['deprecations'] = []
result['deprecations'].extend(self._discovery_deprecation_warnings)
result['deprecations'].extend(discovery_deprecation_warnings)
return wrap_var(result)
return ansible.utils.unsafe_proxy.wrap_var(result)
def _postprocess_response(self, result):
"""
@ -445,7 +436,10 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
"stderr": "stderr data"
}
"""
data = self._parse_returned_data(result)
if ansible_mitogen.utils.ansible_version[:2] >= (2, 19):
data = self._parse_returned_data(result, profile='legacy')
else:
data = self._parse_returned_data(result)
# Cutpasted from the base implementation.
if 'stdout' in data and 'stdout_lines' not in data:
@ -475,49 +469,36 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
# calling exec_command until we run into the right python we'll use
# chicken-and-egg issue, mitogen needs a python to run low_level_execute_command
# which is required by Ansible's discover_interpreter function
if self._finding_python_interpreter:
possible_pythons = [
'/usr/bin/python',
'python3',
'python3.7',
'python3.6',
'python3.5',
'python2.7',
'python2.6',
'/usr/libexec/platform-python',
'/usr/bin/python3',
'python'
]
if self._mitogen_discovering_interpreter:
possible_pythons = self._mitogen_interpreter_candidates
else:
# not used, just adding a filler value
possible_pythons = ['python']
def _run_cmd():
return self._connection.exec_command(
cmd=cmd,
in_data=in_data,
sudoable=sudoable,
mitogen_chdir=chdir,
)
for possible_python in possible_pythons:
try:
self._possible_python_interpreter = possible_python
rc, stdout, stderr = _run_cmd()
# TODO: what exception is thrown?
except:
self._mitogen_interpreter_candidate = possible_python
rc, stdout, stderr = self._connection.exec_command(
cmd, in_data, sudoable, mitogen_chdir=chdir,
)
except BaseException as exc:
# we've reached the last python attempted and failed
# TODO: could use enumerate(), need to check which version of python first had it though
if possible_python == 'python':
if possible_python == possible_pythons[-1]:
raise
else:
LOG.debug(
'%r._low_level_execute_command: candidate=%r ignored: %s, %r',
self, possible_python, type(exc), exc,
)
continue
stdout_text = to_text(stdout, errors=encoding_errors)
stderr_text = to_text(stderr, errors=encoding_errors)
return {
'rc': rc,
'stdout': stdout_text,
'stdout_lines': stdout_text.splitlines(),
'stderr': stderr,
'stderr': stderr_text,
'stderr_lines': stderr_text.splitlines(),
}

@ -26,19 +26,34 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__metaclass__ = type
import collections
import imp
import logging
import os
import re
import sys
import mitogen.master
if sys.version_info >= (3, 4):
import importlib.machinery
import importlib.util
else:
import imp
import mitogen.imports
LOG = logging.getLogger(__name__)
PREFIX = 'ansible.module_utils.'
# Analog of `importlib.machinery.ModuleSpec` or `pkgutil.ModuleInfo`.
# name Unqualified name of the module.
# path Filesystem path of the module.
# kind One of the constants in `imp`, as returned in `imp.find_module()`
# parent `ansible_mitogen.module_finder.Module` of parent package (if any).
Module = collections.namedtuple('Module', 'name path kind parent')
@ -118,14 +133,121 @@ def find_relative(parent, name, path=()):
def scan_fromlist(code):
for level, modname_s, fromlist in mitogen.master.scan_code_imports(code):
"""Return an iterator of (level, name) for explicit imports in a code
object.
Not all names identify a module. `from os import name, path` generates
`(0, 'os.name'), (0, 'os.path')`, but `os.name` is usually a string.
>>> src = 'import a; import b.c; from d.e import f; from g import h, i\\n'
>>> code = compile(src, '<str>', 'exec')
>>> list(scan_fromlist(code))
[(0, 'a'), (0, 'b.c'), (0, 'd.e.f'), (0, 'g.h'), (0, 'g.i')]
"""
for level, modname_s, fromlist in mitogen.imports.codeobj_imports(code):
for name in fromlist:
yield level, '%s.%s' % (modname_s, name)
yield level, str('%s.%s' % (modname_s, name))
if not fromlist:
yield level, modname_s
def walk_imports(code, prefix=None):
"""Return an iterator of names for implicit parent imports & explicit
imports in a code object.
If a prefix is provided, then only children of that prefix are included.
Not all names identify a module. `from os import name, path` generates
`'os', 'os.name', 'os.path'`, but `os.name` is usually a string.
>>> source = 'import a; import b; import b.c; from b.d import e, f\\n'
>>> code = compile(source, '<str>', 'exec')
>>> list(walk_imports(code))
['a', 'b', 'b', 'b.c', 'b', 'b.d', 'b.d.e', 'b.d.f']
>>> list(walk_imports(code, prefix='b'))
['b.c', 'b.d', 'b.d.e', 'b.d.f']
"""
if prefix is None:
prefix = ''
pattern = re.compile(r'(^|\.)(\w+)')
start = len(prefix)
for _, name, fromlist in mitogen.imports.codeobj_imports(code):
if not name.startswith(prefix):
continue
for match in pattern.finditer(name, start):
yield name[:match.end()]
for leaf in fromlist:
yield str('%s.%s' % (name, leaf))
def scan(module_name, module_path, search_path):
# type: (str, str, list[str]) -> list[(str, str, bool)]
"""Return a list of (name, path, is_package) for ansible.module_utils
imports used by an Ansible module.
"""
log = LOG.getChild('scan')
log.debug('%r, %r, %r', module_name, module_path, search_path)
if sys.version_info >= (3, 4):
result = _scan_importlib_find_spec(
module_name, module_path, search_path,
)
log.debug('_scan_importlib_find_spec %r', result)
else:
result = _scan_imp_find_module(module_name, module_path, search_path)
log.debug('_scan_imp_find_module %r', result)
return result
def _scan_importlib_find_spec(module_name, module_path, search_path):
# type: (str, str, list[str]) -> list[(str, str, bool)]
module = importlib.machinery.ModuleSpec(
module_name, loader=None, origin=module_path,
)
prefix = importlib.machinery.ModuleSpec(
PREFIX.rstrip('.'), loader=None,
)
prefix.submodule_search_locations = search_path
queue = collections.deque([module])
specs = {prefix.name: prefix}
while queue:
spec = queue.popleft()
if spec.origin is None:
continue
try:
with open(spec.origin, 'rb') as f:
code = compile(f.read(), spec.name, 'exec')
except Exception as exc:
raise ValueError((exc, module, spec, specs))
for name in walk_imports(code, prefix.name):
if name in specs:
continue
parent_name = name.rpartition('.')[0]
parent = specs[parent_name]
if parent is None or not parent.submodule_search_locations:
specs[name] = None
continue
child = importlib.util._find_spec(
name, parent.submodule_search_locations,
)
if child is None or child.origin is None:
specs[name] = None
continue
specs[name] = child
queue.append(child)
del specs[prefix.name]
return sorted(
(spec.name, spec.origin, spec.submodule_search_locations is not None)
for spec in specs.values() if spec is not None
)
def _scan_imp_find_module(module_name, module_path, search_path):
# type: (str, str, list[str]) -> list[(str, str, bool)]
module = Module(module_name, module_path, imp.PY_SOURCE, None)
stack = [module]
seen = set()

@ -26,8 +26,9 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__metaclass__ = type
import mitogen.core

@ -34,8 +34,9 @@ files/modules known missing.
[0] "Ansible Module Architecture", developing_program_flow_modules.html
"""
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__metaclass__ = type
import json
import logging
@ -43,17 +44,18 @@ import os
import random
import re
from ansible.executor import module_common
from ansible.collections.list import list_collection_dirs
import ansible.collections.list
import ansible.errors
import ansible.module_utils
import ansible.release
import ansible.executor.module_common
import mitogen.core
import mitogen.select
import mitogen.service
import ansible_mitogen.loaders
import ansible_mitogen.parsing
import ansible_mitogen.target
import ansible_mitogen.utils.unsafe
LOG = logging.getLogger(__name__)
@ -169,6 +171,7 @@ class Planner(object):
"""
binding = self._inv.connection.get_binding()
kwargs = ansible_mitogen.utils.unsafe.cast(kwargs)
new = dict((mitogen.core.UnicodeType(k), kwargs[k])
for k in kwargs)
new.setdefault('good_temp_dir',
@ -192,7 +195,7 @@ class BinaryPlanner(Planner):
@classmethod
def detect(cls, path, source):
return module_common._is_binary(source)
return ansible.executor.module_common._is_binary(source)
def get_push_files(self):
return [mitogen.core.to_text(self._inv.module_path)]
@ -203,7 +206,7 @@ class BinaryPlanner(Planner):
module=self._inv.module_name,
path=self._inv.module_path,
json_args=json.dumps(self._inv.module_args),
env=self._inv.env,
env=ansible_mitogen.utils.unsafe.cast(self._inv.env),
**kwargs
)
@ -215,12 +218,15 @@ class ScriptPlanner(BinaryPlanner):
"""
def _rewrite_interpreter(self, path):
"""
Given the original interpreter binary extracted from the script's
interpreter line, look up the associated `ansible_*_interpreter`
variable, render it and return it.
Given the interpreter path (from the script's hashbang line), return
the desired interpreter path. This tries, in order
1. Look up & render the `ansible_*_interpreter` variable, if set
2. Look up the `discovered_interpreter_*` fact, if present
3. The unmodified path from the hashbang line.
:param str path:
Absolute UNIX path to original interpreter.
Absolute path to original interpreter (e.g. '/usr/bin/python').
:returns:
Shell fragment prefix used to execute the script via "/bin/sh -c".
@ -228,13 +234,25 @@ class ScriptPlanner(BinaryPlanner):
involved here, the vanilla implementation uses it and that use is
exploited in common playbooks.
"""
key = u'ansible_%s_interpreter' % os.path.basename(path).strip()
interpreter_name = os.path.basename(path).strip()
key = u'ansible_%s_interpreter' % interpreter_name
try:
template = self._inv.task_vars[key]
except KeyError:
return path
pass
else:
configured_interpreter = self._inv.templar.template(template)
return ansible_mitogen.utils.unsafe.cast(configured_interpreter)
return mitogen.utils.cast(self._inv.templar.template(template))
key = u'discovered_interpreter_%s' % interpreter_name
try:
discovered_interpreter = self._inv.task_vars['ansible_facts'][key]
except KeyError:
pass
else:
return ansible_mitogen.utils.unsafe.cast(discovered_interpreter)
return path
def _get_interpreter(self):
path, arg = ansible_mitogen.parsing.parse_hashbang(
@ -249,7 +267,8 @@ class ScriptPlanner(BinaryPlanner):
if arg:
fragment += ' ' + arg
return fragment, path.startswith('python')
is_python = path.startswith('python')
return fragment, is_python
def get_kwargs(self, **kwargs):
interpreter_fragment, is_python = self._get_interpreter()
@ -269,7 +288,7 @@ class JsonArgsPlanner(ScriptPlanner):
@classmethod
def detect(cls, path, source):
return module_common.REPLACER_JSONARGS in source
return ansible.executor.module_common.REPLACER_JSONARGS in source
class WantJsonPlanner(ScriptPlanner):
@ -298,11 +317,11 @@ class NewStylePlanner(ScriptPlanner):
preprocessing the module.
"""
runner_name = 'NewStyleRunner'
MARKER = re.compile(b'from ansible(?:_collections|\.module_utils)\.')
MARKER = re.compile(br'from ansible(?:_collections|\.module_utils)\.')
@classmethod
def detect(cls, path, source):
return cls.MARKER.search(source) != None
return cls.MARKER.search(source) is not None
def _get_interpreter(self):
return None, None
@ -323,6 +342,67 @@ class NewStylePlanner(ScriptPlanner):
'dnf', # issue #280; py-dnf/hawkey need therapy
'firewalld', # issue #570: ansible module_utils caches dbus conn
'ansible.legacy.dnf', # issue #776
'ansible.builtin.dnf', # issue #832
'freeipa.ansible_freeipa.ipaautomember', # issue #1216
'freeipa.ansible_freeipa.ipaautomountkey',
'freeipa.ansible_freeipa.ipaautomountlocation',
'freeipa.ansible_freeipa.ipaautomountmap',
'freeipa.ansible_freeipa.ipacert',
'freeipa.ansible_freeipa.ipaclient_api',
'freeipa.ansible_freeipa.ipaclient_fix_ca',
'freeipa.ansible_freeipa.ipaclient_fstore',
'freeipa.ansible_freeipa.ipaclient_get_otp',
'freeipa.ansible_freeipa.ipaclient_ipa_conf',
'freeipa.ansible_freeipa.ipaclient_join',
'freeipa.ansible_freeipa.ipaclient_set_hostname',
'freeipa.ansible_freeipa.ipaclient_setup_automount',
'freeipa.ansible_freeipa.ipaclient_setup_certmonger',
'freeipa.ansible_freeipa.ipaclient_setup_firefox',
'freeipa.ansible_freeipa.ipaclient_setup_krb5',
'freeipa.ansible_freeipa.ipaclient_setup_nis',
'freeipa.ansible_freeipa.ipaclient_setup_nss',
'freeipa.ansible_freeipa.ipaclient_setup_ntp',
'freeipa.ansible_freeipa.ipaclient_setup_ssh',
'freeipa.ansible_freeipa.ipaclient_setup_sshd',
'freeipa.ansible_freeipa.ipaclient_temp_krb5',
'freeipa.ansible_freeipa.ipaclient_test',
'freeipa.ansible_freeipa.ipaclient_test_keytab',
'freeipa.ansible_freeipa.ipaconfig',
'freeipa.ansible_freeipa.ipadelegation',
'freeipa.ansible_freeipa.ipadnsconfig',
'freeipa.ansible_freeipa.ipadnsforwardzone',
'freeipa.ansible_freeipa.ipadnsrecord',
'freeipa.ansible_freeipa.ipadnszone',
'freeipa.ansible_freeipa.ipagroup',
'freeipa.ansible_freeipa.ipahbacrule',
'freeipa.ansible_freeipa.ipahbacsvc',
'freeipa.ansible_freeipa.ipahbacsvcgroup',
'freeipa.ansible_freeipa.ipahost',
'freeipa.ansible_freeipa.ipahostgroup',
'freeipa.ansible_freeipa.idoverridegroup',
'freeipa.ansible_freeipa.idoverrideuser',
'freeipa.ansible_freeipa.idp',
'freeipa.ansible_freeipa.idrange',
'freeipa.ansible_freeipa.idview',
'freeipa.ansible_freeipa.ipalocation',
'freeipa.ansible_freeipa.ipanetgroup',
'freeipa.ansible_freeipa.ipapermission',
'freeipa.ansible_freeipa.ipaprivilege',
'freeipa.ansible_freeipa.ipapwpolicy',
'freeipa.ansible_freeipa.iparole',
'freeipa.ansible_freeipa.ipaselfservice',
'freeipa.ansible_freeipa.ipaserver',
'freeipa.ansible_freeipa.ipaservice',
'freeipa.ansible_freeipa.ipaservicedelegationrule',
'freeipa.ansible_freeipa.ipaservicedelegationtarget',
'freeipa.ansible_freeipa.ipasudocmd',
'freeipa.ansible_freeipa.ipasudocmdgroup',
'freeipa.ansible_freeipa.ipasudorule',
'freeipa.ansible_freeipa.ipatopologysegment',
'freeipa.ansible_freeipa.ipatopologysuffix',
'freeipa.ansible_freeipa.ipatrust',
'freeipa.ansible_freeipa.ipauser',
'freeipa.ansible_freeipa.ipavault',
])
def should_fork(self):
@ -362,7 +442,7 @@ class NewStylePlanner(ScriptPlanner):
module_name='ansible_module_%s' % (self._inv.module_name,),
module_path=self._inv.module_path,
search_path=self.get_search_path(),
builtin_path=module_common._MODULE_UTILS_PATH,
builtin_path=ansible.executor.module_common._MODULE_UTILS_PATH,
context=self._inv.connection.context,
)
return self._module_map
@ -405,7 +485,7 @@ class ReplacerPlanner(NewStylePlanner):
@classmethod
def detect(cls, path, source):
return module_common.REPLACER in source
return ansible.executor.module_common.REPLACER in source
class OldStylePlanner(ScriptPlanner):
@ -427,12 +507,6 @@ _planners = [
]
try:
_get_ansible_module_fqn = module_common._get_ansible_module_fqn
except AttributeError:
_get_ansible_module_fqn = None
def py_modname_from_path(name, path):
"""
Fetch the logical name of a new-style module as it might appear in
@ -442,11 +516,12 @@ def py_modname_from_path(name, path):
package hierarchy approximated on the target, enabling relative imports
to function correctly. For example, "ansible.modules.system.setup".
"""
if _get_ansible_module_fqn:
try:
return _get_ansible_module_fqn(path)
except ValueError:
pass
try:
return ansible.executor.module_common._get_ansible_module_fqn(path)
except AttributeError:
pass
except ValueError:
pass
return 'ansible.modules.' + name
@ -464,7 +539,7 @@ def read_file(path):
finally:
os.close(fd)
return mitogen.core.b('').join(bits)
return b''.join(bits)
def _propagate_deps(invocation, planner, context):
@ -492,7 +567,7 @@ def _invoke_async_task(invocation, planner):
call_recv = context.call_async(
ansible_mitogen.target.run_module_async,
job_id=job_id,
timeout_secs=invocation.timeout_secs,
timeout_secs=ansible_mitogen.utils.unsafe.cast(invocation.timeout_secs),
started_sender=started_recv.to_sender(),
kwargs=planner.get_kwargs(),
)
@ -528,12 +603,15 @@ def _invoke_isolated_task(invocation, planner):
context.shutdown()
def _get_planner(name, path, source):
def _get_planner(invocation, source):
for klass in _planners:
if klass.detect(path, source):
LOG.debug('%r accepted %r (filename %r)', klass, name, path)
if klass.detect(invocation.module_path, source):
LOG.debug(
'%r accepted %r (filename %r)',
klass, invocation.module_name, invocation.module_path,
)
return klass
LOG.debug('%r rejected %r', klass, name)
LOG.debug('%r rejected %r', klass, invocation.module_name)
raise ansible.errors.AnsibleError(NO_METHOD_MSG + repr(invocation))
@ -558,13 +636,30 @@ def _fix_py35(invocation, module_source):
invocation._overridden_sources[invocation.module_path] = module_source
def _fix_dnf(invocation, module_source):
"""
Handles edge case where dnf ansible module showed failure due to a missing import in the dnf module.
Specifically addresses errors like "Failed loading plugin 'debuginfo-install': module 'dnf' has no attribute 'cli'".
https://github.com/mitogen-hq/mitogen/issues/1143
This issue is resolved by adding 'dnf.cli' to the import statement in the module source.
This works in vanilla Ansible but not in Mitogen otherwise.
"""
if invocation.module_name in {'ansible.builtin.dnf', 'ansible.legacy.dnf', 'dnf'} and \
invocation.module_path not in invocation._overridden_sources:
module_source = module_source.replace(
b"import dnf\n",
b"import dnf, dnf.cli\n"
)
invocation._overridden_sources[invocation.module_path] = module_source
def _load_collections(invocation):
"""
Special loader that ensures that `ansible_collections` exist as a module path for import
Goes through all collection path possibilities and stores paths to installed collections
Stores them on the current invocation to later be passed to the master service
"""
for collection_path in list_collection_dirs():
for collection_path in ansible.collections.list.list_collection_dirs():
invocation._extra_sys_paths.add(collection_path.decode('utf-8'))
@ -595,9 +690,9 @@ def invoke(invocation):
module_source = invocation.get_module_source()
_fix_py35(invocation, module_source)
_fix_dnf(invocation, module_source)
_planner_by_path[invocation.module_path] = _get_planner(
invocation.module_name,
invocation.module_path,
invocation,
module_source
)

@ -18,23 +18,17 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from ansible.module_utils._text import to_bytes
import base64
from ansible.errors import AnsibleError, AnsibleActionFail, AnsibleActionSkip
from ansible.module_utils.common.text.converters import to_bytes, to_text
from ansible.module_utils.six import string_types
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.action import ActionBase
from ansible.utils.hashing import checksum, md5, secure_hash
from ansible.utils.path import makedirs_safe
from ansible.utils.display import Display
from ansible.utils.hashing import checksum, checksum_s, md5, secure_hash
from ansible.utils.path import makedirs_safe, is_subpath
REMOTE_CHECKSUM_ERRORS = {
'0': "unable to calculate the checksum of the remote file",
'1': "the remote file does not exist",
'2': "no read permission on remote file",
'3': "remote file is a directory, fetch cannot work on directories",
'4': "python isn't present on the system. Unable to compute checksum",
'5': "stdlib json was not found on the remote machine. Only the raw module can work without those installed",
}
display = Display()
class ActionModule(ActionBase):
@ -45,36 +39,94 @@ class ActionModule(ActionBase):
task_vars = dict()
result = super(ActionModule, self).run(tmp, task_vars)
del tmp # tmp no longer has any effect
try:
if self._play_context.check_mode:
result['skipped'] = True
result['msg'] = 'check mode not (yet) supported for this module'
return result
raise AnsibleActionSkip('check mode not (yet) supported for this module')
source = self._task.args.get('src', None)
original_dest = dest = self._task.args.get('dest', None)
flat = boolean(self._task.args.get('flat'), strict=False)
fail_on_missing = boolean(self._task.args.get('fail_on_missing', True), strict=False)
validate_checksum = boolean(self._task.args.get('validate_checksum', True), strict=False)
msg = ''
# validate source and dest are strings FIXME: use basic.py and module specs
source = self._task.args.get('src')
if not isinstance(source, string_types):
result['msg'] = "Invalid type supplied for source option, it must be a string"
msg = "Invalid type supplied for source option, it must be a string"
dest = self._task.args.get('dest')
if not isinstance(dest, string_types):
result['msg'] = "Invalid type supplied for dest option, it must be a string"
msg = "Invalid type supplied for dest option, it must be a string"
if source is None or dest is None:
msg = "src and dest are required"
if result.get('msg'):
result['failed'] = True
return result
if msg:
raise AnsibleActionFail(msg)
source = self._connection._shell.join_path(source)
source = self._remote_expand_user(source)
# calculate checksum for the remote file, don't bother if using
# become as slurp will be used Force remote_checksum to follow
# symlinks because fetch always follows symlinks
remote_checksum = self._remote_checksum(source, all_vars=task_vars, follow=True)
remote_stat = {}
remote_checksum = None
if True:
# Get checksum for the remote file even using become. Mitogen doesn't need slurp.
# Follow symlinks because fetch always follows symlinks
try:
remote_stat = self._execute_remote_stat(source, all_vars=task_vars, follow=True)
except AnsibleError as ae:
result['changed'] = False
result['file'] = source
if fail_on_missing:
result['failed'] = True
result['msg'] = to_text(ae)
else:
result['msg'] = "%s, ignored" % to_text(ae, errors='surrogate_or_replace')
return result
remote_checksum = remote_stat.get('checksum')
if remote_stat.get('exists'):
if remote_stat.get('isdir'):
result['failed'] = True
result['changed'] = False
result['msg'] = "remote file is a directory, fetch cannot work on directories"
# Historically, these don't fail because you may want to transfer
# a log file that possibly MAY exist but keep going to fetch other
# log files. Today, this is better achieved by adding
# ignore_errors or failed_when to the task. Control the behaviour
# via fail_when_missing
if not fail_on_missing:
result['msg'] += ", not transferring, ignored"
del result['changed']
del result['failed']
return result
# use slurp if permissions are lacking or privilege escalation is needed
remote_data = None
if remote_checksum in (None, '1', ''):
slurpres = self._execute_module(module_name='ansible.legacy.slurp', module_args=dict(src=source), task_vars=task_vars)
if slurpres.get('failed'):
if not fail_on_missing:
result['file'] = source
result['changed'] = False
else:
result.update(slurpres)
if 'not found' in slurpres.get('msg', ''):
result['msg'] = "the remote file does not exist, not transferring, ignored"
elif slurpres.get('msg', '').startswith('source is a directory'):
result['msg'] = "remote file is a directory, fetch cannot work on directories"
return result
else:
if slurpres['encoding'] == 'base64':
remote_data = base64.b64decode(slurpres['content'])
if remote_data is not None:
remote_checksum = checksum_s(remote_data)
# calculate the destination name
if os.path.sep not in self._connection._shell.join_path('a', ''):
@ -83,13 +135,14 @@ class ActionModule(ActionBase):
else:
source_local = source
dest = os.path.expanduser(dest)
# ensure we only use file name, avoid relative paths
if not is_subpath(dest, original_dest):
# TODO: ? dest = os.path.expanduser(dest.replace(('../','')))
raise AnsibleActionFail("Detected directory traversal, expected to be contained in '%s' but got '%s'" % (original_dest, dest))
if flat:
if os.path.isdir(to_bytes(dest, errors='surrogate_or_strict')) and not dest.endswith(os.sep):
result['msg'] = "dest is an existing directory, use a trailing slash if you want to fetch src into that directory"
result['file'] = dest
result['failed'] = True
return result
raise AnsibleActionFail("dest is an existing directory, use a trailing slash if you want to fetch src into that directory")
if dest.endswith(os.sep):
# if the path ends with "/", we'll use the source filename as the
# destination filename
@ -106,23 +159,7 @@ class ActionModule(ActionBase):
target_name = self._play_context.remote_addr
dest = "%s/%s/%s" % (self._loader.path_dwim(dest), target_name, source_local)
dest = dest.replace("//", "/")
if remote_checksum in REMOTE_CHECKSUM_ERRORS:
result['changed'] = False
result['file'] = source
result['msg'] = REMOTE_CHECKSUM_ERRORS[remote_checksum]
# Historically, these don't fail because you may want to transfer
# a log file that possibly MAY exist but keep going to fetch other
# log files. Today, this is better achieved by adding
# ignore_errors or failed_when to the task. Control the behaviour
# via fail_when_missing
if fail_on_missing:
result['failed'] = True
del result['changed']
else:
result['msg'] += ", not transferring, ignored"
return result
dest = os.path.normpath(dest)
# calculate checksum for the local file
local_checksum = checksum(dest)
@ -132,7 +169,15 @@ class ActionModule(ActionBase):
makedirs_safe(os.path.dirname(dest))
# fetch the file and check for changes
self._connection.fetch_file(source, dest)
if remote_data is None:
self._connection.fetch_file(source, dest)
else:
try:
f = open(to_bytes(dest, errors='surrogate_or_strict'), 'wb')
f.write(remote_data)
f.close()
except (IOError, OSError) as e:
raise AnsibleActionFail("Failed to fetch the file: %s" % e)
new_checksum = secure_hash(dest)
# For backwards compatibility. We'll return None on FIPS enabled systems
try:
@ -157,10 +202,6 @@ class ActionModule(ActionBase):
result.update(dict(changed=False, md5sum=local_md5, file=source, dest=dest, checksum=local_checksum))
finally:
try:
self._remove_tmp_path(self._connection._shell.tmpdir)
except AttributeError:
# .tmpdir was added to ShellModule in v2.6.0, so old versions don't have it
pass
self._remove_tmp_path(self._connection._shell.tmpdir)
return result

@ -26,14 +26,15 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
from __future__ import unicode_literals
"""
Fetch the connection configuration stack that would be used to connect to a
target, without actually connecting to it.
"""
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__metaclass__ = type
import ansible_mitogen.connection
from ansible.plugins.action import ActionBase

@ -26,16 +26,16 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection

@ -26,16 +26,16 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen.connection
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection

@ -26,16 +26,16 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection

@ -26,16 +26,16 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection

@ -27,32 +27,29 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils.six import iteritems
import ansible.errors
try:
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection
import ansible_mitogen.loaders
_get_result = ansible_mitogen.loaders.connection_loader__get(
'kubectl',
class_only=True,
)
class Connection(ansible_mitogen.connection.Connection):
transport = 'kubectl'
(vanilla_class, load_context) = ansible_mitogen.loaders.connection_loader__get_with_context(
'kubectl',
class_only=True,
)
not_supported_msg = (
'The "mitogen_kubectl" plug-in requires a version of Ansible '
@ -60,20 +57,17 @@ class Connection(ansible_mitogen.connection.Connection):
)
def __init__(self, *args, **kwargs):
if not _get_result:
raise AnsibleConnectionFailure(self.not_supported_msg)
if not Connection.vanilla_class:
raise ansible.errors.AnsibleConnectionFailure(self.not_supported_msg)
super(Connection, self).__init__(*args, **kwargs)
def get_extra_args(self):
try:
# Ansible < 2.10, _get_result is the connection class
connection_options = _get_result.connection_options
except AttributeError:
# Ansible >= 2.10, _get_result is a get_with_context_result
connection_options = _get_result.object.connection_options
connection_options = Connection.vanilla_class.connection_options
parameters = []
for key, option in iteritems(connection_options):
if self.get_task_var('ansible_' + key) is not None:
parameters += [ option, self.get_task_var('ansible_' + key) ]
for key in connection_options:
task_var_name = 'ansible_%s' % key
task_var = self.get_task_var(task_var_name)
if task_var is not None:
parameters += [connection_options[key], task_var]
return parameters

@ -26,27 +26,21 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen.connection
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection
import ansible_mitogen.process
if sys.version_info > (3,):
viewkeys = dict.keys
elif sys.version_info > (2, 7):
viewkeys = dict.viewkeys
else:
viewkeys = lambda dct: set(dct)
viewkeys = getattr(dict, 'viewkeys', dict.keys)
def dict_diff(old, new):

@ -26,16 +26,16 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection

@ -26,16 +26,16 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection

@ -26,16 +26,16 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen.connection
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection

@ -0,0 +1,44 @@
# Copyright 2022, Mitogen contributers
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen
except ImportError:
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection
class Connection(ansible_mitogen.connection.Connection):
transport = 'podman'

@ -26,16 +26,16 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen.connection
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection

@ -26,28 +26,31 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
from ansible.plugins.connection.ssh import (
DOCUMENTATION as _ansible_ssh_DOCUMENTATION,
)
DOCUMENTATION = """
name: mitogen_ssh
author: David Wilson <dw@botanicus.net>
connection: mitogen_ssh
short_description: Connect over SSH via Mitogen
description:
- This connects using an OpenSSH client controlled by the Mitogen for
Ansible extension. It accepts every option the vanilla ssh plugin
accepts.
version_added: "2.5"
options:
"""
""" + _ansible_ssh_DOCUMENTATION.partition('options:\n')[2]
try:
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection
import ansible_mitogen.loaders
@ -55,7 +58,7 @@ import ansible_mitogen.loaders
class Connection(ansible_mitogen.connection.Connection):
transport = 'ssh'
vanilla_class = ansible_mitogen.loaders.connection_loader__get(
(vanilla_class, load_context) = ansible_mitogen.loaders.connection_loader__get_with_context(
'ssh',
class_only=True,
)

@ -26,16 +26,16 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen.connection
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection

@ -26,16 +26,16 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
try:
import ansible_mitogen.connection
import ansible_mitogen
except ImportError:
base_dir = os.path.dirname(__file__)
sys.path.insert(0, os.path.abspath(os.path.join(base_dir, '../../..')))
del base_dir
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.connection

@ -26,8 +26,10 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
#
@ -45,12 +47,10 @@ import sys
# debuggers and isinstance() work predictably.
#
BASE_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), '../../..')
)
if BASE_DIR not in sys.path:
sys.path.insert(0, BASE_DIR)
try:
import ansible_mitogen
except ImportError:
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.strategy
import ansible.plugins.strategy.linear

@ -26,8 +26,10 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
#
@ -45,12 +47,10 @@ import sys
# debuggers and isinstance() work predictably.
#
BASE_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), '../../..')
)
if BASE_DIR not in sys.path:
sys.path.insert(0, BASE_DIR)
try:
import ansible_mitogen
except ImportError:
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.loaders
import ansible_mitogen.strategy

@ -26,8 +26,10 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
#
@ -45,12 +47,10 @@ import sys
# debuggers and isinstance() work predictably.
#
BASE_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), '../../..')
)
if BASE_DIR not in sys.path:
sys.path.insert(0, BASE_DIR)
try:
import ansible_mitogen
except ImportError:
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.loaders
import ansible_mitogen.strategy

@ -26,8 +26,10 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
import os.path
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import sys
#
@ -45,12 +47,10 @@ import sys
# debuggers and isinstance() work predictably.
#
BASE_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), '../../..')
)
if BASE_DIR not in sys.path:
sys.path.insert(0, BASE_DIR)
try:
import ansible_mitogen
except ImportError:
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
import ansible_mitogen.loaders
import ansible_mitogen.strategy

@ -26,7 +26,9 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import atexit
import logging
import multiprocessing
@ -36,9 +38,9 @@ import socket
import signal
import sys
try:
if sys.version_info >= (3, 3):
import faulthandler
except ImportError:
else:
faulthandler = None
try:
@ -59,10 +61,9 @@ import mitogen.utils
import ansible
import ansible.constants as C
import ansible.errors
import ansible_mitogen.logging
import ansible_mitogen.services
from mitogen.core import b
import ansible_mitogen.affinity
@ -178,42 +179,6 @@ def setup_pool(pool):
LOG.debug('Service pool configured: size=%d', pool.size)
def _setup_simplejson(responder):
"""
We support serving simplejson for Python 2.4 targets on Ansible 2.3, at
least so the package's own CI Docker scripts can run without external
help, however newer versions of simplejson no longer support Python
2.4. Therefore override any installed/loaded version with a
2.4-compatible version we ship in the compat/ directory.
"""
responder.whitelist_prefix('simplejson')
# issue #536: must be at end of sys.path, in case existing newer
# version is already loaded.
compat_path = os.path.join(os.path.dirname(__file__), 'compat')
sys.path.append(compat_path)
for fullname, is_pkg, suffix in (
(u'simplejson', True, '__init__.py'),
(u'simplejson.decoder', False, 'decoder.py'),
(u'simplejson.encoder', False, 'encoder.py'),
(u'simplejson.scanner', False, 'scanner.py'),
):
path = os.path.join(compat_path, 'simplejson', suffix)
fp = open(path, 'rb')
try:
source = fp.read()
finally:
fp.close()
responder.add_source_override(
fullname=fullname,
path=path,
source=source,
is_pkg=is_pkg,
)
def _setup_responder(responder):
"""
Configure :class:`mitogen.master.ModuleResponder` to only permit
@ -221,7 +186,6 @@ def _setup_responder(responder):
"""
responder.whitelist_prefix('ansible')
responder.whitelist_prefix('ansible_mitogen')
_setup_simplejson(responder)
# Ansible 2.3 is compatible with Python 2.4 targets, however
# ansible/__init__.py is not. Instead, executor/module_common.py writes
@ -316,11 +280,11 @@ def get_cpu_count(default=None):
class Broker(mitogen.master.Broker):
"""
WorkerProcess maintains at most 2 file descriptors, therefore does not need
WorkerProcess maintains fewer file descriptors, therefore does not need
the exuberant syscall expense of EpollPoller, so override it and restore
the poll() poller.
"""
poller_class = mitogen.core.Poller
poller_class = mitogen.parent.POLLER_LIGHTWEIGHT
class Binding(object):
@ -462,7 +426,7 @@ class ClassicWorkerModel(WorkerModel):
common_setup(_init_logging=_init_logging)
self.parent_sock, self.child_sock = socket.socketpair()
self.parent_sock, self.child_sock = mitogen.core.socketpair()
mitogen.core.set_cloexec(self.parent_sock.fileno())
mitogen.core.set_cloexec(self.child_sock.fileno())
@ -674,7 +638,7 @@ class MuxProcess(object):
try:
# Let the parent know our listening socket is ready.
mitogen.core.io_op(self.model.child_sock.send, b('1'))
mitogen.core.io_op(self.model.child_sock.send, b'1')
# Block until the socket is closed, which happens on parent exit.
mitogen.core.io_op(self.model.child_sock.recv, 1)
finally:

@ -36,8 +36,13 @@ Each class in here has a corresponding Planner class in planners.py that knows
how to build arguments for it, preseed related data, etc.
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import atexit
import imp
import ctypes
import json
import logging
import os
import re
import shlex
@ -47,41 +52,25 @@ import tempfile
import traceback
import types
if sys.version_info >= (3, 4):
import importlib.machinery
else:
import imp
from ansible.module_utils.six.moves import shlex_quote
import mitogen.core
import ansible_mitogen.target # TODO: circular import
from mitogen.core import b
from mitogen.core import bytes_partition
from mitogen.core import str_rpartition
from mitogen.core import to_text
try:
import ctypes
except ImportError:
# Python 2.4
ctypes = None
try:
import json
except ImportError:
# Python 2.4
import simplejson as json
try:
# Cannot use cStringIO as it does not support Unicode.
from StringIO import StringIO
except ImportError:
from io import StringIO
try:
from shlex import quote as shlex_quote
except ImportError:
from pipes import quote as shlex_quote
# Absolute imports for <2.5.
logging = __import__('logging')
# Prevent accidental import of an Ansible module from hanging on stdin read.
# FIXME Should probably be b'{}' or None. Ansible 2.19 has bytes | None = None.
import ansible.module_utils.basic
ansible.module_utils.basic._ANSIBLE_ARGS = '{}'
@ -90,15 +79,13 @@ ansible.module_utils.basic._ANSIBLE_ARGS = '{}'
# explicit call to res_init() on each task invocation. BSD-alikes export it
# directly, Linux #defines it as "__res_init".
libc__res_init = None
if ctypes:
libc = ctypes.CDLL(None)
for symbol in 'res_init', '__res_init':
try:
libc__res_init = getattr(libc, symbol)
except AttributeError:
pass
libc = ctypes.CDLL(None)
for symbol in 'res_init', '__res_init':
try:
libc__res_init = getattr(libc, symbol)
except AttributeError:
pass
iteritems = getattr(dict, 'iteritems', dict.items)
LOG = logging.getLogger(__name__)
@ -109,7 +96,7 @@ def shlex_split_b(s):
bytes.
"""
assert isinstance(s, mitogen.core.BytesType)
if mitogen.core.PY3:
if sys.version_info >= (3, 0):
return [
t.encode('latin1')
for t in shlex.split(s.decode('latin1'), comments=True)
@ -212,13 +199,13 @@ class EnvironmentFileWatcher(object):
for line in fp:
# ' #export foo=some var ' -> ['#export', 'foo=some var ']
bits = shlex_split_b(line)
if (not bits) or bits[0].startswith(b('#')):
if (not bits) or bits[0].startswith(b'#'):
continue
if bits[0] == b('export'):
if bits[0] == b'export':
bits.pop(0)
key, sep, value = bytes_partition(b(' ').join(bits), b('='))
key, sep, value = b' '.join(bits).partition(b'=')
if key and sep:
yield key, value
@ -516,10 +503,71 @@ class ModuleUtilsImporter(object):
sys.modules.pop(fullname, None)
def find_module(self, fullname, path=None):
"""
Return a loader for the module with fullname, if we will load it.
Implements importlib.abc.MetaPathFinder.find_module().
Deprecrated in Python 3.4+, replaced by find_spec().
Raises ImportWarning in Python 3.10+. Removed in Python 3.12.
"""
if fullname in self._by_fullname:
return self
def find_spec(self, fullname, path, target=None):
"""
Return a `ModuleSpec` for module with `fullname` if we will load it.
Otherwise return `None`.
Implements importlib.abc.MetaPathFinder.find_spec(). Python 3.4+.
"""
if fullname.endswith('.'):
return None
try:
module_path, is_package = self._by_fullname[fullname]
except KeyError:
LOG.debug('Skipping %s: not present', fullname)
return None
LOG.debug('Handling %s', fullname)
origin = 'master:%s' % (module_path,)
return importlib.machinery.ModuleSpec(
fullname, loader=self, origin=origin, is_package=is_package,
)
def create_module(self, spec):
"""
Return a module object for the given ModuleSpec.
Implements PEP-451 importlib.abc.Loader API introduced in Python 3.4.
Unlike Loader.load_module() this shouldn't populate sys.modules or
set module attributes. Both are done by Python.
"""
module = types.ModuleType(spec.name)
# FIXME create_module() shouldn't initialise module attributes
module.__file__ = spec.origin
return module
def exec_module(self, module):
"""
Execute the module to initialise it. Don't return anything.
Implements PEP-451 importlib.abc.Loader API, introduced in Python 3.4.
"""
spec = module.__spec__
path, _ = self._by_fullname[spec.name]
source = ansible_mitogen.target.get_small_file(self._context, path)
code = compile(source, path, 'exec', 0, 1)
exec(code, module.__dict__)
self._loaded.add(spec.name)
def load_module(self, fullname):
"""
Return the loaded module specified by fullname.
Implements PEP 302 importlib.abc.Loader.load_module().
Deprecated in Python 3.4+, replaced by create_module() & exec_module().
"""
path, is_pkg = self._by_fullname[fullname]
source = ansible_mitogen.target.get_small_file(self._context, path)
code = compile(source, path, 'exec', 0, 1)
@ -530,7 +578,7 @@ class ModuleUtilsImporter(object):
mod.__path__ = []
mod.__package__ = str(fullname)
else:
mod.__package__ = str(str_rpartition(to_text(fullname), '.')[0])
mod.__package__ = str(to_text(fullname).rpartition('.')[0])
exec(code, mod.__dict__)
self._loaded.add(fullname)
return mod
@ -545,7 +593,7 @@ class TemporaryEnvironment(object):
def __init__(self, env=None):
self.original = dict(os.environ)
self.env = env or {}
for key, value in iteritems(self.env):
for key, value in mitogen.core.iteritems(self.env):
key = mitogen.core.to_text(key)
value = mitogen.core.to_text(value)
if value is None:
@ -585,6 +633,7 @@ class NewStyleStdio(object):
sys.stderr = StringIO()
encoded = json.dumps({'ANSIBLE_MODULE_ARGS': args})
ansible.module_utils.basic._ANSIBLE_ARGS = utf8(encoded)
ansible.module_utils.basic._ANSIBLE_PROFILE = 'legacy'
sys.stdin = StringIO(mitogen.core.to_text(encoded))
self.original_get_path = getattr(ansible.module_utils.basic,
@ -599,7 +648,9 @@ class NewStyleStdio(object):
sys.stdout = self.original_stdout
sys.stderr = self.original_stderr
sys.stdin = self.original_stdin
# FIXME Should probably be b'{}' or None. Ansible 2.19 has bytes | None = None.
ansible.module_utils.basic._ANSIBLE_ARGS = '{}'
ansible.module_utils.basic._ANSIBLE_PROFILE = None
class ProgramRunner(Runner):
@ -753,7 +804,7 @@ class ScriptRunner(ProgramRunner):
self.interpreter_fragment = interpreter_fragment
self.is_python = is_python
b_ENCODING_STRING = b('# -*- coding: utf-8 -*-')
b_ENCODING_STRING = b'# -*- coding: utf-8 -*-'
def _get_program(self):
return self._rewrite_source(
@ -786,13 +837,13 @@ class ScriptRunner(ProgramRunner):
# While Ansible rewrites the #! using ansible_*_interpreter, it is
# never actually used to execute the script, instead it is a shell
# fragment consumed by shell/__init__.py::build_module_command().
new = [b('#!') + utf8(self.interpreter_fragment)]
new = [b'#!' + utf8(self.interpreter_fragment)]
if self.is_python:
new.append(self.b_ENCODING_STRING)
_, _, rest = bytes_partition(s, b('\n'))
_, _, rest = s.partition(b'\n')
new.append(rest)
return b('\n').join(new)
return b'\n'.join(new)
class NewStyleRunner(ScriptRunner):
@ -820,12 +871,17 @@ class NewStyleRunner(ScriptRunner):
synchronization mechanism by importing everything the module will need
prior to detaching.
"""
# I think "custom" means "found in custom module_utils search path",
# e.g. playbook relative dir, ~/.ansible/..., Ansible collection.
for fullname, _, _ in self.module_map['custom']:
mitogen.core.import_module(fullname)
# I think "builtin" means "part of ansible/ansible-base/ansible-core",
# as opposed to Python builtin modules such as sys.
for fullname in self.module_map['builtin']:
try:
mitogen.core.import_module(fullname)
except ImportError:
except ImportError as exc:
# #590: Ansible 2.8 module_utils.distro is a package that
# replaces itself in sys.modules with a non-package during
# import. Prior to replacement, it is a real package containing
@ -836,8 +892,18 @@ class NewStyleRunner(ScriptRunner):
# loop progresses to the next entry and attempts to preload
# 'distro._distro', the import mechanism will fail. So here we
# silently ignore any failure for it.
if fullname != 'ansible.module_utils.distro._distro':
raise
if fullname == 'ansible.module_utils.distro._distro':
continue
# ansible.module_utils.compat.selinux raises ImportError if it
# can't load libselinux.so. The importer would usually catch
# this & skip selinux operations. We don't care about selinux,
# we're using import to get a copy of the module.
if (fullname == 'ansible.module_utils.compat.selinux'
and exc.msg == 'unable to load libselinux.so'):
continue
raise
def _setup_excepthook(self):
"""
@ -890,8 +956,7 @@ class NewStyleRunner(ScriptRunner):
# change the default encoding. This hack was removed from Ansible long ago,
# but not before permeating into many third party modules.
PREHISTORIC_HACK_RE = re.compile(
b(r'reload\s*\(\s*sys\s*\)\s*'
r'sys\s*\.\s*setdefaultencoding\([^)]+\)')
br'reload\s*\(\s*sys\s*\)\s*sys\s*\.\s*setdefaultencoding\([^)]+\)',
)
def _setup_program(self):
@ -899,7 +964,7 @@ class NewStyleRunner(ScriptRunner):
context=self.service_context,
path=self.path,
)
self.source = self.PREHISTORIC_HACK_RE.sub(b(''), source)
self.source = self.PREHISTORIC_HACK_RE.sub(b'', source)
def _get_code(self):
try:
@ -914,10 +979,10 @@ class NewStyleRunner(ScriptRunner):
True, # dont_inherit
))
if mitogen.core.PY3:
if sys.version_info >= (3, 0):
main_module_name = '__main__'
else:
main_module_name = b('__main__')
main_module_name = b'__main__'
def _handle_magic_exception(self, mod, exc):
"""
@ -935,7 +1000,7 @@ class NewStyleRunner(ScriptRunner):
def _run_code(self, code, mod):
try:
if mitogen.core.PY3:
if sys.version_info >= (3, 0):
exec(code, vars(mod))
else:
exec('exec code in vars(mod)')
@ -949,10 +1014,10 @@ class NewStyleRunner(ScriptRunner):
approximation of the original package hierarchy, so that relative
imports function correctly.
"""
pkg, sep, modname = str_rpartition(self.py_module_name, '.')
pkg, sep, _ = self.py_module_name.rpartition('.')
if not sep:
return None
if mitogen.core.PY3:
if sys.version_info >= (3, 0):
return pkg
return pkg.encode()
@ -992,7 +1057,7 @@ class NewStyleRunner(ScriptRunner):
class JsonArgsRunner(ScriptRunner):
JSON_ARGS = b('<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>')
JSON_ARGS = b'<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>'
def _get_args_contents(self):
return json.dumps(self.args).encode()

@ -39,23 +39,26 @@ connections, grant access to files by children, and register for notification
when a child has completed a job.
"""
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__metaclass__ = type
import logging
import os
import os.path
import sys
import threading
import ansible.constants
import mitogen
from ansible.module_utils.six import reraise
import mitogen.core
import mitogen.service
import mitogen.utils
import ansible_mitogen.loaders
import ansible_mitogen.module_finder
import ansible_mitogen.target
import ansible_mitogen.utils
import ansible_mitogen.utils.unsafe
LOG = logging.getLogger(__name__)
@ -66,20 +69,6 @@ LOG = logging.getLogger(__name__)
ansible_mitogen.loaders.shell_loader.get('sh')
if sys.version_info[0] == 3:
def reraise(tp, value, tb):
if value is None:
value = tp()
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
else:
exec(
"def reraise(tp, value, tb=None):\n"
" raise tp, value, tb\n"
)
def _get_candidate_temp_dirs():
try:
# >=2.5
@ -91,7 +80,7 @@ def _get_candidate_temp_dirs():
remote_tmp = ansible.constants.DEFAULT_REMOTE_TMP
system_tmpdirs = ('/var/tmp', '/tmp')
return mitogen.utils.cast([remote_tmp] + list(system_tmpdirs))
return ansible_mitogen.utils.unsafe.cast([remote_tmp] + list(system_tmpdirs))
def key_from_dict(**kwargs):
@ -350,7 +339,12 @@ class ContextService(mitogen.service.Service):
'ansible_mitogen.target',
'mitogen.fork',
'mitogen.service',
)
) + ((
'ansible.module_utils._internal._json._profiles._module_legacy_c2m',
'ansible.module_utils._internal._json._profiles._module_legacy_m2c',
'ansible.module_utils._internal._json._profiles._module_modern_c2m',
'ansible.module_utils._internal._json._profiles._module_legacy_m2c',
) if ansible_mitogen.utils.ansible_version[:2] >= (2, 19) else ())
def _send_module_forwards(self, context):
if hasattr(self.router.responder, 'forward_modules'):

@ -26,7 +26,9 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os
import signal
import threading
@ -39,11 +41,15 @@ except ImportError:
import mitogen.core
import ansible_mitogen.affinity
import ansible_mitogen.loaders
import ansible_mitogen.logging
import ansible_mitogen.mixins
import ansible_mitogen.process
import ansible.executor.process.worker
from ansible.utils.sentinel import Sentinel
import ansible.template
import ansible.utils.sentinel
import ansible.playbook.play_context
import ansible.plugins.loader
def _patch_awx_callback():
@ -54,12 +60,11 @@ def _patch_awx_callback():
# AWX uses sitecustomize.py to force-load this package. If it exists, we're
# running under AWX.
try:
from awx_display_callback.events import EventContext
from awx_display_callback.events import event_context
import awx_display_callback.events
except ImportError:
return
if hasattr(EventContext(), '_local'):
if hasattr(awx_display_callback.events.EventContext(), '_local'):
# Patched version.
return
@ -68,18 +73,18 @@ def _patch_awx_callback():
ctx = tls.setdefault('_ctx', {})
ctx.update(kwargs)
EventContext._local = threading.local()
EventContext.add_local = patch_add_local
awx_display_callback.events.EventContext._local = threading.local()
awx_display_callback.events.EventContext.add_local = patch_add_local
_patch_awx_callback()
def wrap_action_loader__get(name, *args, **kwargs):
def wrap_action_loader__get_with_context(name, *args, **kwargs):
"""
While the mitogen strategy is active, trap action_loader.get() calls,
augmenting any fetched class with ActionModuleMixin, which replaces various
helper methods inherited from ActionBase with implementations that avoid
the use of shell fragments wherever possible.
While the mitogen strategy is active, trap action_loader.get_with_context()
calls, augmenting any fetched class with ActionModuleMixin, which replaces
various helper methods inherited from ActionBase with implementations that
avoid the use of shell fragments wherever possible.
This is used instead of static subclassing as it generalizes to third party
action plugins outside the Ansible tree.
@ -89,13 +94,26 @@ def wrap_action_loader__get(name, *args, **kwargs):
name = 'mitogen_' + name
get_kwargs['collection_list'] = kwargs.pop('collection_list', None)
klass = ansible_mitogen.loaders.action_loader__get(name, **get_kwargs)
(klass, context) = ansible_mitogen.loaders.action_loader__get_with_context(
name,
**get_kwargs
)
if klass:
bases = (ansible_mitogen.mixins.ActionModuleMixin, klass)
adorned_klass = type(str(name), bases, {})
if kwargs.get('class_only'):
return adorned_klass
return adorned_klass(*args, **kwargs)
return ansible.plugins.loader.get_with_context_result(
adorned_klass,
context
)
return ansible.plugins.loader.get_with_context_result(
adorned_klass(*args, **kwargs),
context
)
return ansible.plugins.loader.get_with_context_result(None, context)
REDIRECTED_CONNECTION_PLUGINS = (
@ -107,20 +125,32 @@ REDIRECTED_CONNECTION_PLUGINS = (
'lxc',
'lxd',
'machinectl',
'podman',
'setns',
'ssh',
)
def wrap_connection_loader__get(name, *args, **kwargs):
def wrap_connection_loader__get_with_context(name, *args, **kwargs):
"""
While a Mitogen strategy is active, rewrite connection_loader.get() calls
for some transports into requests for a compatible Mitogen transport.
While a Mitogen strategy is active, rewrite
connection_loader.get_with_context() calls for some transports into
requests for a compatible Mitogen transport.
"""
if name in REDIRECTED_CONNECTION_PLUGINS:
is_play_using_mitogen_connection = None
if len(args) > 0 and isinstance(args[0], ansible.playbook.play_context.PlayContext):
play_context = args[0]
is_play_using_mitogen_connection = play_context.connection in REDIRECTED_CONNECTION_PLUGINS
# assume true if we're not in a play context since we're using a Mitogen strategy
if is_play_using_mitogen_connection is None:
is_play_using_mitogen_connection = True
redirect_connection = name in REDIRECTED_CONNECTION_PLUGINS and is_play_using_mitogen_connection
if redirect_connection:
name = 'mitogen_' + name
return ansible_mitogen.loaders.connection_loader__get(name, *args, **kwargs)
return ansible_mitogen.loaders.connection_loader__get_with_context(name, *args, **kwargs)
def wrap_worker__run(self):
@ -170,8 +200,8 @@ class AnsibleWrappers(object):
Install our PluginLoader monkey patches and update global variables
with references to the real functions.
"""
ansible_mitogen.loaders.action_loader.get = wrap_action_loader__get
ansible_mitogen.loaders.connection_loader.get_with_context = wrap_connection_loader__get
ansible_mitogen.loaders.action_loader.get_with_context = wrap_action_loader__get_with_context
ansible_mitogen.loaders.connection_loader.get_with_context = wrap_connection_loader__get_with_context
global worker__run
worker__run = ansible.executor.process.worker.WorkerProcess.run
@ -181,11 +211,11 @@ class AnsibleWrappers(object):
"""
Uninstall the PluginLoader monkey patches.
"""
ansible_mitogen.loaders.action_loader.get = (
ansible_mitogen.loaders.action_loader__get
ansible_mitogen.loaders.action_loader.get_with_context = (
ansible_mitogen.loaders.action_loader__get_with_context
)
ansible_mitogen.loaders.connection_loader.get_with_context = (
ansible_mitogen.loaders.connection_loader__get
ansible_mitogen.loaders.connection_loader__get_with_context
)
ansible.executor.process.worker.WorkerProcess.run = worker__run
@ -278,7 +308,7 @@ class StrategyMixin(object):
name=task.action,
class_only=True,
)
if play_context.connection is not Sentinel:
if play_context.connection is not ansible.utils.sentinel.Sentinel:
# 2.8 appears to defer computing this until inside the worker.
# TODO: figure out where it has moved.
ansible_mitogen.loaders.connection_loader.get(
@ -324,3 +354,44 @@ class StrategyMixin(object):
self._worker_model.on_strategy_complete()
finally:
ansible_mitogen.process.set_worker_model(None)
def _smuggle_to_connection_reset(self, task, play_context, iterator, target_host):
"""
Create a templar and make it available for use in Connection.reset().
This allows templated connection variables to be used when Mitogen
reconstructs its connection stack.
"""
variables = self._variable_manager.get_vars(
play=iterator._play, host=target_host, task=task,
_hosts=self._hosts_cache, _hosts_all=self._hosts_cache_all,
)
templar = ansible.template.Templar(
loader=self._loader, variables=variables,
)
# Required for remote_user option set by variable (e.g. ansible_user).
# Without it remote_user in ansible.cfg gets used.
play_context = play_context.set_task_and_variable_override(
task=task, variables=variables, templar=templar,
)
play_context.post_validate(templar=templar)
# Required for timeout option set by variable (e.g. ansible_timeout).
# Without it the task timeout keyword (default: 0) gets used.
play_context.update_vars(variables)
# Stash the task and templar somewhere Connection.reset() can find it
play_context.vars.update({
'_mitogen.smuggled.reset_connection': (task, templar),
})
return play_context
def _execute_meta(self, task, play_context, iterator, target_host):
if task.args['_raw_params'] == 'reset_connection':
play_context = self._smuggle_to_connection_reset(
task, play_context, iterator, target_host,
)
return super(StrategyMixin, self)._execute_meta(
task, play_context, iterator, target_host,
)

@ -33,10 +33,15 @@ Helper functions intended to be executed on the target. These are entrypoints
for file transfer, module execution and sundry bits like changing file modes.
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import errno
import grp
import operator
import json
import logging
import os
import pty
import pwd
import re
import signal
@ -47,32 +52,9 @@ import tempfile
import traceback
import types
# Absolute imports for <2.5.
logging = __import__('logging')
import mitogen.core
import mitogen.fork
import mitogen.parent
import mitogen.service
from mitogen.core import b
try:
import json
except ImportError:
import simplejson as json
try:
reduce
except NameError:
# Python 3.x.
from functools import reduce
try:
BaseException
except NameError:
# Python 2.4
BaseException = Exception
# Ansible since PR #41749 inserts "import __main__" into
# ansible.module_utils.basic. Mitogen's importer will refuse such an import, so
@ -82,6 +64,7 @@ if not sys.modules.get(str('__main__')):
sys.modules[str('__main__')] = types.ModuleType(str('__main__'))
import ansible.module_utils.json_utils
import ansible_mitogen.runner
@ -135,7 +118,7 @@ def subprocess__Popen__close_fds(self, but):
continue
fd = int(name, 10)
if fd > 2 and fd != but:
if fd > pty.STDERR_FILENO and fd != but:
try:
os.close(fd)
except OSError:
@ -369,11 +352,6 @@ def init_child(econtext, log_level, candidate_temp_dirs):
LOG.setLevel(log_level)
logging.getLogger('ansible_mitogen').setLevel(log_level)
# issue #536: if the json module is available, remove simplejson from the
# importer whitelist to avoid confusing certain Ansible modules.
if json.__name__ == 'json':
econtext.importer.whitelist.remove('simplejson')
global _fork_parent
if FORK_SUPPORTED:
mitogen.parent.upgrade_router(econtext)
@ -622,8 +600,8 @@ def exec_args(args, in_data='', chdir=None, shell=None, emulate_tty=False):
stdout, stderr = proc.communicate(in_data)
if emulate_tty:
stdout = stdout.replace(b('\n'), b('\r\n'))
return proc.returncode, stdout, stderr or b('')
stdout = stdout.replace(b'\n', b'\r\n')
return proc.returncode, stdout, stderr or b''
def exec_command(cmd, in_data='', chdir=None, shell=None, emulate_tty=False):
@ -652,7 +630,8 @@ def read_path(path):
"""
Fetch the contents of a filesystem `path` as bytes.
"""
return open(path, 'rb').read()
with open(path, 'rb') as f:
return f.read()
def set_file_owner(path, owner, group=None, fd=None):
@ -666,11 +645,10 @@ def set_file_owner(path, owner, group=None, fd=None):
else:
gid = os.getegid()
if fd is not None and hasattr(os, 'fchown'):
os.fchown(fd, (uid, gid))
if fd is not None:
os.fchown(fd, uid, gid)
else:
# Python<2.6
os.chown(path, (uid, gid))
os.chown(path, uid, gid)
def write_path(path, s, owner=None, group=None, mode=None,
@ -737,7 +715,9 @@ def apply_mode_spec(spec, mode):
mask = CHMOD_MASKS[ch]
bits = CHMOD_BITS[ch]
cur_perm_bits = mode & mask
new_perm_bits = reduce(operator.or_, (bits[p] for p in perms), 0)
new_perm_bits = 0
for perm in perms:
new_perm_bits |= bits[perm]
mode &= ~mask
if op == '=':
mode |= new_perm_bits
@ -752,9 +732,7 @@ def set_file_mode(path, spec, fd=None):
"""
Update the permissions of a file using the same syntax as chmod(1).
"""
if isinstance(spec, int):
new_mode = spec
elif not mitogen.core.PY3 and isinstance(spec, long):
if isinstance(spec, mitogen.core.integer_types):
new_mode = spec
elif spec.isdigit():
new_mode = int(spec, 8)
@ -762,7 +740,7 @@ def set_file_mode(path, spec, fd=None):
mode = os.stat(path).st_mode
new_mode = apply_mode_spec(spec, mode)
if fd is not None and hasattr(os, 'fchmod'):
if fd is not None:
os.fchmod(fd, new_mode)
else:
os.chmod(path, new_mode)

@ -26,9 +26,6 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import absolute_import
from __future__ import unicode_literals
"""
Mitogen extends Ansible's target configuration mechanism in several ways that
require some care:
@ -60,28 +57,37 @@ information from PlayContext, and another that takes (almost) all information
from HostVars.
"""
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__metaclass__ = type
import abc
import logging
import os
import ansible.utils.shlex
import ansible.constants as C
import ansible.executor.interpreter_discovery
import ansible.utils.unsafe_proxy
from ansible.module_utils.six import with_metaclass
from ansible.module_utils.parsing.convert_bool import boolean
import ansible_mitogen.utils
import mitogen.core
# this was added in Ansible >= 2.8.0; fallback to the default interpreter if necessary
try:
from ansible.executor.interpreter_discovery import discover_interpreter
except ImportError:
discover_interpreter = lambda action,interpreter_name,discovery_mode,task_vars: '/usr/bin/python'
try:
from ansible.utils.unsafe_proxy import AnsibleUnsafeText
except ImportError:
from ansible.vars.unsafe_proxy import AnsibleUnsafeText
LOG = logging.getLogger(__name__)
import mitogen.core
if ansible_mitogen.utils.ansible_version[:2] >= (2, 19):
_FALLBACK_INTERPRETER = ansible.executor.interpreter_discovery._FALLBACK_INTERPRETER
elif ansible_mitogen.utils.ansible_version[:2] >= (2, 17):
_FALLBACK_INTERPRETER = u'/usr/bin/python3'
else:
_FALLBACK_INTERPRETER = u'/usr/bin/python'
def run_interpreter_discovery_if_necessary(s, task_vars, action, rediscover_python):
def run_interpreter_discovery_if_necessary(s, candidates, task_vars, action, rediscover_python):
"""
Triggers ansible python interpreter discovery if requested.
Caches this value the same way Ansible does it.
@ -89,14 +95,14 @@ def run_interpreter_discovery_if_necessary(s, task_vars, action, rediscover_pyth
it could be different than what's ran on the host
"""
# keep trying different interpreters until we don't error
if action._finding_python_interpreter:
return action._possible_python_interpreter
if action._mitogen_discovering_interpreter:
return action._mitogen_interpreter_candidate
if s in ['auto', 'auto_legacy', 'auto_silent', 'auto_legacy_silent']:
# python is the only supported interpreter_name as of Ansible 2.8.8
interpreter_name = 'python'
discovered_interpreter_config = u'discovered_interpreter_%s' % interpreter_name
if task_vars.get('ansible_facts') is None:
task_vars['ansible_facts'] = {}
@ -104,39 +110,42 @@ def run_interpreter_discovery_if_necessary(s, task_vars, action, rediscover_pyth
# if we're rediscovering python then chances are we're running something like a docker connection
# this will handle scenarios like running a playbook that does stuff + then dynamically creates a docker container,
# then runs the rest of the playbook inside that container, and then rerunning the playbook again
action._rediscovered_python = True
action._mitogen_rediscovered_interpreter = True
# blow away the discovered_interpreter_config cache and rediscover
del task_vars['ansible_facts'][discovered_interpreter_config]
if discovered_interpreter_config not in task_vars['ansible_facts']:
action._finding_python_interpreter = True
try:
s = task_vars[u'ansible_facts'][discovered_interpreter_config]
except KeyError:
action._mitogen_discovering_interpreter = True
action._mitogen_interpreter_candidates = candidates
# fake pipelining so discover_interpreter can be happy
action._connection.has_pipelining = True
s = AnsibleUnsafeText(discover_interpreter(
s = ansible.executor.interpreter_discovery.discover_interpreter(
action=action,
interpreter_name=interpreter_name,
discovery_mode=s,
task_vars=task_vars))
task_vars=task_vars,
)
s = ansible.utils.unsafe_proxy.AnsibleUnsafeText(s)
# cache discovered interpreter
task_vars['ansible_facts'][discovered_interpreter_config] = s
action._connection.has_pipelining = False
else:
s = task_vars['ansible_facts'][discovered_interpreter_config]
# propagate discovered interpreter as fact
action._discovered_interpreter_key = discovered_interpreter_config
action._discovered_interpreter = s
action._finding_python_interpreter = False
action._mitogen_discovering_interpreter = False
action._mitogen_interpreter_candidates = None
return s
def parse_python_path(s, task_vars, action, rediscover_python):
def parse_python_path(s, candidates, task_vars, action, rediscover_python):
"""
Given the string set for ansible_python_interpeter, parse it using shell
syntax and return an appropriate argument vector. If the value detected is
syntax and return an appropriate argument vector. If the value detected is
one of interpreter discovery then run that first. Caches python interpreter
discovery value in `facts_from_task_vars` like how Ansible handles this.
"""
@ -144,10 +153,9 @@ def parse_python_path(s, task_vars, action, rediscover_python):
# if python_path doesn't exist, default to `auto` and attempt to discover it
s = 'auto'
s = run_interpreter_discovery_if_necessary(s, task_vars, action, rediscover_python)
# if unable to determine python_path, fallback to '/usr/bin/python'
s = run_interpreter_discovery_if_necessary(s, candidates, task_vars, action, rediscover_python)
if not s:
s = '/usr/bin/python'
s = _FALLBACK_INTERPRETER
return ansible.utils.shlex.shlex_split(s)
@ -214,6 +222,12 @@ class Spec(with_metaclass(abc.ABCMeta, object)):
:data:`True` if privilege escalation should be active.
"""
@abc.abstractmethod
def become_flags(self):
"""
The command line arguments passed to the become executable.
"""
@abc.abstractmethod
def become_method(self):
"""
@ -244,6 +258,12 @@ class Spec(with_metaclass(abc.ABCMeta, object)):
Path to the Python interpreter on the target machine.
"""
@abc.abstractmethod
def host_key_checking(self):
"""
Whether or not to check the keys of the target machine
"""
@abc.abstractmethod
def private_key_file(self):
"""
@ -285,10 +305,9 @@ class Spec(with_metaclass(abc.ABCMeta, object)):
@abc.abstractmethod
def sudo_args(self):
"""
The list of additional arguments that should be included in a become
The list of additional arguments that should be included in a sudo
invocation.
"""
# TODO: split out into sudo_args/become_args.
@abc.abstractmethod
def mitogen_via(self):
@ -354,6 +373,12 @@ class Spec(with_metaclass(abc.ABCMeta, object)):
The path to the "machinectl" program for the 'setns' transport.
"""
@abc.abstractmethod
def mitogen_podman_path(self):
"""
The path to the "podman" program for the 'podman' transport.
"""
@abc.abstractmethod
def mitogen_ssh_keepalive_interval(self):
"""
@ -390,6 +415,12 @@ class Spec(with_metaclass(abc.ABCMeta, object)):
Value of "ansible_doas_exe" variable.
"""
@abc.abstractmethod
def verbosity(self):
"""
How verbose to make logging or diagnostics output.
"""
class PlayContextSpec(Spec):
"""
@ -406,6 +437,43 @@ class PlayContextSpec(Spec):
# used to run interpreter discovery
self._action = connection._action
def _become_option(self, name):
plugin = self._connection.become
try:
return plugin.get_option(name, self._task_vars, self._play_context)
except AttributeError:
# A few ansible_mitogen connection plugins look more like become
# plugins. They don't quite fit Ansible's plugin.get_option() API.
# https://github.com/mitogen-hq/mitogen/issues/1173
fallback_plugins = {'mitogen_doas', 'mitogen_sudo', 'mitogen_su'}
if self._connection.transport not in fallback_plugins:
raise
fallback_options = {
'become_exe',
'become_flags',
}
if name not in fallback_options:
raise
LOG.info(
'Used fallback=PlayContext.%s for plugin=%r, option=%r',
name, self._connection, name,
)
return getattr(self._play_context, name)
def _connection_option(self, name, fallback_attr=None):
try:
return self._connection.get_option(name, hostvars=self._task_vars)
except KeyError:
if fallback_attr is None:
fallback_attr = name
LOG.info(
'Used fallback=PlayContext.%s for plugin=%r, option=%r',
fallback_attr, self._connection, name,
)
return getattr(self._play_context, fallback_attr)
def transport(self):
return self._transport
@ -413,92 +481,90 @@ class PlayContextSpec(Spec):
return self._inventory_name
def remote_addr(self):
return self._play_context.remote_addr
return self._connection_option('host', fallback_attr='remote_addr')
def remote_user(self):
return self._play_context.remote_user
return self._connection_option('remote_user')
def become(self):
return self._play_context.become
return self._connection.become
def become_flags(self):
return self._become_option('become_flags')
def become_method(self):
return self._play_context.become_method
return self._connection.become.name
def become_user(self):
return self._play_context.become_user
return self._become_option('become_user')
def become_pass(self):
return optional_secret(self._play_context.become_pass)
return optional_secret(self._become_option('become_pass'))
def password(self):
return optional_secret(self._play_context.password)
return optional_secret(self._connection_option('password'))
def port(self):
return self._play_context.port
return self._connection_option('port')
def python_path(self, rediscover_python=False):
s = self._connection.get_task_var('ansible_python_interpreter')
# #511, #536: executor/module_common.py::_get_shebang() hard-wires
# "/usr/bin/python" as the default interpreter path if no other
# interpreter is specified.
# See also
# - ansible_mitogen.connecton.Connection.get_task_var()
try:
delegated_vars = self._task_vars['ansible_delegated_vars']
variables = delegated_vars[self._connection.delegate_to_hostname]
except KeyError:
variables = self._task_vars
interpreter_python = C.config.get_config_value(
'INTERPRETER_PYTHON', variables=variables,
)
interpreter_python_fallback = C.config.get_config_value(
'INTERPRETER_PYTHON_FALLBACK', variables=variables,
)
if '{{' in interpreter_python or '{%' in interpreter_python:
templar = self._connection.templar
interpreter_python = templar.template(interpreter_python)
return parse_python_path(
s,
interpreter_python,
candidates=interpreter_python_fallback,
task_vars=self._task_vars,
action=self._action,
rediscover_python=rediscover_python)
def host_key_checking(self):
return self._connection_option('host_key_checking')
def private_key_file(self):
return self._play_context.private_key_file
return self._connection_option('private_key_file')
def ssh_executable(self):
return C.config.get_config_value("ssh_executable", plugin_type="connection", plugin_name="ssh", variables=self._task_vars.get("vars", {}))
return self._connection_option('ssh_executable')
def timeout(self):
return self._play_context.timeout
return self._connection_option('timeout')
def ansible_ssh_timeout(self):
return (
self._connection.get_task_var('ansible_timeout') or
self._connection.get_task_var('ansible_ssh_timeout') or
self.timeout()
)
return self.timeout()
def ssh_args(self):
return [
mitogen.core.to_text(term)
for s in (
C.config.get_config_value("ssh_args", plugin_type="connection", plugin_name="ssh", variables=self._task_vars.get("vars", {})),
C.config.get_config_value("ssh_common_args", plugin_type="connection", plugin_name="ssh", variables=self._task_vars.get("vars", {})),
C.config.get_config_value("ssh_extra_args", plugin_type="connection", plugin_name="ssh", variables=self._task_vars.get("vars", {}))
self._connection_option('ssh_args'),
self._connection_option('ssh_common_args'),
self._connection_option('ssh_extra_args'),
)
for term in ansible.utils.shlex.shlex_split(s or '')
]
def become_exe(self):
# In Ansible 2.8, PlayContext.become_exe always has a default value due
# to the new options mechanism. Previously it was only set if a value
# ("somewhere") had been specified for the task.
# For consistency in the tests, here we make older Ansibles behave like
# newer Ansibles.
exe = self._play_context.become_exe
if exe is None and self._play_context.become_method == 'sudo':
exe = 'sudo'
return exe
return self._become_option('become_exe')
def sudo_args(self):
return [
mitogen.core.to_text(term)
for term in ansible.utils.shlex.shlex_split(
first_true((
self._play_context.become_flags,
# Ansible <=2.7.
getattr(self._play_context, 'sudo_flags', ''),
# Ansible <=2.3.
getattr(C, 'DEFAULT_BECOME_FLAGS', ''),
getattr(C, 'DEFAULT_SUDO_FLAGS', '')
), default='')
)
]
return ansible.utils.shlex.shlex_split(self.become_flags() or '')
def mitogen_via(self):
return self._connection.get_task_var('mitogen_via')
@ -527,6 +593,9 @@ class PlayContextSpec(Spec):
def mitogen_lxc_info_path(self):
return self._connection.get_task_var('mitogen_lxc_info_path')
def mitogen_podman_path(self):
return self._connection.get_task_var('mitogen_podman_path')
def mitogen_ssh_keepalive_interval(self):
return self._connection.get_task_var('mitogen_ssh_keepalive_interval')
@ -551,6 +620,17 @@ class PlayContextSpec(Spec):
os.environ.get('ANSIBLE_DOAS_EXE')
)
def verbosity(self):
try:
verbosity = self._connection.get_option('verbosity', hostvars=self._task_vars)
except KeyError:
verbosity = self.mitogen_ssh_debug_level()
if verbosity:
return int(verbosity)
return 0
class MitogenViaSpec(Spec):
"""
@ -630,6 +710,9 @@ class MitogenViaSpec(Spec):
def become(self):
return bool(self._become_user)
def become_flags(self):
return self._host_vars.get('ansible_become_flags')
def become_method(self):
return (
self._become_method or
@ -642,12 +725,13 @@ class MitogenViaSpec(Spec):
def become_pass(self):
return optional_secret(
self._host_vars.get('ansible_become_password') or
self._host_vars.get('ansible_become_pass')
self._host_vars.get('ansible_become_pass') or
self._host_vars.get('ansible_become_password')
)
def password(self):
return optional_secret(
self._host_vars.get('ansible_ssh_password') or
self._host_vars.get('ansible_ssh_pass') or
self._host_vars.get('ansible_password')
)
@ -661,15 +745,24 @@ class MitogenViaSpec(Spec):
def python_path(self, rediscover_python=False):
s = self._host_vars.get('ansible_python_interpreter')
# #511, #536: executor/module_common.py::_get_shebang() hard-wires
# "/usr/bin/python" as the default interpreter path if no other
# interpreter is specified.
interpreter_python_fallback = self._host_vars.get(
'ansible_interpreter_python_fallback', [],
)
return parse_python_path(
s,
candidates=interpreter_python_fallback,
task_vars=self._task_vars,
action=self._action,
rediscover_python=rediscover_python)
def host_key_checking(self):
def candidates():
yield self._host_vars.get('ansible_ssh_host_key_checking')
yield self._host_vars.get('ansible_host_key_checking')
yield C.HOST_KEY_CHECKING
val = next((v for v in candidates() if v is not None), True)
return boolean(val)
def private_key_file(self):
# TODO: must come from PlayContext too.
return (
@ -693,12 +786,13 @@ class MitogenViaSpec(Spec):
)
def ssh_args(self):
local_vars = self._task_vars.get("hostvars", {}).get(self._inventory_name, {})
return [
mitogen.core.to_text(term)
for s in (
C.config.get_config_value("ssh_args", plugin_type="connection", plugin_name="ssh", variables=self._task_vars.get("vars", {})),
C.config.get_config_value("ssh_common_args", plugin_type="connection", plugin_name="ssh", variables=self._task_vars.get("vars", {})),
C.config.get_config_value("ssh_extra_args", plugin_type="connection", plugin_name="ssh", variables=self._task_vars.get("vars", {}))
C.config.get_config_value("ssh_args", plugin_type="connection", plugin_name="ssh", variables=local_vars),
C.config.get_config_value("ssh_common_args", plugin_type="connection", plugin_name="ssh", variables=local_vars),
C.config.get_config_value("ssh_extra_args", plugin_type="connection", plugin_name="ssh", variables=local_vars)
)
for term in ansible.utils.shlex.shlex_split(s)
if s
@ -715,7 +809,7 @@ class MitogenViaSpec(Spec):
mitogen.core.to_text(term)
for s in (
self._host_vars.get('ansible_sudo_flags') or '',
self._host_vars.get('ansible_become_flags') or '',
self.become_flags() or '',
)
for term in ansible.utils.shlex.shlex_split(s)
]
@ -739,7 +833,7 @@ class MitogenViaSpec(Spec):
return self._host_vars.get('mitogen_kubectl_path')
def mitogen_lxc_path(self):
return self.host_vars.get('mitogen_lxc_path')
return self._host_vars.get('mitogen_lxc_path')
def mitogen_lxc_attach_path(self):
return self._host_vars.get('mitogen_lxc_attach_path')
@ -747,6 +841,9 @@ class MitogenViaSpec(Spec):
def mitogen_lxc_info_path(self):
return self._host_vars.get('mitogen_lxc_info_path')
def mitogen_podman_path(self):
return self._host_vars.get('mitogen_podman_path')
def mitogen_ssh_keepalive_interval(self):
return self._host_vars.get('mitogen_ssh_keepalive_interval')
@ -770,3 +867,13 @@ class MitogenViaSpec(Spec):
self._host_vars.get('ansible_doas_exe') or
os.environ.get('ANSIBLE_DOAS_EXE')
)
def verbosity(self):
verbosity = self._host_vars.get('ansible_ssh_verbosity')
if verbosity is None:
verbosity = self.mitogen_ssh_debug_level()
if verbosity:
return int(verbosity)
return 0

@ -1,13 +0,0 @@
from __future__ import absolute_import
import distutils.version
import ansible
__all__ = [
'ansible_version',
]
ansible_version = tuple(distutils.version.LooseVersion(ansible.__version__).version)
del distutils
del ansible

@ -0,0 +1,29 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import re
import ansible
__all__ = [
'ansible_version',
]
def _parse(v_string):
# Adapted from distutils.version.LooseVersion.parse()
component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
for component in component_re.split(v_string):
if not component or component == '.':
continue
try:
yield int(component)
except ValueError:
yield component
ansible_version = tuple(_parse(ansible.__version__))
del _parse
del re
del ansible

@ -0,0 +1,123 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import ansible
import ansible.utils.unsafe_proxy
import ansible_mitogen.utils
import mitogen
import mitogen.core
import mitogen.utils
__all__ = [
'cast',
]
def _cast_to_dict(obj): return {cast(k): cast(v) for k, v in obj.items()}
def _cast_to_list(obj): return [cast(v) for v in obj]
def _cast_to_set(obj): return set(cast(v) for v in obj)
def _cast_to_tuple(obj): return tuple(cast(v) for v in obj)
def _cast_unsafe(obj): return obj._strip_unsafe()
def _passthrough(obj): return obj
def _untag(obj): return obj._native_copy()
# A dispatch table to cast objects based on their exact type.
# This is an optimisation, reliable fallbacks are required (e.g. isinstance())
_CAST_DISPATCH = {
bytes: bytes,
dict: _cast_to_dict,
list: _cast_to_list,
mitogen.core.UnicodeType: mitogen.core.UnicodeType,
}
_CAST_DISPATCH.update({t: _passthrough for t in mitogen.utils.PASSTHROUGH})
_CAST_SUBTYPES = [
dict,
list,
]
if hasattr(ansible.utils.unsafe_proxy, 'TrustedAsTemplate'):
import datetime
import ansible.module_utils._internal._datatag
_CAST_DISPATCH.update({
set: _cast_to_set,
tuple: _cast_to_tuple,
ansible.module_utils._internal._datatag._AnsibleTaggedBytes: _untag,
ansible.module_utils._internal._datatag._AnsibleTaggedDate: _untag,
ansible.module_utils._internal._datatag._AnsibleTaggedDateTime: _untag,
ansible.module_utils._internal._datatag._AnsibleTaggedDict: _cast_to_dict,
ansible.module_utils._internal._datatag._AnsibleTaggedFloat: _untag,
ansible.module_utils._internal._datatag._AnsibleTaggedInt: _untag,
ansible.module_utils._internal._datatag._AnsibleTaggedList: _cast_to_list,
ansible.module_utils._internal._datatag._AnsibleTaggedSet: _cast_to_set,
ansible.module_utils._internal._datatag._AnsibleTaggedStr: _untag,
ansible.module_utils._internal._datatag._AnsibleTaggedTime: _untag,
ansible.module_utils._internal._datatag._AnsibleTaggedTuple: _cast_to_tuple,
ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: bytes,
ansible.utils.unsafe_proxy.AnsibleUnsafeText: mitogen.core.UnicodeType,
datetime.date: _passthrough,
datetime.datetime: _passthrough,
datetime.time: _passthrough,
})
_CAST_SUBTYPES.extend([
set,
tuple,
])
elif hasattr(ansible.utils.unsafe_proxy.AnsibleUnsafeText, '_strip_unsafe'):
_CAST_DISPATCH.update({
tuple: _cast_to_list,
ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: _cast_unsafe,
ansible.utils.unsafe_proxy.AnsibleUnsafeText: _cast_unsafe,
ansible.utils.unsafe_proxy.NativeJinjaUnsafeText: _cast_unsafe,
})
_CAST_SUBTYPES.extend([
tuple,
])
elif ansible_mitogen.utils.ansible_version[:2] <= (2, 16):
_CAST_DISPATCH.update({
tuple: _cast_to_list,
ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: bytes,
ansible.utils.unsafe_proxy.AnsibleUnsafeText: mitogen.core.UnicodeType,
})
_CAST_SUBTYPES.extend([
tuple,
])
else:
mitogen_ver = '.'.join(str(v) for v in mitogen.__version__)
raise ImportError("Mitogen %s can't cast Ansible %s objects"
% (mitogen_ver, ansible.__version__))
def cast(obj):
"""
Return obj (or a copy) with subtypes of builtins cast to their supertype.
This is an enhanced version of :func:`mitogen.utils.cast`. In addition it
handles ``ansible.utils.unsafe_proxy.AnsibleUnsafeText`` and variants.
There are types handled by :func:`ansible.utils.unsafe_proxy.wrap_var()`
that this function currently does not handle (e.g. `set()`), or preserve
preserve (e.g. `tuple()`). Future enhancements may change this.
:param obj:
Object to undecorate.
:returns:
Undecorated object.
"""
# Fast path: obj is a known type, dispatch directly
try:
unwrapper = _CAST_DISPATCH[type(obj)]
except KeyError:
pass
else:
return unwrapper(obj)
# Slow path: obj is some unknown subclass
for typ_ in _CAST_SUBTYPES:
if isinstance(obj, typ_):
unwrapper = _CAST_DISPATCH[typ_]
return unwrapper(obj)
return mitogen.utils.cast(obj)

@ -1,14 +0,0 @@
<!doctype html>
<title>Mitogen for Ansible (Redirect)</title>
<script>
{% include "piwik-config.js" %}
var u="https://networkgenomics.com/p/tr/";
_paq.push(['setTrackerUrl', u+'ep']);
</script>
<script src="https://networkgenomics.com/p/tr/js"></script>
<script>
setTimeout(function() {
window.location = 'https://networkgenomics.com/ansible/';
}, 0);
</script>
<meta http-equiv="Refresh" content="0; url=https://networkgenomics.com/ansible/">

@ -1,4 +1,4 @@
<p>
<br>
<a class="github-button" href="https://github.com/dw/mitogen/" data-size="large" data-show-count="true" aria-label="Star dw/mitogen on GitHub">Star</a>
<a class="github-button" href="https://github.com/mitogen-hq/mitogen/" data-size="large" data-show-count="true" aria-label="Star mitogen on GitHub">Star</a>
</p>

@ -14,23 +14,5 @@
{% block footer %}
{{ super() }}
<script>
(function() {
{% include "piwik-config.js" %}
var u="https://networkgenomics.com/p/tr/";
_paq.push(['setTrackerUrl', u+'ep']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript';
g.defer=true; g.async=true; g.src=u+'js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript>
<p>
{% set fulltitle = (title|striptags|e) + titlesuffix -%}
<img src="https://networkgenomics.com/p/tr/ep?idsite=6&action_name={{fulltitle}}" style="border:0" alt="">
</p>
</noscript>
<script async defer src="https://buttons.github.io/buttons.js"></script>
{% endblock %}

@ -1,5 +0,0 @@
window._paq = [];
window._paq.push(['trackPageView']);
window._paq.push(['enableLinkTracking']);
window._paq.push(['enableHeartBeatTimer', 30]);
window._paq.push(['setSiteId', 6]);

@ -18,7 +18,7 @@ The extension is considered stable and real-world use is encouraged.
.. _Ansible: https://www.ansible.com/
.. _Bug reports: https://goo.gl/yLKZiJ
.. _Bug reports: https://github.com/mitogen-hq/mitogen/issues/new/choose
Overview
@ -75,34 +75,6 @@ Installation
``mitogen_host_pinned`` strategies exists to mimic the ``free`` and
``host_pinned`` strategies.
4.
.. raw:: html
<form action="https://networkgenomics.com/save-email/" method="post" id="emailform">
<input type=hidden name="list_name" value="mitogen-announce">
Get notified of new releases and important fixes.
<p>
<input type="email" placeholder="E-mail Address" name="email" style="font-size: 105%;"><br>
<input name="captcha_1" placeholder="Captcha" style="width: 10ex;">
<img class="captcha-image">
<a class="captcha-refresh" href="#">&#x21bb</a>
<button type="submit" style="font-size: 105%;">
Subscribe
</button>
</p>
<div id="emailthanks" style="display:none">
Thanks!
</div>
<p>
</form>
Demo
~~~~
@ -147,8 +119,35 @@ Noteworthy Differences
* Mitogen 0.2.x supports Ansible 2.3-2.9; with Python 2.6, 2.7, or 3.6.
Mitogen 0.3.1+ supports
- Ansible 2.10, 3, and 4; with Python 2.7, or 3.6-3.9
- Ansible 5; with Python 3.8-3.9
+-----------------+-----------------+
| Ansible version | Python versions |
+=================+=================+
| 2.10 | |
+-----------------+ |
| 3 | 2.7, 3.6 - 3.11 |
+-----------------+ |
| 4 | |
+-----------------+-----------------+
| 5 | 3.8 - 3.11 |
+-----------------+-----------------+
| 6 | |
+-----------------+ 3.8 - 3.13 |
| 7 | |
+-----------------+-----------------+
| 8 | 3.9 - 3.13 |
+-----------------+-----------------+
| 9 | |
+-----------------+ 3.10 - 3.14 |
| 10 | |
+-----------------+-----------------+
| 11 | |
+-----------------+ 3.11 - 3.14 |
| 12 | |
+-----------------+-----------------+
| 13 | 3.12 - 3.14 |
+-----------------+-----------------+
Verify your installation is running one of these versions by checking
``ansible --version`` output.
@ -188,9 +187,9 @@ Noteworthy Differences
your_ssh_username = (ALL) NOPASSWD:/usr/bin/python -c*
* The :ans:conn:`~buildah`, :ans:conn:`~docker`, :ans:conn:`~jail`,
:ans:conn:`~kubectl`, :ans:conn:`~local`, :ans:conn:`~lxd`, and
:ans:conn:`~ssh` built-in connection types are supported, along with
Mitogen-specific :ref:`machinectl <machinectl>`, :ref:`mitogen_doas <doas>`,
:ans:conn:`~kubectl`, :ans:conn:`~local`, :ans:conn:`~lxd`,
:ans:conn:`~podman`, & :ans:conn:`~ssh` connection types are supported; also
Mitogen-specific :ref:`mitogen_doas <doas>`, :ref:`machinectl <machinectl>`,
:ref:`mitogen_su <su>`, :ref:`mitogen_sudo <sudo>`, and :ref:`setns <setns>`
types. File bugs to register interest in others.
@ -228,6 +227,15 @@ Noteworthy Differences
part of the core library, and should therefore be straightforward to fix as
part of 0.2.x.
* Connection and become timeouts are applied differently. Mitogen may consider
a connection to have timed out, when Ansible would have waited longer or
indefinately. For example if SSH authentication completes within the
timeout, but execution of login scripts exceeds it - then Mitogen will
consider the task to have timed out and that host to have failed.
..
tests/ansible/integration/ssh/timeouts.yml covers (some of) this behaviour.
..
* SSH and ``become`` are treated distinctly when applying timeouts, and
timeouts apply up to the point when the new interpreter is ready to accept
@ -243,15 +251,14 @@ Noteworthy Differences
* "Module Replacer" style modules are not supported. These rarely appear in
practice, and light web searches failed to reveal many examples of them.
..
* The ``ansible_python_interpreter`` variable is parsed using a restrictive
:mod:`shell-like <shlex>` syntax, permitting values such as ``/usr/bin/env
FOO=bar python`` or ``source /opt/rh/rh-python36/enable && python``, which
occur in practice. Jinja2 templating is also supported for complex task-level
interpreter settings. Ansible `documents this
<https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#ansible-python-interpreter>`_
as an absolute path, however the implementation passes it unquoted through
the shell, permitting arbitrary code to be injected.
* The ``ansible_python_interpreter`` variable is parsed using a restrictive
:mod:`shell-like <shlex>` syntax, permitting values such as ``/usr/bin/env
FOO=bar python`` or ``source /opt/rh/rh-python36/enable && python``.
Jinja2 templating is also supported for complex task-level
interpreter settings. Ansible documents `ansible_python_interpreter
<https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#ansible-python-interpreter>`_
as an absolute path and releases since June 2024 (e.g. Ansible 10.1)
reflect this. Older Ansible releases passed it to the shell unquoted.
..
* Configurations will break that rely on the `hashbang argument splitting
@ -305,7 +312,8 @@ container.
* Intermediary machines cannot use login and become passwords that were
supplied to Ansible interactively. If an intermediary requires a
password, it must be supplied via ``ansible_ssh_pass``,
``ansible_password``, or ``ansible_become_pass`` inventory variables.
``ansible_ssh_password``, ``ansible_password``, or
``ansible_become_pass`` inventory variables.
* Automatic tunnelling of SSH-dependent actions, such as the
``synchronize`` module, is not yet supported. This will be addressed in a
@ -819,6 +827,20 @@ Like the :ans:conn:`local` except connection delegation is supported.
* ``ansible_python_interpreter``
Podman
~~~~~~
Like :ans:conn:`podman` except connection delegation is supported.
* ``ansible_host``: Name of container (default: inventory hostname).
* ``ansible_user``: Name of user within the container to execute as.
* ``mitogen_mask_remote_name``: if :data:`True`, mask the identity of the
Ansible controller process on remote machines. To simplify diagnostics,
Mitogen produces remote processes named like
`"mitogen:user@controller.name:1234"`, however this may be a privacy issue in
some circumstances.
Process Model
^^^^^^^^^^^^^
@ -996,7 +1018,10 @@ Like the :ans:conn:`ssh` except connection delegation is supported.
* ``ansible_port``, ``ssh_port``
* ``ansible_ssh_executable``, ``ssh_executable``
* ``ansible_ssh_private_key_file``
* ``ansible_ssh_pass``, ``ansible_password`` (default: assume passwordless)
* ``ansible_ssh_pass``, ``ansible_ssh_password``, ``ansible_password``
(default: assume passwordless)
* ``ansible_ssh_host_key_checking``, ``ansible_host_key_checking`` (default:
:data:`True`)
* ``ssh_args``, ``ssh_common_args``, ``ssh_extra_args``
* ``mitogen_mask_remote_name``: if :data:`True`, mask the identity of the
Ansible controller process on remote machines. To simplify diagnostics,
@ -1234,18 +1259,17 @@ with ``-vvv``.
However, certain controller hangs may render ``MITOGEN_DUMP_THREAD_STACKS``
ineffective, or occur too infrequently for interactive reproduction. In these
cases `faulthandler <https://faulthandler.readthedocs.io/>`_ may be used:
cases :py:mod:`faulthandler` may be used with Python >= 3.3:
1. For Python 2, ``pip install faulthandler``. This is unnecessary on Python 3.
2. Once the hang occurs, observe the process tree using ``pstree`` or ``ps
1. Once the hang occurs, observe the process tree using ``pstree`` or ``ps
--forest``.
3. The most likely process to be hung is the connection multiplexer, which can
2. The most likely process to be hung is the connection multiplexer, which can
easily be identified as the parent of all SSH client processes.
4. Send ``kill -SEGV <pid>`` to the multiplexer PID, causing it to print all
3. Send ``kill -SEGV <pid>`` to the multiplexer PID, causing it to print all
thread stacks.
5. `File a bug <https://github.com/dw/mitogen/issues/new/>`_ including a copy
of the stacks, along with a description of the last task executing prior to
the hang.
4. `File a bug <https://github.com/mitogen-hq/mitogen/issues/new/>`_
including a copy of the stacks and a description of the last task executing
before the hang
It is possible the hang occurred in a process on a target. If ``strace`` is
available, look for the host name not listed in Ansible output as reporting a
@ -1259,7 +1283,7 @@ on each process whose name begins with ``mitogen:``::
[pid 29858] futex(0x55ea9be52f60, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 0, NULL, 0xffffffff
^C
$
$
This shows one thread waiting on IO (``poll``) and two more waiting on the same
lock. It is taken from a real example of a deadlock due to a forking bug.
@ -1278,7 +1302,7 @@ Sample Profiles
---------------
The summaries below may be reproduced using data and scripts maintained in the
`pcaps branch <https://github.com/dw/mitogen/tree/pcaps/>`_. Traces were
`pcaps branch <https://github.com/mitogen-hq/mitogen/tree/pcaps/>`_. Traces were
recorded using Ansible 2.5.14.
@ -1287,7 +1311,7 @@ Trivial Loop: Local Host
This demonstrates Mitogen vs. SSH pipelining to the local machine running
`bench/loop-100-items.yml
<https://github.com/dw/mitogen/blob/master/tests/ansible/bench/loop-100-items.yml>`_,
<https://github.com/mitogen-hq/mitogen/blob/master/tests/ansible/bench/loop-100-items.yml>`_,
executing a simple command 100 times. Most Ansible controller overhead is
isolated, characterizing just module executor and connection layer performance.
Mitogen requires **63x less bandwidth and 5.9x less time**.
@ -1315,7 +1339,7 @@ File Transfer: UK to France
~~~~~~~~~~~~~~~~~~~~~~~~~~~
`This playbook
<https://github.com/dw/mitogen/blob/master/tests/ansible/regression/issue_140__thread_pileup.yml>`_
<https://github.com/mitogen-hq/mitogen/blob/master/tests/ansible/regression/issue_140__thread_pileup.yml>`_
was used to compare file transfer performance over a ~26 ms link. It uses the
``with_filetree`` loop syntax to copy a directory of 1,000 0-byte files to the
target.
@ -1377,20 +1401,3 @@ Despite the small margin for optimization, Mitogen still manages **6.2x less
bandwidth and 1.8x less time**.
.. image:: images/ansible/pcaps/costapp-uk-india.svg
.. raw:: html
<script src="https://networkgenomics.com/static/js/public_all.js?92d49a3a"></script>
<script>
NetGen = {
public: {
page_id: "operon",
urls: {
save_email: "https://networkgenomics.com/save-email/",
save_email_captcha: "https://networkgenomics.com/save-email/captcha/",
}
}
};
setupEmailForm();
</script>

@ -15,12 +15,473 @@ Release Notes
</style>
To avail of fixes in an unreleased version, please download a ZIP file
`directly from GitHub <https://github.com/dw/mitogen/>`_.
`directly from GitHub <https://github.com/mitogen-hq/mitogen/>`_.
v0.3.1.dev0 (unreleased)
In progress (unreleased)
------------------------
v0.3.39 (2026-01-27)
--------------------
* :gh:issue:`1430` :mod:`mitogen`: Pickle :data:`mitogen.core.GET_RESOURCE`
parameters directly as textual strings (rather than ASCII in byte strings)
* :gh:issue:`1430` :mod:`mitogen`: Explicitly mark messages known to carry
pickled data, using :data:`mitogen.core.Message.ENC_PKL`. This repurposes
the magic field as a content encoding enumeration.
* :gh:issue:`1430` :mod:`mitogen`: Add explicit binary Message encoding,
marked using :data:`mitogen.core.Message.ENC_BIN`.
* :gh:issue:`1430` :mod:`mitogen`: Send :class:`mitogen.service.FileService`
content raw, without pickle encoding
v0.3.38 (2026-01-23)
--------------------
* :gh:issue:`1418` :mod:`mitogen`: Format :class:`mitogen.core.Message` source
and destination as ``<context>:<handle>``, for clarity
* :gh:issue:`1415` :mod:`mitogen`: Put fallbacks & polyfills into
``if sys.version_info`` blocks
* :gh:issue:`1423` tests: Group and unify naming of connection benchmarks
* :gh:issue:`1424` tests: Parameterize connection benchmarks
* :gh:issue:`1424` tests: Standardise output of connection benchmarks
* :gh:issue:`1424` tests: Parameterize throughput benchmark
* :gh:issue:`1424` tests: Parameterize large message benchmark
* :gh:issue:`1424` :mod:`mitogen`: Consolidate all ``range`` and ``xrange``
polyfills into :attr:`mitogen.core.range`
v0.3.37 (2026-01-08)
--------------------
* :gh:issue:`1398` :mod:`mitogen`: Fix :exc:`FileNotFoundError` during
``import requests`` in a Mitogen child
* :gh:issue:`1403` :mod:`mitogen`: Add initial support for
:py:class:`importlib.resource.abc.ResourceReader` protocol
* :gh:issue:`1407` :mod:`mitogen`: Fix :exc:`AttributeError` in
:mod:`mitogen.profiler`
v0.3.36 (2025-12-01)
--------------------
* :gh:issue:`1237` :mod:`mitogen`: Re-declare Python 2.4 compatibility
* :gh:issue:`1385` :mod:`ansible_mitogen`: Remove a use of
``ansible.module_utils.six``
* :gh:issue:`1354` docs: Document Ansible 13 (ansible-core 2.20) support
* :gh:issue:`1354` :mod:`mitogen`: Clarify error message when a module
request would be refused by allow or deny listing
* :gh:issue:`1348` :mod:`mitogen`: Fix hanging process with 100% CPU usage
v0.3.35 (2025-12-01)
--------------------
* :gh:issue:`1132` :mod:`ansible_mitogen` During intrepreter discovery use
Ansible ``INTERPRETER_PYTHON_FALLBACK`` config as list of candidates
v0.3.34 (2025-11-27)
--------------------
* :gh:issue:`1118` CI: Use 2025.02 test images, keeping same OS releases
* :gh:issue:`1358` CI: Bump deprecated macOS 13 runner to macOS 15
* :gh:issue:`1118` CI: Add OS release coverage: AlmaLinux 9
* :gh:issue:`1118` CI: Add OS release coverage: CentOS 5
* :gh:issue:`1118` CI: Add OS release coverage: Debian 12
* :gh:issue:`1118` CI: Add OS release coverage: Ubuntu 22.04, Ubuntu 24.04
* :gh:issue:`1124` :mod:`mitogen`: Log why a module is sent or not sent by
:class:`mitogen.master.ModuleResponder`
* :gh:issue:`1124` :mod:`ansible_mitogen`: Speedup startup by not sending
``__main__`` as a related module
v0.3.33 (2025-11-22)
--------------------
* :gh:issue:`1354` :mod:`ansible_mitogen`: ansible_mitogen: Ansible 13
(ansible-core 2.20) support
v0.3.32 (2025-11-21)
--------------------
* :gh:issue:`1243` :mod:`mitogen`: Pass first stage, context name, & preamble
size as seperate **argv** arguments
* :gh:issue:`1218` :mod:`ansible_mitogen`: Remove maximum Ansible version check
* :gh:issue:`1260` CI: Remove integration of retired lgtm.com
v0.3.31 (2025-11-05)
--------------------
* :gh:issue:`1350` :mod:`ansible_mitogen`: Fix regression when loading plugins
from ``/custom/path/to/mitogen``
v0.3.30 (2025-10-30)
--------------------
* :gh:issue:`1266` Import cleanups
* :gh:issue:`1266` :mod:`ansible_mitogen`: De-duplicate sys.path manipulations
* :gh:issue:`1344` Correct SPDX license declarations
* :gh:issue:`1344` Declare BSD-3-Clause SPDX license in package metadata
* :gh:issue:`1344` :mod:`mitogen`: Use :py:func:`logging.makeLogRecord`
v0.3.29 (2025-09-18)
--------------------
* :gh:issue:`1287` Python 3.14 support
* :gh:issue:`1287` tests: Bump dependencies
v0.3.28 (2025-09-17)
--------------------
* :gh:issue:`1306` :mod:`ansible_mitogen`: Fix non-blocking IO errors in
first stage of bootstrap
* :gh:issue:`1306` CI: Report sudo version on Ansible targets
* :gh:issue:`1306` CI: Move sudo test users defaults into ``/etc/sudoers.d``
* :gh:issue:`1306` preamble_size: Fix variability of measured command size
* :gh:issue:`1306` tests: Count bytes written in ``stdio_test.StdIOTest``
* :gh:issue:`1306` tests: Check stdio is blocking in sudo contexts
* :gh:issue:`1327` :mod:`ansible_mitogen`: Add FreeIPA client modules to the
always-fork list
v0.3.27 (2025-08-20)
--------------------
* :gh:issue:`1325` :mod:`mitogen`: Refactor
``mitogen.master.scan_code_imports()`` as
:func:`mitogen.import.codeobj_imports` and speed-up by 1.5 - 2.5 x
* :gh:issue:`1329` CI: Refactor and de-duplicate Github Actions workflow
* :gh:issue:`1315` CI: macOS: Increase failed logins limit of test users
* :gh:issue:`1325` tests: Improve ``master_test.ScanCodeImportsTest`` coverage
v0.3.26 (2025-08-04)
--------------------
* :gh:issue:`1318` CI: Abbreviate Github Actions job names
* :gh:issue:`1309` :mod:`ansible_mitogen`: Fix ``become_method: doas``
* :gh:issue:`712` :mod:`mitogen`: Fix :exc:`BlockingIOError` & ``EAGAIN``
errors in subprocesses that write to stdio
v0.3.25 (2025-07-29)
--------------------
Ansible 12 has deprecated third-party strategy plugins. This is currently
how Mitogen integrates with Ansible (e.g. `ANSIBLE_STRATEGY=mitogen_linear`).
Running Ansible 12 + Mitogen will currently print a deprecation warning
[DEPRECATION WARNING]: Use of strategy plugins not included in
ansible.builtin are deprecated [...]. This feature will be removed from
ansible-core in a future release.
Ansible + Mitogen will still work for now. Mitogen is considering alternatives
to strategy plugins under :gh:issue:`1278`.
* :gh:issue:`1258` Ansible 12 (ansible-core 2.19) support
v0.3.25b1 (2025-07-21)
----------------------
* :gh:issue:`1303` CI: Switch to archived Debian 10 (buster) apt repository
v0.3.25a3 (2025-07-02)
----------------------
* :gh:issue:`1285` CI: use `result_format = yaml` for Ansible test output,
instead of deprecated `stdout_callback = yaml`
* :gh:issue:`1293` CI: Fix ``ansible_version`` comparisons when an Ansible
release candidate is under test
* :gh:issue:`1275` CI: Test ``ansible_ssh_password`` behaviour without
``sshpass`` installed
* :gh:issue:`1282` :mod:`ansible_mitogen`: Support ``ANSIBLE_SSH_VERBOSITY``
with Ansible 12
v0.3.25a2 (2025-06-21)
----------------------
* :gh:issue:`1274` :mod:`ansible_mitogen`: Replace use of `jsonify()`, which
is deprecated form Ansible 12 (ansible-core 2.19)
v0.3.25a1 (2025-06-05)
----------------------
* :gh:issue:`1258` Initial Ansible 12 (ansible-core 2.19) support
* :gh:issue:`1258` :mod:`ansible_mitogen`: Initial Ansible datatag support
(:gh:anspull:`84621`)
* :gh:issue:`1258` :mod:`ansible_mitogen`: Ansible 12 (ansible-core 2.19) test
jobs
v0.3.24 (2025-05-29)
--------------------
* :gh:issue:`1268` :mod:`mitogen` Only close stdin, stdout, and stderr file
descriptors (0, 1, and 2) if they were open at process startup.
v0.3.23 (2025-04-28)
--------------------
* :gh:issue:`1121` :mod:`mitogen`: Log skipped :py:mod:`termios` attributes
* :gh:issue:`1238` packaging: Avoid :py:mod:`ast`, requires Python = 2.6
* :gh:issue:`1118` CI: Statically specify test usernames and group names
* :gh:issue:`1118` CI: Don't copy SSH private key to temporary dir
* :gh:issue:`1118` CI: Don't share temporary directory between test groupings
* :gh:issue:`1256` CI: Upgrade Github jobs from Ubuntu 20.04 to 22.04 & 24.04
* :gh:issue:`1263` packaging: Fix InvalidVersion in release versions
v0.3.22 (2025-02-04)
--------------------
* :gh:issue:`1213` tests: Enable default Python warnings
* :gh:issue:`1111` :mod:`mitogen`: Replace uses of deprecated
:py:func:`pkgutil.find_loader`
* :gh:issue:`1213` :mod:`mitogen`: Fix unclosed file in first stage
* :gh:issue:`1213` tests: Fix unclosed file in fd_check script
* :gh:issue:`1213` :mod:`ansible_mitogen`: Don't redeclare Ansible interpreter
discovery attributes
* :gh:issue:`1213` :mod:`ansible_mitogen`: Rename Mitogen interpreter discovery
attributes
* :gh:issue:`1213` :mod:`ansible_mitogen`: Decouple possible_pythons order &
error handling
* :gh:issue:`1213` :mod:`ansible_mitogen`: Return ``stderr_lines`` from
``_low_level_execute_command()``
* :gh:issue:`1227` tests: Name transport_config tests that use ``mitogen_via``
* :gh:issue:`1143` :mod:`ansible_mitogen`: Fix dnf module include for dnf.cli
* :gh:issue:`1234` :mod:`ansible_mitogen`: Fix :exc:`TypeError` in
:func:`ansible_mitogen.target.set_file_owner`
v0.3.21 (2025-01-20)
--------------------
* :gh:issue:`1209` docs: Fix Netlify build of website
* :gh:issue:`1216` :mod:`ansible_mitogen`: Add all ansible_freeipa modules to
the always-fork list.
* :gh:issue:`766` :mod:`ansible_mitogen`: Fix ""could not recover task_vars"
and "get_with_context_result object has no attribute _create_control_path"
when using ``kubectl``, ``netconf``, or ``network_cli`` connection plugins.
v0.3.20 (2025-01-07)
--------------------
* :gh:issue:`1079` :mod:`ansible_mitogen`: Fix :ans:mod:`wait_for_connection`
timeout with templated ``ansible_python_interpreter``
* :gh:issue:`1079` :mod:`ansible_mitogen`: Fix templated python interpreter
with `meta: reset_connection`
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated connection timeout
(e.g. ``ansible_timeout``).
* :gh:issue:`740` :mod:`ansible_mitogen`: Respect ``interpreter_python``
in ``ansible.cfg`` and ``ANSIBLE_PYTHON_INTERPRETER`` environment variable.
v0.3.19 (2024-12-02)
--------------------
* :gh:issue:`1129` :mod:`ansible_mitogen`: Ansible 11 support
v0.3.18 (2024-11-07)
--------------------
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated become method
(e.g. ``ansible_become_method``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated become flag
(e.g. ``ansible_become_method``, ``become`` keyword).
v0.3.17 (2024-11-07)
--------------------
* :gh:issue:`1182` CI: Fix incorrect world readable/writable file permissions
on SSH key ``mitogen__has_sudo_pubkey.key`` during Ansible tests.
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated SSH private key file
(e.g. ``ansible_private_key_file``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated SSH host key checking
(e.g. ``ansible_host_key_checking``, ``ansible_ssh_host_key_checking``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated host address
(e.g. ``ansible_host``, ``ansible_ssh_host``)
* :gh:issue:`1184` Test templated SSH host key checking in task vars
v0.3.16 (2024-11-05)
--------------------
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated become executable
(e.g. ``become_exe``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated become executable
arguments (e.g. ``become_flags``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated ssh executable
(``ansible_ssh_executable``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Fixed templated connection options
during a ``meta: reset_connection`` task.
* :gh:issue:`1129` CI: Migrated macOS 12 runners to macOS 13, due to EOL.
v0.3.15 (2024-10-28)
--------------------
* :gh:issue:`905` :mod:`ansible_mitogen`: Support templated SSH command
arguments (e.g. ``ansible_ssh_args``, ``ansible_ssh_extra_args``).
* :gh:issue:`692` tests: Fix and re-enable several sudo tests
* :gh:issue:`1083` :mod:`ansible_mitogen`: Support templated become password
(e.g. ``ansible_become_pass``, ``ansible_sudo_pass``)
v0.3.14 (2024-10-16)
--------------------
* :gh:issue:`1159` CI: Reduce number of Jobs by parameterizing Mitogen Docker
SSH tests
* :gh:issue:`1083` :mod:`ansible_mitogen`: Support templated become username.
v0.3.13 (2024-10-09)
--------------------
* :gh:issue:`1138` CI: Complete migration from Azure DevOps Pipelines to
GitHub Actions
* :gh:issue:`1116` :mod:`ansible_mitogen`: Support for templated variable
`ansible_ssh_user`.
* :gh:issue:`978` :mod:`ansible_mitogen`: Support templated Ansible SSH port.
* :gh:issue:`1073` Python 3.13 support
v0.3.12 (2024-10-07)
--------------------
* :gh:issue:`1106` :mod:`ansible_mitogen`: Support for `ansible_ssh_password`
connection variable, and templated SSH connection password.
* :gh:issue:`1136` tests: Improve Ansible fail_msg formatting.
* :gh:issue:`1137` tests: Ignore inventory files of inactive tests & benchmarks
* :gh:issue:`1138` CI: Add re-actors/alls-green GitHub Actions job to simplify
branch protections configuration.
v0.3.11 (2024-09-30)
--------------------
* :gh:issue:`1127` :mod:`mitogen`: Consolidate mitogen backward compatibility
fallbacks and polyfills into :mod:`mitogen.core`
* :gh:issue:`1127` :mod:`ansible_mitogen`: Remove backward compatibility
fallbacks for Python 2.4 & 2.5.
* :gh:issue:`1127` :mod:`ansible_mitogen`: Remove fallback imports for Ansible
releases before 2.10
* :gh:issue:`1127` :mod:`ansible_mitogen`: Consolidate Python 2 & 3
compatibility
* :gh:issue:`1128` CI: Start migration from Azure DevOps to GitHub Actions
v0.3.10 (2024-09-20)
--------------------
* :gh:issue:`950` Fix Solaris/Illumos/SmartOS compatibility with become
* :gh:issue:`1087` Fix :exc:`mitogen.core.StreamError` when Ansible template
module is called with a ``dest:`` filename that has an extension
* :gh:issue:`1110` Fix :exc:`mitogen.core.StreamError` when Ansible copy
module is called with a file larger than 124 kibibytes
(:data:`ansible_mitogen.connection.Connection.SMALL_FILE_LIMIT`)
* :gh:issue:`905` Initial support for templated ``ansible_ssh_args``,
``ansible_ssh_common_args``, and ``ansible_ssh_extra_args`` variables.
NB: play or task scoped variables will probably still fail.
* :gh:issue:`694` CI: Fixed a race condition and some resource leaks causing
some of intermittent failures when running the test suite.
v0.3.9 (2024-08-13)
-------------------
* :gh:issue:`1097` Respect `ansible_facts.discovered_interpreter_python` when
executing non new-style modules (e.g. JSONARGS style, WANT_JSON style).
* :gh:issue:`1074` Support Ansible 10 (ansible-core 2.17)
v0.3.8 (2024-07-30)
-------------------
* :gh:issue:`952` Fix Ansible `--ask-become-pass`, add test coverage
* :gh:issue:`957` Fix Ansible exception when executing against 10s of hosts
"ValueError: filedescriptor out of range in select()"
* :gh:issue:`1066` Support Ansible `ansible_host_key_checking` & `ansible_ssh_host_key_checking`
* :gh:issue:`1090` CI: Migrate macOS integration tests to macOS 12, drop Python 2.7 jobs
v0.3.7 (2024-04-08)
-------------------
* :gh:issue:`1021` Support for Ansible 8 (ansible-core 2.15)
* tests: Replace uses of ``include:`` & ``import:``, unsupported in Ansible 9
* :gh:issue:`1053` Support for Ansible 9 (ansible-core 2.16)
v0.3.6 (2024-04-04)
-------------------
* :gh:issue:`974` Support Ansible 7
* :gh:issue:`1046` Raise :py:exc:`TypeError` in :func:`<mitogen.util.cast()>`
when casting a string subtype to `bytes()` or `str()` fails. This is
potentially an API breaking change. Failures previously passed silently.
* :gh:issue:`1046` Add :func:`<ansible_mitogen.util.cast()>`, to cast
:class:`ansible.utils.unsafe_proxy.AnsibleUnsafe` objects in Ansible 7+.
v0.3.5 (2024-03-17)
-------------------
* :gh:issue:`987` Support Python 3.11
* :gh:issue:`885` Fix :py:exc:`PermissionError` in :py:mod:`importlib` when
becoming an unprivileged user with Python 3.x
* :gh:issue:`1033` Support `PEP 451 <https://peps.python.org/pep-0451/>`_,
required by Python 3.12
* :gh:issue:`1033` Support Python 3.12
v0.3.4 (2023-07-02)
-------------------
* :gh:issue:`929` Support Ansible 6 and ansible-core 2.13
* :gh:issue:`832` Fix runtime error when using the ansible.builtin.dnf module multiple times
* :gh:issue:`925` :class:`ansible_mitogen.connection.Connection` no longer tries to close the
connection on destruction. This is expected to reduce cases of `mitogen.core.Error: An attempt
was made to enqueue a message with a Broker that has already exitted`. However it may result in
resource leaks.
* :gh:issue:`659` Removed :mod:`mitogen.compat.simplejson`, not needed with Python 2.7+, contained Python 3.x syntax errors
* :gh:issue:`983` CI: Removed PyPI faulthandler requirement from tests
* :gh:issue:`1001` CI: Fixed Debian 9 & 11 tests
v0.3.3 (2022-06-03)
-------------------
* :gh:issue:`906` Support packages dynamically inserted into sys.modules, e.g. `distro` >= 1.7.0 as `ansible.module_utils.distro`.
* :gh:issue:`918` Support Python 3.10
* :gh:issue:`920` Support Ansible :ans:conn:`~podman` connection plugin
* :gh:issue:`836` :func:`mitogen.utils.with_router` decorator preserves the docstring in addition to the name.
* :gh:issue:`936` :ans:mod:`fetch` no longer emits `[DEPRECATION WARNING]: The '_remote_checksum()' method is deprecated.`
v0.3.2 (2022-01-12)
-------------------
* :gh:issue:`891` Correct `Framework :: Ansible` Trove classifier
v0.3.1 (unreleased)
-------------------
* :gh:issue:`874` Support for Ansible 5 (ansible-core 2.12)
* :gh:issue:`774` Fix bootstrap failures on macOS 11.x and 12.x, involving Python 2.7 wrapper
* :gh:issue:`834` Support for Ansible 3 and 4 (ansible-core 2.11)
@ -37,7 +498,7 @@ v0.3.0 (2021-11-24)
-------------------
This release separates itself from the v0.2.X releases. Ansible's API changed too much to support backwards compatibility so from now on, v0.2.X releases will be for Ansible < 2.10 and v0.3.X will be for Ansible 2.10+.
`See here for details <https://github.com/dw/mitogen pull/715#issuecomment-750697248>`_.
`See here for details <https://github.com/mitogen-hq/mitogen/pull/715#issuecomment-750697248>`_.
* :gh:issue:`827` NewStylePlanner: detect `ansible_collections` imports
* :gh:issue:`770` better check for supported Ansible version
@ -58,7 +519,7 @@ v0.2.10 (2021-11-24)
* :gh:issue:`756` ssh connections with `check_host_keys='accept'` would
timeout, when using recent OpenSSH client versions.
* :gh:issue:`758` fix initilialisation of callback plugins in test suite, to address a `KeyError` in
:method:`ansible.plugins.callback.CallbackBase.v2_runner_on_start`
:py:meth:`ansible.plugins.callback.CallbackBase.v2_runner_on_start`
* :gh:issue:`775` Test with Python 3.9
* :gh:issue:`775` Add msvcrt to the default module deny list
@ -144,7 +605,7 @@ Mitogen for Ansible
:linux:man7:`unix` sockets across privilege domains.
* :gh:issue:`467`: an incompatibility running Mitogen under `Molecule
<https://molecule.readthedocs.io/en/stable/>`_ was resolved.
<https://ansible.readthedocs.io/projects/molecule/>`_ was resolved.
* :gh:issue:`547`, :gh:issue:`598`: fix a deadlock during initialization of
connections, ``async`` tasks, tasks using custom :mod:`module_utils`,
@ -1196,9 +1657,8 @@ Core Library
parameter may specify an argument vector prefix rather than a string program
path.
* :gh:issue:`300`: the broker could crash on
OS X during shutdown due to scheduled `kqueue
<https://www.freebsd.org/cgi/man.cgi?query=kqueue>`_ filter changes for
* :gh:issue:`300`: the broker could crash on OS X during shutdown due to
scheduled :freebsd:man2:`kqueue` filter changes for
descriptors that were closed before the IO loop resumes. As a temporary
workaround, kqueue's bulk change feature is not used.

@ -1,10 +1,24 @@
import os
import re
import sys
sys.path.append('..')
sys.path.append('.')
import mitogen
VERSION = '%s.%s.%s' % mitogen.__version__
def changelog_version(path, encoding='utf-8'):
"Return the 1st *stable* (not pre, dev) version in the changelog"
# See also grep_version() in setup.py
# e.g. "0.1.2, (1999-12-31)\n"
version_pattern = re.compile(
r'^v(?P<version>\d+\.\d+\.\d+) \((?P<date>\d\d\d\d-\d\d-\d\d)\)$',
re.MULTILINE,
)
with open(path, encoding=encoding) as f:
match = version_pattern.search(f.read())
return match.group('version')
VERSION = changelog_version('changelog.rst')
author = u'Network Genomics'
copyright = u'2021, the Mitogen authors'
@ -18,7 +32,6 @@ html_show_copyright = False
html_show_sourcelink = False
html_show_sphinx = False
html_sidebars = {'**': ['globaltoc.html', 'github.html']}
html_additional_pages = {'ansible': 'ansible.html'}
html_static_path = ['_static']
html_theme = 'alabaster'
html_theme_options = {
@ -44,16 +57,25 @@ version = VERSION
domainrefs = {
'gh:commit': {
'text': '%s',
'url': 'https://github.com/dw/mitogen/commit/%s',
'url': 'https://github.com/mitogen-hq/mitogen/commit/%s',
},
'gh:issue': {
'text': '#%s',
'url': 'https://github.com/dw/mitogen/issues/%s',
'url': 'https://github.com/mitogen-hq/mitogen/issues/%s',
},
'gh:pull': {
'text': '#%s',
'url': 'https://github.com/dw/mitogen/pull/%s',
'url': 'https://github.com/mitogen-hq/mitogen/pull/%s',
},
'gh:ansissue': {
'text': 'Ansible #%s',
'url': 'https://github.com/ansible/ansible/issues/%s',
},
'gh:anspull': {
'text': 'Ansible #%s',
'url': 'https://github.com/ansible/ansible/pull/%s',
},
'ans:mod': {
'text': '%s module',
'url': 'https://docs.ansible.com/ansible/latest/modules/%s_module.html',
@ -64,30 +86,36 @@ domainrefs = {
},
'freebsd:man2': {
'text': '%s(2)',
'url': 'https://www.freebsd.org/cgi/man.cgi?query=%s',
'url': 'https://man.freebsd.org/cgi/man.cgi?query=%s',
},
'linux:man1': {
'text': '%s(1)',
'url': 'http://man7.org/linux/man-pages/man1/%s.1.html',
'url': 'https://man7.org/linux/man-pages/man1/%s.1.html',
},
'linux:man2': {
'text': '%s(2)',
'url': 'http://man7.org/linux/man-pages/man2/%s.2.html',
'url': 'https://man7.org/linux/man-pages/man2/%s.2.html',
},
'linux:man3': {
'text': '%s(3)',
'url': 'http://man7.org/linux/man-pages/man3/%s.3.html',
'url': 'https://man7.org/linux/man-pages/man3/%s.3.html',
},
'linux:man7': {
'text': '%s(7)',
'url': 'http://man7.org/linux/man-pages/man7/%s.7.html',
'url': 'https://man7.org/linux/man-pages/man7/%s.7.html',
},
}
# > ## Official guidance
# > Query PyPIs JSON API to determine where to download files from.
# > ## Predictable URLs
# > You can use our conveyor service to fetch this file, which exists for
# > cases where using the API is impractical or impossible.
# > -- https://warehouse.pypa.io/api-reference/integration-guide.html#predictable-urls
rst_epilog = """
.. |mitogen_version| replace:: %(VERSION)s
.. |mitogen_url| replace:: `mitogen-%(VERSION)s.tar.gz <https://networkgenomics.com/try/mitogen-%(VERSION)s.tar.gz>`__
.. |mitogen_url| replace:: `mitogen-%(VERSION)s.tar.gz <https://files.pythonhosted.org/packages/source/m/mitogen/mitogen-%(VERSION)s.tar.gz>`__
""" % locals()

@ -116,6 +116,7 @@ sponsorship and outstanding future-thinking of its early adopters.
<ul>
<li>Alex Willmer</li>
<li><a href="https://github.com/momiji">Christian Bourgeois </a></li>
<li><a href="https://underwhelm.net/">Dan Dorman</a> &mdash; - <em>When I truly understand my enemy … then in that very moment I also love him.</em></li>
<li>Daniel Foerster</li>
<li><a href="https://www.deps.co/">Deps</a> &mdash; <em>Private Maven Repository Hosting for Java, Scala, Groovy, Clojure</em></li>
@ -123,13 +124,22 @@ sponsorship and outstanding future-thinking of its early adopters.
<li><a href="https://www.epartment.nl/">Epartment</a></li>
<li><a href="http://andrianaivo.org/">Fidy Andrianaivo</a> &mdash; <em>never let a human do an ansible job ;)</em></li>
<li><a href="https://www.channable.com">rkrzr</a></li>
<li><a href="https://github.com/Nihlus">Jarl Gullberg</a></li>
<li>jgadling</li>
<li>John F Wall &mdash; <em>Making Ansible Great with Massive Parallelism</em></li>
<li><a href="https://github.com/jrosser">Jonathan Rosser</a></li>
<li><a href="https://github.com/jmkeyes">Joshua M. Keyes</a></li>
<li>KennethC</li>
<li><a href="https://github.com/lberruti">Luca Berruti</li>
<li>Lewis Bellwood &mdash; <em>Happy to be apart of a great project.</em></li>
<li>luto</li>
<li><a href="https://github.com/markafarrell">@markafarrell</a></li>
<li><a href="https://mayeu.me/">Mayeu a.k.a Matthieu Maury</a></li>
<li><a href="https://github.com/madsi1m">Michael D'Silva</a></li>
<li><a href="https://github.com/mordekasg">mordek</a></li>
<li><a href="https://twitter.com/nathanhruby">@nathanhruby</a></li>
<li><a href="https://github.com/opoplawski">Orion Poplawski</a></li>
<li><a href="https://github.com/philfry">Philippe Kueck</a></li>
<li><a href="http://pageflows.com/">Ramy</a></li>
<li>Scott Vokes</li>
<li><a href="https://twitter.com/sirtux">Tom Eichhorn</a></li>
@ -138,4 +148,5 @@ sponsorship and outstanding future-thinking of its early adopters.
<li>randy &mdash; <em>desperate for automation</em></li>
<li>Michael & Vicky Twomey-Lee</li>
<li><a href="http://www.wezm.net/">Wesley Moore</a></li>
<li><a href="https://github.com/baryluk">Witold Baryluk</a></li>
</ul>

@ -201,7 +201,7 @@ nested.py:
print('Connect local%d via %s' % (x, context))
context = router.local(via=context, name='local%d' % x)
context.call(os.system, 'pstree -s python -s mitogen')
context.call(subprocess.check_call, ['pstree', '-s', 'python', '-s', 'mitogen'])
Output:

@ -27,14 +27,13 @@ Python Command Line
###################
The Python command line sent to the host is a :mod:`zlib`-compressed [#f2]_ and
base64-encoded copy of the :py:meth:`mitogen.master.Stream._first_stage`
function, which has been carefully optimized to reduce its size. Prior to
compression and encoding, ``CONTEXT_NAME`` is replaced with the desired context
name in the function's source code.
base64-encoded copy of :py:meth:`mitogen.parent.Connection._first_stage`,
which is carefully written to maximize it compatibility and minimize its size.
A simplified illustration of the bootstrap command is
.. code::
python -c 'exec "xxx".decode("base64").decode("zlib")'
python -c 'exec(sys.argv[1].decode("base64").decode("zlib"))' <base64> ...
The command-line arranges for the Python interpreter to decode the base64'd
component, decompress it and execute it as Python code. Base64 is used since
@ -71,8 +70,8 @@ of the large base64-encoded first stage parameter, and to replace **argv[0]**
with something descriptive.
After configuring its ``stdin`` to point to the read end of the pipe, the
parent half of the fork re-executes Python, with **argv[0]** taken from the
``CONTEXT_NAME`` variable earlier substituted into its source code. As no
fork parent re-executes Python with **argv[0]** composed of the Python
interpreter path and a remote name supplied by the Mitogen parent. As no
arguments are provided to this new execution of Python, and since ``stdin`` is
connected to a pipe (whose write end is connected to the first stage), the
Python interpreter begins reading source code to execute from the pipe
@ -1038,7 +1037,7 @@ receive items in the order they are requested, as they become available.
Mitogen enables SSH compression by default, there are circumstances where
disabling SSH compression is desirable, and many scenarios for future
connection methods where transport-layer compression is not supported at
all.
all.
.. [#f2] Compression may seem redundant, however it is basically free and reducing IO
is always a good idea. The 33% / 200 byte saving may mean the presence or

@ -26,7 +26,7 @@ and efficient low-level API on which tools like `Salt`_, `Ansible`_, or
`Fabric`_ can be built, and while the API is quite friendly and comparable to
`Fabric`_, ultimately it is not intended for direct use by consumer software.
.. _Salt: https://docs.saltstack.com/en/latest/
.. _Salt: https://docs.saltproject.io/en/latest/
.. _Ansible: https://docs.ansible.com/
.. _Fabric: https://www.fabfile.org/
@ -101,7 +101,7 @@ to your network topology**.
container='billing0',
)
internal_box.call(os.system, './run-nightly-billing.py')
internal_box.call(subprocess.check_call, ['./run-nightly-billing.py'])
The multiplexer also ensures the remote process is terminated if your Python
program crashes, communication is lost, or the application code running in the
@ -250,7 +250,7 @@ After:
"""
Install our application.
"""
os.system('tar zxvf app.tar.gz')
subprocess.check_call(['tar', 'zxvf', 'app.tar.gz'])
context.call(install_app)
@ -258,7 +258,7 @@ Or even:
.. code-block:: python
context.call(os.system, 'tar zxvf app.tar.gz')
context.call(subprocess.check_call, ['tar', 'zxvf', 'app.tar.gz'])
Exceptions raised by function calls are propagated back to the parent program,
and timeouts can be configured to ensure failed calls do not block progress of
@ -332,12 +332,16 @@ a large fleet of machines, or to alert the parent of unexpected state changes.
Compatibility
#############
Mitogen is compatible with **Python 2.4** released November 2004, making it
``mitogen.*`` is compatible with Python 2.4 - 2.7 and 3.6 onward; making it
suitable for managing a fleet of potentially ancient corporate hardware, such
as Red Hat Enterprise Linux 5, released in 2007.
Every combination of Python 3.x/2.x parent and child should be possible,
however at present only Python 2.4, 2.6, 2.7 and 3.6 are tested automatically.
Every combination of Python 3.x/2.x parent and child should be possible.
Automated testing cannot cover every combination, automated testing tries to
cover the extemities (e.g. Python 3.14 parent -> Python 2.4 child).
``ansible_mitogen.*`` is compatible with Python 2.7 and 3.6 onward; making it
suitable for Ansible 2.10 onward.
Zero Dependencies

@ -0,0 +1,2 @@
[build.environment]
PYTHON_VERSION = "3.8"

@ -1,3 +1,6 @@
docutils<0.18
Jinja2<3
MarkupSafe<2.1
Sphinx==2.1.2; python_version > '3.0'
sphinxcontrib-programoutput==0.14; python_version > '3.0'
alabaster==0.7.10; python_version > '3.0'

@ -8,14 +8,14 @@ Usage:
Where:
<hostname> Hostname to install to.
"""
import os
import subprocess
import sys
import mitogen
def install_app():
os.system('tar zxvf my_app.tar.gz')
subprocess.check_call(['tar', 'zxvf', 'my_app.tar.gz'])
@mitogen.main()

@ -119,7 +119,7 @@ def _chroot(path):
os.chroot(path)
class Operations(fuse.Operations): # fuse.LoggingMixIn,
class Operations(fuse.Operations): # fuse.LoggingMixIn,
def __init__(self, host, path='.'):
self.host = host
self.root = path

@ -61,7 +61,7 @@ def child_main(sender, delay):
Executed on the main thread of the Python interpreter running on each
target machine, Context.call() from the master. It simply sends the output
of the UNIX 'ps' command at regular intervals toward a Receiver on master.
:param mitogen.core.Sender sender:
The Sender to use for delivering our result. This could target
anywhere, but the sender supplied by the master simply causes results

@ -10,7 +10,6 @@ from __future__ import print_function
import hashlib
import io
import os
import spwd
import mitogen.core
import mitogen.master
@ -57,21 +56,6 @@ def streamy_download_file(context, path):
}
def get_password_hash(username):
"""
Fetch a user's password hash.
"""
try:
h = spwd.getspnam(username)
except KeyError:
return None
# mitogen.core.Secret() is a Unicode subclass with a repr() that hides the
# secret data. This keeps secret stuff out of logs. Like blobs, secrets can
# also be serialized.
return mitogen.core.Secret(h)
def md5sum(path):
"""
Return the MD5 checksum for a file.

@ -35,7 +35,7 @@ be expected. On the slave, it is built dynamically during startup.
#: Library version as a tuple.
__version__ = (0, 3, 1, 'dev0')
__version__ = (0, 3, 40, 'dev')
#: This is :data:`False` in slave contexts. Previously it was used to prevent
@ -106,7 +106,8 @@ def main(log_level='INFO', profiling=_default_profiling):
def wrapper(func):
if func.__module__ != '__main__':
return func
import mitogen.parent
import mitogen.core
import mitogen.master
import mitogen.utils
if profiling:
mitogen.core.enable_profiling()

@ -30,7 +30,6 @@
import logging
import mitogen.core
import mitogen.parent

File diff suppressed because it is too large Load Diff

@ -95,15 +95,14 @@ Sequence:
import getopt
import inspect
import os
import pty
import shutil
import socket
import subprocess
import sys
import tempfile
import threading
import mitogen.core
import mitogen.master
import mitogen.parent
from mitogen.core import LOG, IOLOG
@ -179,6 +178,9 @@ class Process(object):
self.control_handle = router.add_handler(self._on_control)
self.stdin_handle = router.add_handler(self._on_stdin)
self.pump = IoPump.build_stream(router.broker)
for fp in stdin, stdout:
fd = fp.fileno()
mitogen.core.set_blocking(fd, False)
self.pump.accept(stdin, stdout)
self.stdin = None
self.control = None
@ -200,7 +202,7 @@ class Process(object):
def _on_stdin(self, msg):
if msg.is_dead:
IOLOG.debug('%r._on_stdin() -> %r', self, data)
IOLOG.debug('%r._on_stdin() -> %r', self, msg)
self.pump.protocol.close()
return
@ -355,8 +357,9 @@ def _fakessh_main(dest_context_id, econtext):
control_handle, stdin_handle)
process = Process(econtext.router,
stdin=os.fdopen(1, 'w+b', 0),
stdout=os.fdopen(0, 'r+b', 0))
stdin=os.fdopen(pty.STDOUT_FILENO, 'w+b', 0),
stdout=os.fdopen(pty.STDIN_FILENO, 'r+b', 0),
)
process.start_master(
stdin=mitogen.core.Sender(dest, stdin_handle),
control=mitogen.core.Sender(dest, control_handle),
@ -418,10 +421,11 @@ def run(dest, router, args, deadline=None, econtext=None):
fakessh = mitogen.parent.Context(router, context_id)
fakessh.name = u'fakessh.%d' % (context_id,)
sock1, sock2 = socket.socketpair()
sock1, sock2 = mitogen.core.socketpair()
stream = mitogen.core.Stream(router, context_id)
stream.name = u'fakessh'
mitogen.core.set_blocking(sock1.fileno(), False)
stream.accept(sock1, sock1)
router.register(fakessh, stream)
@ -437,7 +441,7 @@ def run(dest, router, args, deadline=None, econtext=None):
fp.write(inspect.getsource(mitogen.core))
fp.write('\n')
fp.write('ExternalContext(%r).main()\n' % (
_get_econtext_config(context, sock2),
_get_econtext_config(econtext, sock2),
))
finally:
fp.close()

@ -211,7 +211,7 @@ class Connection(mitogen.parent.Connection):
on_fork()
if self.options.on_fork:
self.options.on_fork()
mitogen.core.set_block(childfp.fileno())
mitogen.core.set_blocking(childfp.fileno(), True)
childfp.send(b('MITO002\n'))

@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: 2025 Mitogen authors <https://github.com/mitogen-hq>
# SPDX-License-Identifier: BSD-3-Clause
# !mitogen: minify_safe
import sys
if sys.version_info >= (3, 14):
from mitogen.imports._py314 import _code_imports
elif sys.version_info >= (3, 6):
from mitogen.imports._py36 import _code_imports
elif sys.version_info >= (2, 5):
from mitogen.imports._py2 import _code_imports_py25 as _code_imports
else:
from mitogen.imports._py2 import _code_imports_py24 as _code_imports
def codeobj_imports(co):
"""
Yield (level, modname, names) tuples by scanning the code object `co`.
Top level `import mod` & `from mod import foo` statements are matched.
Those inside a `class ...` or `def ...` block are currently skipped.
>>> co = compile('import a, b; from c import d, e as f', '<str>', 'exec')
>>> list(codeobj_imports(co)) # doctest: +ELLIPSIS
[(..., 'a', ()), (..., 'b', ()), (..., 'c', ('d', 'e'))]
:return:
Generator producing `(level, modname, names)` tuples, where:
* `level`:
-1 implicit relative (Python 2.x default)
0 absolute (Python 3.x, `from __future__ import absolute_import`)
>0 explicit relative (`from . import a`, `from ..b, import c`)
* `modname`: Name of module to import, or to import `names` from.
* `names`: tuple of names in `from mod import ..`.
"""
return _code_imports(co.co_code, co.co_consts, co.co_names)

@ -0,0 +1,54 @@
# SPDX-FileCopyrightText: 2025 Mitogen authors <https://github.com/mitogen-hq>
# SPDX-License-Identifier: BSD-3-Clause
# !mitogen: minify_safe
import array
import itertools
import opcode
IMPORT_NAME = opcode.opmap['IMPORT_NAME']
LOAD_CONST = opcode.opmap['LOAD_CONST']
def _opargs(code, _have_arg=opcode.HAVE_ARGUMENT):
it = iter(array.array('B', code))
nexti = it.next
for i in it:
if i >= _have_arg:
yield (i, nexti() | (nexti() << 8))
else:
yield (i, None)
def _code_imports_py25(code, consts, names):
it1, it2, it3 = itertools.tee(_opargs(code), 3)
try:
next(it2)
next(it3)
next(it3)
except StopIteration:
return
for oparg1, oparg2, (op3, arg3) in itertools.izip(it1, it2, it3):
if op3 != IMPORT_NAME:
continue
op1, arg1 = oparg1
op2, arg2 = oparg2
if op1 != LOAD_CONST or op2 != LOAD_CONST:
continue
yield (consts[arg1], names[arg3], consts[arg2] or ())
def _code_imports_py24(code, consts, names):
it1, it2 = itertools.tee(_opargs(code), 2)
try:
next(it2)
except StopIteration:
return
for oparg1, (op2, arg2) in itertools.izip(it1, it2):
if op2 != IMPORT_NAME:
continue
op1, arg1 = oparg1
if op1 != LOAD_CONST:
continue
yield (-1, names[arg2], consts[arg1] or ())

@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2025 Mitogen authors <https://github.com/mitogen-hq>
# SPDX-License-Identifier: BSD-3-Clause
# !mitogen: minify_safe
import opcode
IMPORT_NAME = opcode.opmap['IMPORT_NAME']
LOAD_CONST = opcode.opmap['LOAD_CONST']
LOAD_SMALL_INT = opcode.opmap['LOAD_SMALL_INT']
def _code_imports(code, consts, names):
start = 4
while True:
op3_idx = code.find(IMPORT_NAME, start, -1)
if op3_idx < 0:
return
if op3_idx % 2:
start = op3_idx + 1
continue
if code[op3_idx-4] != LOAD_SMALL_INT or code[op3_idx-2] != LOAD_CONST:
start = op3_idx + 2
continue
start = op3_idx + 6
arg1, arg2, arg3 = code[op3_idx-3], code[op3_idx-1], code[op3_idx+1]
yield (arg1, names[arg3], consts[arg2] or ())

@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2025 Mitogen authors <https://github.com/mitogen-hq>
# SPDX-License-Identifier: BSD-3-Clause
# !mitogen: minify_safe
import opcode
IMPORT_NAME = opcode.opmap['IMPORT_NAME']
LOAD_CONST = opcode.opmap['LOAD_CONST']
def _code_imports(code, consts, names):
start = 4
while True:
op3_idx = code.find(IMPORT_NAME, start, -1)
if op3_idx < 0:
return
if op3_idx % 2:
start = op3_idx + 1
continue
if code[op3_idx-4] != LOAD_CONST or code[op3_idx-2] != LOAD_CONST:
start = op3_idx + 2
continue
start = op3_idx + 6
arg1, arg2, arg3 = code[op3_idx-3], code[op3_idx-1], code[op3_idx+1]
yield (consts[arg1], names[arg3], consts[arg2] or ())

@ -28,7 +28,6 @@
# !mitogen: minify_safe
import mitogen.core
import mitogen.parent

@ -28,7 +28,6 @@
# !mitogen: minify_safe
import mitogen.core
import mitogen.parent

@ -28,7 +28,6 @@
# !mitogen: minify_safe
import mitogen.core
import mitogen.parent

@ -35,11 +35,8 @@ be sent to any context that will be used to establish additional child
contexts.
"""
import dis
import errno
import imp
import inspect
import itertools
import logging
import os
import pkgutil
@ -50,21 +47,44 @@ import threading
import types
import zlib
try:
if sys.version_info >= (3, 7):
import importlib.resources
if sys.version_info >= (3, 4):
import importlib.util
from _imp import is_builtin as _is_builtin
def _find_loader(fullname):
try:
maybe_spec = importlib.util.find_spec(fullname)
except (ImportError, AttributeError, TypeError, ValueError):
exc = sys.exc_info()[1]
raise ImportError(*exc.args)
try:
return maybe_spec.loader
except AttributeError:
return None
else:
import imp
from imp import is_builtin as _is_builtin
if sys.version_info >= (2, 5):
from pkgutil import find_loader as _find_loader
else:
from mitogen.compat.pkgutil import find_loader as _find_loader
if sys.version_info >= (2, 7):
import sysconfig
except ImportError:
else:
sysconfig = None
if not hasattr(pkgutil, 'find_loader'):
# find_loader() was new in >=2.5, but the modern pkgutil.py syntax has
# been kept intentionally 2.3 compatible so we can reuse it.
from mitogen.compat import pkgutil
import mitogen
import mitogen.core
import mitogen.imports
import mitogen.minify
import mitogen.parent
from mitogen.core import any
from mitogen.core import b
from mitogen.core import IOLOG
from mitogen.core import LOG
@ -72,20 +92,6 @@ from mitogen.core import str_partition
from mitogen.core import str_rpartition
from mitogen.core import to_text
imap = getattr(itertools, 'imap', map)
izip = getattr(itertools, 'izip', zip)
try:
any
except NameError:
from mitogen.core import any
try:
next
except NameError:
from mitogen.core import next
RLOG = logging.getLogger('mitogen.ctx')
@ -122,7 +128,16 @@ def is_stdlib_name(modname):
"""
Return :data:`True` if `modname` appears to come from the standard library.
"""
if imp.is_builtin(modname) != 0:
# `(_imp|imp).is_builtin()` isn't a documented part of Python's stdlib.
# Returns 1 if modname names a module that is "builtin" to the the Python
# interpreter (e.g. '_sre'). Otherwise 0 (e.g. 're', 'netifaces').
#
# """
# Main is a little special - imp.is_builtin("__main__") will return False,
# but BuiltinImporter is still the most appropriate initial setting for
# its __loader__ attribute.
# """ -- comment in CPython pylifecycle.c:add_main_module()
if _is_builtin(modname) != 0:
return True
module = sys.modules.get(modname)
@ -166,7 +181,7 @@ def get_child_modules(path, fullname):
return [to_text(name) for _, name, _ in pkgutil.iter_modules([mod_path])]
else:
# we loaded some weird package in memory, so we'll see if it has a custom loader we can use
loader = pkgutil.find_loader(fullname)
loader = _find_loader(fullname)
return [to_text(name) for name, _ in loader.iter_modules(None)] if loader else []
@ -232,80 +247,6 @@ if mitogen.is_master:
mitogen.parent._get_core_source = _get_core_source
LOAD_CONST = dis.opname.index('LOAD_CONST')
IMPORT_NAME = dis.opname.index('IMPORT_NAME')
def _getarg(nextb, c):
if c >= dis.HAVE_ARGUMENT:
return nextb() | (nextb() << 8)
if sys.version_info < (3, 0):
def iter_opcodes(co):
# Yield `(op, oparg)` tuples from the code object `co`.
ordit = imap(ord, co.co_code)
nextb = ordit.next
return ((c, _getarg(nextb, c)) for c in ordit)
elif sys.version_info < (3, 6):
def iter_opcodes(co):
# Yield `(op, oparg)` tuples from the code object `co`.
ordit = iter(co.co_code)
nextb = ordit.__next__
return ((c, _getarg(nextb, c)) for c in ordit)
else:
def iter_opcodes(co):
# Yield `(op, oparg)` tuples from the code object `co`.
ordit = iter(co.co_code)
nextb = ordit.__next__
# https://github.com/abarnert/cpython/blob/c095a32f/Python/wordcode.md
return ((c, nextb()) for c in ordit)
def scan_code_imports(co):
"""
Given a code object `co`, scan its bytecode yielding any ``IMPORT_NAME``
and associated prior ``LOAD_CONST`` instructions representing an `Import`
statement or `ImportFrom` statement.
:return:
Generator producing `(level, modname, namelist)` tuples, where:
* `level`: -1 for normal import, 0, for absolute import, and >0 for
relative import.
* `modname`: Name of module to import, or from where `namelist` names
are imported.
* `namelist`: for `ImportFrom`, the list of names to be imported from
`modname`.
"""
opit = iter_opcodes(co)
opit, opit2, opit3 = itertools.tee(opit, 3)
try:
next(opit2)
next(opit3)
next(opit3)
except StopIteration:
return
if sys.version_info >= (2, 5):
for oparg1, oparg2, (op3, arg3) in izip(opit, opit2, opit3):
if op3 == IMPORT_NAME:
op2, arg2 = oparg2
op1, arg1 = oparg1
if op1 == op2 == LOAD_CONST:
yield (co.co_consts[arg1],
co.co_names[arg3],
co.co_consts[arg2] or ())
else:
# Python 2.4 did not yet have 'level', so stack format differs.
for oparg1, (op2, arg2) in izip(opit, opit2):
if op2 == IMPORT_NAME:
op1, arg1 = oparg1
if op1 == LOAD_CONST:
yield (-1, co.co_names[arg2], co.co_consts[arg1] or ())
class ThreadWatcher(object):
"""
Manage threads that wait for another thread to shut down, before invoking
@ -428,15 +369,19 @@ class LogForwarder(object):
if logger is None:
self._cache[logger_name] = logger = logging.getLogger(logger_name)
levelno = int(level_s)
# See logging.Handler.makeRecord()
record = logging.LogRecord(
name=logger.name,
level=int(level_s),
pathname='(unknown file)',
lineno=0,
msg=s,
args=(),
exc_info=None,
record = logging.makeLogRecord(
{
"name": logger.name,
"levelname": logging.getLevelName(levelno),
"levelno": levelno,
"pathname": "(unknown file)",
"lineno": 0,
"msg": s,
"args": (),
"exc_info": None,
}
)
record.mitogen_message = s
record.mitogen_context = self._router.context_by_id(msg.src_id)
@ -453,6 +398,9 @@ class FinderMethod(object):
name according to the running Python interpreter. You'd think this was a
simple task, right? Naive young fellow, welcome to the real world.
"""
def __init__(self):
self.log = LOG.getChild(self.__class__.__name__)
def __repr__(self):
return '%s()' % (type(self).__name__,)
@ -512,42 +460,57 @@ class PkgutilMethod(FinderMethod):
Find `fullname` using :func:`pkgutil.find_loader`.
"""
try:
# If fullname refers to a submodule that's not already imported
# then the containing package is imported.
# Pre-'import spec' this returned None, in Python3.6 it raises
# ImportError.
loader = pkgutil.find_loader(fullname)
loader = _find_loader(fullname)
except ImportError:
e = sys.exc_info()[1]
LOG.debug('%r._get_module_via_pkgutil(%r): %s',
self, fullname, e)
LOG.debug('%r: find_loader(%r) failed: %s', self, fullname, e)
return None
IOLOG.debug('%r._get_module_via_pkgutil(%r) -> %r',
self, fullname, loader)
if not loader:
LOG.debug('%r: find_loader(%r) returned %r, aborting',
self, fullname, loader)
return
try:
path, is_special = _py_filename(loader.get_filename(fullname))
source = loader.get_source(fullname)
is_pkg = loader.is_package(fullname)
# workaround for special python modules that might only exist in memory
if is_special and is_pkg and not source:
source = '\n'
except (AttributeError, ImportError):
# - Per PEP-302, get_source() and is_package() are optional,
# calling them may throw AttributeError.
path = loader.get_filename(fullname)
except (AttributeError, ImportError, ValueError):
# - get_filename() may throw ImportError if pkgutil.find_loader()
# picks a "parent" package's loader for some crap that's been
# stuffed in sys.modules, for example in the case of urllib3:
# "loader for urllib3.contrib.pyopenssl cannot handle
# requests.packages.urllib3.contrib.pyopenssl"
e = sys.exc_info()[1]
LOG.debug('%r: loading %r using %r failed: %s',
self, fullname, loader, e)
LOG.debug('%r: %r.get_file_name(%r) failed: %r', self, loader, fullname, e)
return
path, is_special = _py_filename(path)
try:
source = loader.get_source(fullname)
except AttributeError:
# Per PEP-302, get_source() is optional,
e = sys.exc_info()[1]
LOG.debug('%r: %r.get_source() failed: %r', self, loader, fullname, e)
return
try:
is_pkg = loader.is_package(fullname)
except AttributeError:
# Per PEP-302, is_package() is optional,
e = sys.exc_info()[1]
LOG.debug('%r: %r.is_package(%r) failed: %r', self, loader, fullname, e)
return
# workaround for special python modules that might only exist in memory
if is_special and is_pkg and not source:
source = '\n'
if path is None or source is None:
LOG.debug('%r: path=%r, source=%r, aborting', self, path, source)
return
if isinstance(source, mitogen.core.UnicodeType):
@ -567,23 +530,37 @@ class SysModulesMethod(FinderMethod):
"""
Find `fullname` using its :data:`__file__` attribute.
"""
module = sys.modules.get(fullname)
try:
module = sys.modules[fullname]
except KeyError:
LOG.debug('%r: sys.modules[%r] absent, aborting', self, fullname)
return
if not isinstance(module, types.ModuleType):
LOG.debug('%r: sys.modules[%r] absent or not a regular module',
self, fullname)
LOG.debug('%r: sys.modules[%r] is %r, aborting',
self, fullname, module)
return
try:
resolved_name = module.__name__
except AttributeError:
LOG.debug('%r: %r has no __name__, aborting', self, module)
return
LOG.debug('_get_module_via_sys_modules(%r) -> %r', fullname, module)
alleged_name = getattr(module, '__name__', None)
if alleged_name != fullname:
LOG.debug('sys.modules[%r].__name__ is incorrect, assuming '
'this is a hacky module alias and ignoring it. '
'Got %r, module object: %r',
fullname, alleged_name, module)
if resolved_name != fullname:
LOG.debug('%r: %r.__name__ is %r, aborting',
self, module, resolved_name)
return
try:
path = module.__file__
except AttributeError:
LOG.debug('%r: %r has no __file__, aborting', self, module)
return
path, _ = _py_filename(getattr(module, '__file__', ''))
path, _ = _py_filename(path)
if not path:
LOG.debug('%r: %r.__file__ is %r, aborting', self, module, path)
return
LOG.debug('%r: sys.modules[%r]: found %s', self, fullname, path)
@ -605,13 +582,13 @@ class SysModulesMethod(FinderMethod):
return path, source, is_pkg
class ParentEnumerationMethod(FinderMethod):
class ParentImpEnumerationMethod(FinderMethod):
"""
Attempt to fetch source code by examining the module's (hopefully less
insane) parent package, and if no insane parents exist, simply use
:mod:`sys.path` to search for it from scratch on the filesystem using the
normal Python lookup mechanism.
This is required for older versions of :mod:`ansible.compat.six`,
:mod:`plumbum.colors`, Ansible 2.8 :mod:`ansible.module_utils.distro` and
its submodule :mod:`ansible.module_utils.distro._distro`.
@ -628,10 +605,24 @@ class ParentEnumerationMethod(FinderMethod):
module object or any parent package's :data:`__path__`, since they have all
been overwritten. Some men just want to watch the world burn.
"""
@staticmethod
def _iter_parents(fullname):
"""
>>> list(ParentEnumerationMethod._iter_parents('a'))
[('', 'a')]
>>> list(ParentEnumerationMethod._iter_parents('a.b.c'))
[('a.b', 'c'), ('a', 'b'), ('', 'a')]
"""
while fullname:
fullname, _, modname = str_rpartition(fullname, u'.')
yield fullname, modname
def _find_sane_parent(self, fullname):
"""
Iteratively search :data:`sys.modules` for the least indirect parent of
`fullname` that is loaded and contains a :data:`__path__` attribute.
`fullname` that's from the same package and has a :data:`__path__`
attribute.
:return:
`(parent_name, path, modpath)` tuple, where:
@ -644,21 +635,40 @@ class ParentEnumerationMethod(FinderMethod):
* `modpath`: list of module name components leading from `path`
to the target module.
"""
path = None
modpath = []
while True:
pkgname, _, modname = str_rpartition(to_text(fullname), u'.')
for pkgname, modname in self._iter_parents(fullname):
modpath.insert(0, modname)
if not pkgname:
return [], None, modpath
pkg = sys.modules.get(pkgname)
path = getattr(pkg, '__path__', None)
if pkg and path:
return pkgname.split('.'), path, modpath
try:
pkg = sys.modules[pkgname]
except KeyError:
LOG.debug('%r: sys.modules[%r] absent, skipping', self, pkgname)
continue
try:
resolved_pkgname = pkg.__name__
except AttributeError:
LOG.debug('%r: %r has no __name__, skipping', self, pkg)
continue
if resolved_pkgname != pkgname:
LOG.debug('%r: %r.__name__ is %r, skipping',
self, pkg, resolved_pkgname)
continue
try:
path = pkg.__path__
except AttributeError:
LOG.debug('%r: %r has no __path__, skipping', self, pkg)
continue
LOG.debug('%r: %r lacks __path__ attribute', self, pkgname)
fullname = pkgname
if not path:
LOG.debug('%r: %r.__path__ is %r, skipping', self, pkg, path)
continue
return pkgname.split('.'), path, modpath
def _found_package(self, fullname, path):
path = os.path.join(path, '__init__.py')
@ -690,6 +700,7 @@ class ParentEnumerationMethod(FinderMethod):
def _find_one_component(self, modname, search_path):
try:
#fp, path, (suffix, _, kind) = imp.find_module(modname, search_path)
# FIXME The imp module was removed in Python 3.12.
return imp.find_module(modname, search_path)
except ImportError:
e = sys.exc_info()[1]
@ -701,6 +712,9 @@ class ParentEnumerationMethod(FinderMethod):
"""
See implementation for a description of how this works.
"""
if sys.version_info >= (3, 4):
return None
#if fullname not in sys.modules:
# Don't attempt this unless a module really exists in sys.modules,
# else we could return junk.
@ -729,12 +743,111 @@ class ParentEnumerationMethod(FinderMethod):
return self._found_module(fullname, path, fp)
class ParentSpecEnumerationMethod(ParentImpEnumerationMethod):
def _find_parent_spec(self, fullname):
#history = []
debug = self.log.debug
children = []
for parent_name, child_name in self._iter_parents(fullname):
children.insert(0, child_name)
if not parent_name:
debug('abandoning %r, reached top-level', fullname)
return None, children
try:
parent = sys.modules[parent_name]
except KeyError:
debug('skipping %r, not in sys.modules', parent_name)
continue
try:
spec = parent.__spec__
except AttributeError:
debug('skipping %r: %r.__spec__ is absent',
parent_name, parent)
continue
if not spec:
debug('skipping %r: %r.__spec__=%r',
parent_name, parent, spec)
continue
if spec.name != parent_name:
debug('skipping %r: %r.__spec__.name=%r does not match',
parent_name, parent, spec.name)
continue
if not spec.submodule_search_locations:
debug('skipping %r: %r.__spec__.submodule_search_locations=%r',
parent_name, parent, spec.submodule_search_locations)
continue
return spec, children
raise ValueError('%s._find_parent_spec(%r) unexpectedly reached bottom'
% (self.__class__.__name__, fullname))
def find(self, fullname):
# Returns absolute path, ParentImpEnumerationMethod returns relative
# >>> spec_pem.find('six_brokenpkg._six')[::2]
# ('/Users/alex/src/mitogen/tests/data/importer/six_brokenpkg/_six.py', False)
if sys.version_info < (3, 4):
return None
fullname = to_text(fullname)
spec, children = self._find_parent_spec(fullname)
for child_name in children:
if spec:
name = '%s.%s' % (spec.name, child_name)
submodule_search_locations = spec.submodule_search_locations
else:
name = child_name
submodule_search_locations = None
spec = importlib.util._find_spec(name, submodule_search_locations)
if spec is None:
self.log.debug('%r spec unavailable from %s', fullname, spec)
return None
is_package = spec.submodule_search_locations is not None
if name != fullname:
if not is_package:
self.log.debug('%r appears to be child of non-package %r',
fullname, spec)
return None
continue
if not spec.has_location:
self.log.debug('%r.origin cannot be read as a file', spec)
return None
if os.path.splitext(spec.origin)[1] != '.py':
self.log.debug('%r.origin does not contain Python source code',
spec)
return None
# FIXME This should use loader.get_source()
with open(spec.origin, 'rb') as f:
source = f.read()
return spec.origin, source, is_package
raise ValueError('%s.find(%r) unexpectedly reached bottom'
% (self.__class__.__name__, fullname))
class ModuleFinder(object):
"""
Given the name of a loaded module, make a best-effort attempt at finding
related modules likely needed by a child context requesting the original
module.
"""
# Fullnames of modules that should not be sent as a related module
_related_modules_denylist = frozenset({
'__main__',
})
def __init__(self):
#: Import machinery is expensive, keep :py:meth`:get_module_source`
#: results around.
@ -769,7 +882,8 @@ class ModuleFinder(object):
DefectivePython3xMainMethod(),
PkgutilMethod(),
SysModulesMethod(),
ParentEnumerationMethod(),
ParentSpecEnumerationMethod(),
ParentImpEnumerationMethod(),
]
def get_module_source(self, fullname):
@ -822,6 +936,34 @@ class ModuleFinder(object):
fullname, _, _ = str_rpartition(to_text(fullname), u'.')
yield fullname
def _reject_related_module(self, requested_fullname, related_fullname):
def _log_reject(reason):
LOG.debug(
'%r: Rejected related module %s of requested module %s: %s',
self, related_fullname, requested_fullname, reason,
)
return reason
try:
related_module = sys.modules[related_fullname]
except KeyError:
return _log_reject('sys.modules entry absent')
# Python 2.x "indirection entry"
if related_module is None:
return _log_reject('sys.modules entry is None')
if is_stdlib_name(related_fullname):
return _log_reject('stdlib module')
if 'six.moves' in related_fullname:
return _log_reject('six.moves avoidence')
if related_fullname in self._related_modules_denylist:
return _log_reject('on denylist')
return False
def find_related_imports(self, fullname):
"""
Return a list of non-stdlib modules that are directly imported by
@ -845,7 +987,7 @@ class ModuleFinder(object):
maybe_names = list(self.generate_parent_names(fullname))
co = compile(src, modpath, 'exec')
for level, modname, namelist in scan_code_imports(co):
for level, modname, namelist in mitogen.imports.codeobj_imports(co):
if level == -1:
modnames = [modname, '%s.%s' % (fullname, modname)]
else:
@ -864,9 +1006,7 @@ class ModuleFinder(object):
set(
mitogen.core.to_text(name)
for name in maybe_names
if sys.modules.get(name) is not None
and not is_stdlib_name(name)
and u'six.moves' not in name # TODO: crap
if not self._reject_related_module(fullname, name)
)
))
@ -1029,7 +1169,7 @@ class ModuleResponder(object):
self._cache[fullname] = tup
return tup
def _send_load_module(self, stream, fullname):
def _send_load_module(self, stream, fullname, reason):
if fullname not in stream.protocol.sent_modules:
tup = self._build_tuple(fullname)
msg = mitogen.core.Message.pickled(
@ -1037,8 +1177,10 @@ class ModuleResponder(object):
dst_id=stream.protocol.remote_id,
handle=mitogen.core.LOAD_MODULE,
)
self._log.debug('sending %s (%.2f KiB) to %s',
fullname, len(msg.data) / 1024.0, stream.name)
self._log.debug(
'sending %s %s (%.2f KiB) to %s',
reason, fullname, len(msg.data) / 1024.0, stream.name,
)
self._router._async_route(msg)
stream.protocol.sent_modules.add(fullname)
if tup[2] is not None:
@ -1069,8 +1211,8 @@ class ModuleResponder(object):
# Parent hasn't been sent, so don't load submodule yet.
continue
self._send_load_module(stream, name)
self._send_load_module(stream, fullname)
self._send_load_module(stream, name, 'related')
self._send_load_module(stream, fullname, 'requested')
except Exception:
LOG.debug('While importing %r', fullname, exc_info=True)
self._send_module_load_failed(stream, fullname)
@ -1139,6 +1281,47 @@ class ModuleResponder(object):
self._router.broker.defer(self._forward_modules, context, fullnames)
class ResourceResponder(object):
def __init__(self, router):
self._router = router
self._router.add_handler(
self._on_get_resource,
mitogen.core.GET_RESOURCE,
)
def _on_get_resource(self, msg):
if msg.is_dead:
return
stream = self._router.stream_by_id(msg.src_id)
if stream is None:
return
fullname, resource = msg.unpickle()
try:
content = importlib.resources.read_binary(fullname, resource)
except (FileNotFoundError, IsADirectoryError):
content = None
if content is not None:
self._send_resource(stream, fullname, resource, content)
else:
self._send_not_found(stream, fullname, resource)
def _send_resource(self, stream, fullname, resource, content):
msg = mitogen.core.Message.pickled(
(fullname, resource, content),
dst_id=stream.protocol.remote_id,
handle=mitogen.core.LOAD_RESOURCE,
)
self._router._async_route(msg)
def _send_not_found(self, stream, fullname, resource):
msg = mitogen.core.Message.pickled(
(fullname, resource, None),
dst_id=stream.protocol.remote_id,
handle=mitogen.core.LOAD_RESOURCE,
)
stream.protocol.send(msg)
class Broker(mitogen.core.Broker):
"""
.. note::
@ -1167,7 +1350,7 @@ class Broker(mitogen.core.Broker):
def __init__(self, install_watcher=True):
if install_watcher:
self._watcher = ThreadWatcher.watch(
target=threading.currentThread(),
target=mitogen.core.threading__current_thread(),
on_join=self.shutdown,
)
super(Broker, self).__init__()
@ -1229,6 +1412,7 @@ class Router(mitogen.parent.Router):
def upgrade(self):
self.id_allocator = IdAllocator(self)
self.responder = ModuleResponder(self)
self.resource_responder = ResourceResponder(self)
self.log_forwarder = LogForwarder(self)
self.route_monitor = mitogen.parent.RouteMonitor(router=self)
self.add_handler( # TODO: cutpaste.

@ -104,7 +104,7 @@ def strip_docstrings(tokens):
elif typ == tokenize.NEWLINE:
stack.append(t)
start_line, end_line = stack[0][2][0], stack[-1][3][0]+1
for i in range(start_line, end_line):
for i in mitogen.core.range(start_line, end_line):
yield tokenize.NL, '\n', (i, 0), (i,1), '\n'
for t in stack:
if t[0] in (tokenize.DEDENT, tokenize.INDENT):

@ -35,10 +35,10 @@ Support for operating in a mixed threading/forking environment.
import os
import socket
import sys
import threading
import weakref
import mitogen.core
import mitogen.parent
# List of weakrefs. On Python 2.4, mitogen.core registers its Broker on this
@ -132,9 +132,9 @@ class Corker(object):
`obj` to be written to by one of its threads.
"""
rsock, wsock = mitogen.parent.create_socketpair(size=4096)
mitogen.core.set_blocking(wsock.fileno(), True) # gevent
mitogen.core.set_cloexec(rsock.fileno())
mitogen.core.set_cloexec(wsock.fileno())
mitogen.core.set_block(wsock) # gevent
self._rsocks.append(rsock)
obj.defer(self._do_cork, s, wsock)
@ -158,7 +158,7 @@ class Corker(object):
held. This will not return until each thread acknowledges it has ceased
execution.
"""
current = threading.currentThread()
current = mitogen.core.threading__current_thread()
s = mitogen.core.b('CORK') * ((128 // 4) * 1024)
self._rsocks = []

@ -34,7 +34,7 @@ sent to any child context that is due to become a parent, due to recursive
connection.
"""
import codecs
import binascii
import errno
import fcntl
import getpass
@ -43,6 +43,7 @@ import inspect
import logging
import os
import re
import pty
import signal
import socket
import struct
@ -56,15 +57,13 @@ import zlib
# Absolute imports for <2.5.
select = __import__('select')
try:
import thread
except ImportError:
import threading as thread
import mitogen.core
from mitogen.core import b
from mitogen.core import bytes_partition
from mitogen.core import IOLOG
from mitogen.core import itervalues
from mitogen.core import next
from mitogen.core import thread
LOG = logging.getLogger(__name__)
@ -80,17 +79,7 @@ except IOError:
SELINUX_ENABLED = False
try:
next
except NameError:
# Python 2.4/2.5
from mitogen.core import next
itervalues = getattr(dict, 'itervalues', dict.values)
if mitogen.core.PY3:
xrange = range
if sys.version_info >= (3, 0):
closure_attr = '__closure__'
IM_SELF_ATTR = '__self__'
else:
@ -147,6 +136,8 @@ LINUX_TIOCGPTN = _ioctl_cast(2147767344)
LINUX_TIOCSPTLCK = _ioctl_cast(1074025521)
IS_LINUX = os.uname()[0] == 'Linux'
IS_SOLARIS = os.uname()[0] == 'SunOS'
SIGNAL_BY_NUM = dict(
(getattr(signal, name), name)
@ -233,8 +224,16 @@ def flags(names):
Return the result of ORing a set of (space separated) :py:mod:`termios`
module constants together.
"""
return sum(getattr(termios, name, 0)
for name in names.split())
i = 0
skipped = []
for name in names.split():
try:
i |= getattr(termios, name)
except AttributeError:
skipped.append(name)
if skipped:
LOG.debug('Skipped termios attributes: %s', ', '.join(skipped))
return i
def cfmakeraw(tflags):
@ -243,12 +242,9 @@ def cfmakeraw(tflags):
modified in a manner similar to the `cfmakeraw()` C library function, but
additionally disabling local echo.
"""
# BSD: github.com/freebsd/freebsd/blob/master/lib/libc/gen/termios.c#L162
# Linux: github.com/lattera/glibc/blob/master/termios/cfmakeraw.c#L20
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = tflags
iflag &= ~flags('IMAXBEL IXOFF INPCK BRKINT PARMRK '
'ISTRIP INLCR ICRNL IXON IGNPAR')
iflag &= ~flags('IGNBRK BRKINT PARMRK')
'ISTRIP INLCR ICRNL IXON IGNPAR IGNBRK')
oflag &= ~flags('OPOST')
lflag &= ~flags('ECHO ECHOE ECHOK ECHONL ICANON ISIG '
'IEXTEN NOFLSH TOSTOP PENDIN')
@ -268,7 +264,7 @@ def disable_echo(fd):
termios.tcsetattr(fd, flags, new)
def create_socketpair(size=None):
def create_socketpair(size=None, blocking=None):
"""
Create a :func:`socket.socketpair` for use as a child's UNIX stdio
channels. As socketpairs are bidirectional, they are economical on file
@ -279,14 +275,14 @@ def create_socketpair(size=None):
if size is None:
size = mitogen.core.CHUNK_SIZE
parentfp, childfp = socket.socketpair()
parentfp, childfp = mitogen.core.socketpair(blocking)
for fp in parentfp, childfp:
fp.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, size)
return parentfp, childfp
def create_best_pipe(escalates_privilege=False):
def create_best_pipe(escalates_privilege=False, blocking=None):
"""
By default we prefer to communicate with children over a UNIX socket, as a
single file descriptor can represent bidirectional communication, and a
@ -304,16 +300,19 @@ def create_best_pipe(escalates_privilege=False):
:param bool escalates_privilege:
If :data:`True`, the target program may escalate privileges, causing
SELinux to disconnect AF_UNIX sockets, so avoid those.
:param None|bool blocking:
If :data:`False` or :data:`True`, set non-blocking or blocking mode.
If :data:`None` (default), use default.
:returns:
`(parent_rfp, child_wfp, child_rfp, parent_wfp)`
"""
if (not escalates_privilege) or (not SELINUX_ENABLED):
parentfp, childfp = create_socketpair()
parentfp, childfp = create_socketpair(blocking=blocking)
return parentfp, childfp, childfp, parentfp
parent_rfp, child_wfp = mitogen.core.pipe()
parent_rfp, child_wfp = mitogen.core.pipe(blocking)
try:
child_rfp, parent_wfp = mitogen.core.pipe()
child_rfp, parent_wfp = mitogen.core.pipe(blocking)
return parent_rfp, child_wfp, child_rfp, parent_wfp
except:
parent_rfp.close()
@ -365,13 +364,13 @@ def create_child(args, merge_stdio=False, stderr_pipe=False,
escalates_privilege=escalates_privilege
)
stderr = None
stderr_r = None
if merge_stdio:
stderr = child_wfp
stderr_r, stderr = None, child_wfp
elif stderr_pipe:
stderr_r, stderr = mitogen.core.pipe()
mitogen.core.set_cloexec(stderr_r.fileno())
else:
stderr_r, stderr = None, None
try:
proc = popen(
@ -410,12 +409,14 @@ def _acquire_controlling_tty():
if sys.platform in ('linux', 'linux2'):
# On Linux, the controlling tty becomes the first tty opened by a
# process lacking any prior tty.
os.close(os.open(os.ttyname(2), os.O_RDWR))
if hasattr(termios, 'TIOCSCTTY') and not mitogen.core.IS_WSL:
tty_path = os.ttyname(pty.STDERR_FILENO)
tty_fd = os.open(tty_path, os.O_RDWR)
os.close(tty_fd)
if hasattr(termios, 'TIOCSCTTY') and not mitogen.core.IS_WSL and not IS_SOLARIS:
# #550: prehistoric WSL does not like TIOCSCTTY.
# On BSD an explicit ioctl is required. For some inexplicable reason,
# Python 2.6 on Travis also requires it.
fcntl.ioctl(2, termios.TIOCSCTTY)
fcntl.ioctl(pty.STDERR_FILENO, termios.TIOCSCTTY)
def _linux_broken_devpts_openpty():
@ -479,9 +480,10 @@ def openpty():
master_fp = os.fdopen(master_fd, 'r+b', 0)
slave_fp = os.fdopen(slave_fd, 'r+b', 0)
disable_echo(master_fd)
if not IS_SOLARIS:
disable_echo(master_fd)
disable_echo(slave_fd)
mitogen.core.set_block(slave_fd)
mitogen.core.set_blocking(slave_fd, True)
return master_fp, slave_fp
@ -547,8 +549,8 @@ def hybrid_tty_create_child(args, escalates_privilege=False):
escalates_privilege=escalates_privilege,
)
try:
mitogen.core.set_block(child_rfp)
mitogen.core.set_block(child_wfp)
mitogen.core.set_blocking(child_rfp.fileno(), True)
mitogen.core.set_blocking(child_wfp.fileno(), True)
proc = popen(
args=args,
stdin=child_rfp,
@ -639,7 +641,7 @@ class TimerList(object):
def get_timeout(self):
"""
Return the floating point seconds until the next event is due.
:returns:
Floating point delay, or 0.0, or :data:`None` if no events are
scheduled.
@ -745,8 +747,7 @@ def _upgrade_broker(broker):
broker.timers = TimerList()
LOG.debug('upgraded %r with %r (new: %d readers, %d writers; '
'old: %d readers, %d writers)', old, new,
len(new.readers), len(new.writers),
len(old.readers), len(old.writers))
len(new._rfds), len(new._wfds), len(old._rfds), len(old._wfds))
@mitogen.core.takes_econtext
@ -902,22 +903,18 @@ class CallSpec(object):
class PollPoller(mitogen.core.Poller):
"""
Poller based on the POSIX :linux:man2:`poll` interface. Not available on
some versions of OS X, otherwise it is the preferred poller for small FD
counts, as there is no setup/teardown/configuration system call overhead.
some Python/OS X combinations. Otherwise the preferred poller for small
FD counts; or if many pollers are created, used once, then closed.
There there is no setup/teardown/configuration system call overhead.
"""
SUPPORTED = hasattr(select, 'poll')
_repr = 'PollPoller()'
_readmask = SUPPORTED and select.POLLIN | select.POLLHUP
def __init__(self):
super(PollPoller, self).__init__()
self._pollobj = select.poll()
# TODO: no proof we dont need writemask too
_readmask = (
getattr(select, 'POLLIN', 0) |
getattr(select, 'POLLHUP', 0)
)
def _update(self, fd):
mask = (((fd in self._rfds) and self._readmask) |
((fd in self._wfds) and select.POLLOUT))
@ -952,7 +949,6 @@ class KqueuePoller(mitogen.core.Poller):
Poller based on the FreeBSD/Darwin :freebsd:man2:`kqueue` interface.
"""
SUPPORTED = hasattr(select, 'kqueue')
_repr = 'KqueuePoller()'
def __init__(self):
super(KqueuePoller, self).__init__()
@ -1027,10 +1023,10 @@ class KqueuePoller(mitogen.core.Poller):
class EpollPoller(mitogen.core.Poller):
"""
Poller based on the Linux :linux:man2:`epoll` interface.
Poller based on the Linux :linux:man7:`epoll` interface.
"""
SUPPORTED = hasattr(select, 'epoll')
_repr = 'EpollPoller()'
_inmask = SUPPORTED and select.EPOLLIN | select.EPOLLHUP
def __init__(self):
super(EpollPoller, self).__init__()
@ -1077,9 +1073,6 @@ class EpollPoller(mitogen.core.Poller):
self._wfds.pop(fd, None)
self._control(fd)
_inmask = (getattr(select, 'EPOLLIN', 0) |
getattr(select, 'EPOLLHUP', 0))
def _poll(self, timeout):
the_timeout = -1
if timeout is not None:
@ -1100,18 +1093,14 @@ class EpollPoller(mitogen.core.Poller):
yield data
# 2.4 and 2.5 only had select.select() and select.poll().
for _klass in mitogen.core.Poller, PollPoller, KqueuePoller, EpollPoller:
if _klass.SUPPORTED:
PREFERRED_POLLER = _klass
POLLERS = (EpollPoller, KqueuePoller, PollPoller, mitogen.core.Poller)
PREFERRED_POLLER = next(cls for cls in POLLERS if cls.SUPPORTED)
# For processes that start many threads or connections, it's possible Latch
# will also get high-numbered FDs, and so select() becomes useless there too.
# So swap in our favourite poller.
if PollPoller.SUPPORTED:
mitogen.core.Latch.poller_class = PollPoller
else:
mitogen.core.Latch.poller_class = PREFERRED_POLLER
POLLER_LIGHTWEIGHT = PollPoller.SUPPORTED and PollPoller or PREFERRED_POLLER
mitogen.core.Latch.poller_class = POLLER_LIGHTWEIGHT
class LineLoggingProtocolMixin(object):
@ -1405,10 +1394,10 @@ class Connection(object):
# file descriptor 0 as 100, creates a pipe, then execs a new interpreter
# with a custom argv.
# * Optimized for minimum byte count after minification & compression.
# * 'CONTEXT_NAME' and 'PREAMBLE_COMPRESSED_LEN' are substituted with
# their respective values.
# * CONTEXT_NAME must be prefixed with the name of the Python binary in
# order to allow virtualenvs to detect their install prefix.
# The script preamble_size.py measures this.
#
# macOS tweaks for Python 2.7 must be kept in sync with the the Ansible
# module test_echo_module, used by the integration tests.
# * macOS <= 10.14 (Darwin <= 18) install an unreliable Python version
# switcher as /usr/bin/python, which introspects argv0. To workaround
# it we redirect attempts to call /usr/bin/python with an explicit
@ -1417,16 +1406,16 @@ class Connection(object):
# do something slightly different. The Python executable is patched to
# perform an extra execvp(). I don't fully understand the details, but
# setting PYTHON_LAUNCHED_FROM_WRAPPER=1 avoids it.
# * macOS 13.x (Darwin 22?) may remove python 2.x entirely.
# * macOS 12.3+ (Darwin 21.4+, Monterey) doesn't ship Python.
# https://developer.apple.com/documentation/macos-release-notes/macos-12_3-release-notes#Python
#
# Locals:
# R: read side of interpreter stdin.
# W: write side of interpreter stdin.
# r: read side of core_src FD.
# w: write side of core_src FD.
# C: the decompressed core source.
# Final os.close(2) to avoid --py-debug build from corrupting stream with
# Final os.close(STDERR_FILENO) to avoid --py-debug build corrupting stream with
# "[1234 refs]" during exit.
@staticmethod
def _first_stage():
@ -1440,18 +1429,30 @@ class Connection(object):
os.close(r)
os.close(W)
os.close(w)
if os.uname()[0]=='Darwin'and os.uname()[2][:2]<'19'and sys.executable=='/usr/bin/python':sys.executable='/usr/bin/python2.7'
if os.uname()[0]=='Darwin'and os.uname()[2][:2]in'2021'and sys.version[:3]=='2.7':os.environ['PYTHON_LAUNCHED_FROM_WRAPPER']='1'
if os.uname()[0]+os.uname()[2][:2]+sys.executable=='Darwin19/usr/bin/python':sys.executable+='2.7'
if os.uname()[0]+os.uname()[2][:2]+sys.version[:3]=='Darwin202.7':os.environ['PYTHON_LAUNCHED_FROM_WRAPPER']='1'
if os.uname()[0]+os.uname()[2][:2]+sys.version[:3]=='Darwin212.7':os.environ['PYTHON_LAUNCHED_FROM_WRAPPER']='1'
os.environ['ARGV0']=sys.executable
os.execl(sys.executable,sys.executable+'(mitogen:CONTEXT_NAME)')
os.execl(sys.executable,sys.executable+'(mitogen:%s)'%sys.argv[2])
os.write(1,'MITO000\n'.encode())
C=_(os.fdopen(0,'rb').read(PREAMBLE_COMPRESSED_LEN),'zip')
fp=os.fdopen(W,'wb',0)
fp.write(C)
fp.close()
fp=os.fdopen(w,'wb',0)
fp.write(C)
fp.close()
# Size of the compressed core source to be read
n=int(sys.argv[3])
# Read `len(compressed preamble)` bytes sent by our Mitogen parent.
# `select()` handles non-blocking stdin (e.g. sudo + log_output).
# `C` accumulates compressed bytes.
C=''.encode()
# data chunk
V='V'
# Stop looping if no more data is needed or EOF is detected (empty bytes).
while n-len(C) and V:select.select([0],[],[]);V=os.read(0,n-len(C));C+=V
# Raises `zlib.error` if compressed preamble is truncated or invalid
C=zlib.decompress(C)
f=os.fdopen(W,'wb',0)
f.write(C)
f.close()
f=os.fdopen(w,'wb',0)
f.write(C)
f.close()
os.write(1,'MITO001\n'.encode())
os.close(2)
@ -1469,24 +1470,28 @@ class Connection(object):
return [self.options.python_path]
def get_boot_command(self):
source = inspect.getsource(self._first_stage)
source = textwrap.dedent('\n'.join(source.strip().split('\n')[2:]))
lines = inspect.getsourcelines(self._first_stage)[0][2:]
# Remove line comments, leading indentation, trailing newline
source = textwrap.dedent(''.join(s for s in lines if '#' not in s))[:-1]
source = source.replace(' ', ' ')
source = source.replace('CONTEXT_NAME', self.options.remote_name)
preamble_compressed = self.get_preamble()
source = source.replace('PREAMBLE_COMPRESSED_LEN',
str(len(preamble_compressed)))
compressed = zlib.compress(source.encode(), 9)
encoded = codecs.encode(compressed, 'base64').replace(b('\n'), b(''))
# We can't use bytes.decode() in 3.x since it was restricted to always
# return unicode, so codecs.decode() is used instead. In 3.x
# codecs.decode() requires a bytes object. Since we must be compatible
# with 2.4 (no bytes literal), an extra .encode() either returns the
# same str (2.x) or an equivalent bytes (3.x).
compressor = zlib.compressobj(
zlib.Z_BEST_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS,
)
compressed = compressor.compress(source.encode()) + compressor.flush()
encoded = binascii.b2a_base64(compressed).replace(b('\n'), b(''))
# Just enough to decode, decompress, and exec the first stage.
# Priorities: wider compatibility, faster startup, shorter length.
# `sys.path=...` for https://github.com/python/cpython/issues/115911.
# `import os,select` here (not stage 1) to save a few bytes overall.
return self.get_python_argv() + [
'-c',
'import codecs,os,sys;_=codecs.decode;'
'exec(_(_("%s".encode(),"base64"),"zip"))' % (encoded.decode(),)
'import sys;sys.path=[p for p in sys.path if p];'
'import binascii,os,select,zlib;'
'exec(zlib.decompress(binascii.a2b_base64(sys.argv[1]),-15))',
encoded.decode(),
self.options.remote_name,
str(len(self.get_preamble())),
]
def get_econtext_config(self):
@ -1508,7 +1513,7 @@ class Connection(object):
def get_preamble(self):
suffix = (
'\nExternalContext(%r).main()\n' %\
'\nExternalContext(%r).main()\n' %
(self.get_econtext_config(),)
)
partial = get_core_source_partial()
@ -1649,6 +1654,9 @@ class Connection(object):
stream = self.stream_factory()
stream.conn = self
stream.name = self.options.name or self._get_name()
for fp in self.proc.stdout, self.proc.stdin:
fd = fp.fileno()
mitogen.core.set_blocking(fd, False)
stream.accept(self.proc.stdout, self.proc.stdin)
mitogen.core.listen(stream, 'disconnect', self.on_stdio_disconnect)
@ -1659,6 +1667,8 @@ class Connection(object):
stream = self.stderr_stream_factory()
stream.conn = self
stream.name = self.options.name or self._get_name()
fd = self.proc.stderr.fileno()
mitogen.core.set_blocking(fd, False)
stream.accept(self.proc.stderr, self.proc.stderr)
mitogen.core.listen(stream, 'disconnect', self.on_stderr_disconnect)
@ -1692,9 +1702,7 @@ class Connection(object):
LOG.debug('child for %r started: pid:%r stdin:%r stdout:%r stderr:%r',
self, self.proc.pid,
self.proc.stdin.fileno(),
self.proc.stdout.fileno(),
self.proc.stderr and self.proc.stderr.fileno())
self.proc.stdin, self.proc.stdout, self.proc.stderr)
self.stdio_stream = self._setup_stdio_stream()
if self.context.name is None:
@ -1720,7 +1728,7 @@ class ChildIdAllocator(object):
def __init__(self, router):
self.router = router
self.lock = threading.Lock()
self.it = iter(xrange(0))
self.it = iter(mitogen.core.range(0))
def allocate(self):
"""
@ -1744,7 +1752,7 @@ class ChildIdAllocator(object):
start, end = master.send_await(
mitogen.core.Message(dst_id=0, handle=mitogen.core.ALLOCATE_ID)
)
self.it = iter(xrange(start, end))
self.it = iter(mitogen.core.range(start, end))
finally:
self.lock.release()
@ -2305,6 +2313,11 @@ class Router(mitogen.core.Router):
parent_context=parent,
importer=importer,
)
self.resource_responder = ResourceForwarder(
self,
parent,
importer._resource_requester,
)
self.route_monitor = RouteMonitor(self, parent)
self.add_handler(
fn=self._on_detaching,
@ -2550,7 +2563,7 @@ class Reaper(object):
# because it is setuid, so this is best-effort only.
LOG.debug('%r: sending %s', self.proc, SIGNAL_BY_NUM[signum])
try:
os.kill(self.proc.pid, signum)
self.proc.send_signal(signum)
except OSError:
e = sys.exc_info()[1]
if e.args[0] != errno.EPERM:
@ -2563,9 +2576,8 @@ class Reaper(object):
relatively conservative retries.
"""
delay = 0.05
for _ in xrange(count):
delay *= 1.72
return delay
factor = 1.72
return delay * factor ** count
def _on_broker_shutdown(self):
"""
@ -2670,6 +2682,17 @@ class Process(object):
"""
raise NotImplementedError()
def send_signal(self, sig):
os.kill(self.pid, sig)
def terminate(self):
"Ask the process to gracefully shutdown."
self.send_signal(signal.SIGTERM)
def kill(self):
"Ask the operating system to forcefully destroy the process."
self.send_signal(signal.SIGKILL)
class PopenProcess(Process):
"""
@ -2686,6 +2709,9 @@ class PopenProcess(Process):
def poll(self):
return self.proc.poll()
def send_signal(self, sig):
self.proc.send_signal(sig)
class ModuleForwarder(object):
"""
@ -2777,3 +2803,43 @@ class ModuleForwarder(object):
handle=mitogen.core.LOAD_MODULE,
)
)
class ResourceForwarder(object):
"""
Handle :data:`mitogen.core.GET_RESOURCE` requests from children by
forwarding the request to our parent, or satisfying the request from
our local :class:`mitogen.core.ResourceRequester` cache.
"""
def __init__(self, router, parent_context, requester):
self.router = router
self.parent_context = parent_context
self.requester = requester
router.add_handler(
fn=self._on_get_resource,
handle=mitogen.core.GET_RESOURCE,
persist=True,
policy=is_immediate_child,
)
def _on_get_resource(self, msg):
if msg.is_dead:
return
fullname, resource = msg.unpickle()
callback = lambda: self._on_cache_callback(msg, fullname, resource)
self.requester._request_resource(fullname, resource, callback)
def _on_cache_callback(self, msg, fullname, resource):
stream = self.router.stream_by_id(msg.src_id)
self._send_resource(stream, fullname, resource)
def _send_resource(self, stream, fullname, resource):
content = self.requester._cache[(fullname, resource)]
msg = mitogen.core.Message.pickled(
(fullname, resource, content),
dst_id=stream.protocol.remote_id,
handle=mitogen.core.LOAD_RESOURCE,
)
self.router._async_route(msg)

@ -31,7 +31,6 @@
import logging
import mitogen.core
import mitogen.parent

@ -90,7 +90,7 @@ def merge_stats(outpath, inpaths):
break
time.sleep(0.2)
pstats.dump_stats(outpath)
stats.dump_stats(outpath)
def generate_stats(outpath, tmpdir):

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save