Merge remote-tracking branch 'origin/dmw'

d2eb01f tests: pin idna to last supporting 2.6-compat version.
b90889c tests: pin idna to last supporting 2.6-compat version.
9da6e6a tests: don't call Router.shutdown() twice.
b2b7e7b tests: file_service_test fixes
04755c3 issue #426: tighten up PushFileService types.
8fa3c74 issue #426: RouterMonitor format incorrect for 3->2 forward.
d8b9634 issue #426: PushFileService missing to_text() call.
1e9f344 issue #426: big hack so reset_connection has task_var access
18bfde5 issue #444: update Changelog.
835bead tests: allow running scripts from any subdir.
81c93e1 ci: remove duplicate /usr/bin/time call
ca9ae45 issue #426: TemporaryEnvironment must coerce to Unicode.
4bc0d0e issue #426: apply_mode_spec() must handle bytes.
a8921bb tests: fix scaling in fork_histogram
374a361 docs: try to fix CSS difference between local and rtfd
861be2e docs: wrap text around logos
b084d83 docs: fit SVG viewbox to ansible logo
98d06e2 docs: delete shame.rst to make room for new chapters.
5f3244a docs: import pcaps (using LFS) to regenerate charts.
6936b93 tests: import fork_histogram.py.
5a96d13 issue #426: fix all.yml sorting, one more delegate_to
bd82fa1 issue #426: fix low_level_execute_command.yml breakage.
a6e6bc4 issue #426: to_text filter.
d15f533 Turn on Travis build notifcations, but send them to IRC.
9d87f03 issue #426: disable Ansible smart transport.
49d37bf issue #426: remove LANG and LC_ALL during tests.
a6e6fd1 issue #426: more 2->3 test fixes.
67f710f issue #426: use delegate_to in fixup_perms2 and copy.yml
a67a436 docs: add #374 to Changelog.
2b229a6 docs: add thanks entry.
issue510
David Wilson 6 years ago
commit 9a305acdda

@ -6,7 +6,7 @@ batches = [
[ [
# Must be installed separately, as PyNACL indirect requirement causes # Must be installed separately, as PyNACL indirect requirement causes
# newer version to be installed if done in a single pip run. # newer version to be installed if done in a single pip run.
'pip install "pycparser<2.19"', 'pip install "pycparser<2.19" "idna<2.7"',
'pip install ' 'pip install '
'-r tests/requirements.txt ' '-r tests/requirements.txt '
'-r tests/ansible/requirements.txt', '-r tests/ansible/requirements.txt',

@ -56,5 +56,5 @@ with ci_lib.Fold('job_setup'):
with ci_lib.Fold('ansible'): with ci_lib.Fold('ansible'):
playbook = os.environ.get('PLAYBOOK', 'all.yml') playbook = os.environ.get('PLAYBOOK', 'all.yml')
run('/usr/bin/time ./run_ansible_playbook.py %s -i "%s" %s', run('./run_ansible_playbook.py %s -i "%s" %s',
playbook, HOSTS_DIR, ' '.join(sys.argv[1:])) playbook, HOSTS_DIR, ' '.join(sys.argv[1:]))

@ -15,6 +15,13 @@ try:
except ImportError: except ImportError:
import urllib.parse as urlparse import urllib.parse as urlparse
os.chdir(
os.path.join(
os.path.dirname(__file__),
'..'
)
)
# #
# check_output() monkeypatch cutpasted from testlib.py # check_output() monkeypatch cutpasted from testlib.py

@ -4,7 +4,7 @@ import ci_lib
batches = [ batches = [
[ [
'pip install "pycparser<2.19"', 'pip install "pycparser<2.19" "idna<2.7"',
'pip install -r tests/requirements.txt', 'pip install -r tests/requirements.txt',
], ],
[ [

@ -2,6 +2,7 @@ sudo: required
notifications: notifications:
email: false email: false
irc: "chat.freenode.net#mitogen-builds"
language: python language: python

@ -34,6 +34,7 @@ import os
import pprint import pprint
import random import random
import stat import stat
import sys
import time import time
import jinja2.runtime import jinja2.runtime
@ -842,6 +843,19 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self._mitogen_reset(mode='put') self._mitogen_reset(mode='put')
self._shutdown_broker() self._shutdown_broker()
def _reset_find_task_vars(self):
"""
Monsterous hack: since "meta: reset_connection" does not run from an
action, we cannot capture task variables via :meth:`on_action_run`.
Instead walk the parent frames searching for the `all_vars` local from
StrategyBase._execute_meta(). If this fails, just leave task_vars
unset, likely causing the wrong configuration to be created.
"""
frame = sys._getframe()
while frame and not self._task_vars:
self._task_vars = frame.f_locals.get('all_vars')
frame = frame.f_back
reset_compat_msg = ( reset_compat_msg = (
'Mitogen only supports "reset_connection" on Ansible 2.5.6 or later' 'Mitogen only supports "reset_connection" on Ansible 2.5.6 or later'
) )
@ -853,6 +867,9 @@ class Connection(ansible.plugins.connection.ConnectionBase):
the 'disconnected' state, and informs ContextService the connection is the 'disconnected' state, and informs ContextService the connection is
bad somehow, and should be shut down and discarded. bad somehow, and should be shut down and discarded.
""" """
if self._task_vars is None:
self._reset_find_task_vars()
if self._play_context.remote_addr is None: if self._play_context.remote_addr is None:
# <2.5.6 incorrectly populate PlayContext for reset_connection # <2.5.6 incorrectly populate PlayContext for reset_connection
# https://github.com/ansible/ansible/issues/27520 # https://github.com/ansible/ansible/issues/27520

@ -172,7 +172,7 @@ class BinaryPlanner(Planner):
return module_common._is_binary(self._inv.module_source) return module_common._is_binary(self._inv.module_source)
def get_push_files(self): def get_push_files(self):
return [self._inv.module_path] return [mitogen.core.to_text(self._inv.module_path)]
def get_kwargs(self, **kwargs): def get_kwargs(self, **kwargs):
return super(BinaryPlanner, self).get_kwargs( return super(BinaryPlanner, self).get_kwargs(
@ -285,7 +285,7 @@ class NewStylePlanner(ScriptPlanner):
def get_push_files(self): def get_push_files(self):
return super(NewStylePlanner, self).get_push_files() + [ return super(NewStylePlanner, self).get_push_files() + [
path mitogen.core.to_text(path)
for fullname, path, is_pkg in self.get_module_map()['custom'] for fullname, path, is_pkg in self.get_module_map()['custom']
] ]

@ -454,6 +454,8 @@ class TemporaryEnvironment(object):
self.original = dict(os.environ) self.original = dict(os.environ)
self.env = env or {} self.env = env or {}
for key, value in iteritems(self.env): for key, value in iteritems(self.env):
key = mitogen.core.to_text(key)
value = mitogen.core.to_text(value)
if value is None: if value is None:
os.environ.pop(key, None) os.environ.pop(key, None)
else: else:

@ -492,14 +492,16 @@ class ModuleDepService(mitogen.service.Service):
def _get_builtin_names(self, builtin_path, resolved): def _get_builtin_names(self, builtin_path, resolved):
return [ return [
fullname mitogen.core.to_text(fullname)
for fullname, path, is_pkg in resolved for fullname, path, is_pkg in resolved
if os.path.abspath(path).startswith(builtin_path) if os.path.abspath(path).startswith(builtin_path)
] ]
def _get_custom_tups(self, builtin_path, resolved): def _get_custom_tups(self, builtin_path, resolved):
return [ return [
(fullname, path, is_pkg) (mitogen.core.to_text(fullname),
mitogen.core.to_text(path),
is_pkg)
for fullname, path, is_pkg in resolved for fullname, path, is_pkg in resolved
if not os.path.abspath(path).startswith(builtin_path) if not os.path.abspath(path).startswith(builtin_path)
] ]

@ -669,7 +669,7 @@ def apply_mode_spec(spec, mode):
Given a symbolic file mode change specification in the style of chmod(1) Given a symbolic file mode change specification in the style of chmod(1)
`spec`, apply changes in the specification to the numeric file mode `mode`. `spec`, apply changes in the specification to the numeric file mode `mode`.
""" """
for clause in spec.split(','): for clause in mitogen.core.to_text(spec).split(','):
match = CHMOD_CLAUSE_PAT.match(clause) match = CHMOD_CLAUSE_PAT.match(clause)
who, op, perms = match.groups() who, op, perms = match.groups()
for ch in who or 'a': for ch in who or 'a':

@ -12,6 +12,17 @@ div.body li {
} }
/*
* Undo the hyphens: auto in Sphinx basic.css.
*/
div.body p, div.body dd, div.body li, div.body blockquote {
-moz-hyphens: inherit;
-ms-hyphens: inherit;
-webkit-hyphens: inherit;
hyphens: inherit;
}
/* /*
* Setting :width; on an image causes Sphinx to turn the image into a link, so * Setting :width; on an image causes Sphinx to turn the image into a link, so
@ -27,6 +38,12 @@ div.body li {
width: 150px; width: 150px;
} }
.mitogen-right-200 {
float: right;
padding-left: 8px;
width: 200px;
}
.mitogen-right-225 { .mitogen-right-225 {
float: right; float: right;
padding-left: 8px; padding-left: 8px;
@ -50,3 +67,10 @@ div.body li {
padding-left: 8px; padding-left: 8px;
width: 350px; width: 350px;
} }
.mitogen-logo-wrap {
shape-margin: 8px;
shape-outside: polygon(
100% 0, 50% 10%, 24% 24%, 0% 50%, 24% 75%, 50% 90%, 100% 100%
);
}

@ -1,11 +1,10 @@
.. image:: images/ansible/ansible_mitogen.svg
:class: mitogen-right-225
Mitogen for Ansible Mitogen for Ansible
=================== ===================
.. image:: images/ansible/ansible_mitogen.svg
:class: mitogen-right-200 mitogen-logo-wrap
An extension to `Ansible`_ is included that implements connections over An extension to `Ansible`_ is included that implements connections over
Mitogen, replacing embedded shell invocations with pure-Python equivalents Mitogen, replacing embedded shell invocations with pure-Python equivalents
invoked via highly efficient remote procedure calls to persistent interpreters invoked via highly efficient remote procedure calls to persistent interpreters

@ -180,9 +180,10 @@ Fixes
print a useful hint on failure, as no useful error is normally logged to the print a useful hint on failure, as no useful error is normally logged to the
console by these tools. console by these tools.
* `#391 <https://github.com/dw/mitogen/issues/391>`_: file transfer from 2.x * `#374 <https://github.com/dw/mitogen/issues/374>`_,
controllers to 3.x targets was broken due to a regression caused by `#391 <https://github.com/dw/mitogen/issues/391>`_: file transfer and module
refactoring, and compounded by `#426 execution from 2.x controllers to 3.x targets was broken due to a regression
caused by refactoring, and compounded by `#426
<https://github.com/dw/mitogen/issues/426>`_. <https://github.com/dw/mitogen/issues/426>`_.
* `#400 <https://github.com/dw/mitogen/issues/400>`_: work around a threading * `#400 <https://github.com/dw/mitogen/issues/400>`_: work around a threading
@ -289,6 +290,9 @@ Core Library
* `#439 <https://github.com/dw/mitogen/issues/439>`_: descriptive errors are * `#439 <https://github.com/dw/mitogen/issues/439>`_: descriptive errors are
raised when attempting to invoke unsupported function types. raised when attempting to invoke unsupported function types.
* `#444 <https://github.com/dw/mitogen/issues/444>`_: messages regarding
unforwardable extension module are no longer logged as errors.
* `#453 <https://github.com/dw/mitogen/issues/453>`_: the loggers used in * `#453 <https://github.com/dw/mitogen/issues/453>`_: the loggers used in
children for standard IO redirection have propagation disabled, preventing children for standard IO redirection have propagation disabled, preventing
accidental reconfiguration of the :mod:`logging` package in a child from accidental reconfiguration of the :mod:`logging` package in a child from
@ -328,6 +332,7 @@ bug reports, testing, features and fixes in this release contributed by
`Berend De Schouwer <https://github.com/berenddeschouwer>`_, `Berend De Schouwer <https://github.com/berenddeschouwer>`_,
`Brian Candler <https://github.com/candlerb>`_, `Brian Candler <https://github.com/candlerb>`_,
`Duane Zamrok <https://github.com/dewthefifth>`_, `Duane Zamrok <https://github.com/dewthefifth>`_,
`Eric Chang <https://github.com/changchichung>`_,
`Guy Knights <https://github.com/knightsg>`_, `Guy Knights <https://github.com/knightsg>`_,
`Jiří Vávra <https://github.com/Houbovo>`_, `Jiří Vávra <https://github.com/Houbovo>`_,
`Jonathan Rosser <https://github.com/jrosser>`_, `Jonathan Rosser <https://github.com/jrosser>`_,

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

@ -2,8 +2,6 @@
Mitogen Mitogen
======= =======
Mitogen is a Python library for writing distributed self-replicating programs.
.. raw:: html .. raw:: html
<style> <style>
@ -13,7 +11,9 @@ Mitogen is a Python library for writing distributed self-replicating programs.
</style> </style>
.. image:: images/mitogen.svg .. image:: images/mitogen.svg
:class: mitogen-right-225 :class: mitogen-right-200 mitogen-logo-wrap
Mitogen is a Python library for writing distributed self-replicating programs.
There is no requirement for installing packages, copying files around, writing There is no requirement for installing packages, copying files around, writing
shell snippets, upfront configuration, or providing any secondary link to a shell snippets, upfront configuration, or providing any secondary link to a

@ -0,0 +1,3 @@
*.pcapng filter=lfs diff=lfs merge=lfs -text
run_hostname_100_times_mito.pcap.gz filter=lfs diff=lfs merge=lfs -text
run_hostname_100_times_vanilla.pcap.gz filter=lfs diff=lfs merge=lfs -text

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6d9b4d4ff263003bd16e44c265783e7c1deff19950e453e3adeb8a6ab5052081
size 175120

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7a993832501b7948a38c2e8ced3467d1cb04279a57ddd6afc735ed96ec509c08
size 7623337

@ -1,90 +0,0 @@
Importer Wall Of Shame
----------------------
The following modules and packages violate protocol or best practice in some way:
* They run magic during ``__init.py__`` that makes life hard for Mitogen.
Executing code during module import is always bad, and Mitogen is a concrete
benchmark for why it's bad.
* They install crap in :py:data:`sys.modules` that completely ignore or
partially implement the protocols laid out in PEP-302.
* They "vendor" a third party package, either incompletely, using hacks visible
through the runtime's standard interfaces, or with ancient versions of code
that in turn mess with :py:data:`sys.modules` in some horrible way.
Bugs will probably be filed for these in time, but it does not address the huge
installed base of existing old software versions, so hacks are needed anyway.
``pbr``
=======
It claims to use ``pkg_resources`` to read version information
(``_get_version_from_pkg_metadata()``), which would result in PEP-302 being
reused and everything just working wonderfully, but instead it actually does
direct filesystem access.
**What could it do instead?**
* ``pkg_resources.resource_stream()``
**What Mitogen is forced to do**
When it sees ``pbr`` being loaded, it smodges the process environment with a
``PBR_VERSION`` variable to override any attempt to auto-detect the version.
This will probably break code I haven't seen yet.
``pkg_resources``
=================
Anything that imports ``pkg_resources`` will eventually cause ``pkg_resources``
to try and import and scan ``__main__`` for its ``__requires__`` attribute
(``pkg_resources/__init__.py::_build_master()``). This breaks any app that is
not expecting its ``__main__`` to suddenly be sucked over a network and
injected into a remote process, like py.test.
A future version of Mitogen might have a more general hack that doesn't import
the master's ``__main__`` as ``__main__`` in the slave, avoiding all kinds of
issues like these.
**What could it do instead?**
* Explicit is better than implicit: wait until the magical behaviour is
explicitly requested (i.e. an API call).
* Use ``get("__main__")`` on :py:data:`sys.modules` rather than ``import``, but
this method isn't general enough, it only really helps tools like Mitogen.
**What Mitogen is forced to do**
Examine the stack during every attempt to import ``__main__`` and check if the
requestee module is named ``pkg_resources``, if so then refuse the import.
``six``
=======
The ``six`` module makes some effort to conform to PEP-302, but it is missing
several critical pieces, e.g. the ``__loader__`` attribute. This not only
breaks the Python standard library tooling (such as the :py:mod:`inspect`
module), but also Mitogen. Newer versions of ``six`` improve things somewhat,
but there are still outstanding issues preventing Mitogen from working with
``six``.
This package is sufficiently popular that it must eventually be supported. See
`here for an example issue`_.
.. _here for an example issue: https://github.com/dw/mitogen/issues/31
**What could it do instead?**
* Any custom hacks installed into :py:data:`sys.modules` should support the
protocols laid out in PEP-302.
**What Mitogen is forced to do**
Vendored versions of ``six`` currently don't work at all.

@ -14,7 +14,6 @@ Table Of Contents
api api
examples examples
internals internals
shame
.. toctree:: .. toctree::
:hidden: :hidden:

@ -1626,7 +1626,7 @@ class RouteMonitor(object):
""" """
data = str(target_id) data = str(target_id)
if name: if name:
data = '%s:%s' % (target_id, mitogen.core.b(name)) data = '%s:%s' % (target_id, name)
stream.send( stream.send(
mitogen.core.Message( mitogen.core.Message(
handle=handle, handle=handle,

@ -594,6 +594,7 @@ class PushFileService(Service):
self._sent_by_stream = {} self._sent_by_stream = {}
def get(self, path): def get(self, path):
assert isinstance(path, mitogen.core.UnicodeType)
self._lock.acquire() self._lock.acquire()
try: try:
if path in self._cache: if path in self._cache:
@ -641,7 +642,7 @@ class PushFileService(Service):
with a set of small files and Python modules. with a set of small files and Python modules.
""" """
for path in paths: for path in paths:
self.propagate_to(context, path) self.propagate_to(context, mitogen.core.to_text(path))
self.router.responder.forward_modules(context, modules) self.router.responder.forward_modules(context, modules)
@expose(policy=AllowParents()) @expose(policy=AllowParents())
@ -670,7 +671,7 @@ class PushFileService(Service):
@expose(policy=AllowParents()) @expose(policy=AllowParents())
@no_reply() @no_reply()
@arg_spec({ @arg_spec({
'path': mitogen.core.FsPathTypes, 'path': mitogen.core.UnicodeType,
'data': mitogen.core.Blob, 'data': mitogen.core.Blob,
'context': mitogen.core.Context, 'context': mitogen.core.Context,
}) })

@ -7,11 +7,15 @@ callback_plugins = lib/callback
stdout_callback = nice_stdout stdout_callback = nice_stdout
vars_plugins = lib/vars vars_plugins = lib/vars
library = lib/modules library = lib/modules
filter_plugins = lib/filters
module_utils = lib/module_utils module_utils = lib/module_utils
retry_files_enabled = False retry_files_enabled = False
display_args_to_stdout = True display_args_to_stdout = True
forks = 100 forks = 100
# On MacOS, "smart" with a password set causes Ansible to use paramiko.
transport = ssh
no_target_syslog = True no_target_syslog = True
# Required by integration/ssh/timeouts.yml # Required by integration/ssh/timeouts.yml

@ -8,13 +8,13 @@
dest: /tmp/copy-tiny-file dest: /tmp/copy-tiny-file
content: content:
this is a tiny file. this is a tiny file.
connection: local delegate_to: localhost
- copy: - copy:
dest: /tmp/copy-large-file dest: /tmp/copy-large-file
# Must be larger than Connection.SMALL_SIZE_LIMIT. # Must be larger than Connection.SMALL_SIZE_LIMIT.
content: "{% for x in range(200000) %}x{% endfor %}" content: "{% for x in range(200000) %}x{% endfor %}"
connection: local delegate_to: localhost
# end of making files # end of making files

@ -53,7 +53,7 @@
state: absent state: absent
- name: Create local test file. - name: Create local test file.
connection: local delegate_to: localhost
copy: copy:
content: "weird mode" content: "weird mode"
dest: "/tmp/weird-mode" dest: "/tmp/weird-mode"

@ -1,6 +1,6 @@
# Verify the behaviour of _low_level_execute_command(). # Verify the behaviour of _low_level_execute_command().
- name: integration/action__low_level_execute_command.yml - name: integration/action/low_level_execute_command.yml
hosts: test-targets hosts: test-targets
any_errors_fatal: true any_errors_fatal: true
tasks: tasks:
@ -14,8 +14,8 @@
assert: assert:
that: that:
- 'raw.rc == 0' - 'raw.rc == 0'
- 'raw.stdout_lines == ["2"]' - 'raw.stdout_lines[-1]|to_text == "2"'
- 'raw.stdout == "2"' - 'raw.stdout[-1]|to_text == "2"'
- name: Run raw module with sudo - name: Run raw module with sudo
become: true become: true
@ -27,6 +27,12 @@
assert: assert:
that: that:
- raw.rc == 0 - raw.rc == 0
# WHY DOES VANILLA ANSIBLE INSERT NEWLINES HERE!?!?!?!?!?!ONE # WHY DOES VANILLA INSERT NEWLINES HERE!?!?!?!?!?!ONE
- raw.stdout in ("\r\nroot\r\n", "root\r\n") #- raw.stdout in ("\r\nroot\r\n", "root\r\n")
- raw.stdout_lines in (["", "root"], ["root"]) - '(raw.stdout|to_text).endswith("root\r\n")'
- |
raw.stdout_lines|to_text in (
["\r\n"],
["root\r\n"],
["root"],
)

@ -13,22 +13,22 @@
dest: /tmp/synchronize-action-key dest: /tmp/synchronize-action-key
src: ../../../data/docker/mitogen__has_sudo_pubkey.key src: ../../../data/docker/mitogen__has_sudo_pubkey.key
mode: u=rw,go= mode: u=rw,go=
connection: local delegate_to: localhost
- file: - file:
path: /tmp/sync-test path: /tmp/sync-test
state: absent state: absent
connection: local delegate_to: localhost
- file: - file:
path: /tmp/sync-test path: /tmp/sync-test
state: directory state: directory
connection: local delegate_to: localhost
- copy: - copy:
dest: /tmp/sync-test/item dest: /tmp/sync-test/item
content: "item!" content: "item!"
connection: local delegate_to: localhost
- file: - file:
path: /tmp/sync-test.out path: /tmp/sync-test.out

@ -7,7 +7,6 @@
- import_playbook: async/all.yml - import_playbook: async/all.yml
- import_playbook: become/all.yml - import_playbook: become/all.yml
- import_playbook: connection/all.yml - import_playbook: connection/all.yml
- import_playbook: stub_connections/all.yml
- import_playbook: connection_loader/all.yml - import_playbook: connection_loader/all.yml
- import_playbook: context_service/all.yml - import_playbook: context_service/all.yml
- import_playbook: delegation/all.yml - import_playbook: delegation/all.yml
@ -18,3 +17,4 @@
- import_playbook: runner/all.yml - import_playbook: runner/all.yml
- import_playbook: ssh/all.yml - import_playbook: ssh/all.yml
- import_playbook: strategy/all.yml - import_playbook: strategy/all.yml
- import_playbook: stub_connections/all.yml

@ -3,7 +3,7 @@
- shell: dd if=/dev/urandom of=/tmp/{{file_name}} bs=1024 count={{file_size}} - shell: dd if=/dev/urandom of=/tmp/{{file_name}} bs=1024 count={{file_size}}
args: args:
creates: /tmp/{{file_name}} creates: /tmp/{{file_name}}
connection: local delegate_to: localhost
- copy: - copy:
dest: /tmp/{{file_name}}.out dest: /tmp/{{file_name}}.out
@ -11,7 +11,7 @@
- stat: path=/tmp/{{file_name}} - stat: path=/tmp/{{file_name}}
register: original register: original
connection: local delegate_to: localhost
- stat: path=/tmp/{{file_name}}.out - stat: path=/tmp/{{file_name}}.out
register: copied register: copied
@ -19,4 +19,5 @@
- assert: - assert:
that: that:
- original.stat.checksum == copied.stat.checksum - original.stat.checksum == copied.stat.checksum
- (not is_mitogen) or (original.stat.mtime|int == copied.stat.mtime|int) # Upstream does not preserve timestamps at al.
#- (not is_mitogen) or (original.stat.mtime|int == copied.stat.mtime|int)

@ -9,7 +9,7 @@
- meta: end_play - meta: end_play
when: not is_mitogen when: not is_mitogen
- connection: local - delegate_to: localhost
command: | command: |
ansible-playbook ansible-playbook
-i "{{inventory_file}}" -i "{{inventory_file}}"

@ -1,13 +1,13 @@
# Ensure 'local' connections are grabbed. # Ensure 'local' connections are grabbed.
- name: integration/connection_loader__local_blemished.yml - name: integration/connection_loader/local_blemished.yml
hosts: test-targets hosts: test-targets
any_errors_fatal: true any_errors_fatal: true
tasks: tasks:
- determine_strategy: - determine_strategy:
- custom_python_detect_environment: - custom_python_detect_environment:
connection: local delegate_to: localhost
register: out register: out
- assert: - assert:

@ -0,0 +1,92 @@
# Monkey-patch os.fork() to produce a latency histogram on run completion.
# Requires 'hdrhsitograms' PyPI module.
from __future__ import print_function
import os
import resource
import sys
import time
import ansible.plugins.callback
import hdrh.histogram
def get_fault_count(who=resource.RUSAGE_CHILDREN):
ru = resource.getrusage(who)
return ru.ru_minflt + ru.ru_majflt
class CallbackModule(ansible.plugins.callback.CallbackBase):
hist = None
def v2_playbook_on_start(self, playbook):
if self.hist is not None:
return
self.hist = hdrh.histogram.HdrHistogram(1, int(1e6*60), 3)
self.fork_latency_sum_usec = 0.0
self.install()
def install(self):
self.faults_at_start = get_fault_count(resource.RUSAGE_SELF)
self.run_start_time = time.time()
self.real_fork = os.fork
os.fork = self.my_fork
self_fault_usec = 1.113664156753052
child_fault_usec = 4.734975610975617
dummy_heap_size = int(os.environ.get('FORK_STATS_FAKE_HEAP_MB', '0'))
dummy_heap = 'x' * (dummy_heap_size * 1048576)
def my_fork(self):
# doesnt count last child, oh well
now_faults = get_fault_count()
t0 = time.time()
try:
return self.real_fork()
finally:
latency_usec = (1e6 * (time.time() - t0))
self.fork_latency_sum_usec += latency_usec
self.hist.record_value(latency_usec)
def playbook_on_stats(self, stats):
self_faults = get_fault_count(resource.RUSAGE_SELF) - self.faults_at_start
child_faults = get_fault_count()
run_duration_sec = time.time() - self.run_start_time
fault_wastage_usec = (
((self.self_fault_usec * self_faults) +
(self.child_fault_usec * child_faults))
)
fork_wastage = self.hist.get_total_count()
all_wastage_usec = ((2*self.fork_latency_sum_usec) + fault_wastage_usec)
print('--- Fork statistics ---')
print('Post-boot run duration: %.02f ms, %d total forks' % (
1000 * run_duration_sec,
self.hist.get_total_count(),
))
print('Self faults during boot: %d, post-boot: %d, avg %d/child' % (
self.faults_at_start,
self_faults,
self_faults / self.hist.get_total_count(),
))
print('Total child faults: %d, avg %d/child' % (
child_faults,
child_faults / self.hist.get_total_count(),
))
print('Est. wastage on faults: %d ms, forks+faults+waits: %d ms (%.2f%%)' % (
fault_wastage_usec / 1000,
all_wastage_usec / 1000,
100 * (all_wastage_usec / (run_duration_sec * 1e6)),
))
print('99th%% fork latency: %.03f msec, max %d new tasks/sec' % (
self.hist.get_value_at_percentile(99) / 1000.0,
1e6 / self.hist.get_value_at_percentile(99),
))
self.hist.output_percentile_distribution(sys.stdout, 1000)
print('--- End fork statistics ---')
print()

@ -0,0 +1,27 @@
from ansible.module_utils._text import to_text
try:
Unicode = unicode
except:
Unicode = str
def to_text(s):
"""
Ensure the str or unicode `s` is unicode, and strip away any subclass. Also
works on lists.
"""
if isinstance(s, list):
return [to_text(ss) for ss in s]
if not isinstance(s, Unicode):
s = to_text(s)
return Unicode(s)
class FilterModule(object):
def filters(self):
return {
'to_text': to_text,
}

@ -2,5 +2,6 @@ ansible; python_version >= '2.7'
ansible<2.7; python_version < '2.7' ansible<2.7; python_version < '2.7'
paramiko==2.3.2 # Last 2.6-compat version. paramiko==2.3.2 # Last 2.6-compat version.
google-api-python-client==1.6.5 google-api-python-client==1.6.5
hdrhistogram==0.6.1
PyYAML==3.11; python_version < '2.7' PyYAML==3.11; python_version < '2.7'
PyYAML==3.12; python_version >= '2.7' PyYAML==3.12; python_version >= '2.7'

@ -18,6 +18,12 @@ os.environ.setdefault(
os.path.dirname(os.path.dirname(sys.executable)) os.path.dirname(os.path.dirname(sys.executable))
) )
# Set LANG and LC_ALL to C in order to avoid locale errors spammed by vanilla
# during exec_command().
os.environ.pop('LANG', None)
os.environ.pop('LC_ALL', None)
# Used by delegate_to.yml to ensure "sudo -E" preserves environment. # Used by delegate_to.yml to ensure "sudo -E" preserves environment.
os.environ['I_WAS_PRESERVED'] = '1' os.environ['I_WAS_PRESERVED'] = '1'

@ -97,6 +97,7 @@ class CallFunctionTest(testlib.RouterMixin, testlib.TestCase):
recv = self.local.call_async(time.sleep, 120) recv = self.local.call_async(time.sleep, 120)
time.sleep(0.05) # Ensure GIL is released time.sleep(0.05) # Ensure GIL is released
self.broker.shutdown() self.broker.shutdown()
self.broker_shutdown = True
exc = self.assertRaises(mitogen.core.ChannelError, exc = self.assertRaises(mitogen.core.ChannelError,
lambda: recv.get()) lambda: recv.get())
self.assertEquals(exc.args[0], self.router.respondent_disconnect_msg) self.assertEquals(exc.args[0], self.router.respondent_disconnect_msg)

@ -9,72 +9,100 @@ import testlib
class FetchTest(testlib.RouterMixin, testlib.TestCase): class FetchTest(testlib.RouterMixin, testlib.TestCase):
klass = mitogen.service.FileService klass = mitogen.service.FileService
def replyable_msg(self, **kwargs):
recv = mitogen.core.Receiver(self.router, persist=False)
msg = mitogen.core.Message(
src_id=mitogen.context_id,
reply_to=recv.handle,
**kwargs
)
msg.router = self.router
return recv, msg
def test_unauthorized(self): def test_unauthorized(self):
service = self.klass(self.router) service = self.klass(self.router)
e = self.assertRaises(mitogen.service.Error, recv, msg = self.replyable_msg()
lambda: service.fetch( service.fetch(
path='/etc/shadow', path='/etc/shadow',
sender=None, sender=None,
msg=mitogen.core.Message(), msg=msg,
)
) )
e = self.assertRaises(mitogen.core.CallError,
self.assertEquals(e.args[0], service.unregistered_msg) lambda: recv.get().unpickle())
expect = service.unregistered_msg % ('/etc/shadow',)
self.assertTrue(expect in e.args[0])
def _validate_response(self, resp):
self.assertTrue(isinstance(resp, dict))
self.assertEquals('root', resp['owner'])
self.assertEquals('root', resp['group'])
self.assertTrue(isinstance(resp['mode'], int))
self.assertTrue(isinstance(resp['mtime'], float))
self.assertTrue(isinstance(resp['atime'], float))
self.assertTrue(isinstance(resp['size'], int))
def test_path_authorized(self): def test_path_authorized(self):
recv = mitogen.core.Receiver(self.router) recv = mitogen.core.Receiver(self.router)
service = self.klass(self.router) service = self.klass(self.router)
service.register('/etc/passwd') service.register('/etc/passwd')
self.assertEquals(None, service.fetch( recv, msg = self.replyable_msg()
service.fetch(
path='/etc/passwd', path='/etc/passwd',
sender=recv.to_sender(), sender=recv.to_sender(),
msg=mitogen.core.Message(), msg=msg,
)) )
self._validate_response(recv.get().unpickle())
def test_root_authorized(self): def test_root_authorized(self):
recv = mitogen.core.Receiver(self.router) recv = mitogen.core.Receiver(self.router)
service = self.klass(self.router) service = self.klass(self.router)
service.register_prefix('/') service.register_prefix('/')
self.assertEquals(None, service.fetch( recv, msg = self.replyable_msg()
service.fetch(
path='/etc/passwd', path='/etc/passwd',
sender=recv.to_sender(), sender=recv.to_sender(),
msg=mitogen.core.Message(), msg=msg,
)) )
self._validate_response(recv.get().unpickle())
def test_prefix_authorized(self): def test_prefix_authorized(self):
recv = mitogen.core.Receiver(self.router) recv = mitogen.core.Receiver(self.router)
service = self.klass(self.router) service = self.klass(self.router)
service.register_prefix('/etc') service.register_prefix('/etc')
self.assertEquals(None, service.fetch( recv, msg = self.replyable_msg()
service.fetch(
path='/etc/passwd', path='/etc/passwd',
sender=recv.to_sender(), sender=recv.to_sender(),
msg=mitogen.core.Message(), msg=msg,
)) )
self._validate_response(recv.get().unpickle())
def test_prefix_authorized_abspath_bad(self): def test_prefix_authorized_abspath_bad(self):
recv = mitogen.core.Receiver(self.router) recv = mitogen.core.Receiver(self.router)
service = self.klass(self.router) service = self.klass(self.router)
service.register_prefix('/etc') service.register_prefix('/etc')
self.assertEquals(None, service.fetch( recv, msg = self.replyable_msg()
service.fetch(
path='/etc/foo/bar/../../../passwd', path='/etc/foo/bar/../../../passwd',
sender=recv.to_sender(), sender=recv.to_sender(),
msg=mitogen.core.Message(), msg=msg,
)) )
self.assertEquals(None, recv.get().unpickle())
def test_prefix_authorized_abspath_bad(self): def test_prefix_authorized_abspath_bad(self):
recv = mitogen.core.Receiver(self.router) recv = mitogen.core.Receiver(self.router)
service = self.klass(self.router) service = self.klass(self.router)
service.register_prefix('/etc') service.register_prefix('/etc')
e = self.assertRaises(mitogen.service.Error, recv, msg = self.replyable_msg()
lambda: service.fetch( service.fetch(
path='/etc/../shadow', path='/etc/../shadow',
sender=recv.to_sender(), sender=recv.to_sender(),
msg=mitogen.core.Message(), msg=msg,
)
) )
e = self.assertRaises(mitogen.core.CallError,
self.assertEquals(e.args[0], service.unregistered_msg) lambda: recv.get().unpickle())
expect = service.unregistered_msg % ('/etc/../shadow',)
self.assertTrue(expect in e.args[0])
if __name__ == '__main__': if __name__ == '__main__':

@ -388,13 +388,15 @@ class DockerizedSshDaemon(object):
class BrokerMixin(object): class BrokerMixin(object):
broker_class = mitogen.master.Broker broker_class = mitogen.master.Broker
broker_shutdown = False
def setUp(self): def setUp(self):
super(BrokerMixin, self).setUp() super(BrokerMixin, self).setUp()
self.broker = self.broker_class() self.broker = self.broker_class()
def tearDown(self): def tearDown(self):
self.broker.shutdown() if not self.broker_shutdown:
self.broker.shutdown()
self.broker.join() self.broker.join()
super(BrokerMixin, self).tearDown() super(BrokerMixin, self).tearDown()

Loading…
Cancel
Save