Merge remote-tracking branch 'origin/v028' into stable

* origin/v028: (383 commits)
  Bump version for release.
  docs: update Changelog for 0.2.8.
  issue #627: add test and tweak Reaper behaviour.
  docs: lots more changelog concision
  docs: changelog concision
  docs: more changelog tweaks
  docs: reorder chapters
  docs: versionless <title>
  docs: update supported Ansible version, mention unsupported features
  docs: changelog fixes/tweaks
  issue #590: update Changelog.
  issue #621: send ADD_ROUTE earlier and add test for early logging.
  issue #590: whoops, import missing test modules
  issue #590: rework ParentEnumerationMethod to recursively handle bad modules
  issue #627: reduce the default pool size in a child to 2.
  tests: add a few extra service tests.
  docs: some more hyperlink joy
  docs: more hyperlinks
  docs: add domainrefs plugin to make link aliases everywhere \o/
  docs: link IS_DEAD in changelog
  docs: tweaks to better explain changelog race
  issue #533: update routing to account for DEL_ROUTE propagation race
  tests: use defer_sync() Rather than defer() + ancient sync_with_broker()
  tests: one case from doas_test was invoking su
  tests: hide memory-mapped files from lsof output
  issue #615: remove meaningless test
  issue #625: ignore SIGINT within MuxProcess
  issue #625: use exec() instead of subprocess in mitogen_ansible_playbook
  issue #615: regression test
  issue #615: update Changelog.
  issue #615: ensure 4GB max_message_size is configured for task workers.
  issue #615: update Changelog.
  issue #615: route a dead message to recipients when no reply is expected
  issue #615: fetch_file() might be called with AnsibleUnicode.
  issue #615: redirect 'fetch' action to 'mitogen_fetch'.
  issue #615: extricate slurp brainwrong from mitogen_fetch
  issue #615: ansible: import Ansible fetch.py action plug-in
  issue #533: include object identity of Stream in repr()
  docs: lots more changelog
  issue #595: add buildah to docs and changelog.
  docs: a few more internals.rst additions
  ci: update to Ansible 2.8.3
  tests: another random string changed in 2.8.3
  tests: fix sudo_flags_failure for Ansible 2.8.3
  ci: fix procps command line format warning
  Whoops, merge together lgtm.yml and .lgtm.yml
  issue #440: log Python version during bootstrap.
  docs: update changelog
  issue #558: disable test on OSX to cope with boundless mediocrity
  issue #558, #582: preserve remote tmpdir if caller did not supply one
  issue #613: must await 'exit' and 'disconnect' in wait=False test
  Import LGTM config to disable some stuff
  Fix up another handful of LGTM errors.
  tests: work around AnsibleModule.run_command() race.
  docs: mention another __main__ safeguard
  docs: tweaks
  formatting error
  docs: make Sphinx install soft fail on Python 2.
  issue #598: allow disabling preempt in terraform
  issue #598: update Changelog.
  issue #605: update Changelog.
  issue #605: ansible: share a sem_t instead of a pthread_mutex_t
  issue #613: add tests for all the weird shutdown methods
  Add mitogen.core.now() and use it everywhere; closes #614.
  docs: move decorator docs into core.py and use autodecorator
  preamble_size: make it work on Python 3.
  docs: upgrade Sphinx to 2.1.2, require Python 3 to build docs.
  docs: fix Sphinx warnings, add LogHandler, more docstrings
  docs: tidy up some Changelog text
  issue #615: fix up FileService tests for new logic
  issue #615: another Py3x fix.
  issue #615: Py3x fix.
  issue #615: update Changelog.
  issue #615: use FileService for target->controll file transfers
  issue #482: another Py3 fix
  ci: try removing exclude: to make Azure jobs work again
  compat: fix Py2.4 SyntaxError
  issue #482: remove 'ssh' from checked processes
  ci: Py3 fix
  issue #279: add one more test for max_message_size
  issue #482: ci: add stray process checks to all jobs
  tests: fix format string error
  core: MitogenProtocol.is_privileged was not set in children
  issue #482: tests: fail DockerMixin tests if stray processes exist
  docs: update Changelog.
  issue #586: update Changelog.
  docs: update Changelog.
  [security] core: undirectional routing wasn't respected in some cases
  docs: tidy up Select.all()
  issue #612: update Changelog.
  master: fix TypeError
  pkgutil: fix Python3 compatibility
  parent: use protocol for getting remote_id
  docs: merge signals.rst into internals.rst
  os_fork: do not attempt to cork the active thread.
  parent: fix get_log_level() for split out loggers.
  issue #547: fix service_test failures.
  issue #547: update Changelog.
  issue #547: core/service: race/deadlock-free service pool init
  docs: update Changelog.
  ...
pull/862/head v0.2.8
David Wilson 6 years ago
commit 706a94bc97

@ -3,6 +3,7 @@
import glob
import os
import signal
import sys
import ci_lib
@ -13,11 +14,23 @@ TESTS_DIR = os.path.join(ci_lib.GIT_ROOT, 'tests/ansible')
HOSTS_DIR = os.path.join(ci_lib.TMP, 'hosts')
def pause_if_interactive():
if os.path.exists('/tmp/interactive'):
while True:
signal.pause()
interesting = ci_lib.get_interesting_procs()
with ci_lib.Fold('unit_tests'):
os.environ['SKIP_MITOGEN'] = '1'
ci_lib.run('./run_tests -v')
ci_lib.check_stray_processes(interesting)
with ci_lib.Fold('docker_setup'):
containers = ci_lib.make_containers()
ci_lib.start_containers(containers)
@ -56,8 +69,19 @@ with ci_lib.Fold('job_setup'):
run("sudo apt-get update")
run("sudo apt-get install -y sshpass")
run("bash -c 'sudo ln -vfs /usr/lib/python2.7/plat-x86_64-linux-gnu/_sysconfigdata_nd.py /usr/lib/python2.7 || true'")
run("bash -c 'sudo ln -vfs /usr/lib/python2.7/plat-x86_64-linux-gnu/_sysconfigdata_nd.py $VIRTUAL_ENV/lib/python2.7 || true'")
with ci_lib.Fold('ansible'):
playbook = os.environ.get('PLAYBOOK', 'all.yml')
run('./run_ansible_playbook.py %s -i "%s" %s',
playbook, HOSTS_DIR, ' '.join(sys.argv[1:]))
try:
run('./run_ansible_playbook.py %s -i "%s" %s',
playbook, HOSTS_DIR, ' '.join(sys.argv[1:]))
except:
pause_if_interactive()
raise
ci_lib.check_stray_processes(interesting, containers)
pause_if_interactive()

@ -5,16 +5,27 @@ parameters:
sign: false
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '$(python.version)'
architecture: 'x64'
- script: "PYTHONVERSION=$(python.version) .ci/prep_azure.py"
displayName: "Run prep_azure.py"
- script: .ci/prep_azure.py
displayName: "Install requirements."
# The VSTS-shipped Pythons available via UsePythonVErsion are pure garbage,
# broken symlinks, incorrect permissions and missing codecs. So we use the
# deadsnakes PPA to get sane Pythons, and setup a virtualenv to install our
# stuff into. The virtualenv can probably be removed again, but this was a
# hard-fought battle and for now I am tired of this crap.
- script: |
sudo ln -fs /usr/bin/python$(python.version) /usr/bin/python
/usr/bin/python -m pip install -U virtualenv setuptools wheel
/usr/bin/python -m virtualenv /tmp/venv -p /usr/bin/python$(python.version)
echo "##vso[task.prependpath]/tmp/venv/bin"
displayName: activate venv
- script: .ci/spawn_reverse_shell.py
displayName: "Spawn reverse shell"
- script: .ci/$(MODE)_install.py
displayName: "Install requirements."
displayName: "Run $(MODE)_install.py"
- script: .ci/$(MODE)_tests.py
displayName: Run tests.
displayName: "Run $(MODE)_tests.py"

@ -15,6 +15,9 @@ jobs:
Mito27_27:
python.version: '2.7'
MODE: mitogen
Ans280_27:
python.version: '2.7'
MODE: localhost_ansible
- job: Linux
@ -87,3 +90,13 @@ jobs:
#VER: 2.6.2
#DISTROS: debian
#STRATEGY: linear
Ansible_280_27:
python.version: '2.7'
MODE: ansible
VER: 2.8.0
Ansible_280_35:
python.version: '3.5'
MODE: ansible
VER: 2.8.0

@ -57,8 +57,10 @@ def have_docker():
# -----------------
# Force stdout FD 1 to be a pipe, so tools like pip don't spam progress bars.
# Force line buffering on stdout.
sys.stdout = os.fdopen(1, 'w', 1)
# Force stdout FD 1 to be a pipe, so tools like pip don't spam progress bars.
if 'TRAVIS_HOME' in os.environ:
proc = subprocess.Popen(
args=['stdbuf', '-oL', 'cat'],
@ -86,8 +88,13 @@ def _argv(s, *args):
def run(s, *args, **kwargs):
argv = ['/usr/bin/time', '--'] + _argv(s, *args)
print('Running: %s' % (argv,))
ret = subprocess.check_call(argv, **kwargs)
print('Finished running: %s' % (argv,))
try:
ret = subprocess.check_call(argv, **kwargs)
print('Finished running: %s' % (argv,))
except Exception:
print('Exception occurred while running: %s' % (argv,))
raise
return ret
@ -208,6 +215,46 @@ def make_containers(name_prefix='', port_offset=0):
return lst
# ssh removed from here because 'linear' strategy relies on processes that hang
# around after the Ansible run completes
INTERESTING_COMMS = ('python', 'sudo', 'su', 'doas')
def proc_is_docker(pid):
try:
fp = open('/proc/%s/cgroup' % (pid,), 'r')
except IOError:
return False
try:
return 'docker' in fp.read()
finally:
fp.close()
def get_interesting_procs(container_name=None):
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():
ppid, pid, comm, rest = line.split(None, 3)
if (
(
any(comm.startswith(s) for s in INTERESTING_COMMS) or
'mitogen:' in rest
) and
(
container_name is not None or
(not proc_is_docker(pid))
)
):
out.append((int(pid), line))
return sorted(out)
def start_containers(containers):
if os.environ.get('KEEP'):
return
@ -217,6 +264,7 @@ def start_containers(containers):
"docker rm -f %(name)s || true" % container,
"docker run "
"--rm "
"--cpuset-cpus 0,1 "
"--detach "
"--privileged "
"--cap-add=SYS_PTRACE "
@ -228,9 +276,44 @@ def start_containers(containers):
]
for container in containers
])
for container in containers:
container['interesting'] = get_interesting_procs(container['name'])
return containers
def verify_procs(hostname, old, new):
oldpids = set(pid for pid, _ in old)
if any(pid not in oldpids for pid, _ in new):
print('%r had stray processes running:' % (hostname,))
for pid, line in new:
if pid not in oldpids:
print('New process:', line)
print()
return False
return True
def check_stray_processes(old, containers=None):
ok = True
new = get_interesting_procs()
if old is not None:
ok &= verify_procs('test host machine', old, new)
for container in containers or ():
ok &= verify_procs(
container['name'],
container['interesting'],
get_interesting_procs(container['name'])
)
assert ok, 'stray processes were found'
def dump_file(path):
print()
print('--- %s ---' % (path,))

@ -3,6 +3,7 @@
from __future__ import print_function
import os
import shutil
import sys
import ci_lib
@ -67,9 +68,15 @@ with ci_lib.Fold('job_setup'):
os.environ['ANSIBLE_HOST_KEY_CHECKING'] = 'False'
interesting = ci_lib.get_interesting_procs()
with ci_lib.Fold('first_run'):
ci_lib.run('debops common')
ci_lib.run('debops common %s', ' '.join(sys.argv[1:]))
ci_lib.check_stray_processes(interesting, containers)
with ci_lib.Fold('second_run'):
ci_lib.run('debops common')
ci_lib.run('debops common %s', ' '.join(sys.argv[1:]))
ci_lib.check_stray_processes(interesting, containers)

@ -0,0 +1,16 @@
#!/usr/bin/env python
import ci_lib
batches = [
[
# Must be installed separately, as PyNACL indirect requirement causes
# newer version to be installed if done in a single pip run.
'pip install "pycparser<2.19" "idna<2.7"',
'pip install '
'-r tests/requirements.txt '
'-r tests/ansible/requirements.txt',
]
]
ci_lib.run_batches(batches)

@ -0,0 +1,54 @@
#!/usr/bin/env python
# Run tests/ansible/all.yml under Ansible and Ansible-Mitogen
import glob
import os
import shutil
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'):
os.environ['SKIP_MITOGEN'] = '1'
ci_lib.run('./run_tests -v')
with ci_lib.Fold('job_setup'):
# Don't set -U as that will upgrade Paramiko to a non-2.6 compatible version.
run("pip install -q virtualenv ansible==%s", ci_lib.ANSIBLE_VERSION)
os.chmod(KEY_PATH, int('0600', 8))
if not ci_lib.exists_in_path('sshpass'):
run("brew install http://git.io/sshpass.rb")
with ci_lib.Fold('machine_prep'):
ssh_dir = os.path.expanduser('~/.ssh')
if not os.path.exists(ssh_dir):
os.makedirs(ssh_dir, int('0700', 8))
key_path = os.path.expanduser('~/.ssh/id_rsa')
shutil.copy(KEY_PATH, key_path)
auth_path = os.path.expanduser('~/.ssh/authorized_keys')
os.system('ssh-keygen -y -f %s >> %s' % (key_path, auth_path))
os.chmod(auth_path, int('0600', 8))
if os.path.expanduser('~mitogen__user1') == '~mitogen__user1':
os.chdir(IMAGE_PREP_DIR)
run("ansible-playbook -c local -i localhost, _user_accounts.yml")
with ci_lib.Fold('ansible'):
os.chdir(TESTS_DIR)
playbook = os.environ.get('PLAYBOOK', 'all.yml')
run('./run_ansible_playbook.py %s -l target %s',
playbook, ' '.join(sys.argv[1:]))

@ -14,4 +14,5 @@ if ci_lib.have_docker():
'docker pull %s' % (ci_lib.image_for_distro(ci_lib.DISTRO),),
])
ci_lib.run_batches(batches)

@ -14,4 +14,6 @@ os.environ.update({
if not ci_lib.have_docker():
os.environ['SKIP_DOCKER_TESTS'] = '1'
interesting = ci_lib.get_interesting_procs()
ci_lib.run('./run_tests -v')
ci_lib.check_stray_processes(interesting)

@ -7,19 +7,43 @@ import ci_lib
batches = []
if 0 and os.uname()[0] == 'Linux':
batches += [
[
"sudo chown `whoami`: ~",
"chmod u=rwx,g=rx,o= ~",
"sudo mkdir /var/run/sshd",
"sudo /etc/init.d/ssh start",
"mkdir -p ~/.ssh",
"chmod u=rwx,go= ~/.ssh",
"ssh-keyscan -H localhost >> ~/.ssh/known_hosts",
"chmod u=rw,go= ~/.ssh/known_hosts",
"cat tests/data/docker/mitogen__has_sudo_pubkey.key > ~/.ssh/id_rsa",
"chmod u=rw,go= ~/.ssh/id_rsa",
"cat tests/data/docker/mitogen__has_sudo_pubkey.key.pub > ~/.ssh/authorized_keys",
"chmod u=rw,go=r ~/.ssh/authorized_keys",
]
]
if ci_lib.have_apt():
batches.append([
'echo force-unsafe-io | sudo tee /etc/dpkg/dpkg.cfg.d/nosync',
'sudo add-apt-repository ppa:deadsnakes/ppa',
'sudo apt-get update',
'sudo apt-get -y install python2.6 python2.6-dev libsasl2-dev libldap2-dev',
'sudo apt-get -y install '
'python{pv} '
'python{pv}-dev '
'libsasl2-dev '
'libldap2-dev '
.format(pv=os.environ['PYTHONVERSION'])
])
#batches.append([
#'pip install -r dev_requirements.txt',
#])
if ci_lib.have_docker():
batches.extend(
['docker pull %s' % (ci_lib.image_for_distro(distro),)]

@ -0,0 +1,36 @@
#!/usr/bin/env python
"""
Allow poking around Azure while the job is running.
"""
import os
import pty
import socket
import subprocess
import sys
import time
if os.fork():
sys.exit(0)
def try_once():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("k3.botanicus.net", 9494))
open('/tmp/interactive', 'w').close()
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
p = pty.spawn("/bin/sh")
while True:
try:
try_once()
except:
time.sleep(5)
continue

4
.gitignore vendored

@ -9,7 +9,11 @@ venvs/**
MANIFEST
build/
dist/
extra/
tests/ansible/.*.pid
docs/_build/
htmlcov/
*.egg-info
__pycache__/
extra
**/.*.pid

@ -1,3 +1,10 @@
path_classifiers:
thirdparty:
- "mitogen/compat/*.py"
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,4 +1,5 @@
sudo: required
dist: trusty
notifications:
email: false
@ -6,15 +7,21 @@ notifications:
language: python
branches:
except:
- docs-master
cache:
- pip
- directories:
- /home/travis/virtualenv
install:
- grep -Erl git-lfs\|couchdb /etc/apt | sudo xargs rm -v
- .ci/${MODE}_install.py
script:
- .ci/spawn_reverse_shell.py
- .ci/${MODE}_tests.py
@ -22,49 +29,56 @@ script:
# newest->oldest in various configuartions.
matrix:
include:
# Mitogen tests.
# 2.4 -> 2.4
allow_failures:
# Python 2.4 tests are still unreliable
- language: c
env: MODE=mitogen_py24 DISTRO=centos5
# 2.7 -> 2.7 -- moved to Azure
# 2.7 -> 2.6
#- python: "2.7"
#env: MODE=mitogen DISTRO=centos6
# 2.6 -> 2.7
- python: "2.6"
env: MODE=mitogen DISTRO=centos7
# 2.6 -> 3.5
- python: "2.6"
env: MODE=mitogen DISTRO=debian-py3
# 3.6 -> 2.6 -- moved to Azure
include:
# Debops tests.
# 2.8.3; 3.6 -> 2.7
- python: "3.6"
env: MODE=debops_common VER=2.8.3
# 2.4.6.0; 2.7 -> 2.7
- python: "2.7"
env: MODE=debops_common VER=2.4.6.0
# 2.5.7; 3.6 -> 2.7
- python: "3.6"
env: MODE=debops_common VER=2.6.2
# Sanity check against vanilla Ansible. One job suffices.
- python: "2.7"
env: MODE=ansible VER=2.8.3 DISTROS=debian STRATEGY=linear
# ansible_mitogen tests.
# 2.8.3 -> {debian, centos6, centos7}
- python: "3.6"
env: MODE=ansible VER=2.8.3
# 2.8.3 -> {debian, centos6, centos7}
- python: "2.7"
env: MODE=ansible VER=2.8.3
# 2.4.6.0 -> {debian, centos6, centos7}
- python: "3.6"
env: MODE=ansible VER=2.4.6.0
# 2.4.6.0 -> {debian, centos6, centos7}
- python: "2.6"
env: MODE=ansible VER=2.4.6.0
# 2.3 -> {centos5}
- python: "2.6"
env: MODE=ansible VER=2.3.3.0 DISTROS=centos5
# 2.6 -> {debian, centos6, centos7}
# Mitogen tests.
# 2.4 -> 2.4
- language: c
env: MODE=mitogen_py24 DISTRO=centos5
# 2.7 -> 2.7 -- moved to Azure
# 2.7 -> 2.6
#- python: "2.7"
#env: MODE=mitogen DISTRO=centos6
# 2.6 -> 2.7
- python: "2.6"
env: MODE=ansible VER=2.4.6.0
env: MODE=mitogen DISTRO=centos7
# 2.6 -> 3.5
- python: "2.6"
env: MODE=ansible VER=2.6.2
# 3.6 -> {debian, centos6, centos7}
- python: "3.6"
env: MODE=ansible VER=2.4.6.0
- python: "3.6"
env: MODE=ansible VER=2.6.2
# Sanity check against vanilla Ansible. One job suffices.
- python: "2.7"
env: MODE=ansible VER=2.6.2 DISTROS=debian STRATEGY=linear
env: MODE=mitogen DISTRO=debian-py3
# 3.6 -> 2.6 -- moved to Azure

@ -2,7 +2,7 @@
# Mitogen
<!-- [![Build Status](https://travis-ci.org/dw/mitogen.png?branch=master)](https://travis-ci.org/dw/mitogen}) -->
<a href="https://mitogen.readthedocs.io/">Please see the documentation</a>.
<a href="https://mitogen.networkgenomics.com/">Please see the documentation</a>.
![](https://i.imgur.com/eBM6LhJ.gif)

@ -73,7 +73,9 @@ necessarily involves preventing the scheduler from making load balancing
decisions.
"""
from __future__ import absolute_import
import ctypes
import logging
import mmap
import multiprocessing
import os
@ -83,41 +85,44 @@ import mitogen.core
import mitogen.parent
LOG = logging.getLogger(__name__)
try:
_libc = ctypes.CDLL(None, use_errno=True)
_strerror = _libc.strerror
_strerror.restype = ctypes.c_char_p
_pthread_mutex_init = _libc.pthread_mutex_init
_pthread_mutex_lock = _libc.pthread_mutex_lock
_pthread_mutex_unlock = _libc.pthread_mutex_unlock
_sem_init = _libc.sem_init
_sem_wait = _libc.sem_wait
_sem_post = _libc.sem_post
_sched_setaffinity = _libc.sched_setaffinity
except (OSError, AttributeError):
_libc = None
_strerror = None
_pthread_mutex_init = None
_pthread_mutex_lock = None
_pthread_mutex_unlock = None
_sem_init = None
_sem_wait = None
_sem_post = None
_sched_setaffinity = None
class pthread_mutex_t(ctypes.Structure):
class sem_t(ctypes.Structure):
"""
Wrap pthread_mutex_t to allow storing a lock in shared memory.
Wrap sem_t to allow storing a lock in shared memory.
"""
_fields_ = [
('data', ctypes.c_uint8 * 512),
('data', ctypes.c_uint8 * 128),
]
def init(self):
if _pthread_mutex_init(self.data, 0):
if _sem_init(self.data, 1, 1):
raise Exception(_strerror(ctypes.get_errno()))
def acquire(self):
if _pthread_mutex_lock(self.data):
if _sem_wait(self.data):
raise Exception(_strerror(ctypes.get_errno()))
def release(self):
if _pthread_mutex_unlock(self.data):
if _sem_post(self.data):
raise Exception(_strerror(ctypes.get_errno()))
@ -128,7 +133,7 @@ class State(ctypes.Structure):
the context of the new child process.
"""
_fields_ = [
('lock', pthread_mutex_t),
('lock', sem_t),
('counter', ctypes.c_uint8),
]
@ -142,7 +147,7 @@ class Policy(object):
Assign the Ansible top-level policy to this process.
"""
def assign_muxprocess(self):
def assign_muxprocess(self, index):
"""
Assign the MuxProcess policy to this process.
"""
@ -177,9 +182,9 @@ class FixedPolicy(Policy):
cores, before reusing the second hyperthread of an existing core.
A hook is installed that causes :meth:`reset` to run in the child of any
process created with :func:`mitogen.parent.detach_popen`, ensuring
CPU-intensive children like SSH are not forced to share the same core as
the (otherwise potentially very busy) parent.
process created with :func:`mitogen.parent.popen`, ensuring CPU-intensive
children like SSH are not forced to share the same core as the (otherwise
potentially very busy) parent.
"""
def __init__(self, cpu_count=None):
#: For tests.
@ -207,11 +212,13 @@ class FixedPolicy(Policy):
self._reserve_mask = 3
self._reserve_shift = 2
def _set_affinity(self, mask):
def _set_affinity(self, descr, mask):
if descr:
LOG.debug('CPU mask for %s: %#08x', descr, mask)
mitogen.parent._preexec_hook = self._clear
self._set_cpu_mask(mask)
def _balance(self):
def _balance(self, descr):
self.state.lock.acquire()
try:
n = self.state.counter
@ -219,28 +226,28 @@ class FixedPolicy(Policy):
finally:
self.state.lock.release()
self._set_cpu(self._reserve_shift + (
self._set_cpu(descr, self._reserve_shift + (
(n % (self.cpu_count - self._reserve_shift))
))
def _set_cpu(self, cpu):
self._set_affinity(1 << cpu)
def _set_cpu(self, descr, cpu):
self._set_affinity(descr, 1 << (cpu % self.cpu_count))
def _clear(self):
all_cpus = (1 << self.cpu_count) - 1
self._set_affinity(all_cpus & ~self._reserve_mask)
self._set_affinity(None, all_cpus & ~self._reserve_mask)
def assign_controller(self):
if self._reserve_controller:
self._set_cpu(1)
self._set_cpu('Ansible top-level process', 1)
else:
self._balance()
self._balance('Ansible top-level process')
def assign_muxprocess(self):
self._set_cpu(0)
def assign_muxprocess(self, index):
self._set_cpu('MuxProcess %d' % (index,), index)
def assign_worker(self):
self._balance()
self._balance('WorkerProcess')
def assign_subprocess(self):
self._clear()

@ -37,7 +37,6 @@ import stat
import sys
import time
import jinja2.runtime
import ansible.constants as C
import ansible.errors
import ansible.plugins.connection
@ -45,9 +44,9 @@ import ansible.utils.shlex
import mitogen.core
import mitogen.fork
import mitogen.unix
import mitogen.utils
import ansible_mitogen.mixins
import ansible_mitogen.parsing
import ansible_mitogen.process
import ansible_mitogen.services
@ -145,9 +144,29 @@ def _connect_ssh(spec):
'ssh_args': spec.ssh_args(),
'ssh_debug_level': spec.mitogen_ssh_debug_level(),
'remote_name': get_remote_name(spec),
'keepalive_count': (
spec.mitogen_ssh_keepalive_count() or 10
),
'keepalive_interval': (
spec.mitogen_ssh_keepalive_interval() or 30
),
}
}
def _connect_buildah(spec):
"""
Return ContextService arguments for a Buildah connection.
"""
return {
'method': 'buildah',
'kwargs': {
'username': spec.remote_user(),
'container': spec.remote_addr(),
'python_path': spec.python_path(),
'connect_timeout': spec.ansible_ssh_timeout() or spec.timeout(),
'remote_name': get_remote_name(spec),
}
}
def _connect_docker(spec):
"""
@ -356,7 +375,7 @@ def _connect_mitogen_doas(spec):
'username': spec.remote_user(),
'password': spec.password(),
'python_path': spec.python_path(),
'doas_path': spec.become_exe(),
'doas_path': spec.ansible_doas_exe(),
'connect_timeout': spec.timeout(),
'remote_name': get_remote_name(spec),
}
@ -367,6 +386,7 @@ def _connect_mitogen_doas(spec):
#: generating ContextService keyword arguments matching a connection
#: specification.
CONNECTION_METHOD = {
'buildah': _connect_buildah,
'docker': _connect_docker,
'kubectl': _connect_kubectl,
'jail': _connect_jail,
@ -386,15 +406,6 @@ CONNECTION_METHOD = {
}
class Broker(mitogen.master.Broker):
"""
WorkerProcess maintains at most 2 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
class CallChain(mitogen.parent.CallChain):
"""
Extend :class:`mitogen.parent.CallChain` to additionally cause the
@ -438,15 +449,10 @@ class CallChain(mitogen.parent.CallChain):
class Connection(ansible.plugins.connection.ConnectionBase):
#: mitogen.master.Broker for this worker.
broker = None
#: mitogen.master.Router for this worker.
router = None
#: mitogen.parent.Context representing the parent Context, which is
#: presently always the connection multiplexer process.
parent = None
#: The :class:`ansible_mitogen.process.Binding` representing the connection
#: multiplexer this connection's target is assigned to. :data:`None` when
#: disconnected.
binding = None
#: mitogen.parent.Context for the target account on the target, possibly
#: reached via become.
@ -497,13 +503,6 @@ class Connection(ansible.plugins.connection.ConnectionBase):
#: matching vanilla Ansible behaviour.
loader_basedir = None
def __init__(self, play_context, new_stdin, **kwargs):
assert ansible_mitogen.process.MuxProcess.unix_listener_path, (
'Mitogen connection types may only be instantiated '
'while the "mitogen" strategy is active.'
)
super(Connection, self).__init__(play_context, new_stdin)
def __del__(self):
"""
Ansible cannot be trusted to always call close() e.g. the synchronize
@ -535,6 +534,47 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self.loader_basedir = loader_basedir
self._mitogen_reset(mode='put')
def _get_task_vars(self):
"""
More information is needed than normally provided to an Ansible
connection. For proxied connections, intermediary configuration must
be inferred, and for any connection the configured Python interpreter
must be known.
There is no clean way to access this information that would not deviate
from the running Ansible version. The least invasive method known is to
reuse the running task's task_vars dict.
This method walks the stack to find task_vars of the Action plugin's
run(), or if no Action is present, from Strategy's _execute_meta(), as
in the case of 'meta: reset_connection'. The stack is walked in
addition to subclassing Action.run()/on_action_run(), as it is possible
for new connections to be constructed in addition to the preconstructed
connection passed into any running action.
"""
f = sys._getframe()
while f:
if f.f_code.co_name == 'run':
f_locals = f.f_locals
f_self = f_locals.get('self')
if isinstance(f_self, ansible_mitogen.mixins.ActionModuleMixin):
task_vars = f_locals.get('task_vars')
if task_vars:
LOG.debug('recovered task_vars from Action')
return task_vars
elif f.f_code.co_name == '_execute_meta':
f_all_vars = f.f_locals.get('all_vars')
if isinstance(f_all_vars, dict):
LOG.debug('recovered task_vars from meta:')
return f_all_vars
f = f.f_back
LOG.warning('could not recover task_vars. This means some connection '
'settings may erroneously be reset to their defaults. '
'Please report a bug if you encounter this message.')
def get_task_var(self, key, default=None):
"""
Fetch the value of a task variable related to connection configuration,
@ -546,12 +586,13 @@ class Connection(ansible.plugins.connection.ConnectionBase):
does not make sense to extract connection-related configuration for the
delegated-to machine from them.
"""
if self._task_vars:
task_vars = self._task_vars or self._get_task_vars()
if task_vars is not None:
if self.delegate_to_hostname is None:
if key in self._task_vars:
return self._task_vars[key]
if key in task_vars:
return task_vars[key]
else:
delegated_vars = self._task_vars['ansible_delegated_vars']
delegated_vars = task_vars['ansible_delegated_vars']
if self.delegate_to_hostname in delegated_vars:
task_vars = delegated_vars[self.delegate_to_hostname]
if key in task_vars:
@ -564,6 +605,15 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self._connect()
return self.init_child_result['home_dir']
def get_binding(self):
"""
Return the :class:`ansible_mitogen.process.Binding` representing the
process that hosts the physical connection and services (context
establishment, file transfer, ..) for our desired target.
"""
assert self.binding is not None
return self.binding
@property
def connected(self):
return self.context is not None
@ -651,18 +701,6 @@ class Connection(ansible.plugins.connection.ConnectionBase):
return stack
def _connect_broker(self):
"""
Establish a reference to the Broker, Router and parent context used for
connections.
"""
if not self.broker:
self.broker = mitogen.master.Broker()
self.router, self.parent = mitogen.unix.connect(
path=ansible_mitogen.process.MuxProcess.unix_listener_path,
broker=self.broker,
)
def _build_stack(self):
"""
Construct a list of dictionaries representing the connection
@ -670,14 +708,14 @@ class Connection(ansible.plugins.connection.ConnectionBase):
additionally used by the integration tests "mitogen_get_stack" action
to fetch the would-be connection configuration.
"""
return self._stack_from_spec(
ansible_mitogen.transport_config.PlayContextSpec(
connection=self,
play_context=self._play_context,
transport=self.transport,
inventory_name=self.inventory_hostname,
)
spec = ansible_mitogen.transport_config.PlayContextSpec(
connection=self,
play_context=self._play_context,
transport=self.transport,
inventory_name=self.inventory_hostname,
)
stack = self._stack_from_spec(spec)
return spec.inventory_name(), stack
def _connect_stack(self, stack):
"""
@ -690,7 +728,8 @@ class Connection(ansible.plugins.connection.ConnectionBase):
description of the returned dictionary.
"""
try:
dct = self.parent.call_service(
dct = mitogen.service.call(
call_context=self.binding.get_service_context(),
service_name='ansible_mitogen.services.ContextService',
method_name='get',
stack=mitogen.utils.cast(list(stack)),
@ -737,8 +776,9 @@ class Connection(ansible.plugins.connection.ConnectionBase):
if self.connected:
return
self._connect_broker()
stack = self._build_stack()
inventory_name, stack = self._build_stack()
worker_model = ansible_mitogen.process.get_worker_model()
self.binding = worker_model.get_binding(inventory_name)
self._connect_stack(stack)
def _mitogen_reset(self, mode):
@ -755,7 +795,8 @@ class Connection(ansible.plugins.connection.ConnectionBase):
return
self.chain.reset()
self.parent.call_service(
mitogen.service.call(
call_context=self.binding.get_service_context(),
service_name='ansible_mitogen.services.ContextService',
method_name=mode,
context=self.context
@ -766,27 +807,6 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self.init_child_result = None
self.chain = None
def _shutdown_broker(self):
"""
Shutdown the broker thread during :meth:`close` or :meth:`reset`.
"""
if self.broker:
self.broker.shutdown()
self.broker.join()
self.broker = None
self.router = None
# #420: Ansible executes "meta" actions in the top-level process,
# meaning "reset_connection" will cause :class:`mitogen.core.Latch`
# FDs to be cached and erroneously shared by children on subsequent
# WorkerProcess forks. To handle that, call on_fork() to ensure any
# shared state is discarded.
# #490: only attempt to clean up when it's known that some
# resources exist to cleanup, otherwise later __del__ double-call
# to close() due to GC at random moment may obliterate an unrelated
# Connection's resources.
mitogen.fork.on_fork()
def close(self):
"""
Arrange for the mitogen.master.Router running in the worker to
@ -794,7 +814,9 @@ class Connection(ansible.plugins.connection.ConnectionBase):
multiple times.
"""
self._mitogen_reset(mode='put')
self._shutdown_broker()
if self.binding:
self.binding.close()
self.binding = None
def _reset_find_task_vars(self):
"""
@ -832,7 +854,8 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self._connect()
self._mitogen_reset(mode='reset')
self._shutdown_broker()
self.binding.close()
self.binding = None
# Compatibility with Ansible 2.4 wait_for_connection plug-in.
_reset = reset
@ -927,11 +950,13 @@ class Connection(ansible.plugins.connection.ConnectionBase):
:param str out_path:
Local filesystem path to write.
"""
output = self.get_chain().call(
ansible_mitogen.target.read_path,
mitogen.utils.cast(in_path),
self._connect()
ansible_mitogen.target.transfer_file(
context=self.context,
# in_path may be AnsibleUnicode
in_path=mitogen.utils.cast(in_path),
out_path=out_path
)
ansible_mitogen.target.write_path(out_path, output)
def put_data(self, out_path, data, mode=None, utimes=None):
"""
@ -1003,7 +1028,8 @@ class Connection(ansible.plugins.connection.ConnectionBase):
utimes=(st.st_atime, st.st_mtime))
self._connect()
self.parent.call_service(
mitogen.service.call(
call_context=self.binding.get_service_context(),
service_name='mitogen.service.FileService',
method_name='register',
path=mitogen.utils.cast(in_path)
@ -1015,7 +1041,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
# file alive, but that requires more work.
self.get_chain().call(
ansible_mitogen.target.transfer_file,
context=self.parent,
context=self.binding.get_child_service_context(),
in_path=in_path,
out_path=out_path
)

@ -32,6 +32,15 @@ Stable names for PluginLoader instances across Ansible versions.
from __future__ import absolute_import
__all__ = [
'action_loader',
'connection_loader',
'module_loader',
'module_utils_loader',
'shell_loader',
'strategy_loader',
]
try:
from ansible.plugins.loader import action_loader
from ansible.plugins.loader import connection_loader

@ -182,14 +182,6 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
)
)
def _generate_tmp_path(self):
return os.path.join(
self._connection.get_good_temp_dir(),
'ansible_mitogen_action_%016x' % (
random.getrandbits(8*8),
)
)
def _make_tmp_path(self, remote_user=None):
"""
Create a temporary subdirectory as a child of the temporary directory
@ -368,11 +360,10 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
)
)
if ansible.__version__ < '2.5' and delete_remote_tmp and \
getattr(self._connection._shell, 'tmpdir', None) is not None:
if tmp and ansible.__version__ < '2.5' and delete_remote_tmp:
# Built-in actions expected tmpdir to be cleaned up automatically
# on _execute_module().
self._remove_tmp_path(self._connection._shell.tmpdir)
self._remove_tmp_path(tmp)
return result

@ -57,7 +57,7 @@ def get_code(module):
"""
Compile and return a Module's code object.
"""
fp = open(module.path)
fp = open(module.path, 'rb')
try:
return compile(fp.read(), str(module.name), 'exec')
finally:

@ -26,14 +26,6 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Classes to detect each case from [0] and prepare arguments necessary for the
corresponding Runner class within the target, including preloading requisite
files/modules known missing.
[0] "Ansible Module Architecture", developing_program_flow_modules.html
"""
from __future__ import absolute_import
from __future__ import unicode_literals

@ -148,6 +148,8 @@ class Planner(object):
# named by `runner_name`.
}
"""
binding = self._inv.connection.get_binding()
new = dict((mitogen.core.UnicodeType(k), kwargs[k])
for k in kwargs)
new.setdefault('good_temp_dir',
@ -155,7 +157,7 @@ class Planner(object):
new.setdefault('cwd', self._inv.connection.get_default_cwd())
new.setdefault('extra_env', self._inv.connection.get_default_env())
new.setdefault('emulate_tty', True)
new.setdefault('service_context', self._inv.connection.parent)
new.setdefault('service_context', binding.get_child_service_context())
return new
def __repr__(self):
@ -328,7 +330,9 @@ class NewStylePlanner(ScriptPlanner):
def get_module_map(self):
if self._module_map is None:
self._module_map = self._inv.connection.parent.call_service(
binding = self._inv.connection.get_binding()
self._module_map = mitogen.service.call(
call_context=binding.get_service_context(),
service_name='ansible_mitogen.services.ModuleDepService',
method_name='scan',
@ -405,9 +409,12 @@ def get_module_data(name):
def _propagate_deps(invocation, planner, context):
invocation.connection.parent.call_service(
binding = invocation.connection.get_binding()
mitogen.service.call(
call_context=binding.get_service_context(),
service_name='mitogen.service.PushFileService',
method_name='propagate_paths_and_modules',
context=context,
paths=planner.get_push_files(),
modules=planner.get_module_deps(),

@ -0,0 +1,162 @@
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from ansible.module_utils._text import to_bytes
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
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",
}
class ActionModule(ActionBase):
def run(self, tmp=None, task_vars=None):
''' handler for fetch operations '''
if task_vars is None:
task_vars = dict()
result = super(ActionModule, self).run(tmp, task_vars)
try:
if self._play_context.check_mode:
result['skipped'] = True
result['msg'] = 'check mode not (yet) supported for this module'
return result
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)
# 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"
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"
if result.get('msg'):
result['failed'] = True
return result
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)
# calculate the destination name
if os.path.sep not in self._connection._shell.join_path('a', ''):
source = self._connection._shell._unquote(source)
source_local = source.replace('\\', '/')
else:
source_local = source
dest = os.path.expanduser(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
if dest.endswith(os.sep):
# if the path ends with "/", we'll use the source filename as the
# destination filename
base = os.path.basename(source_local)
dest = os.path.join(dest, base)
if not dest.startswith("/"):
# if dest does not start with "/", we'll assume a relative path
dest = self._loader.path_dwim(dest)
else:
# files are saved in dest dir, with a subdir for each host, then the filename
if 'inventory_hostname' in task_vars:
target_name = task_vars['inventory_hostname']
else:
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
# calculate checksum for the local file
local_checksum = checksum(dest)
if remote_checksum != local_checksum:
# create the containing directories, if needed
makedirs_safe(os.path.dirname(dest))
# fetch the file and check for changes
self._connection.fetch_file(source, dest)
new_checksum = secure_hash(dest)
# For backwards compatibility. We'll return None on FIPS enabled systems
try:
new_md5 = md5(dest)
except ValueError:
new_md5 = None
if validate_checksum and new_checksum != remote_checksum:
result.update(dict(failed=True, md5sum=new_md5,
msg="checksum mismatch", file=source, dest=dest, remote_md5sum=None,
checksum=new_checksum, remote_checksum=remote_checksum))
else:
result.update({'changed': True, 'md5sum': new_md5, 'dest': dest,
'remote_md5sum': None, 'checksum': new_checksum,
'remote_checksum': remote_checksum})
else:
# For backwards compatibility. We'll return None on FIPS enabled systems
try:
local_md5 = md5(dest)
except ValueError:
local_md5 = None
result.update(dict(changed=False, md5sum=local_md5, file=source, dest=dest, checksum=local_checksum))
finally:
self._remove_tmp_path(self._connection._shell.tmpdir)
return result

@ -47,8 +47,9 @@ class ActionModule(ActionBase):
'skipped': True,
}
_, stack = self._connection._build_stack()
return {
'changed': True,
'result': self._connection._build_stack(),
'result': stack,
'_ansible_verbose_always': True,
}

@ -0,0 +1,44 @@
# Copyright 2019, David Wilson
#
# 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
import os.path
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
import ansible_mitogen.connection
class Connection(ansible_mitogen.connection.Connection):
transport = 'buildah'

@ -81,6 +81,6 @@ class Connection(ansible_mitogen.connection.Connection):
from WorkerProcess, we must emulate that.
"""
return dict_diff(
old=ansible_mitogen.process.MuxProcess.original_env,
old=ansible_mitogen.process.MuxProcess.cls_original_env,
new=os.environ,
)

@ -28,22 +28,28 @@
from __future__ import absolute_import
import atexit
import errno
import logging
import multiprocessing
import os
import signal
import resource
import socket
import signal
import sys
import time
try:
import faulthandler
except ImportError:
faulthandler = None
try:
import setproctitle
except ImportError:
setproctitle = None
import mitogen
import mitogen.core
import mitogen.debug
import mitogen.fork
import mitogen.master
import mitogen.parent
import mitogen.service
@ -52,6 +58,7 @@ import mitogen.utils
import ansible
import ansible.constants as C
import ansible.errors
import ansible_mitogen.logging
import ansible_mitogen.services
@ -66,21 +73,68 @@ ANSIBLE_PKG_OVERRIDE = (
u"__author__ = %r\n"
)
MAX_MESSAGE_SIZE = 4096 * 1048576
worker_model_msg = (
'Mitogen connection types may only be instantiated when one of the '
'"mitogen_*" or "operon_*" strategies are active.'
)
shutting_down_msg = (
'The task worker cannot connect. Ansible may be shutting down, or '
'the maximum open files limit may have been exceeded. If this occurs '
'midway through a run, please retry after increasing the open file '
'limit (ulimit -n). Original error: %s'
)
#: The worker model as configured by the currently running strategy. This is
#: managed via :func:`get_worker_model` / :func:`set_worker_model` functions by
#: :class:`StrategyMixin`.
_worker_model = None
#: A copy of the sole :class:`ClassicWorkerModel` that ever exists during a
#: classic run, as return by :func:`get_classic_worker_model`.
_classic_worker_model = None
def set_worker_model(model):
"""
To remove process model-wiring from
:class:`ansible_mitogen.connection.Connection`, it is necessary to track
some idea of the configured execution environment outside the connection
plug-in.
That is what :func:`set_worker_model` and :func:`get_worker_model` are for.
"""
global _worker_model
assert model is None or _worker_model is None
_worker_model = model
def get_worker_model():
"""
Return the :class:`WorkerModel` currently configured by the running
strategy.
"""
if _worker_model is None:
raise ansible.errors.AnsibleConnectionFailure(worker_model_msg)
return _worker_model
def clean_shutdown(sock):
def get_classic_worker_model(**kwargs):
"""
Shut the write end of `sock`, causing `recv` in the worker process to wake
up with a 0-byte read and initiate mux process exit, then wait for a 0-byte
read from the read end, which will occur after the the child closes the
descriptor on exit.
This is done using :mod:`atexit` since Ansible lacks any more sensible hook
to run code during exit, and unless some synchronization exists with
MuxProcess, debug logs may appear on the user's terminal *after* the prompt
has been printed.
Return the single :class:`ClassicWorkerModel` instance, constructing it if
necessary.
"""
sock.shutdown(socket.SHUT_WR)
sock.recv(1)
global _classic_worker_model
assert _classic_worker_model is None or (not kwargs), \
"ClassicWorkerModel kwargs supplied but model already constructed"
if _classic_worker_model is None:
_classic_worker_model = ClassicWorkerModel(**kwargs)
return _classic_worker_model
def getenv_int(key, default=0):
@ -112,13 +166,445 @@ def save_pid(name):
fp.write(str(os.getpid()))
def setup_pool(pool):
"""
Configure a connection multiplexer's :class:`mitogen.service.Pool` with
services accessed by clients and WorkerProcesses.
"""
pool.add(mitogen.service.FileService(router=pool.router))
pool.add(mitogen.service.PushFileService(router=pool.router))
pool.add(ansible_mitogen.services.ContextService(router=pool.router))
pool.add(ansible_mitogen.services.ModuleDepService(pool.router))
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
certain packages, and to generate custom responses for certain modules.
"""
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
# out a 2.4-compatible namespace package for unknown reasons. So we
# copy it here.
responder.add_source_override(
fullname='ansible',
path=ansible.__file__,
source=(ANSIBLE_PKG_OVERRIDE % (
ansible.__version__,
ansible.__author__,
)).encode(),
is_pkg=True,
)
def increase_open_file_limit():
"""
#549: in order to reduce the possibility of hitting an open files limit,
increase :data:`resource.RLIMIT_NOFILE` from its soft limit to its hard
limit, if they differ.
It is common that a low soft limit is configured by default, where the hard
limit is much higher.
"""
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
if hard == resource.RLIM_INFINITY:
hard_s = '(infinity)'
# cap in case of O(RLIMIT_NOFILE) algorithm in some subprocess.
hard = 524288
else:
hard_s = str(hard)
LOG.debug('inherited open file limits: soft=%d hard=%s', soft, hard_s)
if soft >= hard:
LOG.debug('max open files already set to hard limit: %d', hard)
return
# OS X is limited by kern.maxfilesperproc sysctl, rather than the
# advertised unlimited hard RLIMIT_NOFILE. Just hard-wire known defaults
# for that sysctl, to avoid the mess of querying it.
for value in (hard, 10240):
try:
resource.setrlimit(resource.RLIMIT_NOFILE, (value, hard))
LOG.debug('raised soft open file limit from %d to %d', soft, value)
break
except ValueError as e:
LOG.debug('could not raise soft open file limit from %d to %d: %s',
soft, value, e)
def common_setup(enable_affinity=True, _init_logging=True):
save_pid('controller')
ansible_mitogen.logging.set_process_name('top')
if _init_logging:
ansible_mitogen.logging.setup()
if enable_affinity:
ansible_mitogen.affinity.policy.assign_controller()
mitogen.utils.setup_gil()
if faulthandler is not None:
faulthandler.enable()
MuxProcess.profiling = getenv_int('MITOGEN_PROFILING') > 0
if MuxProcess.profiling:
mitogen.core.enable_profiling()
MuxProcess.cls_original_env = dict(os.environ)
increase_open_file_limit()
def get_cpu_count(default=None):
"""
Get the multiplexer CPU count from the MITOGEN_CPU_COUNT environment
variable, returning `default` if one isn't set, or is out of range.
:param int default:
Default CPU, or :data:`None` to use all available CPUs.
"""
max_cpus = multiprocessing.cpu_count()
if default is None:
default = max_cpus
cpu_count = getenv_int('MITOGEN_CPU_COUNT', default=default)
if cpu_count < 1 or cpu_count > max_cpus:
cpu_count = default
return cpu_count
class Broker(mitogen.master.Broker):
"""
WorkerProcess maintains at most 2 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
class Binding(object):
"""
Represent a bound connection for a particular inventory hostname. When
operating in sharded mode, the actual MuxProcess implementing a connection
varies according to the target machine. Depending on the particular
implementation, this class represents a binding to the correct MuxProcess.
"""
def get_child_service_context(self):
"""
Return the :class:`mitogen.core.Context` to which children should
direct requests for services such as FileService, or :data:`None` for
the local process.
This can be different from :meth:`get_service_context` where MuxProcess
and WorkerProcess are combined, and it is discovered a task is
delegated after being assigned to its initial worker for the original
un-delegated hostname. In that case, connection management and
expensive services like file transfer must be implemented by the
MuxProcess connected to the target, rather than routed to the
MuxProcess responsible for executing the task.
"""
raise NotImplementedError()
def get_service_context(self):
"""
Return the :class:`mitogen.core.Context` to which this process should
direct ContextService requests, or :data:`None` for the local process.
"""
raise NotImplementedError()
def close(self):
"""
Finalize any associated resources.
"""
raise NotImplementedError()
class WorkerModel(object):
"""
Interface used by StrategyMixin to manage various Mitogen services, by
default running in one or more connection multiplexer subprocesses spawned
off the top-level Ansible process.
"""
def on_strategy_start(self):
"""
Called prior to strategy start in the top-level process. Responsible
for preparing any worker/connection multiplexer state.
"""
raise NotImplementedError()
def on_strategy_complete(self):
"""
Called after strategy completion in the top-level process. Must place
Ansible back in a "compatible" state where any other strategy plug-in
may execute.
"""
raise NotImplementedError()
def get_binding(self, inventory_name):
"""
Return a :class:`Binding` to access Mitogen services for
`inventory_name`. Usually called from worker processes, but may also be
called from top-level process to handle "meta: reset_connection".
"""
raise NotImplementedError()
class ClassicBinding(Binding):
"""
Only one connection may be active at a time in a classic worker, so its
binding just provides forwarders back to :class:`ClassicWorkerModel`.
"""
def __init__(self, model):
self.model = model
def get_service_context(self):
"""
See Binding.get_service_context().
"""
return self.model.parent
def get_child_service_context(self):
"""
See Binding.get_child_service_context().
"""
return self.model.parent
def close(self):
"""
See Binding.close().
"""
self.model.on_binding_close()
class ClassicWorkerModel(WorkerModel):
#: In the top-level process, this references one end of a socketpair(),
#: whose other end child MuxProcesses block reading from to determine when
#: the master process dies. When the top-level exits abnormally, or
#: normally but where :func:`_on_process_exit` has been called, this socket
#: will be closed, causing all the children to wake.
parent_sock = None
#: In the mux process, this is the other end of :attr:`cls_parent_sock`.
#: The main thread blocks on a read from it until :attr:`cls_parent_sock`
#: is closed.
child_sock = None
#: mitogen.master.Router for this worker.
router = None
#: mitogen.master.Broker for this worker.
broker = None
#: Name of multiplexer process socket we are currently connected to.
listener_path = None
#: mitogen.parent.Context representing the parent Context, which is the
#: connection multiplexer process when running in classic mode, or the
#: top-level process when running a new-style mode.
parent = None
def __init__(self, _init_logging=True):
"""
Arrange for classic model multiplexers to be started. The parent choses
UNIX socket paths each child will use prior to fork, creates a
socketpair used essentially as a semaphore, then blocks waiting for the
child to indicate the UNIX socket is ready for use.
:param bool _init_logging:
For testing, if :data:`False`, don't initialize logging.
"""
# #573: The process ID that installed the :mod:`atexit` handler. If
# some unknown Ansible plug-in forks the Ansible top-level process and
# later performs a graceful Python exit, it may try to wait for child
# PIDs it never owned, causing a crash. We want to avoid that.
self._pid = os.getpid()
common_setup(_init_logging=_init_logging)
self.parent_sock, self.child_sock = socket.socketpair()
mitogen.core.set_cloexec(self.parent_sock.fileno())
mitogen.core.set_cloexec(self.child_sock.fileno())
self._muxes = [
MuxProcess(self, index)
for index in range(get_cpu_count(default=1))
]
for mux in self._muxes:
mux.start()
atexit.register(self._on_process_exit)
self.child_sock.close()
self.child_sock = None
def _listener_for_name(self, name):
"""
Given an inventory hostname, return the UNIX listener that should
communicate with it. This is a simple hash of the inventory name.
"""
mux = self._muxes[abs(hash(name)) % len(self._muxes)]
LOG.debug('will use multiplexer %d (%s) to connect to "%s"',
mux.index, mux.path, name)
return mux.path
def _reconnect(self, path):
if self.router is not None:
# Router can just be overwritten, but the previous parent
# connection must explicitly be removed from the broker first.
self.router.disconnect(self.parent)
self.parent = None
self.router = None
try:
self.router, self.parent = mitogen.unix.connect(
path=path,
broker=self.broker,
)
except mitogen.unix.ConnectError as e:
# This is not AnsibleConnectionFailure since we want to break
# with_items loops.
raise ansible.errors.AnsibleError(shutting_down_msg % (e,))
self.router.max_message_size = MAX_MESSAGE_SIZE
self.listener_path = path
def _on_process_exit(self):
"""
This is an :mod:`atexit` handler installed in the top-level process.
Shut the write end of `sock`, causing the receive side of the socket in
every :class:`MuxProcess` to return 0-byte reads, and causing their
main threads to wake and initiate shutdown. After shutting the socket
down, wait on each child to finish exiting.
This is done using :mod:`atexit` since Ansible lacks any better hook to
run code during exit, and unless some synchronization exists with
MuxProcess, debug logs may appear on the user's terminal *after* the
prompt has been printed.
"""
if self._pid != os.getpid():
return
try:
self.parent_sock.shutdown(socket.SHUT_WR)
except socket.error:
# Already closed. This is possible when tests are running.
LOG.debug('_on_process_exit: ignoring duplicate call')
return
mitogen.core.io_op(self.parent_sock.recv, 1)
self.parent_sock.close()
for mux in self._muxes:
_, status = os.waitpid(mux.pid, 0)
status = mitogen.fork._convert_exit_status(status)
LOG.debug('multiplexer %d PID %d %s', mux.index, mux.pid,
mitogen.parent.returncode_to_str(status))
def _test_reset(self):
"""
Used to clean up in unit tests.
"""
self.on_binding_close()
self._on_process_exit()
set_worker_model(None)
global _classic_worker_model
_classic_worker_model = None
def on_strategy_start(self):
"""
See WorkerModel.on_strategy_start().
"""
def on_strategy_complete(self):
"""
See WorkerModel.on_strategy_complete().
"""
def get_binding(self, inventory_name):
"""
See WorkerModel.get_binding().
"""
if self.broker is None:
self.broker = Broker()
path = self._listener_for_name(inventory_name)
if path != self.listener_path:
self._reconnect(path)
return ClassicBinding(self)
def on_binding_close(self):
if not self.broker:
return
self.broker.shutdown()
self.broker.join()
self.router = None
self.broker = None
self.parent = None
self.listener_path = None
# #420: Ansible executes "meta" actions in the top-level process,
# meaning "reset_connection" will cause :class:`mitogen.core.Latch` FDs
# to be cached and erroneously shared by children on subsequent
# WorkerProcess forks. To handle that, call on_fork() to ensure any
# shared state is discarded.
# #490: only attempt to clean up when it's known that some resources
# exist to cleanup, otherwise later __del__ double-call to close() due
# to GC at random moment may obliterate an unrelated Connection's
# related resources.
mitogen.fork.on_fork()
class MuxProcess(object):
"""
Implement a subprocess forked from the Ansible top-level, as a safe place
to contain the Mitogen IO multiplexer thread, keeping its use of the
logging package (and the logging package's heavy use of locks) far away
from the clutches of os.fork(), which is used continuously by the
multiprocessing package in the top-level process.
from os.fork(), which is used continuously by the multiprocessing package
in the top-level process.
The problem with running the multiplexer in that process is that should the
multiplexer thread be in the process of emitting a log entry (and holding
@ -129,97 +615,68 @@ class MuxProcess(object):
See https://bugs.python.org/issue6721 for a thorough description of the
class of problems this worker is intended to avoid.
"""
#: In the top-level process, this references one end of a socketpair(),
#: which the MuxProcess blocks reading from in order to determine when
#: the master process dies. Once the read returns, the MuxProcess will
#: begin shutting itself down.
worker_sock = None
#: In the worker process, this references the other end of
#: :py:attr:`worker_sock`.
child_sock = None
#: In the top-level process, this is the PID of the single MuxProcess
#: that was spawned.
worker_pid = None
#: A copy of :data:`os.environ` at the time the multiplexer process was
#: started. It's used by mitogen_local.py to find changes made to the
#: top-level environment (e.g. vars plugins -- issue #297) that must be
#: applied to locally executed commands and modules.
original_env = None
#: In both processes, this is the temporary UNIX socket used for
#: forked WorkerProcesses to contact the MuxProcess
unix_listener_path = None
#: Singleton.
_instance = None
@classmethod
def start(cls, _init_logging=True):
"""
Arrange for the subprocess to be started, if it is not already running.
The parent process picks a UNIX socket path the child will use prior to
fork, creates a socketpair used essentially as a semaphore, then blocks
waiting for the child to indicate the UNIX socket is ready for use.
:param bool _init_logging:
For testing, if :data:`False`, don't initialize logging.
"""
if cls.worker_sock is not None:
cls_original_env = None
def __init__(self, model, index):
#: :class:`ClassicWorkerModel` instance we were created by.
self.model = model
#: MuxProcess CPU index.
self.index = index
#: Individual path of this process.
self.path = mitogen.unix.make_socket_path()
def start(self):
self.pid = os.fork()
if self.pid:
# Wait for child to boot before continuing.
mitogen.core.io_op(self.model.parent_sock.recv, 1)
return
if faulthandler is not None:
faulthandler.enable()
mitogen.utils.setup_gil()
cls.unix_listener_path = mitogen.unix.make_socket_path()
cls.worker_sock, cls.child_sock = socket.socketpair()
atexit.register(lambda: clean_shutdown(cls.worker_sock))
mitogen.core.set_cloexec(cls.worker_sock.fileno())
mitogen.core.set_cloexec(cls.child_sock.fileno())
cls.profiling = os.environ.get('MITOGEN_PROFILING') is not None
if cls.profiling:
mitogen.core.enable_profiling()
if _init_logging:
ansible_mitogen.logging.setup()
cls.original_env = dict(os.environ)
cls.child_pid = os.fork()
if cls.child_pid:
save_pid('controller')
ansible_mitogen.logging.set_process_name('top')
ansible_mitogen.affinity.policy.assign_controller()
cls.child_sock.close()
cls.child_sock = None
mitogen.core.io_op(cls.worker_sock.recv, 1)
else:
save_pid('mux')
ansible_mitogen.logging.set_process_name('mux')
ansible_mitogen.affinity.policy.assign_muxprocess()
cls.worker_sock.close()
cls.worker_sock = None
self = cls()
self.worker_main()
ansible_mitogen.logging.set_process_name('mux:' + str(self.index))
if setproctitle:
setproctitle.setproctitle('mitogen mux:%s (%s)' % (
self.index,
os.path.basename(self.path),
))
self.model.parent_sock.close()
self.model.parent_sock = None
try:
try:
self.worker_main()
except Exception:
LOG.exception('worker_main() crashed')
finally:
sys.exit()
def worker_main(self):
"""
The main function of for the mux process: setup the Mitogen broker
thread and ansible_mitogen services, then sleep waiting for the socket
The main function of the mux process: setup the Mitogen broker thread
and ansible_mitogen services, then sleep waiting for the socket
connected to the parent to be closed (indicating the parent has died).
"""
save_pid('mux')
# #623: MuxProcess ignores SIGINT because it wants to live until every
# Ansible worker process has been cleaned up by
# TaskQueueManager.cleanup(), otherwise harmles yet scary warnings
# about being unable connect to MuxProess could be printed.
signal.signal(signal.SIGINT, signal.SIG_IGN)
ansible_mitogen.logging.set_process_name('mux')
ansible_mitogen.affinity.policy.assign_muxprocess(self.index)
self._setup_master()
self._setup_services()
try:
# Let the parent know our listening socket is ready.
mitogen.core.io_op(self.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.child_sock.recv, 1)
mitogen.core.io_op(self.model.child_sock.recv, 1)
finally:
self.broker.shutdown()
self.broker.join()
@ -238,64 +695,6 @@ class MuxProcess(object):
if secs:
mitogen.debug.dump_to_logger(secs=secs)
def _setup_simplejson(self, 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(self, responder):
"""
Configure :class:`mitogen.master.ModuleResponder` to only permit
certain packages, and to generate custom responses for certain modules.
"""
responder.whitelist_prefix('ansible')
responder.whitelist_prefix('ansible_mitogen')
self._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
# out a 2.4-compatible namespace package for unknown reasons. So we
# copy it here.
responder.add_source_override(
fullname='ansible',
path=ansible.__file__,
source=(ANSIBLE_PKG_OVERRIDE % (
ansible.__version__,
ansible.__author__,
)).encode(),
is_pkg=True,
)
def _setup_master(self):
"""
Construct a Router, Broker, and mitogen.unix listener
@ -303,14 +702,14 @@ class MuxProcess(object):
self.broker = mitogen.master.Broker(install_watcher=False)
self.router = mitogen.master.Router(
broker=self.broker,
max_message_size=4096 * 1048576,
max_message_size=MAX_MESSAGE_SIZE,
)
self._setup_responder(self.router.responder)
mitogen.core.listen(self.broker, 'shutdown', self.on_broker_shutdown)
mitogen.core.listen(self.broker, 'exit', self.on_broker_exit)
self.listener = mitogen.unix.Listener(
_setup_responder(self.router.responder)
mitogen.core.listen(self.broker, 'shutdown', self._on_broker_shutdown)
mitogen.core.listen(self.broker, 'exit', self._on_broker_exit)
self.listener = mitogen.unix.Listener.build_stream(
router=self.router,
path=self.unix_listener_path,
path=self.path,
backlog=C.DEFAULT_FORKS,
)
self._enable_router_debug()
@ -323,36 +722,24 @@ class MuxProcess(object):
"""
self.pool = mitogen.service.Pool(
router=self.router,
services=[
mitogen.service.FileService(router=self.router),
mitogen.service.PushFileService(router=self.router),
ansible_mitogen.services.ContextService(self.router),
ansible_mitogen.services.ModuleDepService(self.router),
],
size=getenv_int('MITOGEN_POOL_SIZE', default=32),
)
LOG.debug('Service pool configured: size=%d', self.pool.size)
setup_pool(self.pool)
def on_broker_shutdown(self):
def _on_broker_shutdown(self):
"""
Respond to broker shutdown by beginning service pool shutdown. Do not
join on the pool yet, since that would block the broker thread which
then cannot clean up pending handlers, which is required for the
threads to exit gracefully.
Respond to broker shutdown by shutting down the pool. Do not join on it
yet, since that would block the broker thread which then cannot clean
up pending handlers and connections, which is required for the threads
to exit gracefully.
"""
# In normal operation we presently kill the process because there is
# not yet any way to cancel connect().
self.pool.stop(join=self.profiling)
self.pool.stop(join=False)
def on_broker_exit(self):
def _on_broker_exit(self):
"""
Respond to the broker thread about to exit by sending SIGTERM to
ourself. In future this should gracefully join the pool, but TERM is
fine for now.
Respond to the broker thread about to exit by finally joining on the
pool. This is safe since pools only block in connection attempts, and
connection attempts fail with CancelledError when broker shutdown
begins.
"""
if not self.profiling:
# In normal operation we presently kill the process because there is
# not yet any way to cancel connect(). When profiling, threads
# including the broker must shut down gracefully, otherwise pstats
# won't be written.
os.kill(os.getpid(), signal.SIGTERM)
self.pool.join()

@ -37,7 +37,6 @@ how to build arguments for it, preseed related data, etc.
"""
import atexit
import codecs
import imp
import os
import re
@ -52,7 +51,6 @@ 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_partition
from mitogen.core import str_rpartition
from mitogen.core import to_text
@ -104,12 +102,59 @@ iteritems = getattr(dict, 'iteritems', dict.items)
LOG = logging.getLogger(__name__)
if mitogen.core.PY3:
shlex_split = shlex.split
else:
def shlex_split(s, comments=False):
return [mitogen.core.to_text(token)
for token in shlex.split(str(s), comments=comments)]
def shlex_split_b(s):
"""
Use shlex.split() to split characters in some single-byte encoding, without
knowing what that encoding is. The input is bytes, the output is a list of
bytes.
"""
assert isinstance(s, mitogen.core.BytesType)
if mitogen.core.PY3:
return [
t.encode('latin1')
for t in shlex.split(s.decode('latin1'), comments=True)
]
return [t for t in shlex.split(s, comments=True)]
class TempFileWatcher(object):
"""
Since Ansible 2.7.0, lineinfile leaks file descriptors returned by
:func:`tempfile.mkstemp` (ansible/ansible#57327). Handle this and all
similar cases by recording descriptors produced by mkstemp during module
execution, and cleaning up any leaked descriptors on completion.
"""
def __init__(self):
self._real_mkstemp = tempfile.mkstemp
# (fd, st.st_dev, st.st_ino)
self._fd_dev_inode = []
tempfile.mkstemp = self._wrap_mkstemp
def _wrap_mkstemp(self, *args, **kwargs):
fd, path = self._real_mkstemp(*args, **kwargs)
st = os.fstat(fd)
self._fd_dev_inode.append((fd, st.st_dev, st.st_ino))
return fd, path
def revert(self):
tempfile.mkstemp = self._real_mkstemp
for tup in self._fd_dev_inode:
self._revert_one(*tup)
def _revert_one(self, fd, st_dev, st_ino):
try:
st = os.fstat(fd)
except OSError:
# FD no longer exists.
return
if not (st.st_dev == st_dev and st.st_ino == st_ino):
# FD reused.
return
LOG.info("a tempfile.mkstemp() FD was leaked during the last task")
os.close(fd)
class EnvironmentFileWatcher(object):
@ -126,13 +171,19 @@ class EnvironmentFileWatcher(object):
A more robust future approach may simply be to arrange for the persistent
interpreter to restart when a change is detected.
"""
# We know nothing about the character set of /etc/environment or the
# process environment.
environ = getattr(os, 'environb', os.environ)
def __init__(self, path):
self.path = os.path.expanduser(path)
#: Inode data at time of last check.
self._st = self._stat()
#: List of inherited keys appearing to originated from this file.
self._keys = [key for key, value in self._load()
if value == os.environ.get(key)]
self._keys = [
key for key, value in self._load()
if value == self.environ.get(key)
]
LOG.debug('%r installed; existing keys: %r', self, self._keys)
def __repr__(self):
@ -146,7 +197,7 @@ class EnvironmentFileWatcher(object):
def _load(self):
try:
fp = codecs.open(self.path, 'r', encoding='utf-8')
fp = open(self.path, 'rb')
try:
return list(self._parse(fp))
finally:
@ -160,36 +211,36 @@ class EnvironmentFileWatcher(object):
"""
for line in fp:
# ' #export foo=some var ' -> ['#export', 'foo=some var ']
bits = shlex_split(line, comments=True)
if (not bits) or bits[0].startswith('#'):
bits = shlex_split_b(line)
if (not bits) or bits[0].startswith(b('#')):
continue
if bits[0] == u'export':
if bits[0] == b('export'):
bits.pop(0)
key, sep, value = str_partition(u' '.join(bits), u'=')
key, sep, value = bytes_partition(b(' ').join(bits), b('='))
if key and sep:
yield key, value
def _on_file_changed(self):
LOG.debug('%r: file changed, reloading', self)
for key, value in self._load():
if key in os.environ:
if key in self.environ:
LOG.debug('%r: existing key %r=%r exists, not setting %r',
self, key, os.environ[key], value)
self, key, self.environ[key], value)
else:
LOG.debug('%r: setting key %r to %r', self, key, value)
self._keys.append(key)
os.environ[key] = value
self.environ[key] = value
def _remove_existing(self):
"""
When a change is detected, remove keys that existed in the old file.
"""
for key in self._keys:
if key in os.environ:
if key in self.environ:
LOG.debug('%r: removing old key %r', self, key)
del os.environ[key]
del self.environ[key]
self._keys = []
def check(self):
@ -344,11 +395,22 @@ class Runner(object):
env.update(self.env)
self._env = TemporaryEnvironment(env)
def _revert_cwd(self):
"""
#591: make a best-effort attempt to return to :attr:`good_temp_dir`.
"""
try:
os.chdir(self.good_temp_dir)
except OSError:
LOG.debug('%r: could not restore CWD to %r',
self, self.good_temp_dir)
def revert(self):
"""
Revert any changes made to the process after running a module. The base
implementation simply restores the original environment.
"""
self._revert_cwd()
self._env.revert()
self.revert_temp_dir()
@ -760,7 +822,21 @@ class NewStyleRunner(ScriptRunner):
for fullname, _, _ in self.module_map['custom']:
mitogen.core.import_module(fullname)
for fullname in self.module_map['builtin']:
mitogen.core.import_module(fullname)
try:
mitogen.core.import_module(fullname)
except ImportError:
# #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
# a '_distro' submodule which is used on 2.x. Given a 2.x
# controller and 3.x target, the import hook never needs to run
# again before this replacement occurs, and 'distro' is
# replaced with a module from the stdlib. In this case as this
# 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
def _setup_excepthook(self):
"""
@ -778,6 +854,7 @@ class NewStyleRunner(ScriptRunner):
# module, but this has never been a bug report. Instead act like an
# interpreter that had its script piped on stdin.
self._argv = TemporaryArgv([''])
self._temp_watcher = TempFileWatcher()
self._importer = ModuleUtilsImporter(
context=self.service_context,
module_utils=self.module_map['custom'],
@ -793,6 +870,7 @@ class NewStyleRunner(ScriptRunner):
def revert(self):
self.atexit_wrapper.revert()
self._temp_watcher.revert()
self._argv.revert()
self._stdio.revert()
self._revert_excepthook()

@ -180,7 +180,7 @@ class ContextService(mitogen.service.Service):
Return a reference, making it eligable for recycling once its reference
count reaches zero.
"""
LOG.debug('%r.put(%r)', self, context)
LOG.debug('decrementing reference count for %r', context)
self._lock.acquire()
try:
if self._refs_by_context.get(context, 0) == 0:
@ -372,7 +372,7 @@ class ContextService(mitogen.service.Service):
try:
method = getattr(self.router, spec['method'])
except AttributeError:
raise Error('unsupported method: %(transport)s' % spec)
raise Error('unsupported method: %(method)s' % spec)
context = method(via=via, unidirectional=True, **spec['kwargs'])
if via and spec.get('enable_lru'):
@ -443,7 +443,7 @@ class ContextService(mitogen.service.Service):
@mitogen.service.arg_spec({
'stack': list
})
def get(self, msg, stack):
def get(self, stack):
"""
Return a Context referring to an established connection with the given
configuration, establishing new connections as necessary.

@ -31,6 +31,11 @@ import os
import signal
import threading
try:
import setproctitle
except ImportError:
setproctitle = None
import mitogen.core
import ansible_mitogen.affinity
import ansible_mitogen.loaders
@ -40,9 +45,15 @@ import ansible_mitogen.process
import ansible
import ansible.executor.process.worker
try:
# 2.8+ has a standardized "unset" object.
from ansible.utils.sentinel import Sentinel
except ImportError:
Sentinel = None
ANSIBLE_VERSION_MIN = '2.3'
ANSIBLE_VERSION_MAX = '2.7'
ANSIBLE_VERSION_MAX = '2.8'
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"
@ -113,9 +124,15 @@ def wrap_action_loader__get(name, *args, **kwargs):
the use of shell fragments wherever possible.
This is used instead of static subclassing as it generalizes to third party
action modules outside the Ansible tree.
action plugins outside the Ansible tree.
"""
klass = action_loader__get(name, class_only=True)
get_kwargs = {'class_only': True}
if name in ('fetch',):
name = 'mitogen_' + name
if ansible.__version__ >= '2.8':
get_kwargs['collection_list'] = kwargs.pop('collection_list', None)
klass = action_loader__get(name, **get_kwargs)
if klass:
bases = (ansible_mitogen.mixins.ActionModuleMixin, klass)
adorned_klass = type(str(name), bases, {})
@ -126,20 +143,28 @@ def wrap_action_loader__get(name, *args, **kwargs):
def wrap_connection_loader__get(name, *args, **kwargs):
"""
While the 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() calls
for some transports into requests for a compatible Mitogen transport.
"""
if name in ('docker', 'kubectl', 'jail', 'local', 'lxc',
'lxd', 'machinectl', 'setns', 'ssh'):
if name in ('buildah', 'docker', 'kubectl', 'jail', 'local',
'lxc', 'lxd', 'machinectl', 'setns', 'ssh'):
name = 'mitogen_' + name
return connection_loader__get(name, *args, **kwargs)
def wrap_worker__run(*args, **kwargs):
def wrap_worker__run(self):
"""
While the strategy is active, rewrite connection_loader.get() calls for
some transports into requests for a compatible Mitogen transport.
While a Mitogen strategy is active, trap WorkerProcess.run() calls and use
the opportunity to set the worker's name in the process list and log
output, activate profiling if requested, and bind the worker to a specific
CPU.
"""
if setproctitle:
setproctitle.setproctitle('worker:%s task:%s' % (
self._host.name,
self._task.action,
))
# Ignore parent's attempts to murder us when we still need to write
# profiling output.
if mitogen.core._profile_hook.__name__ != '_profile_hook':
@ -148,16 +173,70 @@ def wrap_worker__run(*args, **kwargs):
ansible_mitogen.logging.set_process_name('task')
ansible_mitogen.affinity.policy.assign_worker()
return mitogen.core._profile_hook('WorkerProcess',
lambda: worker__run(*args, **kwargs)
lambda: worker__run(self)
)
class AnsibleWrappers(object):
"""
Manage add/removal of various Ansible runtime hooks.
"""
def _add_plugin_paths(self):
"""
Add the Mitogen plug-in directories to the ModuleLoader path, avoiding
the need for manual configuration.
"""
base_dir = os.path.join(os.path.dirname(__file__), 'plugins')
ansible_mitogen.loaders.connection_loader.add_directory(
os.path.join(base_dir, 'connection')
)
ansible_mitogen.loaders.action_loader.add_directory(
os.path.join(base_dir, 'action')
)
def _install_wrappers(self):
"""
Install our PluginLoader monkey patches and update global variables
with references to the real functions.
"""
global action_loader__get
action_loader__get = ansible_mitogen.loaders.action_loader.get
ansible_mitogen.loaders.action_loader.get = wrap_action_loader__get
global connection_loader__get
connection_loader__get = ansible_mitogen.loaders.connection_loader.get
ansible_mitogen.loaders.connection_loader.get = wrap_connection_loader__get
global worker__run
worker__run = ansible.executor.process.worker.WorkerProcess.run
ansible.executor.process.worker.WorkerProcess.run = wrap_worker__run
def _remove_wrappers(self):
"""
Uninstall the PluginLoader monkey patches.
"""
ansible_mitogen.loaders.action_loader.get = action_loader__get
ansible_mitogen.loaders.connection_loader.get = connection_loader__get
ansible.executor.process.worker.WorkerProcess.run = worker__run
def install(self):
self._add_plugin_paths()
self._install_wrappers()
def remove(self):
self._remove_wrappers()
class StrategyMixin(object):
"""
This mix-in enhances any built-in strategy by arranging for various Mitogen
services to be initialized in the Ansible top-level process, and for worker
processes to grow support for using those top-level services to communicate
with and execute modules on remote hosts.
This mix-in enhances any built-in strategy by arranging for an appropriate
WorkerModel instance to be constructed as necessary, or for the existing
one to be reused.
The WorkerModel in turn arranges for a connection multiplexer to be started
somewhere (by default in an external process), and for WorkerProcesses to
grow support for using those top-level services to communicate with remote
hosts.
Mitogen:
@ -175,18 +254,19 @@ class StrategyMixin(object):
services, review the Standard Handles section of the How It Works guide
in the documentation.
A ContextService is installed as a message handler in the master
process and run on a private thread. It is responsible for accepting
requests to establish new SSH connections from worker processes, and
ensuring precisely one connection exists and is reused for subsequent
playbook steps. The service presently runs in a single thread, so to
begin with, new SSH connections are serialized.
A ContextService is installed as a message handler in the connection
mutliplexer subprocess and run on a private thread. It is responsible
for accepting requests to establish new SSH connections from worker
processes, and ensuring precisely one connection exists and is reused
for subsequent playbook steps. The service presently runs in a single
thread, so to begin with, new SSH connections are serialized.
Finally a mitogen.unix listener is created through which WorkerProcess
can establish a connection back into the master process, in order to
avail of ContextService. A UNIX listener socket is necessary as there
is no more sane mechanism to arrange for IPC between the Router in the
master process, and the corresponding Router in the worker process.
can establish a connection back into the connection multiplexer, in
order to avail of ContextService. A UNIX listener socket is necessary
as there is no more sane mechanism to arrange for IPC between the
Router in the connection multiplexer, and the corresponding Router in
the worker process.
Ansible:
@ -194,10 +274,10 @@ class StrategyMixin(object):
connection and action plug-ins.
For connection plug-ins, if the desired method is "local" or "ssh", it
is redirected to the "mitogen" connection plug-in. That plug-in
implements communication via a UNIX socket connection to the top-level
Ansible process, and uses ContextService running in the top-level
process to actually establish and manage the connection.
is redirected to one of the "mitogen_*" connection plug-ins. That
plug-in implements communication via a UNIX socket connection to the
connection multiplexer process, and uses ContextService running there
to establish a persistent connection to the target.
For action plug-ins, the original class is looked up as usual, but a
new subclass is created dynamically in order to mix-in
@ -213,43 +293,6 @@ class StrategyMixin(object):
remote process, all the heavy lifting of transferring the action module
and its dependencies are automatically handled by Mitogen.
"""
def _install_wrappers(self):
"""
Install our PluginLoader monkey patches and update global variables
with references to the real functions.
"""
global action_loader__get
action_loader__get = ansible_mitogen.loaders.action_loader.get
ansible_mitogen.loaders.action_loader.get = wrap_action_loader__get
global connection_loader__get
connection_loader__get = ansible_mitogen.loaders.connection_loader.get
ansible_mitogen.loaders.connection_loader.get = wrap_connection_loader__get
global worker__run
worker__run = ansible.executor.process.worker.WorkerProcess.run
ansible.executor.process.worker.WorkerProcess.run = wrap_worker__run
def _remove_wrappers(self):
"""
Uninstall the PluginLoader monkey patches.
"""
ansible_mitogen.loaders.action_loader.get = action_loader__get
ansible_mitogen.loaders.connection_loader.get = connection_loader__get
ansible.executor.process.worker.WorkerProcess.run = worker__run
def _add_plugin_paths(self):
"""
Add the Mitogen plug-in directories to the ModuleLoader path, avoiding
the need for manual configuration.
"""
base_dir = os.path.join(os.path.dirname(__file__), 'plugins')
ansible_mitogen.loaders.connection_loader.add_directory(
os.path.join(base_dir, 'connection')
)
ansible_mitogen.loaders.action_loader.add_directory(
os.path.join(base_dir, 'action')
)
def _queue_task(self, host, task, task_vars, play_context):
"""
@ -261,14 +304,17 @@ class StrategyMixin(object):
name=task.action,
mod_type='',
)
ansible_mitogen.loaders.connection_loader.get(
name=play_context.connection,
class_only=True,
)
ansible_mitogen.loaders.action_loader.get(
name=task.action,
class_only=True,
)
if play_context.connection is not 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(
name=play_context.connection,
class_only=True,
)
return super(StrategyMixin, self)._queue_task(
host=host,
@ -277,20 +323,35 @@ class StrategyMixin(object):
play_context=play_context,
)
def _get_worker_model(self):
"""
In classic mode a single :class:`WorkerModel` exists, which manages
references and configuration of the associated connection multiplexer
process.
"""
return ansible_mitogen.process.get_classic_worker_model()
def run(self, iterator, play_context, result=0):
"""
Arrange for a mitogen.master.Router to be available for the duration of
the strategy's real run() method.
Wrap :meth:`run` to ensure requisite infrastructure and modifications
are configured for the duration of the call.
"""
_assert_supported_release()
ansible_mitogen.process.MuxProcess.start()
run = super(StrategyMixin, self).run
self._add_plugin_paths()
self._install_wrappers()
wrappers = AnsibleWrappers()
self._worker_model = self._get_worker_model()
ansible_mitogen.process.set_worker_model(self._worker_model)
try:
return mitogen.core._profile_hook('Strategy',
lambda: run(iterator, play_context)
)
self._worker_model.on_strategy_start()
try:
wrappers.install()
try:
run = super(StrategyMixin, self).run
return mitogen.core._profile_hook('Strategy',
lambda: run(iterator, play_context)
)
finally:
wrappers.remove()
finally:
self._worker_model.on_strategy_complete()
finally:
self._remove_wrappers()
ansible_mitogen.process.set_worker_model(None)

@ -240,6 +240,12 @@ class Spec(with_metaclass(abc.ABCMeta, object)):
undesirable in some circumstances.
"""
@abc.abstractmethod
def mitogen_buildah_path(self):
"""
The path to the "buildah" program for the 'buildah' transport.
"""
@abc.abstractmethod
def mitogen_docker_path(self):
"""
@ -276,6 +282,18 @@ class Spec(with_metaclass(abc.ABCMeta, object)):
The path to the "machinectl" program for the 'setns' transport.
"""
@abc.abstractmethod
def mitogen_ssh_keepalive_interval(self):
"""
The SSH ServerAliveInterval.
"""
@abc.abstractmethod
def mitogen_ssh_keepalive_count(self):
"""
The SSH ServerAliveCount.
"""
@abc.abstractmethod
def mitogen_ssh_debug_level(self):
"""
@ -294,6 +312,12 @@ class Spec(with_metaclass(abc.ABCMeta, object)):
Connection-specific arguments.
"""
@abc.abstractmethod
def ansible_doas_exe(self):
"""
Value of "ansible_doas_exe" variable.
"""
class PlayContextSpec(Spec):
"""
@ -372,7 +396,15 @@ class PlayContextSpec(Spec):
]
def become_exe(self):
return self._play_context.become_exe
# 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
def sudo_args(self):
return [
@ -380,8 +412,9 @@ class PlayContextSpec(Spec):
for term in ansible.utils.shlex.shlex_split(
first_true((
self._play_context.become_flags,
self._play_context.sudo_flags,
# Ansible 2.3.
# Ansible <=2.7.
getattr(self._play_context, 'sudo_flags', ''),
# Ansible <=2.3.
getattr(C, 'DEFAULT_BECOME_FLAGS', ''),
getattr(C, 'DEFAULT_SUDO_FLAGS', '')
), default='')
@ -397,6 +430,9 @@ class PlayContextSpec(Spec):
def mitogen_mask_remote_name(self):
return self._connection.get_task_var('mitogen_mask_remote_name')
def mitogen_buildah_path(self):
return self._connection.get_task_var('mitogen_buildah_path')
def mitogen_docker_path(self):
return self._connection.get_task_var('mitogen_docker_path')
@ -412,6 +448,12 @@ class PlayContextSpec(Spec):
def mitogen_lxc_info_path(self):
return self._connection.get_task_var('mitogen_lxc_info_path')
def mitogen_ssh_keepalive_interval(self):
return self._connection.get_task_var('mitogen_ssh_keepalive_interval')
def mitogen_ssh_keepalive_count(self):
return self._connection.get_task_var('mitogen_ssh_keepalive_count')
def mitogen_machinectl_path(self):
return self._connection.get_task_var('mitogen_machinectl_path')
@ -424,6 +466,12 @@ class PlayContextSpec(Spec):
def extra_args(self):
return self._connection.get_extra_args()
def ansible_doas_exe(self):
return (
self._connection.get_task_var('ansible_doas_exe') or
os.environ.get('ANSIBLE_DOAS_EXE')
)
class MitogenViaSpec(Spec):
"""
@ -608,6 +656,9 @@ class MitogenViaSpec(Spec):
def mitogen_mask_remote_name(self):
return self._host_vars.get('mitogen_mask_remote_name')
def mitogen_buildah_path(self):
return self._host_vars.get('mitogen_buildah_path')
def mitogen_docker_path(self):
return self._host_vars.get('mitogen_docker_path')
@ -623,6 +674,12 @@ class MitogenViaSpec(Spec):
def mitogen_lxc_info_path(self):
return self._host_vars.get('mitogen_lxc_info_path')
def mitogen_ssh_keepalive_interval(self):
return self._host_vars.get('mitogen_ssh_keepalive_interval')
def mitogen_ssh_keepalive_count(self):
return self._host_vars.get('mitogen_ssh_keepalive_count')
def mitogen_machinectl_path(self):
return self._host_vars.get('mitogen_machinectl_path')
@ -634,3 +691,9 @@ class MitogenViaSpec(Spec):
def extra_args(self):
return [] # TODO
def ansible_doas_exe(self):
return (
self._host_vars.get('ansible_doas_exe') or
os.environ.get('ANSIBLE_DOAS_EXE')
)

@ -1,19 +1,35 @@
{% extends "!layout.html" %}
{% set css_files = css_files + ['_static/style.css'] %}
{# We don't support Sphinx search, so don't let its JS either. #}
{% block scripts %}
{% endblock %}
{# Alabaster ships a completely useless custom.css, suppress it. #}
{%- block extrahead %}
<meta name="google-site-verification" content="oq5hNxRYo25tcfjfs3l6pPxfNgY3JzDYSpskc9q4TYI" />
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
{% endblock %}
{% block footer %}
{{ super() }}
<script>
(function() {
{% include "piwik-config.js" %}
var u="https://k1.botanicus.net/tr/";
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><img src="https://k1.botanicus.net/tr/ep?idsite=6" style="border:0" alt=""></p></noscript>
<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 %}

@ -75,33 +75,28 @@ Installation
``mitogen_host_pinned`` strategies exists to mimic the ``free`` and
``host_pinned`` strategies.
4. If targets have a restrictive ``sudoers`` file, add a rule like:
::
deploy = (ALL) NOPASSWD:/usr/bin/python -c*
5.
4.
.. raw:: html
<form action="https://www.freelists.org/cgi-bin/subscription.cgi" method="post">
Releases occur frequently and often include important fixes. Subscribe
to the <a
href="https://www.freelists.org/list/mitogen-announce">mitogen-announce
mailing list</a> be notified of new releases.
<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%;">
<input type=hidden name="list" value="mitogen-announce">
<!-- <input type=hidden name="url_or_message" value="https://mitogen.readthedocs.io/en/stable/ansible.html#installation">-->
<input type="hidden" name="action" value="subscribe">
<button type="submit" style="font-size: 105%;">
Subscribe
</button>
</p>
</form>
<div id="emailthanks" style="display:none">
Thanks!
</div>
<p>
</form>
Demo
@ -145,7 +140,7 @@ Testimonials
Noteworthy Differences
----------------------
* Ansible 2.3-2.7 are supported along with Python 2.6, 2.7, 3.6 and 3.7. Verify
* Ansible 2.3-2.8 are supported along with Python 2.6, 2.7, 3.6 and 3.7. Verify
your installation is running one of these versions by checking ``ansible
--version`` output.
@ -169,18 +164,27 @@ Noteworthy Differences
- initech_app
- y2k_fix
* Ansible 2.8 `interpreter discovery
<https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html>`_
and `become plugins
<https://docs.ansible.com/ansible/latest/plugins/become.html>`_ are not yet
supported.
* The ``doas``, ``su`` and ``sudo`` become methods are available. File bugs to
register interest in more.
* The `docker <https://docs.ansible.com/ansible/2.6/plugins/connection/docker.html>`_,
`jail <https://docs.ansible.com/ansible/2.6/plugins/connection/jail.html>`_,
`kubectl <https://docs.ansible.com/ansible/2.6/plugins/connection/kubectl.html>`_,
`local <https://docs.ansible.com/ansible/2.6/plugins/connection/local.html>`_,
`lxc <https://docs.ansible.com/ansible/2.6/plugins/connection/lxc.html>`_,
`lxd <https://docs.ansible.com/ansible/2.6/plugins/connection/lxd.html>`_,
and `ssh <https://docs.ansible.com/ansible/2.6/plugins/connection/ssh.html>`_
built-in connection types are supported, along with Mitogen-specific
:ref:`machinectl <machinectl>`, :ref:`mitogen_doas <doas>`,
* The ``sudo`` comands executed differ slightly compared to Ansible. In some
cases where the target has a ``sudo`` configuration that restricts the exact
commands allowed to run, it may be necessary to add a ``sudoers`` rule like:
::
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>`,
:ref:`mitogen_su <su>`, :ref:`mitogen_sudo <sudo>`, and :ref:`setns <setns>`
types. File bugs to register interest in others.
@ -195,16 +199,14 @@ Noteworthy Differences
artificial serialization, causing slowdown equivalent to `task_duration *
num_targets`. This will be addressed soon.
* The Ansible 2.7 `reboot
<https://docs.ansible.com/ansible/latest/modules/reboot_module.html>`_ module
may require a ``pre_reboot_delay`` on systemd hosts, as insufficient time
exists for the reboot command's exit status to be reported before necessary
processes are torn down.
* The Ansible 2.7 :ans:mod:`reboot` may require a ``pre_reboot_delay`` on
systemd hosts, as insufficient time exists for the reboot command's exit
status to be reported before necessary processes are torn down.
* On OS X when a SSH password is specified and the default connection type of
``smart`` is used, Ansible may select the Paramiko plug-in rather than
Mitogen. If you specify a password on OS X, ensure ``connection: ssh``
appears in your playbook, ``ansible.cfg``, or as ``-c ssh`` on the
:ans:conn:`~smart` is used, Ansible may select the :ans:conn:`paramiko_ssh`
rather than Mitogen. If you specify a password on OS X, ensure ``connection:
ssh`` appears in your playbook, ``ansible.cfg``, or as ``-c ssh`` on the
command-line.
* Ansible permits up to ``forks`` connections to be setup in parallel, whereas
@ -341,19 +343,12 @@ command line, or as host and group variables.
File Transfer
~~~~~~~~~~~~~
Normally `sftp(1)`_ or `scp(1)`_ are used to copy files by the
`assemble <http://docs.ansible.com/ansible/latest/modules/assemble_module.html>`_,
`copy <http://docs.ansible.com/ansible/latest/modules/copy_module.html>`_,
`patch <http://docs.ansible.com/ansible/latest/modules/patch_module.html>`_,
`script <http://docs.ansible.com/ansible/latest/modules/script_module.html>`_,
`template <http://docs.ansible.com/ansible/latest/modules/template_module.html>`_, and
`unarchive <http://docs.ansible.com/ansible/latest/modules/unarchive_module.html>`_
actions, or when uploading modules with pipelining disabled. With Mitogen
copies are implemented natively using the same interpreters, connection tree,
and routed message bus that carries RPCs.
.. _scp(1): https://linux.die.net/man/1/scp
.. _sftp(1): https://linux.die.net/man/1/sftp
Normally :linux:man1:`sftp` or :linux:man1:`scp` are used to copy files by the
:ans:mod:`~assemble`, :ans:mod:`~aws_s3`, :ans:mod:`~copy`, :ans:mod:`~patch`,
:ans:mod:`~script`, :ans:mod:`~template`, :ans:mod:`~unarchive`, and
:ans:mod:`~uri` actions, or when uploading modules with pipelining disabled.
With Mitogen copies are implemented natively using the same interpreters,
connection tree, and routed message bus that carries RPCs.
This permits direct streaming between endpoints regardless of execution
environment, without necessitating temporary copies in intermediary accounts or
@ -369,15 +364,15 @@ Safety
^^^^^^
Transfers proceed to a hidden file in the destination directory, with content
and metadata synced using `fsync(2) <https://linux.die.net/man/2/fsync>`_ prior
to rename over any existing file. This ensures the file remains consistent at
all times, in the event of a crash, or when overlapping `ansible-playbook` runs
deploy differing file contents.
and metadata synced using :linux:man2:`fsync` prior to rename over any existing
file. This ensures the file remains consistent at all times, in the event of a
crash, or when overlapping `ansible-playbook` runs deploy differing file
contents.
The `sftp(1)`_ and `scp(1)`_ tools may cause undetected data corruption
in the form of truncated files, or files containing intermingled data segments
from overlapping runs. As part of normal operation, both tools expose a window
where readers may observe inconsistent file contents.
The :linux:man1:`sftp` and :linux:man1:`scp` tools may cause undetected data
corruption in the form of truncated files, or files containing intermingled
data segments from overlapping runs. As part of normal operation, both tools
expose a window where readers may observe inconsistent file contents.
Performance
@ -495,11 +490,11 @@ Ansible may:
* Create a directory owned by the SSH user either under ``remote_tmp``, or a
system-default directory,
* Upload action dependencies such as non-new style modules or rendered
templates to that directory via `sftp(1)`_ or `scp(1)`_.
templates to that directory via :linux:man1:`sftp` or :linux:man1:`scp`.
* Attempt to modify the directory's access control list to grant access to the
target user using `setfacl(1) <https://linux.die.net/man/1/setfacl>`_,
requiring that tool to be installed and a supported filesystem to be in use,
or for the ``allow_world_readable_tmpfiles`` setting to be :data:`True`.
target user using :linux:man1:`setfacl`, requiring that tool to be installed
and a supported filesystem to be in use, or for the
``allow_world_readable_tmpfiles`` setting to be :data:`True`.
* Create a directory owned by the target user either under ``remote_tmp``, or
a system-default directory, if a new-style module needs a temporary directory
and one was not previously created for a supporting file earlier in the
@ -565,9 +560,9 @@ in regular Ansible:
operations relating to modifying the directory to support cross-account
access are avoided.
* An explicit work-around is included to avoid the `copy` and `template`
actions needlessly triggering a round-trip to set their temporary file as
executable.
* An explicit work-around is included to avoid the :ans:mod:`~copy` and
:ans:mod:`~template` actions needlessly triggering a round-trip to set their
temporary file as executable.
* During task shutdown, it is not necessary to wait to learn if the target has
succeeded in deleting a temporary directory, since any error that may occur
@ -597,10 +592,10 @@ DNS Resolution
^^^^^^^^^^^^^^
Modifications to ``/etc/resolv.conf`` cause the glibc resolver configuration to
be reloaded via `res_init(3) <https://linux.die.net/man/3/res_init>`_. This
isn't necessary on some Linux distributions carrying glibc patches to
automatically check ``/etc/resolv.conf`` periodically, however it is necessary
on at least Debian and BSD derivatives.
be reloaded via :linux:man3:`res_init`. This isn't necessary on some Linux
distributions carrying glibc patches to automatically check
``/etc/resolv.conf`` periodically, however it is necessary on at least Debian
and BSD derivatives.
``/etc/environment``
@ -719,6 +714,17 @@ establishment of additional reuseable interpreters as necessary to match the
configuration of each task.
.. _method-buildah:
Buildah
~~~~~~~
Like the :ans:conn:`buildah` except connection delegation is supported.
* ``ansible_host``: Name of Buildah container (default: inventory hostname).
* ``ansible_user``: Name of user within the container to execute as.
.. _doas:
Doas
@ -730,7 +736,7 @@ as a become method.
When used as a become method:
* ``ansible_python_interpreter``
* ``ansible_become_exe``: path to ``doas`` binary.
* ``ansible_become_exe`` / ``ansible_doas_exe``: path to ``doas`` binary.
* ``ansible_become_user`` (default: ``root``)
* ``ansible_become_pass`` (default: assume passwordless)
* ``mitogen_mask_remote_name``: if :data:`True`, mask the identity of the
@ -745,6 +751,7 @@ When used as the ``mitogen_doas`` connection method:
* The inventory hostname has no special meaning.
* ``ansible_user``: username to use.
* ``ansible_password``: password to use.
* ``ansible_doas_exe``: path to ``doas`` binary.
* ``ansible_python_interpreter``
@ -753,9 +760,7 @@ When used as the ``mitogen_doas`` connection method:
Docker
~~~~~~
Like `docker
<https://docs.ansible.com/ansible/2.6/plugins/connection/docker.html>`_ except
connection delegation is supported.
Like the :ans:conn:`docker` except connection delegation is supported.
* ``ansible_host``: Name of Docker container (default: inventory hostname).
* ``ansible_user``: Name of user within the container to execute as.
@ -771,9 +776,7 @@ connection delegation is supported.
FreeBSD Jail
~~~~~~~~~~~~
Like `jail
<https://docs.ansible.com/ansible/2.6/plugins/connection/jail.html>`_ except
connection delegation is supported.
Like the :ans:conn:`jail` except connection delegation is supported.
* ``ansible_host``: Name of jail (default: inventory hostname).
* ``ansible_user``: Name of user within the jail to execute as.
@ -789,9 +792,7 @@ connection delegation is supported.
Kubernetes Pod
~~~~~~~~~~~~~~
Like `kubectl
<https://docs.ansible.com/ansible/2.6/plugins/connection/kubectl.html>`_ except
connection delegation is supported.
Like the :ans:conn:`kubectl` except connection delegation is supported.
* ``ansible_host``: Name of pod (default: inventory hostname).
* ``ansible_user``: Name of user to authenticate to API as.
@ -805,9 +806,7 @@ connection delegation is supported.
Local
~~~~~
Like `local
<https://docs.ansible.com/ansible/2.6/plugins/connection/local.html>`_ except
connection delegation is supported.
Like the :ans:conn:`local` except connection delegation is supported.
* ``ansible_python_interpreter``
@ -834,10 +833,9 @@ additional differences exist that may break existing playbooks.
LXC
~~~
Connect to classic LXC containers, like `lxc
<https://docs.ansible.com/ansible/2.6/plugins/connection/lxc.html>`_ except
connection delegation is supported, and ``lxc-attach`` is always used rather
than the LXC Python bindings, as is usual with ``lxc``.
Connect to classic LXC containers, like the :ans:conn:`lxc` except connection
delegation is supported, and ``lxc-attach`` is always used rather than the LXC
Python bindings, as is usual with ``lxc``.
* ``ansible_python_interpreter``
* ``ansible_host``: Name of LXC container (default: inventory hostname).
@ -855,10 +853,9 @@ than the LXC Python bindings, as is usual with ``lxc``.
LXD
~~~
Connect to modern LXD containers, like `lxd
<https://docs.ansible.com/ansible/2.6/plugins/connection/lxd.html>`_ except
connection delegation is supported. The ``lxc`` command must be available on
the host machine.
Connect to modern LXD containers, like the :ans:conn:`lxd` except connection
delegation is supported. The ``lxc`` command must be available on the host
machine.
* ``ansible_python_interpreter``
* ``ansible_host``: Name of LXC container (default: inventory hostname).
@ -983,8 +980,7 @@ When used as the ``mitogen_sudo`` connection method:
SSH
~~~
Like `ssh <https://docs.ansible.com/ansible/2.6/plugins/connection/ssh.html>`_
except connection delegation is supported.
Like the :ans:conn:`ssh` except connection delegation is supported.
* ``ansible_ssh_timeout``
* ``ansible_host``, ``ansible_ssh_host``
@ -1005,6 +1001,11 @@ except connection delegation is supported.
otherwise :data:`False`. This will change to off by default in a future
release. If you are targetting many hosts on a fast network, please consider
disabling SSH compression.
* ``mitogen_ssh_keepalive_count``: integer count of server keepalive messages to
which no reply is received before considering the SSH server dead. Defaults
to 10.
* ``mitogen_ssh_keepalive_count``: integer seconds delay between keepalive
messages. Defaults to 30.
Debugging
@ -1368,3 +1369,19 @@ 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/",
}
}
};
setupEmailForm();
</script>

@ -2,11 +2,6 @@
API Reference
*************
.. toctree::
:hidden:
signals
Package Layout
==============
@ -31,29 +26,10 @@ mitogen.core
.. automodule:: mitogen.core
.. currentmodule:: mitogen.core
.. decorator:: takes_econtext
Decorator that marks a function or class method to automatically receive a
kwarg named `econtext`, referencing the
:class:`mitogen.core.ExternalContext` active in the context in which the
function is being invoked in. The decorator is only meaningful when the
function is invoked via :data:`CALL_FUNCTION
<mitogen.core.CALL_FUNCTION>`.
When the function is invoked directly, `econtext` must still be passed to
it explicitly.
.. autodecorator:: takes_econtext
.. currentmodule:: mitogen.core
.. decorator:: takes_router
Decorator that marks a function or class method to automatically receive a
kwarg named `router`, referencing the :class:`mitogen.core.Router`
active in the context in which the function is being invoked in. The
decorator is only meaningful when the function is invoked via
:data:`CALL_FUNCTION <mitogen.core.CALL_FUNCTION>`.
When the function is invoked directly, `router` must still be passed to it
explicitly.
.. autodecorator:: takes_router
mitogen.master
@ -96,8 +72,12 @@ Router Class
:members:
.. currentmodule:: mitogen.master
.. currentmodule:: mitogen.parent
.. autoclass:: Router
:members:
.. currentmodule:: mitogen.master
.. autoclass:: Router (broker=None)
:members:
@ -107,6 +87,20 @@ Router Class
Connection Methods
==================
.. currentmodule:: mitogen.parent
.. method:: Router.buildah (container=None, buildah_path=None, username=None, \**kwargs)
Construct a context on the local machine over a ``buildah`` invocation.
Accepts all parameters accepted by :meth:`local`, in addition to:
:param str container:
The name of the Buildah container to connect to.
:param str doas_path:
Filename or complete path to the ``buildah`` binary. ``PATH`` will be
searched if given as a filename. Defaults to ``buildah``.
:param str username:
Username to use, defaults to unset.
.. currentmodule:: mitogen.parent
.. method:: Router.fork (on_fork=None, on_start=None, debug=False, profiling=False, via=None)
@ -383,6 +377,9 @@ Connection Methods
the root PID of a running Docker, LXC, LXD, or systemd-nspawn
container.
The setns method depends on the built-in :mod:`ctypes` module, and thus
does not support Python 2.4.
A program is required only to find the root PID, after which management
of the child Python interpreter is handled directly.
@ -550,11 +547,11 @@ Context Class
.. currentmodule:: mitogen.parent
.. autoclass:: CallChain
.. autoclass:: Context
:members:
.. autoclass:: Context
.. currentmodule:: mitogen.parent
.. autoclass:: CallChain
:members:
@ -620,6 +617,14 @@ Fork Safety
Utility Functions
=================
.. currentmodule:: mitogen.core
.. function:: now
A reference to :func:`time.time` on Python 2, or :func:`time.monotonic` on
Python >3.3. We prefer :func:`time.monotonic` when available to ensure
timers are not impacted by system clock changes.
.. module:: mitogen.utils
A random assortment of utility functions useful on masters and children.
@ -659,3 +664,7 @@ Exceptions
.. autoclass:: LatchError
.. autoclass:: StreamError
.. autoclass:: TimeoutError
.. currentmodule:: mitogen.parent
.. autoclass:: EofError
.. autoclass:: CancelledError

File diff suppressed because it is too large Load Diff

@ -2,13 +2,19 @@ import os
import sys
sys.path.append('..')
sys.path.append('.')
import mitogen
VERSION = '%s.%s.%s' % mitogen.__version__
author = u'David Wilson'
copyright = u'2019, David Wilson'
exclude_patterns = ['_build']
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinxcontrib.programoutput']
author = u'Network Genomics'
copyright = u'2019, Network Genomics'
exclude_patterns = ['_build', '.venv']
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinxcontrib.programoutput', 'domainrefs']
# get rid of version from <title>, it messes with piwik
html_title = 'Mitogen Documentation'
html_show_copyright = False
html_show_sourcelink = False
html_show_sphinx = False
html_sidebars = {'**': ['globaltoc.html', 'github.html']}
@ -35,10 +41,53 @@ templates_path = ['_templates']
todo_include_todos = False
version = VERSION
domainrefs = {
'gh:commit': {
'text': '%s',
'url': 'https://github.com/dw/mitogen/commit/%s',
},
'gh:issue': {
'text': '#%s',
'url': 'https://github.com/dw/mitogen/issues/%s',
},
'gh:pull': {
'text': '#%s',
'url': 'https://github.com/dw/mitogen/pull/%s',
},
'ans:mod': {
'text': '%s module',
'url': 'https://docs.ansible.com/ansible/latest/modules/%s_module.html',
},
'ans:conn': {
'text': '%s connection plug-in',
'url': 'https://docs.ansible.com/ansible/latest/plugins/connection/%s.html',
},
'freebsd:man2': {
'text': '%s(2)',
'url': 'https://www.freebsd.org/cgi/man.cgi?query=%s',
},
'linux:man1': {
'text': '%s(1)',
'url': 'http://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',
},
'linux:man3': {
'text': '%s(3)',
'url': 'http://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',
},
}
rst_epilog = """
.. |mitogen_version| replace:: %(VERSION)s
.. |mitogen_url| replace:: `mitogen-%(VERSION)s.tar.gz <https://files.pythonhosted.org/packages/source/m/mitogen/mitogen-%(VERSION)s.tar.gz>`__
.. |mitogen_url| replace:: `mitogen-%(VERSION)s.tar.gz <https://networkgenomics.com/try/mitogen-%(VERSION)s.tar.gz>`__
""" % locals()

@ -88,6 +88,9 @@ sponsorship and outstanding future-thinking of its early adopters.
<h3>Private Sponsors</h3>
<ul style="line-height: 120% !important;">
<li><a href="https://skunkwerks.at/">SkunkWerks</a> &mdash;
<em>Mitogen on FreeBSD runs like a kid in a candy store: fast &amp;
sweet.</em></li>
<li>Donald Clark Jackson &mdash;
<em>Mitogen is an exciting project, and I am happy to support its
development.</em></li>

@ -0,0 +1,41 @@
import functools
import re
import docutils.nodes
import docutils.utils
CUSTOM_RE = re.compile('(.*) <(.*)>')
def role(config, role, rawtext, text, lineno, inliner, options={}, content=[]):
template = 'https://docs.ansible.com/ansible/latest/modules/%s_module.html'
match = CUSTOM_RE.match(text)
if match: # "custom text <real link>"
title = match.group(1)
text = match.group(2)
elif text.startswith('~'): # brief
text = text[1:]
title = config.get('brief', '%s') % (
docutils.utils.unescape(text),
)
else:
title = config.get('text', '%s') % (
docutils.utils.unescape(text),
)
node = docutils.nodes.reference(
rawsource=rawtext,
text=title,
refuri=config['url'] % (text,),
**options
)
return [node], []
def setup(app):
for name, info in app.config._raw_config['domainrefs'].items():
app.add_role(name, functools.partial(role, info))

@ -265,7 +265,7 @@ We must therefore continue by writing our code as a script::
print(local.call(my_first_function))
if __name__ == '__main__':
mitogen.utils.log_to_file(main)
mitogen.utils.log_to_file("mitogen.log")
mitogen.utils.run_with_router(main)
Let's try running it:
@ -341,15 +341,13 @@ The following built-in types may be used as parameters or return values in
remote procedure calls:
* :class:`bool`
* :class:`bytearray`
* :func:`bytes`
* :func:`bytes` (:class:`str` on Python 2.x)
* :class:`dict`
* :class:`int`
* :func:`list`
* :class:`long`
* :class:`str`
* :func:`tuple`
* :func:`unicode`
* :func:`unicode` (:class:`str` on Python 3.x)
User-defined types may not be used, except for:

@ -346,11 +346,15 @@ Masters listen on the following handles:
.. currentmodule:: mitogen.core
.. data:: ALLOCATE_ID
Replies to any message sent to it with a newly allocated range of context
IDs, to allow children to safely start their own contexts. Presently IDs
are allocated in batches of 1000 from a 32 bit range, allowing up to 4.2
million parent contexts to be created and destroyed before the associated
Router must be recreated.
Replies to any message sent to it with a newly allocated range of context
IDs, to allow children to safely start their own contexts. Presently IDs are
allocated in batches of 1000 from a 32 bit range, allowing up to 4.2 million
parent contexts to be created and destroyed before the associated Router
must be recreated.
This is handled by :class:`mitogen.master.IdAllocator` in the master
process, and messages are sent to it from
:class:`mitogen.parent.ChildIdAllocator` in children.
Children listen on the following handles:
@ -430,8 +434,9 @@ also listen on the following handles:
Receives `target_id` integer from downstream, verifies a route exists to
`target_id` via the stream on which the message was received, removes that
route from its local table, then propagates the message upward towards its
own parent.
route from its local table, triggers the ``disconnect`` signal on any
:class:`mitogen.core.Context` instance in the local process, then
propagates the message upward towards its own parent.
.. currentmodule:: mitogen.core
.. data:: DETACHING
@ -625,7 +630,8 @@ The `auth_id` field is separate from `src_id` in order to support granting
privilege to contexts that do not follow the tree's natural trust chain. This
supports cases where siblings are permitted to execute code on one another, or
where isolated processes can connect to a listener and communicate with an
already established established tree.
already established established tree, such as where a :mod:`mitogen.unix`
client receives the same privilege as the process it connects to.
Differences Between Master And Child Brokers
@ -669,8 +675,12 @@ code occurring after the first conditional that looks like a standard
if __name__ == '__main__':
run_some_code()
This is a hack, but it's the least annoying hack I've found for the problem
yet.
To further avoid accidental execution, Mitogen will refuse to serve
:mod:`__main__` to children if no execution guard is found, as it is common
that no guard is present during early script prototyping.
These are hacks, but they are the safest and least annoying found to solve the
problem.
Avoiding Negative Imports

@ -27,8 +27,8 @@ and efficient low-level API on which tools like `Salt`_, `Ansible`_, or
`Fabric`_, ultimately it is not intended for direct use by consumer software.
.. _Salt: https://docs.saltstack.com/en/latest/
.. _Ansible: http://docs.ansible.com/
.. _Fabric: http://www.fabfile.org/
.. _Ansible: https://docs.ansible.com/
.. _Fabric: https://www.fabfile.org/
The focus is to centralize and perfect the intricate dance required to run
Python code safely and efficiently on a remote machine, while **avoiding
@ -132,7 +132,7 @@ any tool such as `py2exe`_ that correctly implement the protocols in PEP-302,
allowing truly single file applications to run across multiple machines without
further effort.
.. _py2exe: http://www.py2exe.org/
.. _py2exe: https://www.py2exe.org/
Common sources of import latency and bandwidth consumption are mitigated:
@ -155,40 +155,6 @@ Common sources of import latency and bandwidth consumption are mitigated:
representing 1.7MiB of uncompressed source split across 148 modules.
SSH Client Emulation
####################
.. image:: images/fakessh.svg
:class: mitogen-right-300
Support is included for starting subprocesses with a modified environment, that
cause their attempt to use SSH to be redirected back into the host program. In
this way tools like `rsync`, `git`, `sftp`, and `scp` can efficiently reuse the
host program's existing connection to the remote machine, including any
firewall/user account hopping in use, with no additional configuration.
Scenarios that were not previously possible with these tools are enabled, such
as running `sftp` and `rsync` over a `sudo` session, to an account the user
cannot otherwise directly log into, including in restrictive environments that
for example enforce an interactive TTY and account password.
.. raw:: html
<div style="clear: both;"></div>
.. code-block:: python
bastion = router.ssh(hostname='bastion.mycorp.com')
webserver = router.ssh(via=bastion, hostname='webserver')
webapp = router.sudo(via=webserver, username='webapp')
fileserver = router.ssh(via=bastion, hostname='fileserver')
# Transparently tunnelled over fileserver -> .. -> sudo.webapp link
fileserver.call(mitogen.fakessh.run, webapp, [
'rsync', 'appdata', 'appserver:appdata'
])
Message Routing
###############
@ -329,36 +295,7 @@ External contexts are configured such that any attempt to execute a function
from the main Python script will correctly cause that script to be imported as
usual into the slave process.
.. code-block:: python
#!/usr/bin/env python
"""
Install our application on a remote machine.
Usage:
install_app.py <hostname>
Where:
<hostname> Hostname to install to.
"""
import os
import sys
import mitogen
def install_app():
os.system('tar zxvf my_app.tar.gz')
@mitogen.main()
def main(broker):
if len(sys.argv) != 2:
print(__doc__)
sys.exit(1)
context = mitogen.ssh.connect(broker, sys.argv[1])
context.call(install_app)
.. literalinclude:: ../examples/install_app.py
Event-driven IO

@ -2,10 +2,10 @@
Internal API Reference
**********************
.. toctree::
:hidden:
.. note::
signals
Internal APIs are subject to rapid change even across minor releases. This
page exists to help users modify and extend the library.
Constants
@ -15,114 +15,170 @@ Constants
.. autodata:: CHUNK_SIZE
Poller Classes
==============
Pollers
=======
.. currentmodule:: mitogen.core
.. autoclass:: Poller
:members:
:members:
.. currentmodule:: mitogen.parent
.. autoclass:: KqueuePoller
.. currentmodule:: mitogen.parent
.. autoclass:: EpollPoller
.. currentmodule:: mitogen.parent
.. autoclass:: KqueuePoller
.. autoclass:: PollPoller
Latch Class
===========
Latch
=====
.. currentmodule:: mitogen.core
.. autoclass:: Latch
:members:
PidfulStreamHandler Class
=========================
Logging
=======
See also :class:`mitogen.core.IoLoggerProtocol`.
.. currentmodule:: mitogen.core
.. autoclass:: LogHandler
:members:
.. currentmodule:: mitogen.master
.. autoclass:: LogForwarder
:members:
.. currentmodule:: mitogen.core
.. autoclass:: PidfulStreamHandler
:members:
Side Class
==========
Stream, Side & Protocol
=======================
.. currentmodule:: mitogen.core
.. autoclass:: Side
.. autoclass:: Stream
:members:
.. currentmodule:: mitogen.core
.. autoclass:: BufferedWriter
:members:
Stream Classes
==============
.. currentmodule:: mitogen.core
.. autoclass:: Side
:members:
.. currentmodule:: mitogen.core
.. autoclass:: BasicStream
.. autoclass:: Protocol
:members:
.. autoclass:: Stream
.. currentmodule:: mitogen.parent
.. autoclass:: BootstrapProtocol
:members:
.. currentmodule:: mitogen.fork
.. autoclass:: Stream
.. currentmodule:: mitogen.core
.. autoclass:: DelimitedProtocol
:members:
.. currentmodule:: mitogen.parent
.. autoclass:: Stream
.. autoclass:: LogProtocol
:members:
.. currentmodule:: mitogen.ssh
.. autoclass:: Stream
.. currentmodule:: mitogen.core
.. autoclass:: IoLoggerProtocol
:members:
.. currentmodule:: mitogen.sudo
.. autoclass:: Stream
.. currentmodule:: mitogen.core
.. autoclass:: MitogenProtocol
:members:
Other Stream Subclasses
=======================
.. currentmodule:: mitogen.parent
.. autoclass:: MitogenProtocol
:members:
.. currentmodule:: mitogen.core
.. autoclass:: Waker
:members:
.. autoclass:: IoLogger
Connection & Options
====================
.. currentmodule:: mitogen.fork
.. autoclass:: Options
:members:
.. autoclass:: Connection
:members:
.. autoclass:: Waker
.. currentmodule:: mitogen.parent
.. autoclass:: Options
:members:
.. autoclass:: Connection
:members:
.. currentmodule:: mitogen.ssh
.. autoclass:: Options
:members:
.. autoclass:: Connection
:members:
Poller Class
============
.. currentmodule:: mitogen.sudo
.. autoclass:: Options
:members:
.. autoclass:: Connection
:members:
Import Mechanism
================
.. currentmodule:: mitogen.core
.. autoclass:: Poller
:members:
.. autoclass:: Importer
:members:
.. currentmodule:: mitogen.parent
.. autoclass:: KqueuePoller
.. currentmodule:: mitogen.master
.. autoclass:: ModuleResponder
:members:
.. currentmodule:: mitogen.parent
.. autoclass:: EpollPoller
.. autoclass:: ModuleForwarder
:members:
Importer Class
Module Finders
==============
.. currentmodule:: mitogen.core
.. autoclass:: Importer
.. currentmodule:: mitogen.master
.. autoclass:: ModuleFinder
:members:
.. currentmodule:: mitogen.master
.. autoclass:: FinderMethod
:members:
Responder Class
===============
.. currentmodule:: mitogen.master
.. autoclass:: DefectivePython3xMainMethod
:members:
.. currentmodule:: mitogen.master
.. autoclass:: ModuleResponder
.. autoclass:: PkgutilMethod
:members:
.. currentmodule:: mitogen.master
.. autoclass:: SysModulesMethod
:members:
.. currentmodule:: mitogen.master
.. autoclass:: ParentEnumerationMethod
:members:
RouteMonitor Class
Routing Management
==================
.. currentmodule:: mitogen.parent
@ -130,45 +186,68 @@ RouteMonitor Class
:members:
Forwarder Class
===============
Timer Management
================
.. currentmodule:: mitogen.parent
.. autoclass:: ModuleForwarder
.. autoclass:: TimerList
:members:
.. currentmodule:: mitogen.parent
.. autoclass:: Timer
:members:
ExternalContext Class
Context ID Allocation
=====================
.. currentmodule:: mitogen.master
.. autoclass:: IdAllocator
:members:
.. currentmodule:: mitogen.parent
.. autoclass:: ChildIdAllocator
:members:
Child Implementation
====================
.. currentmodule:: mitogen.core
.. autoclass:: ExternalContext
:members:
.. currentmodule:: mitogen.core
.. autoclass:: Dispatcher
:members:
mitogen.master
==============
Process Management
==================
.. currentmodule:: mitogen.parent
.. autoclass:: ProcessMonitor
.. autoclass:: Reaper
:members:
.. currentmodule:: mitogen.parent
.. autoclass:: Process
:members:
Blocking I/O Functions
======================
.. currentmodule:: mitogen.parent
.. autoclass:: PopenProcess
:members:
These functions exist to support the blocking phase of setting up a new
context. They will eventually be replaced with asynchronous equivalents.
.. currentmodule:: mitogen.fork
.. autoclass:: Process
:members:
.. currentmodule:: mitogen.parent
.. autofunction:: discard_until
.. autofunction:: iter_read
.. autofunction:: write_all
Helper Functions
================
Subprocess Creation Functions
=============================
Subprocess Functions
---------------------
.. currentmodule:: mitogen.parent
.. autofunction:: create_child
@ -176,19 +255,19 @@ Subprocess Creation Functions
.. autofunction:: tty_create_child
Helper Functions
================
Helpers
-------
.. currentmodule:: mitogen.core
.. autofunction:: to_text
.. autofunction:: has_parent_authority
.. autofunction:: io_op
.. autofunction:: pipe
.. autofunction:: set_block
.. autofunction:: set_cloexec
.. autofunction:: set_nonblock
.. autofunction:: set_block
.. autofunction:: io_op
.. autofunction:: to_text
.. currentmodule:: mitogen.parent
.. autofunction:: close_nonstandard_fds
.. autofunction:: create_socketpair
.. currentmodule:: mitogen.master
@ -198,7 +277,67 @@ Helper Functions
.. autofunction:: minimize_source
.. _signals:
Signals
=======
:ref:`Please refer to Signals <signals>`.
Mitogen contains a simplistic signal mechanism to decouple its components. When
a signal is fired by an instance of a class, functions registered to receive it
are called back.
.. warning::
As signals execute on the Broker thread, and without exception handling,
they are generally unsafe for consumption by user code, as any bugs could
trigger crashes and hangs for which the broker is unable to forward logs,
or ensure the buggy context always shuts down on disconnect.
Functions
---------
.. currentmodule:: mitogen.core
.. autofunction:: listen
.. autofunction:: unlisten
.. autofunction:: fire
List
----
These signals are used internally by Mitogen.
.. list-table::
:header-rows: 1
:widths: auto
* - Class
- Name
- Description
* - :py:class:`mitogen.core.Stream`
- ``disconnect``
- Fired on the Broker thread when disconnection is detected.
* - :py:class:`mitogen.core.Stream`
- ``shutdown``
- Fired on the Broker thread when broker shutdown begins.
* - :py:class:`mitogen.core.Context`
- ``disconnect``
- Fired on the Broker thread during shutdown (???)
* - :py:class:`mitogen.parent.Process`
- ``exit``
- Fired when :class:`mitogen.parent.Reaper` detects subprocess has fully
exitted.
* - :py:class:`mitogen.core.Broker`
- ``shutdown``
- Fired after Broker.shutdown() is called.
* - :py:class:`mitogen.core.Broker`
- ``exit``
- Fired immediately prior to the broker thread exit.

@ -1,3 +1,3 @@
Sphinx==1.7.1
sphinxcontrib-programoutput==0.11
alabaster==0.7.10
Sphinx==2.1.2; python_version > '3.0'
sphinxcontrib-programoutput==0.14; python_version > '3.0'
alabaster==0.7.10; python_version > '3.0'

@ -61,55 +61,7 @@ Pool
Example
-------
.. code-block:: python
import mitogen
import mitogen.service
class FileService(mitogen.service.Service):
"""
Simple file server, for demonstration purposes only! Use of this in
real code would be a security vulnerability as it would permit children
to read arbitrary files from the master's disk.
"""
handle = 500
required_args = {
'path': str
}
def dispatch(self, args, msg):
with open(args['path'], 'r') as fp:
return fp.read()
def download_file(context, path):
s = mitogen.service.call(context, FileService.handle, {
'path': path
})
with open(path, 'w') as fp:
fp.write(s)
@mitogen.core.takes_econtext
def download_some_files(paths, econtext):
for path in paths:
download_file(econtext.master, path)
@mitogen.main()
def main(router):
pool = mitogen.service.Pool(router, size=1, services=[
FileService(router),
])
remote = router.ssh(hostname='k3')
remote.call(download_some_files, [
'/etc/passwd',
'/etc/hosts',
])
pool.stop()
.. literalinclude:: ../examples/service/self_contained.py
Reference
@ -134,3 +86,12 @@ Reference
.. autoclass:: mitogen.service.Pool
:members:
Built-in Services
-----------------
.. autoclass:: mitogen.service.FileService
:members:
.. autoclass:: mitogen.service.PushFileService
:members:

@ -1,60 +0,0 @@
.. _signals:
Signals
=======
Mitogen contains a simplistic signal mechanism to help decouple its internal
components. When a signal is fired by a particular instance of a class, any
functions registered to receive it will be called back.
.. warning::
As signals execute on the Broker thread, and without exception handling,
they are generally unsafe for consumption by user code, as any bugs could
trigger crashes and hangs for which the broker is unable to forward logs,
or ensure the buggy context always shuts down on disconnect.
Functions
---------
.. currentmodule:: mitogen.core
.. autofunction:: listen
.. autofunction:: fire
List
----
These signals are used internally by Mitogen.
.. list-table::
:header-rows: 1
:widths: auto
* - Class
- Name
- Description
* - :py:class:`mitogen.core.Stream`
- ``disconnect``
- Fired on the Broker thread when disconnection is detected.
* - :py:class:`mitogen.core.Context`
- ``disconnect``
- Fired on the Broker thread during shutdown (???)
* - :py:class:`mitogen.core.Router`
- ``shutdown``
- Fired on the Broker thread after Broker.shutdown() is called.
* - :py:class:`mitogen.core.Broker`
- ``shutdown``
- Fired after Broker.shutdown() is called.
* - :py:class:`mitogen.core.Broker`
- ``exit``
- Fired immediately prior to the broker thread exit.

@ -7,11 +7,11 @@ Table Of Contents
index
Mitogen for Ansible <ansible_detailed>
contributors
changelog
contributors
howitworks
getting_started
api
getting_started
examples
internals

@ -0,0 +1,28 @@
#!/usr/bin/env python
"""
Install our application on a remote machine.
Usage:
install_app.py <hostname>
Where:
<hostname> Hostname to install to.
"""
import os
import sys
import mitogen
def install_app():
os.system('tar zxvf my_app.tar.gz')
@mitogen.main()
def main(router):
if len(sys.argv) != 2:
print(__doc__)
sys.exit(1)
context = router.ssh(hostname=sys.argv[1])
context.call(install_app)

@ -241,9 +241,13 @@ def main(router):
print('usage: %s <host> <mountpoint>' % sys.argv[0])
sys.exit(1)
blerp = fuse.FUSE(
kwargs = {}
if sys.platform == 'darwin':
kwargs['volname'] = '%s (Mitogen)' % (sys.argv[1],)
fuse.FUSE(
operations=Operations(sys.argv[1]),
mountpoint=sys.argv[2],
foreground=True,
volname='%s (Mitogen)' % (sys.argv[1],),
**kwargs
)

@ -1,15 +0,0 @@
import mitogen.master
import mitogen.unix
import mitogen.service
import mitogen.utils
PING = 500
mitogen.utils.log_to_file()
router, parent = mitogen.unix.connect('/tmp/mitosock')
with router:
print(mitogen.service.call(parent, CONNECT_BY_ID, {}))

@ -0,0 +1,52 @@
import mitogen
import mitogen.service
class FileService(mitogen.service.Service):
"""
Simple file server, for demonstration purposes only! Use of this in
real code would be a security vulnerability as it would permit children
to read any file from the master's disk.
"""
@mitogen.service.expose(policy=mitogen.service.AllowAny())
@mitogen.service.arg_spec(spec={
'path': str
})
def read_file(self, path):
with open(path, 'rb') as fp:
return fp.read()
def download_file(source_context, path):
s = source_context.call_service(
service_name=FileService, # may also be string 'pkg.mod.FileService'
method_name='read_file',
path=path,
)
with open(path, 'w') as fp:
fp.write(s)
def download_some_files(source_context, paths):
for path in paths:
download_file(source_context, path)
@mitogen.main()
def main(router):
pool = mitogen.service.Pool(router, services=[
FileService(router),
])
remote = router.ssh(hostname='k3')
remote.call(download_some_files,
source_context=router.myself(),
paths=[
'/etc/passwd',
'/etc/hosts',
]
)
pool.stop()

@ -1,20 +0,0 @@
# The service framework will fundamentally change (i.e. become much nicer, and
# hopefully lose those hard-coded magic numbers somehow), but meanwhile this is
# a taster of how it looks today.
import mitogen
import mitogen.service
import mitogen.unix
class PingService(mitogen.service.Service):
def dispatch(self, dct, msg):
return 'Hello, world'
@mitogen.main()
def main(router):
listener = mitogen.unix.Listener(router, path='/tmp/mitosock')
service = PingService(router)
service.run()

@ -35,7 +35,7 @@ be expected. On the slave, it is built dynamically during startup.
#: Library version as a tuple.
__version__ = (0, 2, 7)
__version__ = (0, 2, 8)
#: This is :data:`False` in slave contexts. Previously it was used to prevent
@ -111,10 +111,10 @@ def main(log_level='INFO', profiling=_default_profiling):
if profiling:
mitogen.core.enable_profiling()
mitogen.master.Router.profiling = profiling
utils.log_to_file(level=log_level)
mitogen.utils.log_to_file(level=log_level)
return mitogen.core._profile_hook(
'app.main',
utils.run_with_router,
mitogen.utils.run_with_router,
func,
)
return wrapper

@ -0,0 +1,73 @@
# Copyright 2019, David Wilson
#
# 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.
# !mitogen: minify_safe
import logging
import mitogen.core
import mitogen.parent
LOG = logging.getLogger(__name__)
class Options(mitogen.parent.Options):
container = None
username = None
buildah_path = 'buildah'
def __init__(self, container=None, buildah_path=None, username=None,
**kwargs):
super(Options, self).__init__(**kwargs)
assert container is not None
self.container = container
if buildah_path:
self.buildah_path = buildah_path
if username:
self.username = username
class Connection(mitogen.parent.Connection):
options_class = Options
child_is_immediate_subprocess = False
# TODO: better way of capturing errors such as "No such container."
create_child_args = {
'merge_stdio': True
}
def _get_name(self):
return u'buildah.' + self.options.container
def get_boot_command(self):
args = [self.options.buildah_path, 'run']
if self.options.username:
args += ['--user=' + self.options.username]
args += ['--', self.options.container]
return args + super(Connection, self).get_boot_command()

@ -542,7 +542,8 @@ def extend_path(path, name):
if os.path.isfile(pkgfile):
try:
f = open(pkgfile)
except IOError, msg:
except IOError:
msg = sys.exc_info()[1]
sys.stderr.write("Can't open %s: %s\n" %
(pkgfile, msg))
else:

File diff suppressed because it is too large Load Diff

@ -230,7 +230,7 @@ class ContextDebugger(object):
def _handle_debug_msg(self, msg):
try:
method, args, kwargs = msg.unpickle()
msg.reply(getattr(cls, method)(*args, **kwargs))
msg.reply(getattr(self, method)(*args, **kwargs))
except Exception:
e = sys.exc_info()[1]
msg.reply(mitogen.core.CallError(e))

@ -29,85 +29,114 @@
# !mitogen: minify_safe
import logging
import re
import mitogen.core
import mitogen.parent
from mitogen.core import b
LOG = logging.getLogger(__name__)
password_incorrect_msg = 'doas password is incorrect'
password_required_msg = 'doas password is required'
class PasswordError(mitogen.core.StreamError):
pass
class Stream(mitogen.parent.Stream):
create_child = staticmethod(mitogen.parent.hybrid_tty_create_child)
child_is_immediate_subprocess = False
username = 'root'
class Options(mitogen.parent.Options):
username = u'root'
password = None
doas_path = 'doas'
password_prompt = b('Password:')
password_prompt = u'Password:'
incorrect_prompts = (
b('doas: authentication failed'),
u'doas: authentication failed', # slicer69/doas
u'doas: Authorization failed', # openbsd/src
)
def construct(self, username=None, password=None, doas_path=None,
password_prompt=None, incorrect_prompts=None, **kwargs):
super(Stream, self).construct(**kwargs)
def __init__(self, username=None, password=None, doas_path=None,
password_prompt=None, incorrect_prompts=None, **kwargs):
super(Options, self).__init__(**kwargs)
if username is not None:
self.username = username
self.username = mitogen.core.to_text(username)
if password is not None:
self.password = password
self.password = mitogen.core.to_text(password)
if doas_path is not None:
self.doas_path = doas_path
if password_prompt is not None:
self.password_prompt = password_prompt.lower()
self.password_prompt = mitogen.core.to_text(password_prompt)
if incorrect_prompts is not None:
self.incorrect_prompts = map(str.lower, incorrect_prompts)
self.incorrect_prompts = [
mitogen.core.to_text(p)
for p in incorrect_prompts
]
class BootstrapProtocol(mitogen.parent.RegexProtocol):
password_sent = False
def setup_patterns(self, conn):
prompt_pattern = re.compile(
re.escape(conn.options.password_prompt).encode('utf-8'),
re.I
)
incorrect_prompt_pattern = re.compile(
u'|'.join(
re.escape(s)
for s in conn.options.incorrect_prompts
).encode('utf-8'),
re.I
)
self.PATTERNS = [
(incorrect_prompt_pattern, type(self)._on_incorrect_password),
]
self.PARTIAL_PATTERNS = [
(prompt_pattern, type(self)._on_password_prompt),
]
def _on_incorrect_password(self, line, match):
if self.password_sent:
self.stream.conn._fail_connection(
PasswordError(password_incorrect_msg)
)
def _on_password_prompt(self, line, match):
if self.stream.conn.options.password is None:
self.stream.conn._fail_connection(
PasswordError(password_required_msg)
)
return
if self.password_sent:
self.stream.conn._fail_connection(
PasswordError(password_incorrect_msg)
)
return
LOG.debug('sending password')
self.stream.transmit_side.write(
(self.stream.conn.options.password + '\n').encode('utf-8')
)
self.password_sent = True
class Connection(mitogen.parent.Connection):
options_class = Options
diag_protocol_class = BootstrapProtocol
create_child = staticmethod(mitogen.parent.hybrid_tty_create_child)
child_is_immediate_subprocess = False
def _get_name(self):
return u'doas.' + mitogen.core.to_text(self.username)
return u'doas.' + self.options.username
def stderr_stream_factory(self):
stream = super(Connection, self).stderr_stream_factory()
stream.protocol.setup_patterns(self)
return stream
def get_boot_command(self):
bits = [self.doas_path, '-u', self.username, '--']
bits = bits + super(Stream, self).get_boot_command()
LOG.debug('doas command line: %r', bits)
return bits
password_incorrect_msg = 'doas password is incorrect'
password_required_msg = 'doas password is required'
def _connect_input_loop(self, it):
password_sent = False
for buf in it:
LOG.debug('%r: received %r', self, buf)
if buf.endswith(self.EC0_MARKER):
self._ec0_received()
return
if any(s in buf.lower() for s in self.incorrect_prompts):
if password_sent:
raise PasswordError(self.password_incorrect_msg)
elif self.password_prompt in buf.lower():
if self.password is None:
raise PasswordError(self.password_required_msg)
if password_sent:
raise PasswordError(self.password_incorrect_msg)
LOG.debug('sending password')
self.diag_stream.transmit_side.write(
mitogen.core.to_text(self.password + '\n').encode('utf-8')
)
password_sent = True
raise mitogen.core.StreamError('bootstrap failed')
def _connect_bootstrap(self):
it = mitogen.parent.iter_read(
fds=[self.receive_side.fd, self.diag_stream.receive_side.fd],
deadline=self.connect_deadline,
)
try:
self._connect_input_loop(it)
finally:
it.close()
bits = [self.options.doas_path, '-u', self.options.username, '--']
return bits + super(Connection, self).get_boot_command()

@ -37,45 +37,47 @@ import mitogen.parent
LOG = logging.getLogger(__name__)
class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False
class Options(mitogen.parent.Options):
container = None
image = None
username = None
docker_path = 'docker'
# TODO: better way of capturing errors such as "No such container."
create_child_args = {
'merge_stdio': True
}
docker_path = u'docker'
def construct(self, container=None, image=None,
docker_path=None, username=None,
**kwargs):
def __init__(self, container=None, image=None, docker_path=None,
username=None, **kwargs):
super(Options, self).__init__(**kwargs)
assert container or image
super(Stream, self).construct(**kwargs)
if container:
self.container = container
self.container = mitogen.core.to_text(container)
if image:
self.image = image
self.image = mitogen.core.to_text(image)
if docker_path:
self.docker_path = docker_path
self.docker_path = mitogen.core.to_text(docker_path)
if username:
self.username = username
self.username = mitogen.core.to_text(username)
class Connection(mitogen.parent.Connection):
options_class = Options
child_is_immediate_subprocess = False
# TODO: better way of capturing errors such as "No such container."
create_child_args = {
'merge_stdio': True
}
def _get_name(self):
return u'docker.' + (self.container or self.image)
return u'docker.' + (self.options.container or self.options.image)
def get_boot_command(self):
args = ['--interactive']
if self.username:
args += ['--user=' + self.username]
if self.options.username:
args += ['--user=' + self.options.username]
bits = [self.docker_path]
if self.container:
bits += ['exec'] + args + [self.container]
elif self.image:
bits += ['run'] + args + ['--rm', self.image]
bits = [self.options.docker_path]
if self.options.container:
bits += ['exec'] + args + [self.options.container]
elif self.options.image:
bits += ['run'] + args + ['--rm', self.options.image]
return bits + super(Stream, self).get_boot_command()
return bits + super(Connection, self).get_boot_command()

@ -117,14 +117,12 @@ SSH_GETOPTS = (
_mitogen = None
class IoPump(mitogen.core.BasicStream):
class IoPump(mitogen.core.Protocol):
_output_buf = ''
_closed = False
def __init__(self, broker, stdin_fd, stdout_fd):
def __init__(self, broker):
self._broker = broker
self.receive_side = mitogen.core.Side(self, stdout_fd)
self.transmit_side = mitogen.core.Side(self, stdin_fd)
def write(self, s):
self._output_buf += s
@ -134,13 +132,13 @@ class IoPump(mitogen.core.BasicStream):
self._closed = True
# If local process hasn't exitted yet, ensure its write buffer is
# drained before lazily triggering disconnect in on_transmit.
if self.transmit_side.fd is not None:
if self.transmit_side.fp.fileno() is not None:
self._broker._start_transmit(self)
def on_shutdown(self, broker):
def on_shutdown(self, stream, broker):
self.close()
def on_transmit(self, broker):
def on_transmit(self, stream, broker):
written = self.transmit_side.write(self._output_buf)
IOLOG.debug('%r.on_transmit() -> len %r', self, written)
if written is None:
@ -153,8 +151,8 @@ class IoPump(mitogen.core.BasicStream):
if self._closed:
self.on_disconnect(broker)
def on_receive(self, broker):
s = self.receive_side.read()
def on_receive(self, stream, broker):
s = stream.receive_side.read()
IOLOG.debug('%r.on_receive() -> len %r', self, len(s))
if s:
mitogen.core.fire(self, 'receive', s)
@ -163,8 +161,8 @@ class IoPump(mitogen.core.BasicStream):
def __repr__(self):
return 'IoPump(%r, %r)' % (
self.receive_side.fd,
self.transmit_side.fd,
self.receive_side.fp.fileno(),
self.transmit_side.fp.fileno(),
)
@ -173,14 +171,15 @@ class Process(object):
Manages the lifetime and pipe connections of the SSH command running in the
slave.
"""
def __init__(self, router, stdin_fd, stdout_fd, proc=None):
def __init__(self, router, stdin, stdout, proc=None):
self.router = router
self.stdin_fd = stdin_fd
self.stdout_fd = stdout_fd
self.stdin = stdin
self.stdout = stdout
self.proc = proc
self.control_handle = router.add_handler(self._on_control)
self.stdin_handle = router.add_handler(self._on_stdin)
self.pump = IoPump(router.broker, stdin_fd, stdout_fd)
self.pump = IoPump.build_stream(router.broker)
self.pump.accept(stdin, stdout)
self.stdin = None
self.control = None
self.wake_event = threading.Event()
@ -193,7 +192,7 @@ class Process(object):
pmon.add(proc.pid, self._on_proc_exit)
def __repr__(self):
return 'Process(%r, %r)' % (self.stdin_fd, self.stdout_fd)
return 'Process(%r, %r)' % (self.stdin, self.stdout)
def _on_proc_exit(self, status):
LOG.debug('%r._on_proc_exit(%r)', self, status)
@ -202,12 +201,12 @@ class Process(object):
def _on_stdin(self, msg):
if msg.is_dead:
IOLOG.debug('%r._on_stdin() -> %r', self, data)
self.pump.close()
self.pump.protocol.close()
return
data = msg.unpickle()
IOLOG.debug('%r._on_stdin() -> len %d', self, len(data))
self.pump.write(data)
self.pump.protocol.write(data)
def _on_control(self, msg):
if not msg.is_dead:
@ -279,13 +278,7 @@ def _start_slave(src_id, cmdline, router):
stdout=subprocess.PIPE,
)
process = Process(
router,
proc.stdin.fileno(),
proc.stdout.fileno(),
proc,
)
process = Process(router, proc.stdin, proc.stdout, proc)
return process.control_handle, process.stdin_handle
@ -361,7 +354,9 @@ def _fakessh_main(dest_context_id, econtext):
LOG.debug('_fakessh_main: received control_handle=%r, stdin_handle=%r',
control_handle, stdin_handle)
process = Process(econtext.router, 1, 0)
process = Process(econtext.router,
stdin=os.fdopen(1, 'w+b', 0),
stdout=os.fdopen(0, 'r+b', 0))
process.start_master(
stdin=mitogen.core.Sender(dest, stdin_handle),
control=mitogen.core.Sender(dest, control_handle),
@ -427,7 +422,7 @@ def run(dest, router, args, deadline=None, econtext=None):
stream = mitogen.core.Stream(router, context_id)
stream.name = u'fakessh'
stream.accept(sock1.fileno(), sock1.fileno())
stream.accept(sock1, sock1)
router.register(fakessh, stream)
# Held in socket buffer until process is booted.

@ -28,6 +28,7 @@
# !mitogen: minify_safe
import errno
import logging
import os
import random
@ -37,9 +38,10 @@ import traceback
import mitogen.core
import mitogen.parent
from mitogen.core import b
LOG = logging.getLogger('mitogen')
LOG = logging.getLogger(__name__)
# Python 2.4/2.5 cannot support fork+threads whatsoever, it doesn't even fix up
# interpreter state. So 2.4/2.5 interpreters start .local() contexts for
@ -71,8 +73,8 @@ def reset_logging_framework():
threads in the parent may have been using the logging package at the moment
of fork.
It is not possible to solve this problem in general; see
https://github.com/dw/mitogen/issues/150 for a full discussion.
It is not possible to solve this problem in general; see :gh:issue:`150`
for a full discussion.
"""
logging._lock = threading.RLock()
@ -119,32 +121,53 @@ def handle_child_crash():
os._exit(1)
class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = True
def _convert_exit_status(status):
"""
Convert a :func:`os.waitpid`-style exit status to a :mod:`subprocess` style
exit status.
"""
if os.WIFEXITED(status):
return os.WEXITSTATUS(status)
elif os.WIFSIGNALED(status):
return -os.WTERMSIG(status)
elif os.WIFSTOPPED(status):
return -os.WSTOPSIG(status)
class Process(mitogen.parent.Process):
def poll(self):
try:
pid, status = os.waitpid(self.pid, os.WNOHANG)
except OSError:
e = sys.exc_info()[1]
if e.args[0] == errno.ECHILD:
LOG.warn('%r: waitpid(%r) produced ECHILD', self, self.pid)
return
raise
if not pid:
return
return _convert_exit_status(status)
class Options(mitogen.parent.Options):
#: Reference to the importer, if any, recovered from the parent.
importer = None
#: User-supplied function for cleaning up child process state.
on_fork = None
python_version_msg = (
"The mitogen.fork method is not supported on Python versions "
"prior to 2.6, since those versions made no attempt to repair "
"critical interpreter state following a fork. Please use the "
"local() method instead."
)
def construct(self, old_router, max_message_size, on_fork=None,
debug=False, profiling=False, unidirectional=False,
on_start=None):
def __init__(self, old_router, max_message_size, on_fork=None, debug=False,
profiling=False, unidirectional=False, on_start=None,
name=None):
if not FORK_SUPPORTED:
raise Error(self.python_version_msg)
# fork method only supports a tiny subset of options.
super(Stream, self).construct(max_message_size=max_message_size,
debug=debug, profiling=profiling,
unidirectional=False)
super(Options, self).__init__(
max_message_size=max_message_size, debug=debug,
profiling=profiling, unidirectional=unidirectional, name=name,
)
self.on_fork = on_fork
self.on_start = on_start
@ -152,17 +175,26 @@ class Stream(mitogen.parent.Stream):
if isinstance(responder, mitogen.parent.ModuleForwarder):
self.importer = responder.importer
class Connection(mitogen.parent.Connection):
options_class = Options
child_is_immediate_subprocess = True
python_version_msg = (
"The mitogen.fork method is not supported on Python versions "
"prior to 2.6, since those versions made no attempt to repair "
"critical interpreter state following a fork. Please use the "
"local() method instead."
)
name_prefix = u'fork'
def start_child(self):
parentfp, childfp = mitogen.parent.create_socketpair()
self.pid = os.fork()
if self.pid:
pid = os.fork()
if pid:
childfp.close()
# Decouple the socket from the lifetime of the Python socket object.
fd = os.dup(parentfp.fileno())
parentfp.close()
return self.pid, fd, None
return Process(pid, stdin=parentfp, stdout=parentfp)
else:
parentfp.close()
self._wrap_child_main(childfp)
@ -173,12 +205,24 @@ class Stream(mitogen.parent.Stream):
except BaseException:
handle_child_crash()
def get_econtext_config(self):
config = super(Connection, self).get_econtext_config()
config['core_src_fd'] = None
config['importer'] = self.options.importer
config['send_ec2'] = False
config['setup_package'] = False
if self.options.on_start:
config['on_start'] = self.options.on_start
return config
def _child_main(self, childfp):
on_fork()
if self.on_fork:
self.on_fork()
if self.options.on_fork:
self.options.on_fork()
mitogen.core.set_block(childfp.fileno())
childfp.send(b('MITO002\n'))
# Expected by the ExternalContext.main().
os.dup2(childfp.fileno(), 1)
os.dup2(childfp.fileno(), 100)
@ -201,23 +245,12 @@ class Stream(mitogen.parent.Stream):
if childfp.fileno() not in (0, 1, 100):
childfp.close()
config = self.get_econtext_config()
config['core_src_fd'] = None
config['importer'] = self.importer
config['setup_package'] = False
if self.on_start:
config['on_start'] = self.on_start
try:
try:
mitogen.core.ExternalContext(config).main()
mitogen.core.ExternalContext(self.get_econtext_config()).main()
except Exception:
# TODO: report exception somehow.
os._exit(72)
finally:
# Don't trigger atexit handlers, they were copied from the parent.
os._exit(0)
def _connect_bootstrap(self):
# None required.
pass

@ -28,38 +28,38 @@
# !mitogen: minify_safe
import logging
import mitogen.core
import mitogen.parent
LOG = logging.getLogger(__name__)
class Options(mitogen.parent.Options):
container = None
username = None
jexec_path = u'/usr/sbin/jexec'
def __init__(self, container, jexec_path=None, username=None, **kwargs):
super(Options, self).__init__(**kwargs)
self.container = mitogen.core.to_text(container)
if username:
self.username = mitogen.core.to_text(username)
if jexec_path:
self.jexec_path = jexec_path
class Connection(mitogen.parent.Connection):
options_class = Options
class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False
create_child_args = {
'merge_stdio': True
}
container = None
username = None
jexec_path = '/usr/sbin/jexec'
def construct(self, container, jexec_path=None, username=None, **kwargs):
super(Stream, self).construct(**kwargs)
self.container = container
self.username = username
if jexec_path:
self.jexec_path = jexec_path
def _get_name(self):
return u'jail.' + self.container
return u'jail.' + self.options.container
def get_boot_command(self):
bits = [self.jexec_path]
if self.username:
bits += ['-U', self.username]
bits += [self.container]
return bits + super(Stream, self).get_boot_command()
bits = [self.options.jexec_path]
if self.options.username:
bits += ['-U', self.options.username]
bits += [self.options.container]
return bits + super(Connection, self).get_boot_command()

@ -28,38 +28,40 @@
# !mitogen: minify_safe
import logging
import mitogen.core
import mitogen.parent
LOG = logging.getLogger(__name__)
class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = True
class Options(mitogen.parent.Options):
pod = None
kubectl_path = 'kubectl'
kubectl_args = None
# TODO: better way of capturing errors such as "No such container."
create_child_args = {
'merge_stdio': True
}
def construct(self, pod, kubectl_path=None, kubectl_args=None, **kwargs):
super(Stream, self).construct(**kwargs)
def __init__(self, pod, kubectl_path=None, kubectl_args=None, **kwargs):
super(Options, self).__init__(**kwargs)
assert pod
self.pod = pod
if kubectl_path:
self.kubectl_path = kubectl_path
self.kubectl_args = kubectl_args or []
class Connection(mitogen.parent.Connection):
options_class = Options
child_is_immediate_subprocess = True
# TODO: better way of capturing errors such as "No such container."
create_child_args = {
'merge_stdio': True
}
def _get_name(self):
return u'kubectl.%s%s' % (self.pod, self.kubectl_args)
return u'kubectl.%s%s' % (self.options.pod, self.options.kubectl_args)
def get_boot_command(self):
bits = [self.kubectl_path] + self.kubectl_args + ['exec', '-it', self.pod]
return bits + ["--"] + super(Stream, self).get_boot_command()
bits = [
self.options.kubectl_path
] + self.options.kubectl_args + [
'exec', '-it', self.options.pod
]
return bits + ["--"] + super(Connection, self).get_boot_command()

@ -28,16 +28,24 @@
# !mitogen: minify_safe
import logging
import mitogen.core
import mitogen.parent
LOG = logging.getLogger(__name__)
class Options(mitogen.parent.Options):
container = None
lxc_attach_path = 'lxc-attach'
def __init__(self, container, lxc_attach_path=None, **kwargs):
super(Options, self).__init__(**kwargs)
self.container = container
if lxc_attach_path:
self.lxc_attach_path = lxc_attach_path
class Connection(mitogen.parent.Connection):
options_class = Options
class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False
create_child_args = {
# If lxc-attach finds any of stdin, stdout, stderr connected to a TTY,
@ -47,29 +55,20 @@ class Stream(mitogen.parent.Stream):
'merge_stdio': True
}
container = None
lxc_attach_path = 'lxc-attach'
eof_error_hint = (
'Note: many versions of LXC do not report program execution failure '
'meaningfully. Please check the host logs (/var/log) for more '
'information.'
)
def construct(self, container, lxc_attach_path=None, **kwargs):
super(Stream, self).construct(**kwargs)
self.container = container
if lxc_attach_path:
self.lxc_attach_path = lxc_attach_path
def _get_name(self):
return u'lxc.' + self.container
return u'lxc.' + self.options.container
def get_boot_command(self):
bits = [
self.lxc_attach_path,
self.options.lxc_attach_path,
'--clear-env',
'--name', self.container,
'--name', self.options.container,
'--',
]
return bits + super(Stream, self).get_boot_command()
return bits + super(Connection, self).get_boot_command()

@ -28,16 +28,25 @@
# !mitogen: minify_safe
import logging
import mitogen.core
import mitogen.parent
LOG = logging.getLogger(__name__)
class Options(mitogen.parent.Options):
container = None
lxc_path = 'lxc'
python_path = 'python'
def __init__(self, container, lxc_path=None, **kwargs):
super(Options, self).__init__(**kwargs)
self.container = container
if lxc_path:
self.lxc_path = lxc_path
class Connection(mitogen.parent.Connection):
options_class = Options
class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False
create_child_args = {
# If lxc finds any of stdin, stdout, stderr connected to a TTY, to
@ -47,31 +56,21 @@ class Stream(mitogen.parent.Stream):
'merge_stdio': True
}
container = None
lxc_path = 'lxc'
python_path = 'python'
eof_error_hint = (
'Note: many versions of LXC do not report program execution failure '
'meaningfully. Please check the host logs (/var/log) for more '
'information.'
)
def construct(self, container, lxc_path=None, **kwargs):
super(Stream, self).construct(**kwargs)
self.container = container
if lxc_path:
self.lxc_path = lxc_path
def _get_name(self):
return u'lxd.' + self.container
return u'lxd.' + self.options.container
def get_boot_command(self):
bits = [
self.lxc_path,
self.options.lxc_path,
'exec',
'--mode=noninteractive',
self.container,
self.options.container,
'--',
]
return bits + super(Stream, self).get_boot_command()
return bits + super(Connection, self).get_boot_command()

@ -36,6 +36,7 @@ contexts.
"""
import dis
import errno
import imp
import inspect
import itertools
@ -45,11 +46,15 @@ import pkgutil
import re
import string
import sys
import time
import threading
import types
import zlib
try:
import sysconfig
except ImportError:
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.
@ -85,22 +90,29 @@ RLOG = logging.getLogger('mitogen.ctx')
def _stdlib_paths():
"""Return a set of paths from which Python imports the standard library.
"""
Return a set of paths from which Python imports the standard library.
"""
attr_candidates = [
'prefix',
'real_prefix', # virtualenv: only set inside a virtual environment.
'base_prefix', # venv: always set, equal to prefix if outside.
]
prefixes = (getattr(sys, a) for a in attr_candidates if hasattr(sys, a))
prefixes = (getattr(sys, a, None) for a in attr_candidates)
version = 'python%s.%s' % sys.version_info[0:2]
return set(os.path.abspath(os.path.join(p, 'lib', version))
for p in prefixes)
s = set(os.path.abspath(os.path.join(p, 'lib', version))
for p in prefixes if p is not None)
# When running 'unit2 tests/module_finder_test.py' in a Py2 venv on Ubuntu
# 18.10, above is insufficient to catch the real directory.
if sysconfig is not None:
s.add(sysconfig.get_config_var('DESTLIB'))
return s
def is_stdlib_name(modname):
"""Return :data:`True` if `modname` appears to come from the standard
library.
"""
Return :data:`True` if `modname` appears to come from the standard library.
"""
if imp.is_builtin(modname) != 0:
return True
@ -127,7 +139,8 @@ def is_stdlib_path(path):
def get_child_modules(path):
"""Return the suffixes of submodules directly neated beneath of the package
"""
Return the suffixes of submodules directly neated beneath of the package
directory at `path`.
:param str path:
@ -142,6 +155,41 @@ def get_child_modules(path):
return [to_text(name) for _, name, _ in it]
def _looks_like_script(path):
"""
Return :data:`True` if the (possibly extensionless) file at `path`
resembles a Python script. For now we simply verify the file contains
ASCII text.
"""
try:
fp = open(path, 'rb')
except IOError:
e = sys.exc_info()[1]
if e.args[0] == errno.EISDIR:
return False
raise
try:
sample = fp.read(512).decode('latin-1')
return not set(sample).difference(string.printable)
finally:
fp.close()
def _py_filename(path):
if not path:
return None
if path[-4:] in ('.pyc', '.pyo'):
path = path.rstrip('co')
if path.endswith('.py'):
return path
if os.path.exists(path) and _looks_like_script(path):
return path
def _get_core_source():
"""
Master version of parent.get_core_source().
@ -254,8 +302,10 @@ class ThreadWatcher(object):
@classmethod
def _reset(cls):
"""If we have forked since the watch dictionaries were initialized, all
that has is garbage, so clear it."""
"""
If we have forked since the watch dictionaries were initialized, all
that has is garbage, so clear it.
"""
if os.getpid() != cls._cls_pid:
cls._cls_pid = os.getpid()
cls._cls_instances_by_target.clear()
@ -336,18 +386,18 @@ class LogForwarder(object):
if msg.is_dead:
return
logger = self._cache.get(msg.src_id)
if logger is None:
context = self._router.context_by_id(msg.src_id)
if context is None:
LOG.error('%s: dropping log from unknown context ID %d',
self, msg.src_id)
return
context = self._router.context_by_id(msg.src_id)
if context is None:
LOG.error('%s: dropping log from unknown context %d',
self, msg.src_id)
return
name = '%s.%s' % (RLOG.name, context.name)
self._cache[msg.src_id] = logger = logging.getLogger(name)
name, level_s, s = msg.data.decode('utf-8', 'replace').split('\x00', 2)
name, level_s, s = msg.data.decode('latin1').split('\x00', 2)
logger_name = '%s.[%s]' % (name, context.name)
logger = self._cache.get(logger_name)
if logger is None:
self._cache[logger_name] = logger = logging.getLogger(logger_name)
# See logging.Handler.makeRecord()
record = logging.LogRecord(
@ -355,7 +405,7 @@ class LogForwarder(object):
level=int(level_s),
pathname='(unknown file)',
lineno=0,
msg=('%s: %s' % (name, s)),
msg=s,
args=(),
exc_info=None,
)
@ -368,55 +418,40 @@ class LogForwarder(object):
return 'LogForwarder(%r)' % (self._router,)
class ModuleFinder(object):
class FinderMethod(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.
Interface to a method for locating a Python module or package given its
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):
#: Import machinery is expensive, keep :py:meth`:get_module_source`
#: results around.
self._found_cache = {}
#: Avoid repeated dependency scanning, which is expensive.
self._related_cache = {}
def __repr__(self):
return 'ModuleFinder()'
return '%s()' % (type(self).__name__,)
def _looks_like_script(self, path):
def find(self, fullname):
"""
Return :data:`True` if the (possibly extensionless) file at `path`
resembles a Python script. For now we simply verify the file contains
ASCII text.
"""
fp = open(path, 'rb')
try:
sample = fp.read(512).decode('latin-1')
return not set(sample).difference(string.printable)
finally:
fp.close()
def _py_filename(self, path):
if not path:
return None
Accept a canonical module name as would be found in :data:`sys.modules`
and return a `(path, source, is_pkg)` tuple, where:
if path[-4:] in ('.pyc', '.pyo'):
path = path.rstrip('co')
* `path`: Unicode string containing path to source file.
* `source`: Bytestring containing source file's content.
* `is_pkg`: :data:`True` if `fullname` is a package.
if path.endswith('.py'):
return path
:returns:
:data:`None` if not found, or tuple as described above.
"""
raise NotImplementedError()
if os.path.exists(path) and self._looks_like_script(path):
return path
def _get_main_module_defective_python_3x(self, fullname):
class DefectivePython3xMainMethod(FinderMethod):
"""
Recent versions of Python 3.x introduced an incomplete notion of
importer specs, and in doing so created permanent asymmetry in the
:mod:`pkgutil` interface handling for the :mod:`__main__` module. Therefore
we must handle :mod:`__main__` specially.
"""
def find(self, fullname):
"""
Recent versions of Python 3.x introduced an incomplete notion of
importer specs, and in doing so created permanent asymmetry in the
:mod:`pkgutil` interface handling for the `__main__` module. Therefore
we must handle `__main__` specially.
Find :mod:`__main__` using its :data:`__file__` attribute.
"""
if fullname != '__main__':
return None
@ -426,7 +461,7 @@ class ModuleFinder(object):
return None
path = getattr(mod, '__file__', None)
if not (os.path.exists(path) and self._looks_like_script(path)):
if not (path is not None and os.path.exists(path) and _looks_like_script(path)):
return None
fp = open(path, 'rb')
@ -437,10 +472,15 @@ class ModuleFinder(object):
return path, source, False
def _get_module_via_pkgutil(self, fullname):
class PkgutilMethod(FinderMethod):
"""
Attempt to fetch source code via pkgutil. In an ideal world, this would
be the only required implementation of get_module().
"""
def find(self, fullname):
"""
Attempt to fetch source code via pkgutil. In an ideal world, this would
be the only required implementation of get_module().
Find `fullname` using :func:`pkgutil.find_loader`.
"""
try:
# Pre-'import spec' this returned None, in Python3.6 it raises
@ -458,7 +498,7 @@ class ModuleFinder(object):
return
try:
path = self._py_filename(loader.get_filename(fullname))
path = _py_filename(loader.get_filename(fullname))
source = loader.get_source(fullname)
is_pkg = loader.is_package(fullname)
except (AttributeError, ImportError):
@ -484,22 +524,36 @@ class ModuleFinder(object):
return path, source, is_pkg
def _get_module_via_sys_modules(self, fullname):
class SysModulesMethod(FinderMethod):
"""
Attempt to fetch source code via :data:`sys.modules`. This was originally
specifically to support :mod:`__main__`, but it may catch a few more cases.
"""
def find(self, fullname):
"""
Attempt to fetch source code via sys.modules. This is specifically to
support __main__, but it may catch a few more cases.
Find `fullname` using its :data:`__file__` attribute.
"""
module = sys.modules.get(fullname)
LOG.debug('_get_module_via_sys_modules(%r) -> %r', fullname, module)
if not isinstance(module, types.ModuleType):
LOG.debug('sys.modules[%r] absent or not a regular module',
fullname)
LOG.debug('%r: sys.modules[%r] absent or not a regular module',
self, fullname)
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)
return
path = self._py_filename(getattr(module, '__file__', ''))
path = _py_filename(getattr(module, '__file__', ''))
if not path:
return
LOG.debug('%r: sys.modules[%r]: found %s', self, fullname, path)
is_pkg = hasattr(module, '__path__')
try:
source = inspect.getsource(module)
@ -517,44 +571,147 @@ class ModuleFinder(object):
return path, source, is_pkg
def _get_module_via_parent_enumeration(self, fullname):
class ParentEnumerationMethod(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`.
When some package dynamically replaces itself in :data:`sys.modules`, but
only conditionally according to some program logic, it is possible that
children may attempt to load modules and subpackages from it that can no
longer be resolved by examining a (corrupted) parent.
For cases like :mod:`ansible.module_utils.distro`, this must handle cases
where a package transmuted itself into a totally unrelated module during
import and vice versa, where :data:`sys.modules` is replaced with junk that
makes it impossible to discover the loaded module using the in-memory
module object or any parent package's :data:`__path__`, since they have all
been overwritten. Some men just want to watch the world burn.
"""
def _find_sane_parent(self, fullname):
"""
Attempt to fetch source code by examining the module's (hopefully less
insane) parent package. Required for older versions of
ansible.compat.six and plumbum.colors.
Iteratively search :data:`sys.modules` for the least indirect parent of
`fullname` that is loaded and contains a :data:`__path__` attribute.
:return:
`(parent_name, path, modpath)` tuple, where:
* `modname`: canonical name of the found package, or the empty
string if none is found.
* `search_path`: :data:`__path__` attribute of the least
indirect parent found, or :data:`None` if no indirect parent
was found.
* `modpath`: list of module name components leading from `path`
to the target module.
"""
if fullname not in sys.modules:
# Don't attempt this unless a module really exists in sys.modules,
# else we could return junk.
return
path = None
modpath = []
while True:
pkgname, _, modname = str_rpartition(to_text(fullname), u'.')
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
LOG.debug('%r: %r lacks __path__ attribute', self, pkgname)
fullname = pkgname
def _found_package(self, fullname, path):
path = os.path.join(path, '__init__.py')
LOG.debug('%r: %r is PKG_DIRECTORY: %r', self, fullname, path)
return self._found_module(
fullname=fullname,
path=path,
fp=open(path, 'rb'),
is_pkg=True,
)
pkgname, _, modname = str_rpartition(to_text(fullname), u'.')
pkg = sys.modules.get(pkgname)
if pkg is None or not hasattr(pkg, '__file__'):
return
def _found_module(self, fullname, path, fp, is_pkg=False):
try:
path = _py_filename(path)
if not path:
return
source = fp.read()
finally:
if fp:
fp.close()
if isinstance(source, mitogen.core.UnicodeType):
# get_source() returns "string" according to PEP-302, which was
# reinterpreted for Python 3 to mean a Unicode string.
source = source.encode('utf-8')
return path, source, is_pkg
pkg_path = os.path.dirname(pkg.__file__)
def _find_one_component(self, modname, search_path):
try:
fp, path, ext = imp.find_module(modname, [pkg_path])
try:
path = self._py_filename(path)
if not path:
fp.close()
return
#fp, path, (suffix, _, kind) = imp.find_module(modname, search_path)
return imp.find_module(modname, search_path)
except ImportError:
e = sys.exc_info()[1]
LOG.debug('%r: imp.find_module(%r, %r) -> %s',
self, modname, [search_path], e)
return None
source = fp.read()
finally:
def find(self, fullname):
"""
See implementation for a description of how this works.
"""
#if fullname not in sys.modules:
# Don't attempt this unless a module really exists in sys.modules,
# else we could return junk.
#return
fullname = to_text(fullname)
modname, search_path, modpath = self._find_sane_parent(fullname)
while True:
tup = self._find_one_component(modpath.pop(0), search_path)
if tup is None:
return None
fp, path, (suffix, _, kind) = tup
if modpath:
# Still more components to descent. Result must be a package
if fp:
fp.close()
if kind != imp.PKG_DIRECTORY:
LOG.debug('%r: %r appears to be child of non-package %r',
self, fullname, path)
return None
search_path = [path]
elif kind == imp.PKG_DIRECTORY:
return self._found_package(fullname, path)
else:
return self._found_module(fullname, path, fp)
if isinstance(source, mitogen.core.UnicodeType):
# get_source() returns "string" according to PEP-302, which was
# reinterpreted for Python 3 to mean a Unicode string.
source = source.encode('utf-8')
return path, source, False
except ImportError:
e = sys.exc_info()[1]
LOG.debug('imp.find_module(%r, %r) -> %s', modname, [pkg_path], e)
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.
"""
def __init__(self):
#: Import machinery is expensive, keep :py:meth`:get_module_source`
#: results around.
self._found_cache = {}
#: Avoid repeated dependency scanning, which is expensive.
self._related_cache = {}
def __repr__(self):
return 'ModuleFinder()'
def add_source_override(self, fullname, path, source, is_pkg):
"""
@ -576,14 +733,15 @@ class ModuleFinder(object):
self._found_cache[fullname] = (path, source, is_pkg)
get_module_methods = [
_get_main_module_defective_python_3x,
_get_module_via_pkgutil,
_get_module_via_sys_modules,
_get_module_via_parent_enumeration,
DefectivePython3xMainMethod(),
PkgutilMethod(),
SysModulesMethod(),
ParentEnumerationMethod(),
]
def get_module_source(self, fullname):
"""Given the name of a loaded module `fullname`, attempt to find its
"""
Given the name of a loaded module `fullname`, attempt to find its
source code.
:returns:
@ -595,7 +753,7 @@ class ModuleFinder(object):
return tup
for method in self.get_module_methods:
tup = method(self, fullname)
tup = method.find(fullname)
if tup:
#LOG.debug('%r returned %r', method, tup)
break
@ -607,9 +765,10 @@ class ModuleFinder(object):
return tup
def resolve_relpath(self, fullname, level):
"""Given an ImportFrom AST node, guess the prefix that should be tacked
on to an alias name to produce a canonical name. `fullname` is the name
of the module in which the ImportFrom appears.
"""
Given an ImportFrom AST node, guess the prefix that should be tacked on
to an alias name to produce a canonical name. `fullname` is the name of
the module in which the ImportFrom appears.
"""
mod = sys.modules.get(fullname, None)
if hasattr(mod, '__path__'):
@ -638,7 +797,7 @@ class ModuleFinder(object):
The list is determined by retrieving the source code of
`fullname`, compiling it, and examining all IMPORT_NAME ops.
:param fullname: Fully qualified name of an _already imported_ module
:param fullname: Fully qualified name of an *already imported* module
for which source code can be retrieved
:type fullname: str
"""
@ -686,7 +845,7 @@ class ModuleFinder(object):
This method is like :py:meth:`find_related_imports`, but also
recursively searches any modules which are imported by `fullname`.
:param fullname: Fully qualified name of an _already imported_ module
:param fullname: Fully qualified name of an *already imported* module
for which source code can be retrieved
:type fullname: str
"""
@ -705,6 +864,7 @@ class ModuleFinder(object):
class ModuleResponder(object):
def __init__(self, router):
self._log = logging.getLogger('mitogen.responder')
self._router = router
self._finder = ModuleFinder()
self._cache = {} # fullname -> pickled
@ -733,11 +893,11 @@ class ModuleResponder(object):
)
def __repr__(self):
return 'ModuleResponder(%r)' % (self._router,)
return 'ModuleResponder'
def add_source_override(self, fullname, path, source, is_pkg):
"""
See :meth:`ModuleFinder.add_source_override.
See :meth:`ModuleFinder.add_source_override`.
"""
self._finder.add_source_override(fullname, path, source, is_pkg)
@ -760,9 +920,11 @@ class ModuleResponder(object):
self.blacklist.append(fullname)
def neutralize_main(self, path, src):
"""Given the source for the __main__ module, try to find where it
begins conditional execution based on a "if __name__ == '__main__'"
guard, and remove any code after that point."""
"""
Given the source for the __main__ module, try to find where it begins
conditional execution based on a "if __name__ == '__main__'" guard, and
remove any code after that point.
"""
match = self.MAIN_RE.search(src)
if match:
return src[:match.start()]
@ -770,7 +932,7 @@ class ModuleResponder(object):
if b('mitogen.main(') in src:
return src
LOG.error(self.main_guard_msg, path)
self._log.error(self.main_guard_msg, path)
raise ImportError('refused')
def _make_negative_response(self, fullname):
@ -789,8 +951,7 @@ class ModuleResponder(object):
if path and is_stdlib_path(path):
# Prevent loading of 2.x<->3.x stdlib modules! This costs one
# RTT per hit, so a client-side solution is also required.
LOG.debug('%r: refusing to serve stdlib module %r',
self, fullname)
self._log.debug('refusing to serve stdlib module %r', fullname)
tup = self._make_negative_response(fullname)
self._cache[fullname] = tup
return tup
@ -798,21 +959,21 @@ class ModuleResponder(object):
if source is None:
# TODO: make this .warning() or similar again once importer has its
# own logging category.
LOG.debug('_build_tuple(%r): could not locate source', fullname)
self._log.debug('could not find source for %r', fullname)
tup = self._make_negative_response(fullname)
self._cache[fullname] = tup
return tup
if self.minify_safe_re.search(source):
# If the module contains a magic marker, it's safe to minify.
t0 = time.time()
t0 = mitogen.core.now()
source = mitogen.minify.minimize_source(source).encode('utf-8')
self.minify_secs += time.time() - t0
self.minify_secs += mitogen.core.now() - t0
if is_pkg:
pkg_present = get_child_modules(path)
LOG.debug('_build_tuple(%r, %r) -> %r',
path, fullname, pkg_present)
self._log.debug('%s is a package at %s with submodules %r',
fullname, path, pkg_present)
else:
pkg_present = None
@ -836,17 +997,17 @@ class ModuleResponder(object):
return tup
def _send_load_module(self, stream, fullname):
if fullname not in stream.sent_modules:
if fullname not in stream.protocol.sent_modules:
tup = self._build_tuple(fullname)
msg = mitogen.core.Message.pickled(
tup,
dst_id=stream.remote_id,
dst_id=stream.protocol.remote_id,
handle=mitogen.core.LOAD_MODULE,
)
LOG.debug('%s: sending module %s (%.2f KiB)',
stream.name, fullname, len(msg.data) / 1024.0)
self._log.debug('sending %s (%.2f KiB) to %s',
fullname, len(msg.data) / 1024.0, stream.name)
self._router._async_route(msg)
stream.sent_modules.add(fullname)
stream.protocol.sent_modules.add(fullname)
if tup[2] is not None:
self.good_load_module_count += 1
self.good_load_module_size += len(msg.data)
@ -855,23 +1016,23 @@ class ModuleResponder(object):
def _send_module_load_failed(self, stream, fullname):
self.bad_load_module_count += 1
stream.send(
stream.protocol.send(
mitogen.core.Message.pickled(
self._make_negative_response(fullname),
dst_id=stream.remote_id,
dst_id=stream.protocol.remote_id,
handle=mitogen.core.LOAD_MODULE,
)
)
def _send_module_and_related(self, stream, fullname):
if fullname in stream.sent_modules:
if fullname in stream.protocol.sent_modules:
return
try:
tup = self._build_tuple(fullname)
for name in tup[4]: # related
parent, _, _ = str_partition(name, '.')
if parent != fullname and parent not in stream.sent_modules:
if parent != fullname and parent not in stream.protocol.sent_modules:
# Parent hasn't been sent, so don't load submodule yet.
continue
@ -890,25 +1051,25 @@ class ModuleResponder(object):
return
fullname = msg.data.decode()
LOG.debug('%s requested module %s', stream.name, fullname)
self._log.debug('%s requested module %s', stream.name, fullname)
self.get_module_count += 1
if fullname in stream.sent_modules:
if fullname in stream.protocol.sent_modules:
LOG.warning('_on_get_module(): dup request for %r from %r',
fullname, stream)
t0 = time.time()
t0 = mitogen.core.now()
try:
self._send_module_and_related(stream, fullname)
finally:
self.get_module_secs += time.time() - t0
self.get_module_secs += mitogen.core.now() - t0
def _send_forward_module(self, stream, context, fullname):
if stream.remote_id != context.context_id:
stream.send(
if stream.protocol.remote_id != context.context_id:
stream.protocol._send(
mitogen.core.Message(
data=b('%s\x00%s' % (context.context_id, fullname)),
handle=mitogen.core.FORWARD_MODULE,
dst_id=stream.remote_id,
dst_id=stream.protocol.remote_id,
)
)
@ -977,6 +1138,7 @@ class Broker(mitogen.core.Broker):
on_join=self.shutdown,
)
super(Broker, self).__init__()
self.timers = mitogen.parent.TimerList()
def shutdown(self):
super(Broker, self).shutdown()
@ -1122,6 +1284,21 @@ class Router(mitogen.parent.Router):
class IdAllocator(object):
"""
Allocate IDs for new contexts constructed locally, and blocks of IDs for
children to allocate their own IDs using
:class:`mitogen.parent.ChildIdAllocator` without risk of conflict, and
without necessitating network round-trips for each new context.
This class responds to :data:`mitogen.core.ALLOCATE_ID` messages received
from children by replying with fresh block ID allocations.
The master's :class:`IdAllocator` instance can be accessed via
:attr:`mitogen.master.Router.id_allocator`.
"""
#: Block allocations are made in groups of 1000 by default.
BLOCK_SIZE = 1000
def __init__(self, router):
self.router = router
self.next_id = 1
@ -1134,14 +1311,12 @@ class IdAllocator(object):
def __repr__(self):
return 'IdAllocator(%r)' % (self.router,)
BLOCK_SIZE = 1000
def allocate(self):
"""
Arrange for a unique context ID to be allocated and associated with a
route leading to the active context. In masters, the ID is generated
directly, in children it is forwarded to the master via a
:data:`mitogen.core.ALLOCATE_ID` message.
Allocate a context ID by directly incrementing an internal counter.
:returns:
The new context ID.
"""
self.lock.acquire()
try:
@ -1152,6 +1327,15 @@ class IdAllocator(object):
self.lock.release()
def allocate_block(self):
"""
Allocate a block of IDs for use in a child context.
This function is safe to call from any thread.
:returns:
Tuple of the form `(id, end_id)` where `id` is the first usable ID
and `end_id` is the last usable ID.
"""
self.lock.acquire()
try:
id_ = self.next_id

@ -44,7 +44,8 @@ else:
def minimize_source(source):
"""Remove comments and docstrings from Python `source`, preserving line
"""
Remove comments and docstrings from Python `source`, preserving line
numbers and syntax of empty blocks.
:param str source:
@ -62,7 +63,8 @@ def minimize_source(source):
def strip_comments(tokens):
"""Drop comment tokens from a `tokenize` stream.
"""
Drop comment tokens from a `tokenize` stream.
Comments on lines 1-2 are kept, to preserve hashbang and encoding.
Trailing whitespace is remove from all lines.
@ -84,7 +86,8 @@ def strip_comments(tokens):
def strip_docstrings(tokens):
"""Replace docstring tokens with NL tokens in a `tokenize` stream.
"""
Replace docstring tokens with NL tokens in a `tokenize` stream.
Any STRING token not part of an expression is deemed a docstring.
Indented docstrings are not yet recognised.
@ -119,7 +122,8 @@ def strip_docstrings(tokens):
def reindent(tokens, indent=' '):
"""Replace existing indentation in a token steam, with `indent`.
"""
Replace existing indentation in a token steam, with `indent`.
"""
old_levels = []
old_level = 0

@ -35,6 +35,7 @@ Support for operating in a mixed threading/forking environment.
import os
import socket
import sys
import threading
import weakref
import mitogen.core
@ -157,6 +158,7 @@ class Corker(object):
held. This will not return until each thread acknowledges it has ceased
execution.
"""
current = threading.currentThread()
s = mitogen.core.b('CORK') * ((128 // 4) * 1024)
self._rsocks = []
@ -164,12 +166,14 @@ class Corker(object):
# participation of a broker in order to complete.
for pool in self.pools:
if not pool.closed:
for x in range(pool.size):
self._cork_one(s, pool)
for th in pool._threads:
if th != current:
self._cork_one(s, pool)
for broker in self.brokers:
if broker._alive:
self._cork_one(s, broker)
if broker._thread != current:
self._cork_one(s, broker)
# Pause until we can detect every thread has entered write().
for rsock in self._rsocks:

File diff suppressed because it is too large Load Diff

@ -28,7 +28,8 @@
# !mitogen: minify_safe
"""mitogen.profiler
"""
mitogen.profiler
Record and report cProfile statistics from a run. Creates one aggregated
output file, one aggregate containing only workers, and one for the
top-level process.
@ -56,28 +57,25 @@ Example:
from __future__ import print_function
import os
import pstats
import cProfile
import shutil
import subprocess
import sys
import tempfile
import time
import mitogen.core
def try_merge(stats, path):
try:
stats.add(path)
return True
except Exception as e:
print('Failed. Race? Will retry. %s' % (e,))
print('%s failed. Will retry. %s' % (path, e))
return False
def merge_stats(outpath, inpaths):
first, rest = inpaths[0], inpaths[1:]
for x in range(5):
for x in range(1):
try:
stats = pstats.Stats(first)
except EOFError:
@ -152,7 +150,7 @@ def do_stat(tmpdir, sort, *args):
def main():
if len(sys.argv) < 2 or sys.argv[1] not in ('record', 'report', 'stat'):
sys.stderr.write(__doc__)
sys.stderr.write(__doc__.lstrip())
sys.exit(1)
func = globals()['do_' + sys.argv[1]]

@ -57,9 +57,7 @@ class Select(object):
If `oneshot` is :data:`True`, then remove each receiver as it yields a
result; since :meth:`__iter__` terminates once the final receiver is
removed, this makes it convenient to respond to calls made in parallel:
.. code-block:: python
removed, this makes it convenient to respond to calls made in parallel::
total = 0
recvs = [c.call_async(long_running_operation) for c in contexts]
@ -98,7 +96,7 @@ class Select(object):
for msg in mitogen.select.Select(selects):
print(msg.unpickle())
:class:`Select` may be used to mix inter-thread and inter-process IO:
:class:`Select` may be used to mix inter-thread and inter-process IO::
latch = mitogen.core.Latch()
start_thread(latch)
@ -124,9 +122,10 @@ class Select(object):
@classmethod
def all(cls, receivers):
"""
Take an iterable of receivers and retrieve a :class:`Message` from
each, returning the result of calling `msg.unpickle()` on each in turn.
Results are returned in the order they arrived.
Take an iterable of receivers and retrieve a :class:`Message
<mitogen.core.Message>` from each, returning the result of calling
:meth:`Message.unpickle() <mitogen.core.Message.unpickle>` on each in
turn. Results are returned in the order they arrived.
This is sugar for handling batch :meth:`Context.call_async
<mitogen.parent.Context.call_async>` invocations:
@ -226,8 +225,15 @@ class Select(object):
raise Error(self.owned_msg)
recv.notify = self._put
# Avoid race by polling once after installation.
if not recv.empty():
# After installing the notify function, _put() will potentially begin
# receiving calls from other threads immediately, but not for items
# they already had buffered. For those we call _put(), possibly
# duplicating the effect of other _put() being made concurrently, such
# that the Select ends up with more items in its buffer than exist in
# the underlying receivers. We handle the possibility of receivers
# marked notified yet empty inside Select.get(), so this should be
# robust.
for _ in range(recv.size()):
self._put(recv)
not_present_msg = 'Instance is not a member of this Select'
@ -261,18 +267,26 @@ class Select(object):
self.remove(recv)
self._latch.close()
def empty(self):
def size(self):
"""
Return the number of items currently buffered.
As with :class:`Queue.Queue`, `0` may be returned even though a
subsequent call to :meth:`get` will succeed, since a message may be
posted at any moment between :meth:`size` and :meth:`get`.
As with :class:`Queue.Queue`, `>0` may be returned even though a
subsequent call to :meth:`get` will block, since another waiting thread
may be woken at any moment between :meth:`size` and :meth:`get`.
"""
Return :data:`True` if calling :meth:`get` would block.
return sum(recv.size() for recv in self._receivers)
As with :class:`Queue.Queue`, :data:`True` may be returned even though
a subsequent call to :meth:`get` will succeed, since a message may be
posted at any moment between :meth:`empty` and :meth:`get`.
def empty(self):
"""
Return `size() == 0`.
:meth:`empty` may return :data:`False` even when :meth:`get` would
block if another thread has drained a receiver added to this select.
This can be avoided by only consuming each receiver from a single
thread.
.. deprecated:: 0.2.8
Use :meth:`size` instead.
"""
return self._latch.empty()
@ -329,5 +343,6 @@ class Select(object):
# A receiver may have been queued with no result if another
# thread drained it before we woke up, or because another
# thread drained it between add() calling recv.empty() and
# self._put(). In this case just sleep again.
# self._put(), or because Select.add() caused duplicate _put()
# calls. In this case simply retry.
continue

@ -29,6 +29,7 @@
# !mitogen: minify_safe
import grp
import logging
import os
import os.path
import pprint
@ -36,12 +37,10 @@ import pwd
import stat
import sys
import threading
import time
import mitogen.core
import mitogen.select
from mitogen.core import b
from mitogen.core import LOG
from mitogen.core import str_rpartition
try:
@ -54,7 +53,8 @@ except NameError:
return True
DEFAULT_POOL_SIZE = 16
LOG = logging.getLogger(__name__)
_pool = None
_pool_pid = None
#: Serialize pool construction.
@ -77,19 +77,51 @@ else:
def get_or_create_pool(size=None, router=None):
global _pool
global _pool_pid
_pool_lock.acquire()
try:
if _pool_pid != os.getpid():
_pool = Pool(router, [], size=size or DEFAULT_POOL_SIZE,
overwrite=True)
# In case of Broker shutdown crash, Pool can cause 'zombie'
# processes.
mitogen.core.listen(router.broker, 'shutdown',
lambda: _pool.stop(join=False))
_pool_pid = os.getpid()
return _pool
finally:
_pool_lock.release()
my_pid = os.getpid()
if _pool is None or _pool.closed or my_pid != _pool_pid:
# Avoid acquiring heavily contended lock if possible.
_pool_lock.acquire()
try:
if _pool_pid != my_pid:
_pool = Pool(
router,
services=[],
size=size or 2,
overwrite=True,
recv=mitogen.core.Dispatcher._service_recv,
)
# In case of Broker shutdown crash, Pool can cause 'zombie'
# processes.
mitogen.core.listen(router.broker, 'shutdown',
lambda: _pool.stop(join=True))
_pool_pid = os.getpid()
finally:
_pool_lock.release()
return _pool
def get_thread_name():
return threading.currentThread().getName()
def call(service_name, method_name, call_context=None, **kwargs):
"""
Call a service registered with this pool, using the calling thread as a
host.
"""
if isinstance(service_name, mitogen.core.BytesType):
service_name = service_name.encode('utf-8')
elif not isinstance(service_name, mitogen.core.UnicodeType):
service_name = service_name.name() # Service.name()
if call_context:
return call_context.call_service(service_name, method_name, **kwargs)
else:
pool = get_or_create_pool()
invoker = pool.get_invoker(service_name, msg=None)
return getattr(invoker.service, method_name)(**kwargs)
def validate_arg_spec(spec, args):
@ -239,12 +271,13 @@ class Invoker(object):
if not policies:
raise mitogen.core.CallError('Method has no policies set.')
if not all(p.is_authorized(self.service, msg) for p in policies):
raise mitogen.core.CallError(
self.unauthorized_msg,
method_name,
self.service.name()
)
if msg is not None:
if not all(p.is_authorized(self.service, msg) for p in policies):
raise mitogen.core.CallError(
self.unauthorized_msg,
method_name,
self.service.name()
)
required = getattr(method, 'mitogen_service__arg_spec', {})
validate_arg_spec(required, kwargs)
@ -264,7 +297,7 @@ class Invoker(object):
except Exception:
if no_reply:
LOG.exception('While calling no-reply method %s.%s',
type(self.service).__name__,
self.service.name(),
func_name(method))
else:
raise
@ -445,13 +478,19 @@ class Pool(object):
program's configuration or its input data.
:param mitogen.core.Router router:
Router to listen for ``CALL_SERVICE`` messages on.
:class:`mitogen.core.Router` to listen for
:data:`mitogen.core.CALL_SERVICE` messages.
:param list services:
Initial list of services to register.
:param mitogen.core.Receiver recv:
:data:`mitogen.core.CALL_SERVICE` receiver to reuse. This is used by
:func:`get_or_create_pool` to hand off a queue of messages from the
Dispatcher stub handler while avoiding a race.
"""
activator_class = Activator
def __init__(self, router, services=(), size=1, overwrite=False):
def __init__(self, router, services=(), size=1, overwrite=False,
recv=None):
self.router = router
self._activator = self.activator_class()
self._ipc_latch = mitogen.core.Latch()
@ -472,12 +511,22 @@ class Pool(object):
}
self._invoker_by_name = {}
if recv is not None:
# When inheriting from mitogen.core.Dispatcher, we must remove its
# stub notification function before adding it to our Select. We
# always overwrite this receiver since the standard service.Pool
# handler policy differs from the one inherited from
# core.Dispatcher.
recv.notify = None
self._select.add(recv)
self._func_by_source[recv] = self._on_service_call
for service in services:
self.add(service)
self._py_24_25_compat()
self._threads = []
for x in range(size):
name = 'mitogen.service.Pool.%x.worker-%d' % (id(self), x,)
name = 'mitogen.Pool.%04x.%d' % (id(self) & 0xffff, x,)
thread = threading.Thread(
name=name,
target=mitogen.core._profile_hook,
@ -485,7 +534,6 @@ class Pool(object):
)
thread.start()
self._threads.append(thread)
LOG.debug('%r: initialized', self)
def _py_24_25_compat(self):
@ -524,15 +572,18 @@ class Pool(object):
invoker.service.on_shutdown()
def get_invoker(self, name, msg):
self._lock.acquire()
try:
invoker = self._invoker_by_name.get(name)
if not invoker:
service = self._activator.activate(self, name, msg)
invoker = service.invoker_class(service=service)
self._invoker_by_name[name] = invoker
finally:
self._lock.release()
invoker = self._invoker_by_name.get(name)
if invoker is None:
# Avoid acquiring lock if possible.
self._lock.acquire()
try:
invoker = self._invoker_by_name.get(name)
if not invoker:
service = self._activator.activate(self, name, msg)
invoker = service.invoker_class(service=service)
self._invoker_by_name[name] = invoker
finally:
self._lock.release()
return invoker
@ -582,9 +633,12 @@ class Pool(object):
while not self.closed:
try:
event = self._select.get_event()
except (mitogen.core.ChannelError, mitogen.core.LatchError):
e = sys.exc_info()[1]
LOG.debug('%r: channel or latch closed, exitting: %s', self, e)
except mitogen.core.LatchError:
LOG.debug('thread %s exiting gracefully', get_thread_name())
return
except mitogen.core.ChannelError:
LOG.debug('thread %s exiting with error: %s',
get_thread_name(), sys.exc_info()[1])
return
func = self._func_by_source[event.source]
@ -597,16 +651,14 @@ class Pool(object):
try:
self._worker_run()
except Exception:
th = threading.currentThread()
LOG.exception('%r: worker %r crashed', self, th.getName())
LOG.exception('%r: worker %r crashed', self, get_thread_name())
raise
def __repr__(self):
th = threading.currentThread()
return 'mitogen.service.Pool(%#x, size=%d, th=%r)' % (
id(self),
return 'Pool(%04x, size=%d, th=%r)' % (
id(self) & 0xffff,
len(self._threads),
th.getName(),
get_thread_name(),
)
@ -625,7 +677,7 @@ class PushFileService(Service):
"""
Push-based file service. Files are delivered and cached in RAM, sent
recursively from parent to child. A child that requests a file via
:meth:`get` will block until it has ben delivered by a parent.
:meth:`get` will block until it has been delivered by a parent.
This service will eventually be merged into FileService.
"""
@ -658,10 +710,12 @@ class PushFileService(Service):
def _forward(self, context, path):
stream = self.router.stream_by_id(context.context_id)
child = mitogen.core.Context(self.router, stream.remote_id)
child = self.router.context_by_id(stream.protocol.remote_id)
sent = self._sent_by_stream.setdefault(stream, set())
if path in sent:
if child.context_id != context.context_id:
LOG.debug('requesting %s forward small file to %s: %s',
child, context, path)
child.call_service_async(
service_name=self.name(),
method_name='forward',
@ -669,6 +723,8 @@ class PushFileService(Service):
context=context
).close()
else:
LOG.debug('requesting %s cache and forward small file to %s: %s',
child, context, path)
child.call_service_async(
service_name=self.name(),
method_name='store_and_forward',
@ -691,7 +747,7 @@ class PushFileService(Service):
"""
for path in paths:
self.propagate_to(context, mitogen.core.to_text(path))
self.router.responder.forward_modules(context, modules)
#self.router.responder.forward_modules(context, modules) TODO
@expose(policy=AllowParents())
@arg_spec({
@ -699,8 +755,8 @@ class PushFileService(Service):
'path': mitogen.core.FsPathTypes,
})
def propagate_to(self, context, path):
LOG.debug('%r.propagate_to(%r, %r)', self, context, path)
if path not in self._cache:
LOG.debug('caching small file %s', path)
fp = open(path, 'rb')
try:
self._cache[path] = mitogen.core.Blob(fp.read())
@ -718,7 +774,7 @@ class PushFileService(Service):
def store_and_forward(self, path, data, context):
LOG.debug('%r.store_and_forward(%r, %r, %r) %r',
self, path, data, context,
threading.currentThread().getName())
get_thread_name())
self._lock.acquire()
try:
self._cache[path] = data
@ -891,7 +947,7 @@ class FileService(Service):
# The IO loop pumps 128KiB chunks. An ideal message is a multiple of this,
# odd-sized messages waste one tiny write() per message on the trailer.
# Therefore subtract 10 bytes pickle overhead + 24 bytes header.
IO_SIZE = mitogen.core.CHUNK_SIZE - (mitogen.core.Stream.HEADER_LEN + (
IO_SIZE = mitogen.core.CHUNK_SIZE - (mitogen.core.Message.HEADER_LEN + (
len(
mitogen.core.Message.pickled(
mitogen.core.Blob(b(' ') * mitogen.core.CHUNK_SIZE)
@ -965,7 +1021,11 @@ class FileService(Service):
:raises Error:
Unregistered path, or Sender did not match requestee context.
"""
if path not in self._paths and not self._prefix_is_authorized(path):
if (
(path not in self._paths) and
(not self._prefix_is_authorized(path)) and
(not mitogen.core._has_parent_authority(msg.auth_id))
):
msg.reply(mitogen.core.CallError(
Error(self.unregistered_msg % (path,))
))
@ -1047,7 +1107,7 @@ class FileService(Service):
:meth:`fetch`.
"""
LOG.debug('get_file(): fetching %r from %r', path, context)
t0 = time.time()
t0 = mitogen.core.now()
recv = mitogen.core.Receiver(router=context.router)
metadata = context.call_service(
service_name=cls.name(),
@ -1081,5 +1141,6 @@ class FileService(Service):
path, metadata['size'], received_bytes)
LOG.debug('target.get_file(): fetched %d bytes of %r from %r in %dms',
metadata['size'], path, context, 1000 * (time.time() - t0))
metadata['size'], path, context,
1000 * (mitogen.core.now() - t0))
return ok, metadata

@ -116,9 +116,15 @@ def get_machinectl_pid(path, name):
raise Error("could not find PID from machinectl output.\n%s", output)
class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False
GET_LEADER_BY_KIND = {
'docker': ('docker_path', get_docker_pid),
'lxc': ('lxc_info_path', get_lxc_pid),
'lxd': ('lxc_path', get_lxd_pid),
'machinectl': ('machinectl_path', get_machinectl_pid),
}
class Options(mitogen.parent.Options):
container = None
username = 'root'
kind = None
@ -128,24 +134,17 @@ class Stream(mitogen.parent.Stream):
lxc_info_path = 'lxc-info'
machinectl_path = 'machinectl'
GET_LEADER_BY_KIND = {
'docker': ('docker_path', get_docker_pid),
'lxc': ('lxc_info_path', get_lxc_pid),
'lxd': ('lxc_path', get_lxd_pid),
'machinectl': ('machinectl_path', get_machinectl_pid),
}
def construct(self, container, kind, username=None, docker_path=None,
lxc_path=None, lxc_info_path=None, machinectl_path=None,
**kwargs):
super(Stream, self).construct(**kwargs)
if kind not in self.GET_LEADER_BY_KIND:
def __init__(self, container, kind, username=None, docker_path=None,
lxc_path=None, lxc_info_path=None, machinectl_path=None,
**kwargs):
super(Options, self).__init__(**kwargs)
if kind not in GET_LEADER_BY_KIND:
raise Error('unsupported container kind: %r', kind)
self.container = container
self.container = mitogen.core.to_text(container)
self.kind = kind
if username:
self.username = username
self.username = mitogen.core.to_text(username)
if docker_path:
self.docker_path = docker_path
if lxc_path:
@ -155,6 +154,11 @@ class Stream(mitogen.parent.Stream):
if machinectl_path:
self.machinectl_path = machinectl_path
class Connection(mitogen.parent.Connection):
options_class = Options
child_is_immediate_subprocess = False
# Order matters. https://github.com/karelzak/util-linux/commit/854d0fe/
NS_ORDER = ('ipc', 'uts', 'net', 'pid', 'mnt', 'user')
@ -189,15 +193,15 @@ class Stream(mitogen.parent.Stream):
try:
os.setgroups([grent.gr_gid
for grent in grp.getgrall()
if self.username in grent.gr_mem])
pwent = pwd.getpwnam(self.username)
if self.options.username in grent.gr_mem])
pwent = pwd.getpwnam(self.options.username)
os.setreuid(pwent.pw_uid, pwent.pw_uid)
# shadow-4.4/libmisc/setupenv.c. Not done: MAIL, PATH
os.environ.update({
'HOME': pwent.pw_dir,
'SHELL': pwent.pw_shell or '/bin/sh',
'LOGNAME': self.username,
'USER': self.username,
'LOGNAME': self.options.username,
'USER': self.options.username,
})
if ((os.path.exists(pwent.pw_dir) and
os.access(pwent.pw_dir, os.X_OK))):
@ -217,7 +221,7 @@ class Stream(mitogen.parent.Stream):
# namespaces, meaning starting new threads in the exec'd program will
# fail. The solution is forking, so inject a /bin/sh call to achieve
# this.
argv = super(Stream, self).get_boot_command()
argv = super(Connection, self).get_boot_command()
# bash will exec() if a single command was specified and the shell has
# nothing left to do, so "; exit $?" gives bash a reason to live.
return ['/bin/sh', '-c', '%s; exit $?' % (mitogen.parent.Argv(argv),)]
@ -226,13 +230,12 @@ class Stream(mitogen.parent.Stream):
return mitogen.parent.create_child(args, preexec_fn=self.preexec_fn)
def _get_name(self):
return u'setns.' + self.container
return u'setns.' + self.options.container
def connect(self):
self.name = self._get_name()
attr, func = self.GET_LEADER_BY_KIND[self.kind]
tool_path = getattr(self, attr)
self.leader_pid = func(tool_path, self.container)
def connect(self, **kwargs):
attr, func = GET_LEADER_BY_KIND[self.options.kind]
tool_path = getattr(self.options, attr)
self.leader_pid = func(tool_path, self.options.container)
LOG.debug('Leader PID for %s container %r: %d',
self.kind, self.container, self.leader_pid)
super(Stream, self).connect()
self.options.kind, self.options.container, self.leader_pid)
return super(Connection, self).connect(**kwargs)

@ -29,7 +29,7 @@
# !mitogen: minify_safe
"""
Functionality to allow establishing new slave contexts over an SSH connection.
Construct new children via the OpenSSH client.
"""
import logging
@ -42,7 +42,6 @@ except ImportError:
import mitogen.parent
from mitogen.core import b
from mitogen.core import bytes_partition
try:
any
@ -50,84 +49,124 @@ except NameError:
from mitogen.core import any
LOG = logging.getLogger('mitogen')
LOG = logging.getLogger(__name__)
auth_incorrect_msg = 'SSH authentication is incorrect'
password_incorrect_msg = 'SSH password is incorrect'
password_required_msg = 'SSH password was requested, but none specified'
hostkey_config_msg = (
'SSH requested permission to accept unknown host key, but '
'check_host_keys=ignore. This is likely due to ssh_args= '
'conflicting with check_host_keys=. Please correct your '
'configuration.'
)
hostkey_failed_msg = (
'Host key checking is enabled, and SSH reported an unrecognized or '
'mismatching host key.'
)
# sshpass uses 'assword' because it doesn't lowercase the input.
PASSWORD_PROMPT = b('password')
HOSTKEY_REQ_PROMPT = b('are you sure you want to continue connecting (yes/no)?')
HOSTKEY_FAIL = b('host key verification failed.')
PASSWORD_PROMPT_PATTERN = re.compile(
b('password'),
re.I
)
HOSTKEY_REQ_PATTERN = re.compile(
b(r'are you sure you want to continue connecting \(yes/no\)\?'),
re.I
)
HOSTKEY_FAIL_PATTERN = re.compile(
b(r'host key verification failed\.'),
re.I
)
# [user@host: ] permission denied
PERMDENIED_RE = re.compile(
('(?:[^@]+@[^:]+: )?' # Absent in OpenSSH <7.5
'Permission denied').encode(),
# issue #271: work around conflict with user shell reporting 'permission
# denied' e.g. during chdir($HOME) by only matching it at the start of the
# line.
PERMDENIED_PATTERN = re.compile(
b('^(?:[^@]+@[^:]+: )?' # Absent in OpenSSH <7.5
'Permission denied'),
re.I
)
DEBUG_PATTERN = re.compile(b('^debug[123]:'))
DEBUG_PREFIXES = (b('debug1:'), b('debug2:'), b('debug3:'))
class PasswordError(mitogen.core.StreamError):
pass
def filter_debug(stream, it):
"""
Read line chunks from it, either yielding them directly, or building up and
logging individual lines if they look like SSH debug output.
class HostKeyError(mitogen.core.StreamError):
pass
This contains the mess of dealing with both line-oriented input, and partial
lines such as the password prompt.
Yields `(line, partial)` tuples, where `line` is the line, `partial` is
:data:`True` if no terminating newline character was present and no more
data exists in the read buffer. Consuming code can use this to unreliably
detect the presence of an interactive prompt.
class SetupProtocol(mitogen.parent.RegexProtocol):
"""
This protocol is attached to stderr of the SSH client. It responds to
various interactive prompts as required.
"""
# The `partial` test is unreliable, but is only problematic when verbosity
# is enabled: it's possible for a combination of SSH banner, password
# prompt, verbose output, timing and OS buffering specifics to create a
# situation where an otherwise newline-terminated line appears to not be
# terminated, due to a partial read(). If something is broken when
# ssh_debug_level>0, this is the first place to look.
state = 'start_of_line'
buf = b('')
for chunk in it:
buf += chunk
while buf:
if state == 'start_of_line':
if len(buf) < 8:
# short read near buffer limit, block awaiting at least 8
# bytes so we can discern a debug line, or the minimum
# interesting token from above or the bootstrap
# ('password', 'MITO000\n').
break
elif any(buf.startswith(p) for p in DEBUG_PREFIXES):
state = 'in_debug'
else:
state = 'in_plain'
elif state == 'in_debug':
if b('\n') not in buf:
break
line, _, buf = bytes_partition(buf, b('\n'))
LOG.debug('%s: %s', stream.name,
mitogen.core.to_text(line.rstrip()))
state = 'start_of_line'
elif state == 'in_plain':
line, nl, buf = bytes_partition(buf, b('\n'))
yield line + nl, not (nl or buf)
if nl:
state = 'start_of_line'
password_sent = False
def _on_host_key_request(self, line, match):
if self.stream.conn.options.check_host_keys == 'accept':
LOG.debug('%s: accepting host key', self.stream.name)
self.stream.transmit_side.write(b('yes\n'))
return
class PasswordError(mitogen.core.StreamError):
pass
# _host_key_prompt() should never be reached with ignore or enforce
# mode, SSH should have handled that. User's ssh_args= is conflicting
# with ours.
self.stream.conn._fail_connection(HostKeyError(hostkey_config_msg))
def _on_host_key_failed(self, line, match):
self.stream.conn._fail_connection(HostKeyError(hostkey_failed_msg))
def _on_permission_denied(self, line, match):
if self.stream.conn.options.password is not None and \
self.password_sent:
self.stream.conn._fail_connection(
PasswordError(password_incorrect_msg)
)
elif PASSWORD_PROMPT_PATTERN.search(line) and \
self.stream.conn.options.password is None:
# Permission denied (password,pubkey)
self.stream.conn._fail_connection(
PasswordError(password_required_msg)
)
else:
self.stream.conn._fail_connection(
PasswordError(auth_incorrect_msg)
)
def _on_password_prompt(self, line, match):
LOG.debug('%s: (password prompt): %s', self.stream.name, line)
if self.stream.conn.options.password is None:
self.stream.conn._fail(PasswordError(password_required_msg))
class HostKeyError(mitogen.core.StreamError):
pass
self.stream.transmit_side.write(
(self.stream.conn.options.password + '\n').encode('utf-8')
)
self.password_sent = True
def _on_debug_line(self, line, match):
text = mitogen.core.to_text(line.rstrip())
LOG.debug('%s: %s', self.stream.name, text)
PATTERNS = [
(DEBUG_PATTERN, _on_debug_line),
(HOSTKEY_FAIL_PATTERN, _on_host_key_failed),
(PERMDENIED_PATTERN, _on_permission_denied),
]
PARTIAL_PATTERNS = [
(PASSWORD_PROMPT_PATTERN, _on_password_prompt),
(HOSTKEY_REQ_PATTERN, _on_host_key_request),
]
class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False
class Options(mitogen.parent.Options):
#: Default to whatever is available as 'python' on the remote machine,
#: overriding sys.executable use.
python_path = 'python'
@ -141,19 +180,19 @@ class Stream(mitogen.parent.Stream):
hostname = None
username = None
port = None
identity_file = None
password = None
ssh_args = None
check_host_keys_msg = 'check_host_keys= must be set to accept, enforce or ignore'
def construct(self, hostname, username=None, ssh_path=None, port=None,
check_host_keys='enforce', password=None, identity_file=None,
compression=True, ssh_args=None, keepalive_enabled=True,
keepalive_count=3, keepalive_interval=15,
identities_only=True, ssh_debug_level=None, **kwargs):
super(Stream, self).construct(**kwargs)
def __init__(self, hostname, username=None, ssh_path=None, port=None,
check_host_keys='enforce', password=None, identity_file=None,
compression=True, ssh_args=None, keepalive_enabled=True,
keepalive_count=3, keepalive_interval=15,
identities_only=True, ssh_debug_level=None, **kwargs):
super(Options, self).__init__(**kwargs)
if check_host_keys not in ('accept', 'enforce', 'ignore'):
raise ValueError(self.check_host_keys_msg)
@ -175,143 +214,81 @@ class Stream(mitogen.parent.Stream):
if ssh_debug_level:
self.ssh_debug_level = ssh_debug_level
self._init_create_child()
class Connection(mitogen.parent.Connection):
options_class = Options
diag_protocol_class = SetupProtocol
child_is_immediate_subprocess = False
def _get_name(self):
s = u'ssh.' + mitogen.core.to_text(self.options.hostname)
if self.options.port and self.options.port != 22:
s += u':%s' % (self.options.port,)
return s
def _requires_pty(self):
"""
Return :data:`True` if the configuration requires a PTY to be
allocated. This is only true if we must interactively accept host keys,
or type a password.
Return :data:`True` if a PTY to is required for this configuration,
because it must interactively accept host keys or type a password.
"""
return (self.check_host_keys == 'accept' or
self.password is not None)
return (
self.options.check_host_keys == 'accept' or
self.options.password is not None
)
def _init_create_child(self):
def create_child(self, **kwargs):
"""
Initialize the base class :attr:`create_child` and
:attr:`create_child_args` according to whether we need a PTY or not.
Avoid PTY use when possible to avoid a scaling limitation.
"""
if self._requires_pty():
self.create_child = mitogen.parent.hybrid_tty_create_child
return mitogen.parent.hybrid_tty_create_child(**kwargs)
else:
self.create_child = mitogen.parent.create_child
self.create_child_args = {
'stderr_pipe': True,
}
return mitogen.parent.create_child(stderr_pipe=True, **kwargs)
def get_boot_command(self):
bits = [self.ssh_path]
if self.ssh_debug_level:
bits += ['-' + ('v' * min(3, self.ssh_debug_level))]
bits = [self.options.ssh_path]
if self.options.ssh_debug_level:
bits += ['-' + ('v' * min(3, self.options.ssh_debug_level))]
else:
# issue #307: suppress any login banner, as it may contain the
# password prompt, and there is no robust way to tell the
# difference.
bits += ['-o', 'LogLevel ERROR']
if self.username:
bits += ['-l', self.username]
if self.port is not None:
bits += ['-p', str(self.port)]
if self.identities_only and (self.identity_file or self.password):
if self.options.username:
bits += ['-l', self.options.username]
if self.options.port is not None:
bits += ['-p', str(self.options.port)]
if self.options.identities_only and (self.options.identity_file or
self.options.password):
bits += ['-o', 'IdentitiesOnly yes']
if self.identity_file:
bits += ['-i', self.identity_file]
if self.compression:
if self.options.identity_file:
bits += ['-i', self.options.identity_file]
if self.options.compression:
bits += ['-o', 'Compression yes']
if self.keepalive_enabled:
if self.options.keepalive_enabled:
bits += [
'-o', 'ServerAliveInterval %s' % (self.keepalive_interval,),
'-o', 'ServerAliveCountMax %s' % (self.keepalive_count,),
'-o', 'ServerAliveInterval %s' % (
self.options.keepalive_interval,
),
'-o', 'ServerAliveCountMax %s' % (
self.options.keepalive_count,
),
]
if not self._requires_pty():
bits += ['-o', 'BatchMode yes']
if self.check_host_keys == 'enforce':
if self.options.check_host_keys == 'enforce':
bits += ['-o', 'StrictHostKeyChecking yes']
if self.check_host_keys == 'accept':
if self.options.check_host_keys == 'accept':
bits += ['-o', 'StrictHostKeyChecking ask']
elif self.check_host_keys == 'ignore':
elif self.options.check_host_keys == 'ignore':
bits += [
'-o', 'StrictHostKeyChecking no',
'-o', 'UserKnownHostsFile /dev/null',
'-o', 'GlobalKnownHostsFile /dev/null',
]
if self.ssh_args:
bits += self.ssh_args
bits.append(self.hostname)
base = super(Stream, self).get_boot_command()
if self.options.ssh_args:
bits += self.options.ssh_args
bits.append(self.options.hostname)
base = super(Connection, self).get_boot_command()
return bits + [shlex_quote(s).strip() for s in base]
def _get_name(self):
s = u'ssh.' + mitogen.core.to_text(self.hostname)
if self.port:
s += u':%s' % (self.port,)
return s
auth_incorrect_msg = 'SSH authentication is incorrect'
password_incorrect_msg = 'SSH password is incorrect'
password_required_msg = 'SSH password was requested, but none specified'
hostkey_config_msg = (
'SSH requested permission to accept unknown host key, but '
'check_host_keys=ignore. This is likely due to ssh_args= '
'conflicting with check_host_keys=. Please correct your '
'configuration.'
)
hostkey_failed_msg = (
'Host key checking is enabled, and SSH reported an unrecognized or '
'mismatching host key.'
)
def _host_key_prompt(self):
if self.check_host_keys == 'accept':
LOG.debug('%s: accepting host key', self.name)
self.diag_stream.transmit_side.write(b('yes\n'))
return
# _host_key_prompt() should never be reached with ignore or enforce
# mode, SSH should have handled that. User's ssh_args= is conflicting
# with ours.
raise HostKeyError(self.hostkey_config_msg)
def _connect_input_loop(self, it):
password_sent = False
for buf, partial in filter_debug(self, it):
LOG.debug('%s: stdout: %s', self.name, buf.rstrip())
if buf.endswith(self.EC0_MARKER):
self._ec0_received()
return
elif HOSTKEY_REQ_PROMPT in buf.lower():
self._host_key_prompt()
elif HOSTKEY_FAIL in buf.lower():
raise HostKeyError(self.hostkey_failed_msg)
elif PERMDENIED_RE.match(buf):
# issue #271: work around conflict with user shell reporting
# 'permission denied' e.g. during chdir($HOME) by only matching
# it at the start of the line.
if self.password is not None and password_sent:
raise PasswordError(self.password_incorrect_msg)
elif PASSWORD_PROMPT in buf and self.password is None:
# Permission denied (password,pubkey)
raise PasswordError(self.password_required_msg)
else:
raise PasswordError(self.auth_incorrect_msg)
elif partial and PASSWORD_PROMPT in buf.lower():
if self.password is None:
raise PasswordError(self.password_required_msg)
LOG.debug('%s: sending password', self.name)
self.diag_stream.transmit_side.write(
(self.password + '\n').encode()
)
password_sent = True
raise mitogen.core.StreamError('bootstrap failed')
def _connect_bootstrap(self):
fds = [self.receive_side.fd]
if self.diag_stream is not None:
fds.append(self.diag_stream.receive_side.fd)
it = mitogen.parent.iter_read(fds=fds, deadline=self.connect_deadline)
try:
self._connect_input_loop(it)
finally:
it.close()

@ -29,10 +29,10 @@
# !mitogen: minify_safe
import logging
import re
import mitogen.core
import mitogen.parent
from mitogen.core import b
try:
any
@ -42,87 +42,119 @@ except NameError:
LOG = logging.getLogger(__name__)
password_incorrect_msg = 'su password is incorrect'
password_required_msg = 'su password is required'
class PasswordError(mitogen.core.StreamError):
pass
class Stream(mitogen.parent.Stream):
# TODO: BSD su cannot handle stdin being a socketpair, but it does let the
# child inherit fds from the parent. So we can still pass a socketpair in
# for hybrid_tty_create_child(), there just needs to be either a shell
# snippet or bootstrap support for fixing things up afterwards.
create_child = staticmethod(mitogen.parent.tty_create_child)
child_is_immediate_subprocess = False
class SetupBootstrapProtocol(mitogen.parent.BootstrapProtocol):
password_sent = False
def setup_patterns(self, conn):
"""
su options cause the regexes used to vary. This is a mess, requires
reworking.
"""
incorrect_pattern = re.compile(
mitogen.core.b('|').join(
re.escape(s.encode('utf-8'))
for s in conn.options.incorrect_prompts
),
re.I
)
prompt_pattern = re.compile(
re.escape(
conn.options.password_prompt.encode('utf-8')
),
re.I
)
self.PATTERNS = mitogen.parent.BootstrapProtocol.PATTERNS + [
(incorrect_pattern, type(self)._on_password_incorrect),
]
self.PARTIAL_PATTERNS = mitogen.parent.BootstrapProtocol.PARTIAL_PATTERNS + [
(prompt_pattern, type(self)._on_password_prompt),
]
def _on_password_prompt(self, line, match):
LOG.debug('%r: (password prompt): %r',
self.stream.name, line.decode('utf-8', 'replace'))
if self.stream.conn.options.password is None:
self.stream.conn._fail_connection(
PasswordError(password_required_msg)
)
return
if self.password_sent:
self.stream.conn._fail_connection(
PasswordError(password_incorrect_msg)
)
return
self.stream.transmit_side.write(
(self.stream.conn.options.password + '\n').encode('utf-8')
)
self.password_sent = True
def _on_password_incorrect(self, line, match):
self.stream.conn._fail_connection(
PasswordError(password_incorrect_msg)
)
#: Once connected, points to the corresponding DiagLogStream, allowing it to
#: be disconnected at the same time this stream is being torn down.
username = 'root'
class Options(mitogen.parent.Options):
username = u'root'
password = None
su_path = 'su'
password_prompt = b('password:')
password_prompt = u'password:'
incorrect_prompts = (
b('su: sorry'), # BSD
b('su: authentication failure'), # Linux
b('su: incorrect password'), # CentOS 6
b('authentication is denied'), # AIX
u'su: sorry', # BSD
u'su: authentication failure', # Linux
u'su: incorrect password', # CentOS 6
u'authentication is denied', # AIX
)
def construct(self, username=None, password=None, su_path=None,
password_prompt=None, incorrect_prompts=None, **kwargs):
super(Stream, self).construct(**kwargs)
def __init__(self, username=None, password=None, su_path=None,
password_prompt=None, incorrect_prompts=None, **kwargs):
super(Options, self).__init__(**kwargs)
if username is not None:
self.username = username
self.username = mitogen.core.to_text(username)
if password is not None:
self.password = password
self.password = mitogen.core.to_text(password)
if su_path is not None:
self.su_path = su_path
if password_prompt is not None:
self.password_prompt = password_prompt.lower()
self.password_prompt = password_prompt
if incorrect_prompts is not None:
self.incorrect_prompts = map(str.lower, incorrect_prompts)
self.incorrect_prompts = [
mitogen.core.to_text(p)
for p in incorrect_prompts
]
class Connection(mitogen.parent.Connection):
options_class = Options
stream_protocol_class = SetupBootstrapProtocol
# TODO: BSD su cannot handle stdin being a socketpair, but it does let the
# child inherit fds from the parent. So we can still pass a socketpair in
# for hybrid_tty_create_child(), there just needs to be either a shell
# snippet or bootstrap support for fixing things up afterwards.
create_child = staticmethod(mitogen.parent.tty_create_child)
child_is_immediate_subprocess = False
def _get_name(self):
return u'su.' + mitogen.core.to_text(self.username)
return u'su.' + self.options.username
def stream_factory(self):
stream = super(Connection, self).stream_factory()
stream.protocol.setup_patterns(self)
return stream
def get_boot_command(self):
argv = mitogen.parent.Argv(super(Stream, self).get_boot_command())
return [self.su_path, self.username, '-c', str(argv)]
password_incorrect_msg = 'su password is incorrect'
password_required_msg = 'su password is required'
def _connect_input_loop(self, it):
password_sent = False
for buf in it:
LOG.debug('%r: received %r', self, buf)
if buf.endswith(self.EC0_MARKER):
self._ec0_received()
return
if any(s in buf.lower() for s in self.incorrect_prompts):
if password_sent:
raise PasswordError(self.password_incorrect_msg)
elif self.password_prompt in buf.lower():
if self.password is None:
raise PasswordError(self.password_required_msg)
if password_sent:
raise PasswordError(self.password_incorrect_msg)
LOG.debug('sending password')
self.transmit_side.write(
mitogen.core.to_text(self.password + '\n').encode('utf-8')
)
password_sent = True
raise mitogen.core.StreamError('bootstrap failed')
def _connect_bootstrap(self):
it = mitogen.parent.iter_read(
fds=[self.receive_side.fd],
deadline=self.connect_deadline,
)
try:
self._connect_input_loop(it)
finally:
it.close()
argv = mitogen.parent.Argv(super(Connection, self).get_boot_command())
return [self.options.su_path, self.options.username, '-c', str(argv)]

@ -35,11 +35,13 @@ import re
import mitogen.core
import mitogen.parent
from mitogen.core import b
LOG = logging.getLogger(__name__)
password_incorrect_msg = 'sudo password is incorrect'
password_required_msg = 'sudo password is required'
# These are base64-encoded UTF-8 as our existing minifier/module server
# struggles with Unicode Python source in some (forgotten) circumstances.
PASSWORD_PROMPTS = [
@ -99,14 +101,13 @@ PASSWORD_PROMPTS = [
PASSWORD_PROMPT_RE = re.compile(
u'|'.join(
base64.b64decode(s).decode('utf-8')
mitogen.core.b('|').join(
base64.b64decode(s)
for s in PASSWORD_PROMPTS
)
),
re.I
)
PASSWORD_PROMPT = b('password')
SUDO_OPTIONS = [
#(False, 'bool', '--askpass', '-A')
#(False, 'str', '--auth-type', '-a')
@ -181,10 +182,7 @@ def option(default, *args):
return default
class Stream(mitogen.parent.Stream):
create_child = staticmethod(mitogen.parent.hybrid_tty_create_child)
child_is_immediate_subprocess = False
class Options(mitogen.parent.Options):
sudo_path = 'sudo'
username = 'root'
password = None
@ -195,15 +193,16 @@ class Stream(mitogen.parent.Stream):
selinux_role = None
selinux_type = None
def construct(self, username=None, sudo_path=None, password=None,
preserve_env=None, set_home=None, sudo_args=None,
login=None, selinux_role=None, selinux_type=None, **kwargs):
super(Stream, self).construct(**kwargs)
def __init__(self, username=None, sudo_path=None, password=None,
preserve_env=None, set_home=None, sudo_args=None,
login=None, selinux_role=None, selinux_type=None, **kwargs):
super(Options, self).__init__(**kwargs)
opts = parse_sudo_flags(sudo_args or [])
self.username = option(self.username, username, opts.user)
self.sudo_path = option(self.sudo_path, sudo_path)
self.password = password or None
if password:
self.password = mitogen.core.to_text(password)
self.preserve_env = option(self.preserve_env,
preserve_env, opts.preserve_env)
self.set_home = option(self.set_home, set_home, opts.set_home)
@ -211,67 +210,62 @@ class Stream(mitogen.parent.Stream):
self.selinux_role = option(self.selinux_role, selinux_role, opts.role)
self.selinux_type = option(self.selinux_type, selinux_type, opts.type)
class SetupProtocol(mitogen.parent.RegexProtocol):
password_sent = False
def _on_password_prompt(self, line, match):
LOG.debug('%s: (password prompt): %s',
self.stream.name, line.decode('utf-8', 'replace'))
if self.stream.conn.options.password is None:
self.stream.conn._fail_connection(
PasswordError(password_required_msg)
)
return
if self.password_sent:
self.stream.conn._fail_connection(
PasswordError(password_incorrect_msg)
)
return
self.stream.transmit_side.write(
(self.stream.conn.options.password + '\n').encode('utf-8')
)
self.password_sent = True
PARTIAL_PATTERNS = [
(PASSWORD_PROMPT_RE, _on_password_prompt),
]
class Connection(mitogen.parent.Connection):
diag_protocol_class = SetupProtocol
options_class = Options
create_child = staticmethod(mitogen.parent.hybrid_tty_create_child)
create_child_args = {
'escalates_privilege': True,
}
child_is_immediate_subprocess = False
def _get_name(self):
return u'sudo.' + mitogen.core.to_text(self.username)
return u'sudo.' + mitogen.core.to_text(self.options.username)
def get_boot_command(self):
# Note: sudo did not introduce long-format option processing until July
# 2013, so even though we parse long-format options, supply short-form
# to the sudo command.
bits = [self.sudo_path, '-u', self.username]
if self.preserve_env:
bits = [self.options.sudo_path, '-u', self.options.username]
if self.options.preserve_env:
bits += ['-E']
if self.set_home:
if self.options.set_home:
bits += ['-H']
if self.login:
if self.options.login:
bits += ['-i']
if self.selinux_role:
bits += ['-r', self.selinux_role]
if self.selinux_type:
bits += ['-t', self.selinux_type]
bits = bits + ['--'] + super(Stream, self).get_boot_command()
LOG.debug('sudo command line: %r', bits)
return bits
password_incorrect_msg = 'sudo password is incorrect'
password_required_msg = 'sudo password is required'
def _connect_input_loop(self, it):
password_sent = False
for buf in it:
LOG.debug('%s: received %r', self.name, buf)
if buf.endswith(self.EC0_MARKER):
self._ec0_received()
return
match = PASSWORD_PROMPT_RE.search(buf.decode('utf-8').lower())
if match is not None:
LOG.debug('%s: matched password prompt %r',
self.name, match.group(0))
if self.password is None:
raise PasswordError(self.password_required_msg)
if password_sent:
raise PasswordError(self.password_incorrect_msg)
self.diag_stream.transmit_side.write(
(mitogen.core.to_text(self.password) + '\n').encode('utf-8')
)
password_sent = True
raise mitogen.core.StreamError('bootstrap failed')
def _connect_bootstrap(self):
fds = [self.receive_side.fd]
if self.diag_stream is not None:
fds.append(self.diag_stream.receive_side.fd)
it = mitogen.parent.iter_read(
fds=fds,
deadline=self.connect_deadline,
)
if self.options.selinux_role:
bits += ['-r', self.options.selinux_role]
if self.options.selinux_type:
bits += ['-t', self.options.selinux_type]
try:
self._connect_input_loop(it)
finally:
it.close()
return bits + ['--'] + super(Connection, self).get_boot_command()

@ -36,6 +36,7 @@ have the same privilege (auth_id) as the current process.
"""
import errno
import logging
import os
import socket
import struct
@ -45,7 +46,24 @@ import tempfile
import mitogen.core
import mitogen.master
from mitogen.core import LOG
LOG = logging.getLogger(__name__)
class Error(mitogen.core.Error):
"""
Base for errors raised by :mod:`mitogen.unix`.
"""
pass
class ConnectError(Error):
"""
Raised when :func:`mitogen.unix.connect` fails to connect to the listening
socket.
"""
#: UNIX error number reported by underlying exception.
errno = None
def is_path_dead(path):
@ -65,9 +83,38 @@ def make_socket_path():
return tempfile.mktemp(prefix='mitogen_unix_', suffix='.sock')
class Listener(mitogen.core.BasicStream):
class ListenerStream(mitogen.core.Stream):
def on_receive(self, broker):
sock, _ = self.receive_side.fp.accept()
try:
self.protocol.on_accept_client(sock)
except:
sock.close()
raise
class Listener(mitogen.core.Protocol):
stream_class = ListenerStream
keep_alive = True
@classmethod
def build_stream(cls, router, path=None, backlog=100):
if not path:
path = make_socket_path()
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
if os.path.exists(path) and is_path_dead(path):
LOG.debug('%r: deleting stale %r', cls.__name__, path)
os.unlink(path)
sock.bind(path)
os.chmod(path, int('0600', 8))
sock.listen(backlog)
stream = super(Listener, cls).build_stream(router, path)
stream.accept(sock, sock)
router.broker.start_receive(stream)
return stream
def __repr__(self):
return '%s.%s(%r)' % (
__name__,
@ -75,20 +122,9 @@ class Listener(mitogen.core.BasicStream):
self.path,
)
def __init__(self, router, path=None, backlog=100):
def __init__(self, router, path):
self._router = router
self.path = path or make_socket_path()
self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
if os.path.exists(self.path) and is_path_dead(self.path):
LOG.debug('%r: deleting stale %r', self, self.path)
os.unlink(self.path)
self._sock.bind(self.path)
os.chmod(self.path, int('0600', 8))
self._sock.listen(backlog)
self.receive_side = mitogen.core.Side(self, self._sock.fileno())
router.broker.start_receive(self)
self.path = path
def _unlink_socket(self):
try:
@ -100,69 +136,91 @@ class Listener(mitogen.core.BasicStream):
raise
def on_shutdown(self, broker):
broker.stop_receive(self)
broker.stop_receive(self.stream)
self._unlink_socket()
self._sock.close()
self.receive_side.closed = True
self.stream.receive_side.close()
def _accept_client(self, sock):
def on_accept_client(self, sock):
sock.setblocking(True)
try:
pid, = struct.unpack('>L', sock.recv(4))
except (struct.error, socket.error):
LOG.error('%r: failed to read remote identity: %s',
self, sys.exc_info()[1])
LOG.error('listener: failed to read remote identity: %s',
sys.exc_info()[1])
return
context_id = self._router.id_allocator.allocate()
context = mitogen.parent.Context(self._router, context_id)
stream = mitogen.core.Stream(self._router, context_id)
stream.name = u'unix_client.%d' % (pid,)
stream.auth_id = mitogen.context_id
stream.is_privileged = True
try:
sock.send(struct.pack('>LLL', context_id, mitogen.context_id,
os.getpid()))
except socket.error:
LOG.error('%r: failed to assign identity to PID %d: %s',
self, pid, sys.exc_info()[1])
LOG.error('listener: failed to assign identity to PID %d: %s',
pid, sys.exc_info()[1])
return
LOG.debug('%r: accepted %r', self, stream)
stream.accept(sock.fileno(), sock.fileno())
context = mitogen.parent.Context(self._router, context_id)
stream = mitogen.core.MitogenProtocol.build_stream(
router=self._router,
remote_id=context_id,
auth_id=mitogen.context_id,
)
stream.name = u'unix_client.%d' % (pid,)
stream.accept(sock, sock)
LOG.debug('listener: accepted connection from PID %d: %s',
pid, stream.name)
self._router.register(context, stream)
def on_receive(self, broker):
sock, _ = self._sock.accept()
try:
self._accept_client(sock)
finally:
sock.close()
def _connect(path, broker, sock):
try:
# ENOENT, ECONNREFUSED
sock.connect(path)
# ECONNRESET
sock.send(struct.pack('>L', os.getpid()))
mitogen.context_id, remote_id, pid = struct.unpack('>LLL', sock.recv(12))
except socket.error:
e = sys.exc_info()[1]
ce = ConnectError('could not connect to %s: %s', path, e.args[1])
ce.errno = e.args[0]
raise ce
def connect(path, broker=None):
LOG.debug('unix.connect(path=%r)', path)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(path)
sock.send(struct.pack('>L', os.getpid()))
mitogen.context_id, remote_id, pid = struct.unpack('>LLL', sock.recv(12))
mitogen.parent_id = remote_id
mitogen.parent_ids = [remote_id]
LOG.debug('unix.connect(): local ID is %r, remote is %r',
LOG.debug('client: local ID is %r, remote is %r',
mitogen.context_id, remote_id)
router = mitogen.master.Router(broker=broker)
stream = mitogen.core.Stream(router, remote_id)
stream.accept(sock.fileno(), sock.fileno())
stream = mitogen.core.MitogenProtocol.build_stream(router, remote_id)
stream.accept(sock, sock)
stream.name = u'unix_listener.%d' % (pid,)
mitogen.core.listen(stream, 'disconnect', _cleanup)
mitogen.core.listen(router.broker, 'shutdown',
lambda: router.disconnect_stream(stream))
context = mitogen.parent.Context(router, remote_id)
router.register(context, stream)
return router, context
mitogen.core.listen(router.broker, 'shutdown',
lambda: router.disconnect_stream(stream))
sock.close()
return router, context
def connect(path, broker=None):
LOG.debug('client: connecting to %s', path)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
return _connect(path, broker, sock)
except:
sock.close()
raise
def _cleanup():
"""
Reset mitogen.context_id and friends when our connection to the parent is
lost. Per comments on #91, these globals need to move to the Router so
fix-ups like this become unnecessary.
"""
mitogen.context_id = 0
mitogen.parent_id = None
mitogen.parent_ids = []

@ -39,7 +39,6 @@ import mitogen.master
import mitogen.parent
LOG = logging.getLogger('mitogen')
iteritems = getattr(dict, 'iteritems', dict.items)
if mitogen.core.PY3:

@ -19,15 +19,19 @@ import mitogen.sudo
router = mitogen.master.Router()
context = mitogen.parent.Context(router, 0)
stream = mitogen.ssh.Stream(router, 0, max_message_size=0, hostname='foo')
options = mitogen.ssh.Options(max_message_size=0, hostname='foo')
conn = mitogen.ssh.Connection(options, router)
conn.context = context
print('SSH command size: %s' % (len(' '.join(stream.get_boot_command())),))
print('Preamble size: %s (%.2fKiB)' % (
len(stream.get_preamble()),
len(stream.get_preamble()) / 1024.0,
print('SSH command size: %s' % (len(' '.join(conn.get_boot_command())),))
print('Bootstrap (mitogen.core) size: %s (%.2fKiB)' % (
len(conn.get_preamble()),
len(conn.get_preamble()) / 1024.0,
))
print('')
if '--dump' in sys.argv:
print(zlib.decompress(stream.get_preamble()))
print(zlib.decompress(conn.get_preamble()))
exit()
@ -55,7 +59,7 @@ for mod in (
original_size = len(original)
minimized = mitogen.minify.minimize_source(original)
minimized_size = len(minimized)
compressed = zlib.compress(minimized, 9)
compressed = zlib.compress(minimized.encode(), 9)
compressed_size = len(compressed)
print(
'%-25s'

@ -0,0 +1,4 @@
# show process affinities for running ansible-playbook
who="$1"
[ ! "$who" ] && who=ansible-playbook
for i in $(pgrep -f "$who") ; do taskset -c -p $i ; done|cut -d: -f2|sort -n |uniq -c

@ -0,0 +1,39 @@
#
# Bash helpers for debugging.
#
# Tell Ansible to write PID files for the mux and top-level process to CWD.
export MITOGEN_SAVE_PIDS=1
# strace -ff -p $(muxpid)
muxpid() {
cat .ansible-mux.pid
}
# gdb -p $(anspid)
anspid() {
cat .ansible-controller.pid
}
# perf top -git $(muxtids)
# perf top -git $(muxtids)
muxtids() {
ls /proc/$(muxpid)/task | tr \\n ,
}
# perf top -git $(anstids)
anstids() {
ls /proc/$(anspid)/task | tr \\n ,
}
# ttrace $(muxpid) [.. options ..]
# strace only threads of PID, not children
ttrace() {
local pid=$1; shift;
local s=""
for i in $(ls /proc/$pid/task) ; do
s="-p $i $s"
done
strace $s "$@"
}

@ -0,0 +1,47 @@
# coding=UTF-8
# Generate the fragment used to make email release announcements
# usage: release-notes.py 0.2.6
import sys
import urllib
import lxml.html
import subprocess
response = urllib.urlopen('https://mitogen.networkgenomics.com/changelog.html')
tree = lxml.html.parse(response)
prefix = 'v' + sys.argv[1].replace('.', '-')
for elem in tree.getroot().cssselect('div.section[id]'):
if elem.attrib['id'].startswith(prefix):
break
else:
print('cant find')
for child in tree.getroot().cssselect('body > *'):
child.getparent().remove(child)
body, = tree.getroot().cssselect('body')
body.append(elem)
proc = subprocess.Popen(
args=['w3m', '-T', 'text/html', '-dump', '-cols', '72'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
stdout, _ = proc.communicate(input=(lxml.html.tostring(tree)))
stdout = stdout.decode('UTF-8')
stdout = stdout.translate({
ord(u''): None,
ord(u''): ord(u'*'),
ord(u''): ord(u"'"),
ord(u''): ord(u'"'),
ord(u''): ord(u'"'),
})
print(stdout)

@ -13,6 +13,9 @@ retry_files_enabled = False
display_args_to_stdout = True
forks = 100
# We use lots of deprecated functionality to support older versions.
deprecation_warnings = False
# issue #434; hosts/delegate_to; integration/delegate_to
remote_user = ansible-cfg-remote-user

@ -0,0 +1,4 @@
- hosts: test-targets
tasks:
- include_tasks: _includes.yml
with_sequence: start=1 end=1000

@ -0,0 +1,2 @@
terraform.tfstate*
.terraform

@ -0,0 +1,3 @@
default:
terraform fmt

@ -1,19 +1,89 @@
- hosts: controller
vars:
git_username: '{{ lookup("pipe", "git config --global user.name") }}'
git_email: '{{ lookup("pipe", "git config --global user.email") }}'
- hosts: all
become: true
tasks:
- apt: name={{item}} state=installed
with_items:
- openvpn
- tcpdump
- python-pip
- python-virtualenv
- strace
- libldap2-dev
- linux-perf
- libsasl2-dev
- build-essential
- git
- rsync
- file:
path: /etc/openvpn
state: directory
- copy:
dest: /etc/openvpn/secret
mode: '0600'
content: |
-----BEGIN OpenVPN Static key V1-----
f94005e4206828e281eb397aefd69b37
ebe6cd39057d5641c5d8dd539cd07651
557d94d0077852bd8f92b68bef927169
c5f0e42ac962a2cbbed35e107ffa0e71
1a2607c6bcd919ec5846917b20eb6684
c7505152815d6ed7b4420714777a3d4a
8edb27ca81971cba7a1e88fe3936e13b
85e9be6706a30cd1334836ed0f08e899
78942329a330392dff42e4570731ac24
9330358aaa6828c07ecb41fb9c498a89
1e0435c5a45bfed390cd2104073634ef
b00f9fae1d3c49ef5de51854103edac9
5ff39c9dfc66ae270510b2ffa74d87d2
9d4b3844b1e1473237bc6dc78fb03e2e
643ce58e667a532efceec7177367fb37
a16379a51e0a8c8e3ec00a59952b79d4
-----END OpenVPN Static key V1-----
- copy:
dest: /etc/openvpn/k3.conf
content: |
remote k3.botanicus.net
dev tun
ifconfig 10.18.0.1 10.18.0.2
secret secret
- shell: systemctl enable openvpn@k3.service
- shell: systemctl start openvpn@k3.service
- lineinfile:
line: "{{item}}"
path: /etc/sysctl.conf
register: sysctl_conf
become: true
with_items:
- "net.ipv4.ip_forward=1"
- "kernel.perf_event_paranoid=-1"
- shell: /sbin/sysctl -p
when: sysctl_conf.changed
- copy:
dest: /etc/rc.local
mode: "0744"
content: |
#!/bin/bash
iptables -t nat -F;
iptables -t nat -X;
iptables -t nat -A POSTROUTING -j MASQUERADE;
- shell: systemctl daemon-reload
- shell: systemctl enable rc-local
- shell: systemctl start rc-local
- hosts: all
vars:
git_username: '{{ lookup("pipe", "git config --global user.name") }}'
git_email: '{{ lookup("pipe", "git config --global user.email") }}'
tasks:
- copy:
src: ~/.ssh/id_gitlab
dest: ~/.ssh/id_gitlab
@ -23,38 +93,6 @@
dest: ~/.ssh/config
src: ssh_config.j2
- lineinfile:
line: "{{item}}"
path: /etc/sysctl.conf
become: true
with_items:
- net.ipv4.ip_forward=1
- kernel.perf_event_paranoid=-1
register: sysctl_conf
- shell: /sbin/sysctl -p
when: sysctl_conf.changed
become: true
- shell: |
iptables -t nat -F;
iptables -t nat -X;
iptables -t nat -A POSTROUTING -j MASQUERADE;
become: true
- apt: name={{item}} state=installed
become: true
with_items:
- python-pip
- python-virtualenv
- strace
- libldap2-dev
- linux-perf
- libsasl2-dev
- build-essential
- git
- rsync
- shell: "rsync -a ~/.ssh {{inventory_hostname}}:"
connection: local
@ -119,4 +157,3 @@
path: ~/prj/ansible/inventory/gcloud.py
state: link
src: ~/mitogen/tests/ansible/lib/inventory/gcloud.py

@ -1,11 +0,0 @@
- hosts: localhost
tasks:
- command: date +%Y%m%d-%H%M%S
register: out
- set_fact:
instance_name: "controller-{{out.stdout}}"
- command: >
gcloud compute instances create {{instance_name}} --can-ip-forward --machine-type=n1-standard-8 --preemptible --scopes=compute-ro --image-project=debian-cloud --image-family=debian-9

@ -0,0 +1,149 @@
variable "node-count" {
default = 0
}
variable "preemptible" {
default = true
}
variable "big" {
default = false
}
provider "google" {
project = "mitogen-load-testing"
region = "europe-west1"
zone = "europe-west1-d"
}
resource "google_compute_instance" "controller" {
name = "ansible-controller"
machine_type = "${var.big ? "n1-highcpu-32" : "custom-1-1024"}"
allow_stopping_for_update = true
can_ip_forward = true
boot_disk {
initialize_params {
image = "debian-cloud/debian-9"
}
}
scheduling {
preemptible = true
automatic_restart = false
}
network_interface {
subnetwork = "${google_compute_subnetwork.loadtest-subnet.self_link}"
access_config = {}
}
provisioner "local-exec" {
command = <<-EOF
ip=${google_compute_instance.controller.network_interface.0.access_config.0.nat_ip};
ssh-keygen -R $ip;
ssh-keyscan $ip >> ~/.ssh/known_hosts;
sed -ri -e "s/.*CONTROLLER_IP_HERE.*/ Hostname $ip/" ~/.ssh/config;
ansible-playbook -i $ip, controller.yml
EOF
}
}
resource "google_compute_network" "loadtest" {
name = "loadtest"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "loadtest-subnet" {
name = "loadtest-subnet"
ip_cidr_range = "10.19.0.0/16"
network = "${google_compute_network.loadtest.id}"
}
resource "google_compute_firewall" "allow-all-in" {
name = "allow-all-in"
network = "${google_compute_network.loadtest.name}"
direction = "INGRESS"
allow {
protocol = "all"
}
}
resource "google_compute_firewall" "allow-all-out" {
name = "allow-all-out"
network = "${google_compute_network.loadtest.name}"
direction = "EGRESS"
allow {
protocol = "all"
}
}
resource "google_compute_route" "route-nodes-via-controller" {
name = "route-nodes-via-controller"
dest_range = "0.0.0.0/0"
network = "${google_compute_network.loadtest.name}"
next_hop_instance = "${google_compute_instance.controller.self_link}"
next_hop_instance_zone = "${google_compute_instance.controller.zone}"
priority = 800
tags = ["node"]
}
resource "google_compute_instance_template" "node" {
name = "node"
tags = ["node"]
machine_type = "custom-1-1024"
scheduling {
preemptible = "${var.preemptible}"
automatic_restart = false
}
disk {
source_image = "debian-cloud/debian-9"
auto_delete = true
boot = true
}
network_interface {
subnetwork = "${google_compute_subnetwork.loadtest-subnet.self_link}"
}
}
#
# Compute Engine tops out at 1000 VMs per group
#
resource "google_compute_instance_group_manager" "nodes-a" {
name = "nodes-a"
base_instance_name = "node"
instance_template = "${google_compute_instance_template.node.self_link}"
target_size = "${var.node-count / 4}"
}
resource "google_compute_instance_group_manager" "nodes-b" {
name = "nodes-b"
base_instance_name = "node"
instance_template = "${google_compute_instance_template.node.self_link}"
target_size = "${var.node-count / 4}"
}
resource "google_compute_instance_group_manager" "nodes-c" {
name = "nodes-c"
base_instance_name = "node"
instance_template = "${google_compute_instance_template.node.self_link}"
target_size = "${var.node-count / 4}"
}
resource "google_compute_instance_group_manager" "nodes-d" {
name = "nodes-d"
base_instance_name = "node"
instance_template = "${google_compute_instance_template.node.self_link}"
target_size = "${var.node-count / 4}"
}

@ -7,6 +7,10 @@
ansible_user: mitogen__has_sudo_pubkey
ansible_become_pass: has_sudo_pubkey_password
ansible_ssh_private_key_file: /tmp/synchronize-action-key
# https://github.com/ansible/ansible/issues/56629
ansible_ssh_pass: ''
ansible_password: ''
tasks:
# must copy git file to set proper file mode.
- copy:

@ -9,9 +9,10 @@
# Verify output of a single async job.
- name: start 2 second op
# Sleep after writing; see https://github.com/ansible/ansible/issues/51393
shell: |
echo alldone;
sleep 1;
echo alldone
async: 1000
poll: 0
register: job1
@ -37,7 +38,12 @@
- result1.ansible_job_id == job1.ansible_job_id
- result1.attempts <= 100000
- result1.changed == True
- result1.cmd == "sleep 1;\n echo alldone"
# ansible/b72e989e1837ccad8dcdc926c43ccbc4d8cdfe44
- |
(ansible_version.full >= '2.8' and
result1.cmd == "echo alldone;\nsleep 1;\n") or
(ansible_version.full < '2.8' and
result1.cmd == "echo alldone;\n sleep 1;")
- result1.delta|length == 14
- result1.start|length == 26
- result1.finished == 1

@ -20,5 +20,6 @@
- job1.failed == True
- |
job1.msg == "async task did not complete within the requested time" or
job1.msg == "async task did not complete within the requested time - 1s" or
job1.msg == "Job reached maximum time limit of 1 seconds."

@ -17,5 +17,6 @@
- out.failed
- |
('sudo: no such option: --derps' in out.msg) or
("sudo: invalid option -- '-'" in out.module_stderr) or
("sudo: unrecognized option `--derps'" in out.module_stderr) or
("sudo: unrecognized option '--derps'" in out.module_stderr)

@ -16,6 +16,7 @@
that: |
out.failed and (
('password is required' in out.msg) or
('Missing sudo password' in out.msg) or
('password is required' in out.module_stderr)
)

@ -37,6 +37,8 @@
'hostname': 'alias-host',
'identities_only': False,
'identity_file': null,
'keepalive_interval': 30,
'keepalive_count': 10,
'password': null,
'port': null,
'python_path': ["/usr/bin/python"],
@ -65,6 +67,8 @@
'hostname': 'cd-normal-alias',
'identities_only': False,
'identity_file': null,
'keepalive_interval': 30,
'keepalive_count': 10,
'password': null,
'port': null,
'python_path': ["/usr/bin/python"],

@ -27,7 +27,7 @@
'remote_name': null,
'password': null,
'username': 'root',
'sudo_path': null,
'sudo_path': 'sudo',
'sudo_args': ['-H', '-S', '-n'],
},
'method': 'sudo',

@ -71,6 +71,8 @@
'hostname': 'alias-host',
'identities_only': False,
'identity_file': null,
'keepalive_interval': 30,
'keepalive_count': 10,
'password': null,
'port': null,
"python_path": ["/usr/bin/python"],
@ -112,6 +114,8 @@
'hostname': 'alias-host',
'identities_only': False,
'identity_file': null,
'keepalive_interval': 30,
'keepalive_count': 10,
'password': null,
'port': null,
"python_path": ["/usr/bin/python"],
@ -164,6 +168,8 @@
'hostname': 'cd-normal-normal',
'identities_only': False,
'identity_file': null,
'keepalive_interval': 30,
'keepalive_count': 10,
'password': null,
'port': null,
"python_path": ["/usr/bin/python"],
@ -205,6 +211,8 @@
'hostname': 'alias-host',
'identities_only': False,
'identity_file': null,
'keepalive_interval': 30,
'keepalive_count': 10,
'password': null,
'port': null,
"python_path": ["/usr/bin/python"],
@ -233,6 +241,8 @@
'hostname': 'cd-normal-alias',
'identities_only': False,
'identity_file': null,
'keepalive_interval': 30,
'keepalive_count': 10,
'password': null,
'port': null,
"python_path": ["/usr/bin/python"],
@ -285,6 +295,8 @@
'hostname': 'cd-newuser-normal-normal',
'identities_only': False,
'identity_file': null,
'keepalive_interval': 30,
'keepalive_count': 10,
'password': null,
'port': null,
"python_path": ["/usr/bin/python"],
@ -327,6 +339,8 @@
'hostname': 'alias-host',
'identities_only': False,
'identity_file': null,
'keepalive_interval': 30,
'keepalive_count': 10,
'password': null,
'port': null,
"python_path": ["/usr/bin/python"],

@ -24,7 +24,7 @@
- mitogen_action_script:
script: |
self._connection._connect()
result['dump'] = self._connection.parent.call_service(
result['dump'] = self._connection.get_binding().get_service_context().call_service(
service_name='ansible_mitogen.services.ContextService',
method_name='dump'
)
@ -39,7 +39,7 @@
- mitogen_action_script:
script: |
self._connection._connect()
result['dump'] = self._connection.parent.call_service(
result['dump'] = self._connection.get_binding().get_service_context().call_service(
service_name='ansible_mitogen.services.ContextService',
method_name='dump'
)

@ -7,6 +7,10 @@
- meta: end_play
when: not is_mitogen
# Too much hassle to make this work for OSX
- meta: end_play
when: ansible_system != 'Linux'
- shell: 'cat /proc/$PPID/cmdline | tr \\0 \\n'
register: out
- debug: var=out

@ -12,7 +12,14 @@
that:
- not out.changed
- out.rc == 1
- out.msg == "MODULE FAILURE"
# ansible/62d8c8fde6a76d9c567ded381e9b34dad69afcd6
- |
(ansible_version.full < '2.7' and out.msg == "MODULE FAILURE") or
(ansible_version.full >= '2.7' and
out.msg == (
"MODULE FAILURE\n" +
"See stdout/stderr for the exact error"
))
- out.module_stdout == ""
- "'Traceback (most recent call last)' in out.module_stderr"
- "\"NameError: name 'kaboom' is not defined\" in out.module_stderr"

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

Loading…
Cancel
Save