diff --git a/.ci/ansible_tests.py b/.ci/ansible_tests.py
index 98e45ab8..4df2dc70 100755
--- a/.ci/ansible_tests.py
+++ b/.ci/ansible_tests.py
@@ -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()
diff --git a/.ci/azure-pipelines-steps.yml b/.ci/azure-pipelines-steps.yml
index a377d795..e880eded 100644
--- a/.ci/azure-pipelines-steps.yml
+++ b/.ci/azure-pipelines-steps.yml
@@ -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"
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index dc5f7162..920e82a1 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -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
diff --git a/.ci/ci_lib.py b/.ci/ci_lib.py
index e1cb84d5..dc7a02a8 100644
--- a/.ci/ci_lib.py
+++ b/.ci/ci_lib.py
@@ -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,))
diff --git a/.ci/debops_common_tests.py b/.ci/debops_common_tests.py
index b0e2e4e8..e8f2907b 100755
--- a/.ci/debops_common_tests.py
+++ b/.ci/debops_common_tests.py
@@ -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)
diff --git a/.ci/localhost_ansible_install.py b/.ci/localhost_ansible_install.py
new file mode 100755
index 00000000..0cb47374
--- /dev/null
+++ b/.ci/localhost_ansible_install.py
@@ -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)
diff --git a/.ci/localhost_ansible_tests.py b/.ci/localhost_ansible_tests.py
new file mode 100755
index 00000000..f7e1ecbd
--- /dev/null
+++ b/.ci/localhost_ansible_tests.py
@@ -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:]))
diff --git a/.ci/mitogen_install.py b/.ci/mitogen_install.py
index 72bc75e3..b8862f89 100755
--- a/.ci/mitogen_install.py
+++ b/.ci/mitogen_install.py
@@ -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)
diff --git a/.ci/mitogen_tests.py b/.ci/mitogen_tests.py
index 36928ac9..4de94b4c 100755
--- a/.ci/mitogen_tests.py
+++ b/.ci/mitogen_tests.py
@@ -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)
diff --git a/.ci/prep_azure.py b/.ci/prep_azure.py
index 5199a87e..344564e8 100755
--- a/.ci/prep_azure.py
+++ b/.ci/prep_azure.py
@@ -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),)]
diff --git a/.ci/spawn_reverse_shell.py b/.ci/spawn_reverse_shell.py
new file mode 100755
index 00000000..8a6b9500
--- /dev/null
+++ b/.ci/spawn_reverse_shell.py
@@ -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
+
diff --git a/.gitignore b/.gitignore
index e244ca12..aa75f691 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,11 @@ venvs/**
MANIFEST
build/
dist/
+extra/
+tests/ansible/.*.pid
docs/_build/
htmlcov/
*.egg-info
__pycache__/
+extra
+**/.*.pid
diff --git a/.lgtm.yml b/.lgtm.yml
index 3e45b21e..a8e91c02 100644
--- a/.lgtm.yml
+++ b/.lgtm.yml
@@ -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
diff --git a/.travis.yml b/.travis.yml
index 921ad12b..580ced0b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -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
diff --git a/README.md b/README.md
index 5ef2447f..da93a80b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
# Mitogen
-Please see the documentation.
+Please see the documentation.

diff --git a/ansible_mitogen/affinity.py b/ansible_mitogen/affinity.py
index 09a6acee..67e16d8a 100644
--- a/ansible_mitogen/affinity.py
+++ b/ansible_mitogen/affinity.py
@@ -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()
diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py
index b5f28d34..2dd3bfa9 100644
--- a/ansible_mitogen/connection.py
+++ b/ansible_mitogen/connection.py
@@ -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
)
diff --git a/ansible_mitogen/loaders.py b/ansible_mitogen/loaders.py
index ff06c0c5..99294c1f 100644
--- a/ansible_mitogen/loaders.py
+++ b/ansible_mitogen/loaders.py
@@ -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
diff --git a/ansible_mitogen/mixins.py b/ansible_mitogen/mixins.py
index 890467fd..eee1ecd7 100644
--- a/ansible_mitogen/mixins.py
+++ b/ansible_mitogen/mixins.py
@@ -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
diff --git a/ansible_mitogen/module_finder.py b/ansible_mitogen/module_finder.py
index 633e3cad..89aa2beb 100644
--- a/ansible_mitogen/module_finder.py
+++ b/ansible_mitogen/module_finder.py
@@ -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:
diff --git a/ansible_mitogen/parsing.py b/ansible_mitogen/parsing.py
index 525e60cf..27fca7cd 100644
--- a/ansible_mitogen/parsing.py
+++ b/ansible_mitogen/parsing.py
@@ -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
diff --git a/ansible_mitogen/planner.py b/ansible_mitogen/planner.py
index 2eebd36d..96b06995 100644
--- a/ansible_mitogen/planner.py
+++ b/ansible_mitogen/planner.py
@@ -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(),
diff --git a/ansible_mitogen/plugins/action/mitogen_fetch.py b/ansible_mitogen/plugins/action/mitogen_fetch.py
new file mode 100644
index 00000000..1844efd8
--- /dev/null
+++ b/ansible_mitogen/plugins/action/mitogen_fetch.py
@@ -0,0 +1,162 @@
+# (c) 2012-2014, Michael DeHaan
+#
+# 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 .
+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
diff --git a/ansible_mitogen/plugins/action/mitogen_get_stack.py b/ansible_mitogen/plugins/action/mitogen_get_stack.py
index 12afbfba..171f84ea 100644
--- a/ansible_mitogen/plugins/action/mitogen_get_stack.py
+++ b/ansible_mitogen/plugins/action/mitogen_get_stack.py
@@ -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,
}
diff --git a/ansible_mitogen/plugins/connection/mitogen_buildah.py b/ansible_mitogen/plugins/connection/mitogen_buildah.py
new file mode 100644
index 00000000..017214b2
--- /dev/null
+++ b/ansible_mitogen/plugins/connection/mitogen_buildah.py
@@ -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'
diff --git a/ansible_mitogen/plugins/connection/mitogen_local.py b/ansible_mitogen/plugins/connection/mitogen_local.py
index 24b84a03..a98c834c 100644
--- a/ansible_mitogen/plugins/connection/mitogen_local.py
+++ b/ansible_mitogen/plugins/connection/mitogen_local.py
@@ -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,
)
diff --git a/ansible_mitogen/process.py b/ansible_mitogen/process.py
index e4e61e8b..1fc7bf80 100644
--- a/ansible_mitogen/process.py
+++ b/ansible_mitogen/process.py
@@ -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()
diff --git a/ansible_mitogen/runner.py b/ansible_mitogen/runner.py
index 30c36be7..5cf171b6 100644
--- a/ansible_mitogen/runner.py
+++ b/ansible_mitogen/runner.py
@@ -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()
diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py
index a7c0e46f..e6c41e5b 100644
--- a/ansible_mitogen/services.py
+++ b/ansible_mitogen/services.py
@@ -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.
diff --git a/ansible_mitogen/strategy.py b/ansible_mitogen/strategy.py
index b9211fcc..8f093999 100644
--- a/ansible_mitogen/strategy.py
+++ b/ansible_mitogen/strategy.py
@@ -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)
diff --git a/ansible_mitogen/transport_config.py b/ansible_mitogen/transport_config.py
index ad1cab3e..aa4a16d0 100644
--- a/ansible_mitogen/transport_config.py
+++ b/ansible_mitogen/transport_config.py
@@ -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')
+ )
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
index 297ab9ef..f5fe42b0 100644
--- a/docs/_templates/layout.html
+++ b/docs/_templates/layout.html
@@ -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 %}
+
+
+{% endblock %}
+
{% block footer %}
{{ super() }}
-
+
+
{% endblock %}
diff --git a/docs/ansible_detailed.rst b/docs/ansible_detailed.rst
index 5b541a14..22d7223f 100644
--- a/docs/ansible_detailed.rst
+++ b/docs/ansible_detailed.rst
@@ -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
-
+
+ Thanks!
+
+
+
+
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
+ `_
+ and `become plugins
+ `_ are not yet
+ supported.
+
* The ``doas``, ``su`` and ``sudo`` become methods are available. File bugs to
register interest in more.
-* The `docker `_,
- `jail `_,
- `kubectl `_,
- `local `_,
- `lxc `_,
- `lxd `_,
- and `ssh `_
- built-in connection types are supported, along with Mitogen-specific
- :ref:`machinectl `, :ref:`mitogen_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 `, :ref:`mitogen_doas `,
:ref:`mitogen_su `, :ref:`mitogen_sudo `, and :ref:`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
- `_ 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 `_,
-`copy `_,
-`patch `_,
-`script `_,
-`template `_, and
-`unarchive `_
-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) `_ 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) `_,
- 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) `_. 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
-`_ 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
-`_ 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
-`_ 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
-`_ 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
-`_ 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
-`_ 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 `_
-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
+
+
+
diff --git a/docs/api.rst b/docs/api.rst
index db39ad99..7ab3274e 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -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
- `.
-
- 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 `.
-
- 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
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 0b20a852..fe15ca27 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -15,12 +15,286 @@ Release Notes
-v0.2.8 (unreleased)
+v0.2.9 (unreleased)
-------------------
To avail of fixes in an unreleased version, please download a ZIP file
`directly from GitHub `_.
+*(no changes)*
+
+
+v0.2.8 (2019-08-18)
+-------------------
+
+This release includes Ansible 2.8 and SELinux support, fixes for two deadlocks,
+and major internal design overhauls in preparation for future functionality.
+
+
+Enhancements
+~~~~~~~~~~~~
+
+* :gh:issue:`556`,
+ :gh:issue:`587`: Ansible 2.8 is supported. `Become plugins
+ `_ and
+ `interpreter discovery
+ `_
+ are not yet handled.
+
+* :gh:issue:`419`, :gh:issue:`470`, file descriptor usage is approximately
+ halved, as it is no longer necessary to separately manage read and write
+ sides to work around a design problem.
+
+* :gh:issue:`419`: setup for all connections happens almost entirely on one
+ thread, reducing contention and context switching early in a run.
+
+* :gh:issue:`419`: Connection setup is better pipelined, eliminating some
+ network round-trips. Most infrastructure is in place to support future
+ removal of the final round-trips between a target booting and receiving
+ function calls.
+
+* :gh:pull:`595`: the :meth:`~mitogen.parent.Router.buildah` connection method
+ is available to manipulate `Buildah `_ containers, and
+ is exposed to Ansible as the :ans:conn:`buildah`.
+
+* :gh:issue:`615`: a modified :ans:mod:`fetch` implements streaming transfer
+ even when ``become`` is active, avoiding excess CPU usage and memory spikes,
+ and improving performance. A copy of two 512 MiB files drops from 47 seconds
+ to 7 seconds, with peak memory usage dropping from 10.7 GiB to 64.8 MiB.
+
+* `Operon `_ no longer requires a custom
+ library installation, both Ansible and Operon are supported by a single
+ Mitogen release.
+
+* The ``MITOGEN_CPU_COUNT`` variable shards the connection multiplexer into
+ per-CPU workers. This may improve throughput for large runs involving file
+ transfer, and is required for future functionality. One multiplexer starts by
+ default, to match existing behaviour.
+
+* :gh:commit:`d6faff06`, :gh:commit:`807cbef9`, :gh:commit:`e93762b3`,
+ :gh:commit:`50bfe4c7`: locking is avoided on hot paths, and some locks are
+ released before waking a thread that must immediately acquire the same lock.
+
+
+Mitogen for Ansible
+~~~~~~~~~~~~~~~~~~~
+
+* :gh:issue:`363`: fix an obscure race matching *Permission denied* errors from
+ some versions of ``su`` running on heavily loaded machines.
+
+* :gh:issue:`410`: Uses of :linux:man7:`unix` sockets are replaced with
+ traditional :linux:man7:`pipe` pairs when SELinux is detected, to work around
+ a broken heuristic in common SELinux policies that prevents inheriting
+ :linux:man7:`unix` sockets across privilege domains.
+
+* `#467 `_: an incompatibility
+ running Mitogen under `Molecule
+ `_ was resolved.
+
+* :gh:issue:`547`, :gh:issue:`598`: fix a deadlock during initialization of
+ connections, ``async`` tasks, tasks using custom :mod:`module_utils`,
+ ``mitogen_task_isolation: fork`` modules, and modules present on an internal
+ blacklist. This would manifest as a timeout or hang, was easily hit, had been
+ present since 0.2.0, and likely impacted many users.
+
+* :gh:issue:`549`: the open file limit is increased to the permitted hard
+ limit. It is common for distributions to ship with a higher hard limit than
+ the default soft limit, allowing *"too many open files"* errors to be avoided
+ more often in large runs without user intervention.
+
+* :gh:issue:`558`, :gh:issue:`582`: on Ansible 2.3 a directory was
+ unconditionally deleted after the first module belonging to an action plug-in
+ had executed, causing the :ans:mod:`unarchive` to fail.
+
+* :gh:issue:`578`: the extension could crash while rendering an error due to an
+ incorrect format string.
+
+* :gh:issue:`590`: the importer can handle modules that replace themselves in
+ :data:`sys.modules` with completely unrelated modules during import, as in
+ the case of Ansible 2.8 :mod:`ansible.module_utils.distro`.
+
+* :gh:issue:`591`: the working directory is reset between tasks to ensure
+ :func:`os.getcwd` cannot fail, in the same way :class:`AnsibleModule`
+ resets it during initialization. However this restore happens before the
+ module executes, ensuring code that calls :func:`os.getcwd` prior to
+ :class:`AnsibleModule` initialization, such as the Ansible 2.7
+ :ans:mod:`pip`, cannot fail due to the actions of a prior task.
+
+* :gh:issue:`593`: the SSH connection method exposes
+ ``mitogen_ssh_keepalive_interval`` and ``mitogen_ssh_keepalive_count``
+ variables, and the default timeout for an SSH server has been increased from
+ `15*3` seconds to `30*10` seconds.
+
+* :gh:issue:`600`: functionality to reflect changes to ``/etc/environment`` did
+ not account for Unicode file contents. The file may now use any single byte
+ encoding.
+
+* :gh:issue:`602`: connection configuration is more accurately inferred for
+ :ans:mod:`meta: reset_connection ` the :ans:mod:`synchronize`, and for
+ any action plug-ins that establish additional connections.
+
+* :gh:issue:`598`, :gh:issue:`605`: fix a deadlock managing a shared counter
+ used for load balancing, present since 0.2.4.
+
+* :gh:issue:`615`: streaming is implemented for the :ans:mod:`fetch` and other
+ actions that transfer files from targets to the controller. Previously files
+ delivered were sent in one message, requiring them to fit in RAM and be
+ smaller than an internal message size sanity check. Transfers from controller
+ to targets have been streaming since 0.2.0.
+
+* :gh:commit:`7ae926b3`: the :ans:mod:`lineinfile` leaks writable temporary
+ file descriptors since Ansible 2.7.0. When :ans:mod:`~lineinfile` created or
+ modified a script, and that script was later executed, the execution could
+ fail with "*text file busy*". Temporary descriptors are now tracked and
+ cleaned up on exit for all modules.
+
+
+Core Library
+~~~~~~~~~~~~
+
+* Log readability is improving and many :func:`repr` strings are more
+ descriptive. The old pseudo-function-call format is migrating to
+ readable output where possible. For example, *"Stream(ssh:123).connect()"*
+ might be written *"connecting to ssh:123"*.
+
+* In preparation for reducing default log output, many messages are delivered
+ to per-component loggers, including messages originating from children,
+ enabling :mod:`logging` aggregation to function as designed. An importer
+ message like::
+
+ 12:00:00 D mitogen.ctx.remotehost mitogen: loading module "foo"
+
+ Might instead be logged to the ``mitogen.importer.[remotehost]`` logger::
+
+ 12:00:00 D mitogen.importer.[remotehost] loading module "foo"
+
+ Allowing a filter or handler for ``mitogen.importer`` to select that logger
+ in every process. This introduces a small risk of leaking memory in
+ long-lived programs, as logger objects are internally persistent.
+
+* :func:`bytearray` was removed from the list of supported serialization types.
+ It was never portable between Python versions, unused, and never made much
+ sense to support.
+
+* :gh:issue:`170`: to improve subprocess
+ management and asynchronous connect, a :class:`~mitogen.parent.TimerList`
+ interface is available, accessible as :attr:`Broker.timers` in an
+ asynchronous context.
+
+* :gh:issue:`419`: the internal
+ :class:`~mitogen.core.Stream` has been refactored into many new classes,
+ modularizing protocol behaviour, output buffering, line-oriented input
+ parsing, option handling and connection management. Connection setup is
+ internally asynchronous, laying most groundwork for fully asynchronous
+ connect, proxied Ansible become plug-ins, and in-process SSH.
+
+* :gh:issue:`169`,
+ :gh:issue:`419`: zombie subprocess reaping
+ has vastly improved, by using timers to efficiently poll for a child to exit,
+ and delaying shutdown while any subprocess remains. Polling avoids
+ process-global configuration such as a `SIGCHLD` handler, or
+ :func:`signal.set_wakeup_fd` available in modern Python.
+
+* :gh:issue:`256`, :gh:issue:`419`: most :func:`os.dup` use was eliminated,
+ along with most manual file descriptor management. Descriptors are trapped in
+ :func:`os.fdopen` objects at creation, ensuring a leaked object will close
+ itself, and ensuring every descriptor is fused to a `closed` flag, preventing
+ historical bugs where a double close could destroy unrelated descriptors.
+
+* :gh:issue:`533`: routing accounts for
+ a race between a parent (or cousin) sending a message to a child via an
+ intermediary, where the child had recently disconnected, and
+ :data:`~mitogen.core.DEL_ROUTE` propagating from the intermediary
+ to the sender, informing it that the child no longer exists. This condition
+ is detected at the intermediary and a :ref:`dead message ` is
+ returned to the sender.
+
+ Previously since the intermediary had already removed its route for the
+ child, the *route messages upwards* rule would be triggered, causing the
+ message (with a privileged :ref:`src_id/auth_id `) to be
+ sent upstream, resulting in a ``bad auth_id`` error logged at the first
+ upstream parent, and a possible hang due to a request message being dropped.
+
+* :gh:issue:`586`: fix import of
+ :mod:`__main__` on later versions of Python 3 when running from the
+ interactive console.
+
+* :gh:issue:`606`: fix example code on the
+ documentation front page.
+
+* :gh:issue:`612`: fix various errors
+ introduced by stream refactoring.
+
+* :gh:issue:`615`: when routing fails to
+ deliver a message for some reason other than the sender cannot or should not
+ reach the recipient, and no reply-to address is present on the message,
+ instead send a :ref:`dead message ` to the original recipient. This
+ ensures a descriptive message is delivered to a thread sleeping on the reply
+ to a function call, where the reply might be dropped due to exceeding the
+ maximum configured message size.
+
+* :gh:issue:`624`: the number of threads used for a child's automatically
+ initialized service thread pool has been reduced from 16 to 2. This may drop
+ to 1 in future, and become configurable via a :class:`Router` option.
+
+* :gh:commit:`a5536c35`: avoid quadratic
+ buffer management when logging lines received from a child's redirected
+ standard IO.
+
+* :gh:commit:`49a6446a`: the
+ :meth:`empty` methods of :class:`~mitogen.core.Latch`,
+ :class:`~mitogen.core.Receiver` and :class:`~mitogen.select.Select` are
+ obsoleted by a more general :meth:`size` method. :meth:`empty` will be
+ removed in 0.3
+
+* :gh:commit:`ecc570cb`: previously
+ :meth:`mitogen.select.Select.add` would enqueue one wake event when adding an
+ existing receiver, latch or subselect that contained multiple buffered items,
+ causing :meth:`get` calls to block or fail even though data existed to return.
+
+* :gh:commit:`5924af15`: *[security]*
+ unidirectional routing, where contexts may optionally only communicate with
+ parents and never siblings (so that air-gapped networks cannot be
+ unintentionally bridged) was not inherited when a child was initiated
+ directly from an another child. This did not effect Ansible, since the
+ controller initiates any new child used for routing, only forked tasks are
+ initiated by children.
+
+
+Thanks!
+~~~~~~~
+
+Mitogen would not be possible without the support of users. A huge thanks for
+bug reports, testing, features and fixes in this release contributed by
+`Andreas Hubert `_.
+`Anton Markelov `_,
+`Dan `_,
+`Dave Cottlehuber `_,
+`Denis Krienbühl `_,
+`El Mehdi CHAOUKI `_,
+`Florent Dutheil `_,
+`James Hogarth `_,
+`Jordan Webb `_,
+`Julian Andres Klode `_,
+`Marc Hartmayer `_,
+`Nigel Metheringham `_,
+`Orion Poplawski `_,
+`Pieter Voet `_,
+`Stefane Fermigier `_,
+`Szabó Dániel Ernő `_,
+`Ulrich Schreiner `_,
+`Vincent S. Cojot `_,
+`yen `_,
+`Yuki Nishida `_,
+`@alexhexabeam `_,
+`@DavidVentura `_,
+`@dbiegunski `_,
+`@ghp-rr `_,
+`@migalsp `_,
+`@rizzly `_,
+`@SQGE `_, and
+`@tho86 `_.
+
v0.2.7 (2019-05-19)
-------------------
@@ -31,27 +305,27 @@ on Ansible 2.8, which is not yet supported.
Fixes
~~~~~
-* `#557 `_: fix a crash when running
+* :gh:issue:`557`: fix a crash when running
on machines with high CPU counts.
-* `#570 `_: the ``firewalld`` module
- internally caches a dbus name that changes across ``firewalld`` restarts,
- causing a failure if the service is restarted between ``firewalld`` module invocations.
+* :gh:issue:`570`: the :ans:mod:`firewalld` internally caches a dbus name that
+ changes across :ans:mod:`~firewalld` restarts, causing a failure if the
+ service is restarted between :ans:mod:`~firewalld` module invocations.
-* `#575 `_: fix a crash when
+* :gh:issue:`575`: fix a crash when
rendering an error message to indicate no usable temporary directories could
be found.
-* `#576 `_: fix a crash during
+* :gh:issue:`576`: fix a crash during
startup on SuSE Linux 11, due to an incorrect version compatibility check in
the Mitogen code.
-* `#581 `_: a
+* :gh:issue:`581`: a
``mitogen_mask_remote_name`` Ansible variable is exposed, to allow masking
the username, hostname and process ID of ``ansible-playbook`` running on the
controller machine.
-* `#587 `_: display a friendly
+* :gh:issue:`587`: display a friendly
message when running on an unsupported version of Ansible, to cope with
potential influx of 2.8-related bug reports.
@@ -73,53 +347,53 @@ v0.2.6 (2019-03-06)
Fixes
~~~~~
-* `#542 `_: some versions of OS X
+* :gh:issue:`542`: some versions of OS X
ship a default Python that does not support :func:`select.poll`. Restore the
0.2.3 behaviour of defaulting to Kqueue in this case, but still prefer
:func:`select.poll` if it is available.
-* `#545 `_: an optimization
- introduced in `#493 `_ caused a
+* :gh:issue:`545`: an optimization
+ introduced in :gh:issue:`493` caused a
64-bit integer to be assigned to a 32-bit field on ARM 32-bit targets,
causing runs to fail.
-* `#548 `_: `mitogen_via=` could fail
+* :gh:issue:`548`: `mitogen_via=` could fail
when the selected transport was set to ``smart``.
-* `#550 `_: avoid some broken
+* :gh:issue:`550`: avoid some broken
TTY-related `ioctl()` calls on Windows Subsystem for Linux 2016 Anniversary
Update.
-* `#554 `_: third party Ansible
+* :gh:issue:`554`: third party Ansible
action plug-ins that invoked :func:`_make_tmp_path` repeatedly could trigger
an assertion failure.
-* `#555 `_: work around an old idiom
+* :gh:issue:`555`: work around an old idiom
that reloaded :mod:`sys` in order to change the interpreter's default encoding.
-* `ffae0355 `_: needless
+* :gh:commit:`ffae0355`: needless
information was removed from the documentation and installation procedure.
Core Library
~~~~~~~~~~~~
-* `#535 `_: to support function calls
+* :gh:issue:`535`: to support function calls
on a service pool from another thread, :class:`mitogen.select.Select`
additionally permits waiting on :class:`mitogen.core.Latch`.
-* `#535 `_:
+* :gh:issue:`535`:
:class:`mitogen.service.Pool.defer` allows any function to be enqueued for
the thread pool from another thread.
-* `#535 `_: a new
+* :gh:issue:`535`: a new
:mod:`mitogen.os_fork` module provides a :func:`os.fork` wrapper that pauses
thread activity during fork. On Python<2.6, :class:`mitogen.core.Broker` and
:class:`mitogen.service.Pool` automatically record their existence so that a
:func:`os.fork` monkey-patch can automatically pause them for any attempt to
start a subprocess.
-* `ca63c26e `_:
+* :gh:commit:`ca63c26e`:
:meth:`mitogen.core.Latch.put`'s `obj` argument was made optional.
@@ -144,47 +418,47 @@ v0.2.5 (2019-02-14)
Fixes
~~~~~
-* `#511 `_,
- `#536 `_: changes in 0.2.4 to
+* :gh:issue:`511`,
+ :gh:issue:`536`: changes in 0.2.4 to
repair ``delegate_to`` handling broke default ``ansible_python_interpreter``
handling. Test coverage was added.
-* `#532 `_: fix a race in the service
+* :gh:issue:`532`: fix a race in the service
used to propagate Ansible modules, that could easily manifest when starting
asynchronous tasks in a loop.
-* `#536 `_: changes in 0.2.4 to
+* :gh:issue:`536`: changes in 0.2.4 to
support Python 2.4 interacted poorly with modules that imported
``simplejson`` from a controller that also loaded an incompatible newer
version of ``simplejson``.
-* `#537 `_: a swapped operator in the
+* :gh:issue:`537`: a swapped operator in the
CPU affinity logic meant 2 cores were reserved on 1`_: the source distribution
+* :gh:issue:`538`: the source distribution
includes a ``LICENSE`` file.
-* `#539 `_: log output is no longer
+* :gh:issue:`539`: log output is no longer
duplicated when the Ansible ``log_path`` setting is enabled.
-* `#540 `_: the ``stderr`` stream of
+* :gh:issue:`540`: the ``stderr`` stream of
async module invocations was previously discarded.
-* `#541 `_: Python error logs
+* :gh:issue:`541`: Python error logs
originating from the ``boto`` package are quiesced, and only appear in
``-vvv`` output. This is since EC2 modules may trigger errors during normal
operation, when retrying transiently failing requests.
-* `748f5f67 `_,
- `21ad299d `_,
- `8ae6ca1d `_,
- `7fd0d349 `_:
+* :gh:commit:`748f5f67`,
+ :gh:commit:`21ad299d`,
+ :gh:commit:`8ae6ca1d`,
+ :gh:commit:`7fd0d349`:
the ``ansible_ssh_host``, ``ansible_ssh_user``, ``ansible_user``,
``ansible_become_method``, and ``ansible_ssh_port`` variables more correctly
match typical behaviour when ``mitogen_via=`` is active.
-* `2a8567b4 `_: fix a race
+* :gh:commit:`2a8567b4`: fix a race
initializing a child's service thread pool on Python 3.4+, due to a change in
locking scheme used by the Python import mechanism.
@@ -212,57 +486,54 @@ on the connection multiplexer.
Enhancements
^^^^^^^^^^^^
-* `#76 `_,
- `#351 `_,
- `#352 `_: disconnect propagation
+* :gh:issue:`76`,
+ :gh:issue:`351`,
+ :gh:issue:`352`: disconnect propagation
has improved, allowing Ansible to cancel waits for responses from abruptly
disconnected targets. This ensures a task will reliably fail rather than
hang, for example on network failure or EC2 instance maintenance.
-* `#369 `_,
- `#407 `_: :meth:`Connection.reset`
- is implemented, allowing `meta: reset_connection
- `_ to shut
+* :gh:issue:`369`,
+ :gh:issue:`407`: :meth:`Connection.reset`
+ is implemented, allowing :ans:mod:`meta: reset_connection ` to shut
down the remote interpreter as documented, and improving support for the
- `reboot
- `_
- module.
+ :ans:mod:`reboot`.
-* `09aa27a6 `_: the
+* :gh:commit:`09aa27a6`: the
``mitogen_host_pinned`` strategy wraps the ``host_pinned`` strategy
introduced in Ansible 2.7.
-* `#477 `_: Python 2.4 is fully
+* :gh:issue:`477`: Python 2.4 is fully
supported by the core library and tested automatically, in any parent/child
combination of 2.4, 2.6, 2.7 and 3.6 interpreters.
-* `#477 `_: Ansible 2.3 is fully
+* :gh:issue:`477`: Ansible 2.3 is fully
supported and tested automatically. In combination with the core library
Python 2.4 support, this allows Red Hat Enterprise Linux 5 targets to be
managed with Mitogen. The ``simplejson`` package need not be installed on
such targets, as is usually required by Ansible.
-* `#412 `_: to simplify diagnosing
+* :gh:issue:`412`: to simplify diagnosing
connection configuration problems, Mitogen ships a ``mitogen_get_stack``
action that is automatically added to the action plug-in path. See
:ref:`mitogen-get-stack` for more information.
-* `152effc2 `_,
- `bd4b04ae `_: a CPU affinity
+* :gh:commit:`152effc2`,
+ :gh:commit:`bd4b04ae`: a CPU affinity
policy was added for Linux controllers, reducing latency and SMP overhead on
hot paths exercised for every task. This yielded a 19% speedup in a 64-target
job composed of many short tasks, and should easily be visible as a runtime
improvement in many-host runs.
-* `2b44d598 `_: work around a
+* :gh:commit:`2b44d598`: work around a
defective caching mechanism by pre-heating it before spawning workers. This
saves 40% runtime on a synthetic repetitive task.
-* `0979422a `_: an expensive
+* :gh:commit:`0979422a`: an expensive
dependency scanning step was redundantly invoked for every task,
bottlenecking the connection multiplexer.
-* `eaa990a97 `_: a new
+* :gh:commit:`eaa990a97`: a new
``mitogen_ssh_compression`` variable is supported, allowing Mitogen's default
SSH compression to be disabled. SSH compression is a large contributor to CPU
usage in many-target runs, and severely limits file transfer. On a `"shell:
@@ -270,124 +541,115 @@ Enhancements
task with compression, rising to 3 KiB without. File transfer throughput
rises from ~25MiB/s when enabled to ~200MiB/s when disabled.
-* `#260 `_,
- `a18a083c `_: brokers no
+* :gh:issue:`260`,
+ :gh:commit:`a18a083c`: brokers no
longer wait for readiness indication to transmit, and instead assume
transmission will succeed. As this is usually true, one loop iteration and
two poller reconfigurations are avoided, yielding a significant reduction in
interprocess round-trip latency.
-* `#415 `_,
- `#491 `_,
- `#493 `_: the interface employed
- for in-process queues changed from `kqueue
- `_ / `epoll
- `_ to `poll()
- `_, which requires no setup
- or teardown, yielding a 38% latency reduction for inter-thread communication.
+* :gh:issue:`415`, :gh:issue:`491`, :gh:issue:`493`: the interface employed
+ for in-process queues changed from :freebsd:man2:`kqueue` /
+ :linux:man7:`epoll` to :linux:man2:`poll`, which requires no setup or
+ teardown, yielding a 38% latency reduction for inter-thread communication.
Fixes
^^^^^
-* `#251 `_,
- `#359 `_,
- `#396 `_,
- `#401 `_,
- `#404 `_,
- `#412 `_,
- `#434 `_,
- `#436 `_,
- `#465 `_: connection delegation and
+* :gh:issue:`251`,
+ :gh:issue:`359`,
+ :gh:issue:`396`,
+ :gh:issue:`401`,
+ :gh:issue:`404`,
+ :gh:issue:`412`,
+ :gh:issue:`434`,
+ :gh:issue:`436`,
+ :gh:issue:`465`: connection delegation and
``delegate_to:`` handling suffered a major regression in 0.2.3. The 0.2.2
behaviour has been restored, and further work has been made to improve the
compatibility of connection delegation's configuration building methods.
-* `#323 `_,
- `#333 `_: work around a Windows
+* :gh:issue:`323`,
+ :gh:issue:`333`: work around a Windows
Subsystem for Linux bug that caused tracebacks to appear during shutdown.
-* `#334 `_: the SSH method
+* :gh:issue:`334`: the SSH method
tilde-expands private key paths using Ansible's logic. Previously the path
was passed unmodified to SSH, which expanded it using :func:`pwd.getpwnam`.
This differs from :func:`os.path.expanduser`, which uses the ``HOME``
environment variable if it is set, causing behaviour to diverge when Ansible
was invoked across user accounts via ``sudo``.
-* `#364 `_: file transfers from
+* :gh:issue:`364`: file transfers from
controllers running Python 2.7.2 or earlier could be interrupted due to a
forking bug in the :mod:`tempfile` module.
-* `#370 `_: the Ansible
- `reboot `_
- module is supported.
+* :gh:issue:`370`: the Ansible :ans:mod:`reboot` is supported.
-* `#373 `_: the LXC and LXD methods
- print a useful hint on failure, as no useful error is normally logged to the
- console by these tools.
+* :gh:issue:`373`: the LXC and LXD methods print a useful hint on failure, as
+ no useful error is normally logged to the console by these tools.
-* `#374 `_,
- `#391 `_: file transfer and module
+* :gh:issue:`374`,
+ :gh:issue:`391`: file transfer and module
execution from 2.x controllers to 3.x targets was broken due to a regression
- caused by refactoring, and compounded by `#426
- `_.
+ caused by refactoring, and compounded by :gh:issue:`426`.
-* `#400 `_: work around a threading
+* :gh:issue:`400`: work around a threading
bug in the AWX display callback when running with high verbosity setting.
-* `#409 `_: the setns method was
+* :gh:issue:`409`: the setns method was
silently broken due to missing tests. Basic coverage was added to prevent a
recurrence.
-* `#409 `_: the LXC and LXD methods
+* :gh:issue:`409`: the LXC and LXD methods
support ``mitogen_lxc_path`` and ``mitogen_lxc_attach_path`` variables to
control the location of third pary utilities.
-* `#410 `_: the sudo method supports
+* :gh:issue:`410`: the sudo method supports
the SELinux ``--type`` and ``--role`` options.
-* `#420 `_: if a :class:`Connection`
+* :gh:issue:`420`: if a :class:`Connection`
was constructed in the Ansible top-level process, for example while executing
``meta: reset_connection``, resources could become undesirably shared in
subsequent children.
-* `#426 `_: an oversight while
+* :gh:issue:`426`: an oversight while
porting to Python 3 meant no automated 2->3 tests were running. A significant
number of 2->3 bugs were fixed, mostly in the form of Unicode/bytes
mismatches.
-* `#429 `_: the ``sudo`` method can
+* :gh:issue:`429`: the ``sudo`` method can
now recognize internationalized password prompts.
-* `#362 `_,
- `#435 `_: the previous fix for slow
+* :gh:issue:`362`,
+ :gh:issue:`435`: the previous fix for slow
Python 2.x subprocess creation on Red Hat caused newly spawned children to
have a reduced open files limit. A more intrusive fix has been added to
directly address the problem without modifying the subprocess environment.
-* `#397 `_,
- `#454 `_: the previous approach to
+* :gh:issue:`397`,
+ :gh:issue:`454`: the previous approach to
handling modern Ansible temporary file cleanup was too aggressive, and could
trigger early finalization of Cython-based extension modules, leading to
segmentation faults.
-* `#499 `_: the ``allow_same_user``
+* :gh:issue:`499`: the ``allow_same_user``
Ansible configuration setting is respected.
-* `#527 `_: crashes in modules are
+* :gh:issue:`527`: crashes in modules are
trapped and reported in a manner that matches Ansible. In particular, a
module crash no longer leads to an exception that may crash the corresponding
action plug-in.
-* `dc1d4251 `_: the
- ``synchronize`` module could fail with the Docker transport due to a missing
- attribute.
+* :gh:commit:`dc1d4251`: the :ans:mod:`synchronize` could fail with the Docker
+ transport due to a missing attribute.
-* `599da068 `_: fix a race
+* :gh:commit:`599da068`: fix a race
when starting async tasks, where it was possible for the controller to
observe no status file on disk before the task had a chance to write one.
-* `2c7af9f04 `_: Ansible
+* :gh:commit:`2c7af9f04`: Ansible
modules were repeatedly re-transferred. The bug was hidden by the previously
mandatorily enabled SSH compression.
@@ -395,7 +657,7 @@ Fixes
Core Library
~~~~~~~~~~~~
-* `#76 `_: routing records the
+* :gh:issue:`76`: routing records the
destination context IDs ever received on each stream, and when disconnection
occurs, propagates :data:`mitogen.core.DEL_ROUTE` messages towards every
stream that ever communicated with the disappearing peer, rather than simply
@@ -404,166 +666,166 @@ Core Library
receivers to wake with :class:`mitogen.core.ChannelError`, even when one
participant is not a parent of the other.
-* `#109 `_,
- `57504ba6 `_: newer Python 3
+* :gh:issue:`109`,
+ :gh:commit:`57504ba6`: newer Python 3
releases explicitly populate :data:`sys.meta_path` with importer internals,
causing Mitogen to install itself at the end of the importer chain rather
than the front.
-* `#310 `_: support has returned for
+* :gh:issue:`310`: support has returned for
trying to figure out the real source of non-module objects installed in
:data:`sys.modules`, so they can be imported. This is needed to handle syntax
sugar used by packages like :mod:`plumbum`.
-* `#349 `_: an incorrect format
+* :gh:issue:`349`: an incorrect format
string could cause large stack traces when attempting to import built-in
modules on Python 3.
-* `#387 `_,
- `#413 `_: dead messages include an
+* :gh:issue:`387`,
+ :gh:issue:`413`: dead messages include an
optional reason in their body. This is used to cause
:class:`mitogen.core.ChannelError` to report far more useful diagnostics at
the point the error occurs that previously would have been buried in debug
log output from an unrelated context.
-* `#408 `_: a variety of fixes were
+* :gh:issue:`408`: a variety of fixes were
made to restore Python 2.4 compatibility.
-* `#399 `_,
- `#437 `_: ignore a
+* :gh:issue:`399`,
+ :gh:issue:`437`: ignore a
:class:`DeprecationWarning` to avoid failure of the ``su`` method on Python
3.7.
-* `#405 `_: if an oversized message
+* :gh:issue:`405`: if an oversized message
is rejected, and it has a ``reply_to`` set, a dead message is returned to the
sender. This ensures function calls exceeding the configured maximum size
crash rather than hang.
-* `#406 `_:
+* :gh:issue:`406`:
:class:`mitogen.core.Broker` did not call :meth:`mitogen.core.Poller.close`
during shutdown, leaking the underlying poller FD in masters and parents.
-* `#406 `_: connections could leak
+* :gh:issue:`406`: connections could leak
FDs when a child process failed to start.
-* `#288 `_,
- `#406 `_,
- `#417 `_: connections could leave
+* :gh:issue:`288`,
+ :gh:issue:`406`,
+ :gh:issue:`417`: connections could leave
FD wrapper objects that had not been closed lying around to be closed during
garbage collection, causing reused FD numbers to be closed at random moments.
-* `#411 `_: the SSH method typed
+* :gh:issue:`411`: the SSH method typed
"``y``" rather than the requisite "``yes``" when `check_host_keys="accept"`
was configured. This would lead to connection timeouts due to the hung
response.
-* `#414 `_,
- `#425 `_: avoid deadlock of forked
+* :gh:issue:`414`,
+ :gh:issue:`425`: avoid deadlock of forked
children by reinitializing the :mod:`mitogen.service` pool lock.
-* `#416 `_: around 1.4KiB of memory
+* :gh:issue:`416`: around 1.4KiB of memory
was leaked on every RPC, due to a list of strong references keeping alive any
handler ever registered for disconnect notification.
-* `#418 `_: the
+* :gh:issue:`418`: the
:func:`mitogen.parent.iter_read` helper would leak poller FDs, because
execution of its :keyword:`finally` block was delayed on Python 3. Now
callers explicitly close the generator when finished.
-* `#422 `_: the fork method could
+* :gh:issue:`422`: the fork method could
fail to start if :data:`sys.stdout` was opened in block buffered mode, and
buffered data was pending in the parent prior to fork.
-* `#438 `_: a descriptive error is
+* :gh:issue:`438`: a descriptive error is
logged when stream corruption is detected.
-* `#439 `_: descriptive errors are
+* :gh:issue:`439`: descriptive errors are
raised when attempting to invoke unsupported function types.
-* `#444 `_: messages regarding
+* :gh:issue:`444`: messages regarding
unforwardable extension module are no longer logged as errors.
-* `#445 `_: service pools unregister
+* :gh:issue:`445`: service pools unregister
the :data:`mitogen.core.CALL_SERVICE` handle at shutdown, ensuring any
outstanding messages are either processed by the pool as it shuts down, or
have dead messages sent in reply to them, preventing peer contexts from
hanging due to a forgotten buffered message.
-* `#446 `_: given thread A calling
+* :gh:issue:`446`: given thread A calling
:meth:`mitogen.core.Receiver.close`, and thread B, C, and D sleeping in
:meth:`mitogen.core.Receiver.get`, previously only one sleeping thread would
be woken with :class:`mitogen.core.ChannelError` when the receiver was
closed. Now all threads are woken per the docstring.
-* `#447 `_: duplicate attempts to
+* :gh:issue:`447`: duplicate attempts to
invoke :meth:`mitogen.core.Router.add_handler` cause an error to be raised,
ensuring accidental re-registration of service pools are reported correctly.
-* `#448 `_: the import hook
+* :gh:issue:`448`: the import hook
implementation now raises :class:`ModuleNotFoundError` instead of
:class:`ImportError` in Python 3.6 and above, to cope with an upcoming
version of the :mod:`subprocess` module requiring this new subclass to be
raised.
-* `#453 `_: the loggers used in
+* :gh:issue:`453`: the loggers used in
children for standard IO redirection have propagation disabled, preventing
accidental reconfiguration of the :mod:`logging` package in a child from
setting up a feedback loop.
-* `#456 `_: a descriptive error is
+* :gh:issue:`456`: a descriptive error is
logged when :meth:`mitogen.core.Broker.defer` is called after the broker has
shut down, preventing new messages being enqueued that will never be sent,
and subsequently producing a program hang.
-* `#459 `_: the beginnings of a
+* :gh:issue:`459`: the beginnings of a
:meth:`mitogen.master.Router.get_stats` call has been added. The initial
statistics cover the module loader only.
-* `#462 `_: Mitogen could fail to
+* :gh:issue:`462`: Mitogen could fail to
open a PTY on broken Linux systems due to a bad interaction between the glibc
:func:`grantpt` function and an incorrectly mounted ``/dev/pts`` filesystem.
Since correct group ownership is not required in most scenarios, when this
problem is detected, the PTY is allocated and opened directly by the library.
-* `#479 `_: Mitogen could fail to
+* :gh:issue:`479`: Mitogen could fail to
import :mod:`__main__` on Python 3.4 and newer due to a breaking change in
the :mod:`pkgutil` API. The program's main script is now handled specially.
-* `#481 `_: the version of `sudo`
+* :gh:issue:`481`: the version of `sudo`
that shipped with CentOS 5 replaced itself with the program to be executed,
and therefore did not hold any child PTY open on our behalf. The child
context is updated to preserve any PTY FD in order to avoid the kernel
sending `SIGHUP` early during startup.
-* `#523 `_: the test suite didn't
+* :gh:issue:`523`: the test suite didn't
generate a code coverage report if any test failed.
-* `#524 `_: Python 3.6+ emitted a
+* :gh:issue:`524`: Python 3.6+ emitted a
:class:`DeprecationWarning` for :func:`mitogen.utils.run_with_router`.
-* `#529 `_: Code coverage of the
+* :gh:issue:`529`: Code coverage of the
test suite was not measured across all Python versions.
-* `16ca111e `_: handle OpenSSH
+* :gh:commit:`16ca111e`: handle OpenSSH
7.5 permission denied prompts when ``~/.ssh/config`` rewrites are present.
-* `9ec360c2 `_: a new
+* :gh:commit:`9ec360c2`: a new
:meth:`mitogen.core.Broker.defer_sync` utility function is provided.
-* `f20e0bba `_:
+* :gh:commit:`f20e0bba`:
:meth:`mitogen.service.FileService.register_prefix` permits granting
unprivileged access to whole filesystem subtrees, rather than single files at
a time.
-* `8f85ee03 `_:
+* :gh:commit:`8f85ee03`:
:meth:`mitogen.core.Router.myself` returns a :class:`mitogen.core.Context`
referring to the current process.
-* `824c7931 `_: exceptions
+* :gh:commit:`824c7931`: exceptions
raised by the import hook were updated to include probable reasons for
a failure.
-* `57b652ed `_: a stray import
+* :gh:commit:`57b652ed`: a stray import
meant an extra roundtrip and ~4KiB of data was wasted for any context that
imported :mod:`mitogen.parent`.
@@ -620,51 +882,40 @@ Mitogen for Ansible
Enhancements
^^^^^^^^^^^^
-* `#315 `_,
- `#392 `_: Ansible 2.6 and 2.7 are
+* :gh:pull:`315`,
+ :gh:issue:`392`: Ansible 2.6 and 2.7 are
supported.
-* `#321 `_,
- `#336 `_: temporary file handling
- was simplified, undoing earlier damage caused by compatibility fixes,
- improving 2.6 compatibility, and avoiding two network roundtrips for every
- related action
- (`assemble `_,
- `aws_s3 `_,
- `copy `_,
- `patch `_,
- `script `_,
- `template `_,
- `unarchive `_,
- `uri `_). See
- :ref:`ansible_tempfiles` for a complete description.
-
-* `#376 `_,
- `#377 `_: the ``kubectl`` connection
- type is now supported. Contributed by Yannig Perré.
-
-* `084c0ac0 `_: avoid a
- roundtrip in
- `copy `_ and
- `template `_
- due to an unfortunate default.
-
-* `7458dfae `_: avoid a
+* :gh:issue:`321`, :gh:issue:`336`: temporary file handling was simplified,
+ undoing earlier damage caused by compatibility fixes, improving 2.6
+ compatibility, and avoiding two network roundtrips for every related action
+ (:ans:mod:`~assemble`, :ans:mod:`~aws_s3`, :ans:mod:`~copy`,
+ :ans:mod:`~patch`, :ans:mod:`~script`, :ans:mod:`~template`,
+ :ans:mod:`~unarchive`, :ans:mod:`~uri`). See :ref:`ansible_tempfiles` for a
+ complete description.
+
+* :gh:pull:`376`, :gh:pull:`377`: the ``kubectl`` connection type is now
+ supported. Contributed by Yannig Perré.
+
+* :gh:commit:`084c0ac0`: avoid a roundtrip in :ans:mod:`~copy` and
+ :ans:mod:`~template` due to an unfortunate default.
+
+* :gh:commit:`7458dfae`: avoid a
roundtrip when transferring files smaller than 124KiB. Copy and template
actions are now 2-RTT, reducing runtime for a 20-iteration template loop over
a 250 ms link from 30 seconds to 10 seconds compared to v0.2.2, down from 120
seconds compared to vanilla.
-* `#337 `_: To avoid a scaling
+* :gh:issue:`337`: To avoid a scaling
limitation, a PTY is no longer allocated for an SSH connection unless the
configuration specifies a password.
-* `d62e6e2a `_: many-target
+* :gh:commit:`d62e6e2a`: many-target
runs executed the dependency scanner redundantly due to missing
synchronization, wasting significant runtime in the connection multiplexer.
In one case work was reduced by 95%, which may manifest as faster runs.
-* `5189408e `_: threads are
+* :gh:commit:`5189408e`: threads are
cooperatively scheduled, minimizing `GIL
`_ contention, and
reducing context switching by around 90%. This manifests as an overall
@@ -683,62 +934,62 @@ Enhancements
Fixes
^^^^^
-* `#251 `_,
- `#340 `_: Connection Delegation
+* :gh:issue:`251`,
+ :gh:issue:`340`: Connection Delegation
could establish connections to the wrong target when ``delegate_to:`` is
present.
-* `#291 `_: when Mitogen had
+* :gh:issue:`291`: when Mitogen had
previously been installed using ``pip`` or ``setuptools``, the globally
installed version could conflict with a newer version bundled with an
extension that had been installed using the documented steps. Now the bundled
library always overrides over any system-installed copy.
-* `#324 `_: plays with a
+* :gh:issue:`324`: plays with a
`custom module_utils `_
would fail due to fallout from the Python 3 port and related tests being
disabled.
-* `#331 `_: the connection
+* :gh:issue:`331`: the connection
multiplexer subprocess always exits before the main Ansible process, ensuring
logs generated by it do not overwrite the user's prompt when ``-vvv`` is
enabled.
-* `#332 `_: support a new
+* :gh:issue:`332`: support a new
:func:`sys.excepthook`-based module exit mechanism added in Ansible 2.6.
-* `#338 `_: compatibility: changes to
+* :gh:issue:`338`: compatibility: changes to
``/etc/environment`` and ``~/.pam_environment`` made by a task are reflected
in the runtime environment of subsequent tasks. See
:ref:`ansible_process_env` for a complete description.
-* `#343 `_: the sudo ``--login``
+* :gh:issue:`343`: the sudo ``--login``
option is supported.
-* `#344 `_: connections no longer
+* :gh:issue:`344`: connections no longer
fail when the controller's login username contains slashes.
-* `#345 `_: the ``IdentitiesOnly
+* :gh:issue:`345`: the ``IdentitiesOnly
yes`` option is no longer supplied to OpenSSH by default, better matching
Ansible's behaviour.
-* `#355 `_: tasks configured to run
+* :gh:issue:`355`: tasks configured to run
in an isolated forked subprocess were forked from the wrong parent context.
This meant built-in modules overridden via a custom ``module_utils`` search
path may not have had any effect.
-* `#362 `_: to work around a slow
+* :gh:issue:`362`: to work around a slow
algorithm in the :mod:`subprocess` module, the maximum number of open files
in processes running on the target is capped to 512, reducing the work
required to start a subprocess by >2000x in default CentOS configurations.
-* `#397 `_: recent Mitogen master
+* :gh:issue:`397`: recent Mitogen master
versions could fail to clean up temporary directories in a number of
circumstances, and newer Ansibles moved to using :mod:`atexit` to effect
temporary directory cleanup in some circumstances.
-* `b9112a9c `_,
- `2c287801 `_: OpenSSH 7.5
+* :gh:commit:`b9112a9c`,
+ :gh:commit:`2c287801`: OpenSSH 7.5
permission denied prompts are now recognized. Contributed by Alex Willmer.
* A missing check caused an exception traceback to appear when using the
@@ -758,53 +1009,53 @@ Core Library
related function calls to a target context, cancelling the chain if an
exception occurs.
-* `#305 `_: fix a long-standing minor
+* :gh:issue:`305`: fix a long-standing minor
race relating to the logging framework, where *no route for Message..*
would frequently appear during startup.
-* `#313 `_:
+* :gh:issue:`313`:
:meth:`mitogen.parent.Context.call` was documented as capable of accepting
static methods. While possible on Python 2.x the result is ugly, and in every
case it should be trivial to replace with a classmethod. The documentation
was fixed.
-* `#337 `_: to avoid a scaling
+* :gh:issue:`337`: to avoid a scaling
limitation, a PTY is no longer allocated for each OpenSSH client if it can be
avoided. PTYs are only allocated if a password is supplied, or when
`host_key_checking=accept`. This is since Linux has a default of 4096 PTYs
(``kernel.pty.max``), while OS X has a default of 127 and an absolute maximum
of 999 (``kern.tty.ptmx_max``).
-* `#339 `_: the LXD connection method
+* :gh:issue:`339`: the LXD connection method
was erroneously executing LXC Classic commands.
-* `#345 `_: the SSH connection method
+* :gh:issue:`345`: the SSH connection method
allows optionally disabling ``IdentitiesOnly yes``.
-* `#356 `_: if the master Python
+* :gh:issue:`356`: if the master Python
process does not have :data:`sys.executable` set, the default Python
interpreter used for new children on the local machine defaults to
``"/usr/bin/python"``.
-* `#366 `_,
- `#380 `_: attempts by children to
+* :gh:issue:`366`,
+ :gh:issue:`380`: attempts by children to
import :mod:`__main__` where the main program module lacks an execution guard
are refused, and an error is logged. This prevents a common and highly
confusing error when prototyping new scripts.
-* `#371 `_: the LXC connection method
+* :gh:pull:`371`: the LXC connection method
uses a more compatible method to establish an non-interactive session.
Contributed by Brian Candler.
-* `af2ded66 `_: add
+* :gh:commit:`af2ded66`: add
:func:`mitogen.fork.on_fork` to allow non-Mitogen managed process forks to
clean up Mitogen resources in the child.
-* `d6784242 `_: the setns method
+* :gh:commit:`d6784242`: the setns method
always resets ``HOME``, ``SHELL``, ``LOGNAME`` and ``USER`` environment
variables to an account in the target container, defaulting to ``root``.
-* `830966bf