Merge pull request #715 from s1113950/collectionsSupport

Ansible 2.10 + Collections support
pull/714/head^2 v0.3.0-rc.0
Steven Robertson 4 years ago committed by GitHub
commit 9463728e6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -11,7 +11,8 @@ batches = [
'pip install ' 'pip install '
'-r tests/requirements.txt ' '-r tests/requirements.txt '
'-r tests/ansible/requirements.txt', '-r tests/ansible/requirements.txt',
'pip install -q ansible=={0}'.format(ci_lib.ANSIBLE_VERSION) # encoding is required for installing ansible 2.10 with pip2, otherwise we get a UnicodeDecode error
'LC_CTYPE=en_US.UTF-8 LANG=en_US.UTF-8 pip install -q ansible=={0}'.format(ci_lib.ANSIBLE_VERSION)
] ]
] ]

@ -37,9 +37,6 @@ with ci_lib.Fold('docker_setup'):
with ci_lib.Fold('job_setup'): 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 ansible==%s", ci_lib.ANSIBLE_VERSION)
os.chdir(TESTS_DIR) os.chdir(TESTS_DIR)
os.chmod('../data/docker/mitogen__has_sudo_pubkey.key', int('0600', 7)) os.chmod('../data/docker/mitogen__has_sudo_pubkey.key', int('0600', 7))
@ -75,7 +72,7 @@ with ci_lib.Fold('job_setup'):
with ci_lib.Fold('ansible'): with ci_lib.Fold('ansible'):
playbook = os.environ.get('PLAYBOOK', 'all.yml') playbook = os.environ.get('PLAYBOOK', 'all.yml')
try: try:
run('./run_ansible_playbook.py %s -i "%s" %s', run('./run_ansible_playbook.py %s -i "%s" -vvv %s',
playbook, HOSTS_DIR, ' '.join(sys.argv[1:])) playbook, HOSTS_DIR, ' '.join(sys.argv[1:]))
except: except:
pause_if_interactive() pause_if_interactive()

@ -8,23 +8,7 @@ steps:
- script: "PYTHONVERSION=$(python.version) .ci/prep_azure.py" - script: "PYTHONVERSION=$(python.version) .ci/prep_azure.py"
displayName: "Run prep_azure.py" displayName: "Run prep_azure.py"
# 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: | - script: |
# need wheel before building virtualenv because of bdist_wheel and setuptools deps
# Mac's System Integrity Protection prevents symlinking /usr/bin
# and Azure isn't allowing disabling it apparently: https://developercommunityapi.westus.cloudapp.azure.com/idea/558702/allow-disabling-sip-on-microsoft-hosted-macos-agen.html
# the || will activate when running python3 tests
# TODO: get python3 tests passing
(sudo ln -fs /usr/bin/python$(python.version) /usr/bin/python &&
/usr/bin/python -m pip install -U pip wheel setuptools &&
/usr/bin/python -m pip install -U virtualenv &&
/usr/bin/python -m virtualenv /tmp/venv -p /usr/bin/python$(python.version)) ||
(sudo /usr/bin/python$(python.version) -m pip install -U pip wheel setuptools &&
/usr/bin/python$(python.version) -m venv /tmp/venv)
echo "##vso[task.prependpath]/tmp/venv/bin" echo "##vso[task.prependpath]/tmp/venv/bin"
displayName: activate venv displayName: activate venv

@ -6,19 +6,30 @@
jobs: jobs:
- job: Mac - job: Mac
# vanilla Ansible is really slow
timeoutInMinutes: 120
steps: steps:
- template: azure-pipelines-steps.yml - template: azure-pipelines-steps.yml
pool: pool:
vmImage: macOS-10.14 vmImage: macOS-10.15
strategy: strategy:
matrix: matrix:
Mito27_27: Mito27_27:
python.version: '2.7.18' python.version: '2.7'
MODE: mitogen MODE: mitogen
Ans288_27: VER: 2.10.0
python.version: '2.7.18' # TODO: test python3, python3 tests are broken
Ans210_27:
python.version: '2.7'
MODE: localhost_ansible
VER: 2.10.0
# NOTE: this hangs when ran in Ubuntu 18.04
Vanilla_210_27:
python.version: '2.7'
MODE: localhost_ansible MODE: localhost_ansible
VER: 2.8.8 VER: 2.10.0
STRATEGY: linear
- job: Linux - job: Linux
@ -35,6 +46,7 @@ jobs:
python.version: '2.7' python.version: '2.7'
MODE: mitogen MODE: mitogen
DISTRO: debian DISTRO: debian
VER: 2.10.0
#MitoPy27CentOS6_26: #MitoPy27CentOS6_26:
#python.version: '2.7' #python.version: '2.7'
@ -45,12 +57,13 @@ jobs:
python.version: '3.6' python.version: '3.6'
MODE: mitogen MODE: mitogen
DISTRO: centos6 DISTRO: centos6
VER: 2.10.0
Mito37Debian_27: Mito37Debian_27:
python.version: '3.7' python.version: '3.7'
MODE: mitogen MODE: mitogen
DISTRO: debian DISTRO: debian
VER: 2.9.6 VER: 2.10.0
#Py26CentOS7: #Py26CentOS7:
#python.version: '2.7' #python.version: '2.7'
@ -94,17 +107,12 @@ jobs:
#DISTROS: debian #DISTROS: debian
#STRATEGY: linear #STRATEGY: linear
Ansible_280_27: Ansible_210_27:
python.version: '2.7' python.version: '2.7'
MODE: ansible MODE: ansible
VER: 2.8.0 VER: 2.10.0
Ansible_280_35: Ansible_210_35:
python.version: '3.5' python.version: '3.5'
MODE: ansible MODE: ansible
VER: 2.8.0 VER: 2.10.0
Ansible_296_37:
python.version: '3.7'
MODE: ansible
VER: 2.9.6

@ -49,6 +49,10 @@ def have_apt():
proc = subprocess.Popen('apt --help >/dev/null 2>/dev/null', shell=True) proc = subprocess.Popen('apt --help >/dev/null 2>/dev/null', shell=True)
return proc.wait() == 0 return proc.wait() == 0
def have_brew():
proc = subprocess.Popen('brew help >/dev/null 2>/dev/null', shell=True)
return proc.wait() == 0
def have_docker(): def have_docker():
proc = subprocess.Popen('docker info >/dev/null 2>/dev/null', shell=True) proc = subprocess.Popen('docker info >/dev/null 2>/dev/null', shell=True)

@ -10,9 +10,11 @@ ci_lib.run_batches([
# Must be installed separately, as PyNACL indirect requirement causes # Must be installed separately, as PyNACL indirect requirement causes
# newer version to be installed if done in a single pip run. # newer version to be installed if done in a single pip run.
'pip install "pycparser<2.19"', 'pip install "pycparser<2.19"',
'pip install -qqqU debops==0.7.2 ansible==%s' % ci_lib.ANSIBLE_VERSION, 'pip install -qqq debops[ansible]==2.1.2 ansible==%s' % ci_lib.ANSIBLE_VERSION,
], ],
[ [
'docker pull %s' % (ci_lib.image_for_distro('debian'),), 'docker pull %s' % (ci_lib.image_for_distro('debian'),),
], ],
]) ])
ci_lib.run('ansible-galaxy collection install debops.debops:==2.1.2')

@ -26,12 +26,14 @@ with ci_lib.Fold('job_setup'):
ci_lib.run('debops-init %s', project_dir) ci_lib.run('debops-init %s', project_dir)
os.chdir(project_dir) os.chdir(project_dir)
ansible_strategy_plugin = "{}/ansible_mitogen/plugins/strategy".format(ci_lib.GIT_ROOT)
with open('.debops.cfg', 'w') as fp: with open('.debops.cfg', 'w') as fp:
fp.write( fp.write(
"[ansible defaults]\n" "[ansible defaults]\n"
"strategy_plugins = %s/ansible_mitogen/plugins/strategy\n" "strategy_plugins = {}\n"
"strategy = mitogen_linear\n" "strategy = mitogen_linear\n"
% (ci_lib.GIT_ROOT,) .format(ansible_strategy_plugin)
) )
with open(vars_path, 'w') as fp: with open(vars_path, 'w') as fp:

@ -7,7 +7,8 @@ batches = [
# Must be installed separately, as PyNACL indirect requirement causes # Must be installed separately, as PyNACL indirect requirement causes
# newer version to be installed if done in a single pip run. # newer version to be installed if done in a single pip run.
# Separately install ansible based on version passed in from azure-pipelines.yml or .travis.yml # Separately install ansible based on version passed in from azure-pipelines.yml or .travis.yml
'pip install "pycparser<2.19" "idna<2.7"', # Don't set -U as that will upgrade Paramiko to a non-2.6 compatible version.
'pip install "pycparser<2.19" "idna<2.7" virtualenv',
'pip install ' 'pip install '
'-r tests/requirements.txt ' '-r tests/requirements.txt '
'-r tests/ansible/requirements.txt', '-r tests/ansible/requirements.txt',

@ -20,12 +20,15 @@ with ci_lib.Fold('unit_tests'):
with ci_lib.Fold('job_setup'): 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)) os.chmod(KEY_PATH, int('0600', 8))
# NOTE: sshpass v1.06 causes errors so pegging to 1.05 -> "msg": "Error when changing password","out": "passwd: DS error: eDSAuthFailed\n",
# there's a checksum error with "brew install http://git.io/sshpass.rb" though, so installing manually
if not ci_lib.exists_in_path('sshpass'): if not ci_lib.exists_in_path('sshpass'):
run("brew install http://git.io/sshpass.rb") os.system("curl -O -L https://sourceforge.net/projects/sshpass/files/sshpass/1.05/sshpass-1.05.tar.gz && \
tar xvf sshpass-1.05.tar.gz && \
cd sshpass-1.05 && \
./configure && \
sudo make install")
with ci_lib.Fold('machine_prep'): with ci_lib.Fold('machine_prep'):

@ -30,8 +30,20 @@ if 0 and os.uname()[0] == 'Linux':
] ]
] ]
# setup venv, need all python commands in 1 list to be subprocessed at the same time
venv_steps = []
need_to_fix_psycopg2 = False
is_python3 = os.environ['PYTHONVERSION'].startswith('3')
# @dw: 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.
if ci_lib.have_apt(): if ci_lib.have_apt():
batches.append([ venv_steps.extend([
'echo force-unsafe-io | sudo tee /etc/dpkg/dpkg.cfg.d/nosync', 'echo force-unsafe-io | sudo tee /etc/dpkg/dpkg.cfg.d/nosync',
'sudo add-apt-repository ppa:deadsnakes/ppa', 'sudo add-apt-repository ppa:deadsnakes/ppa',
'sudo apt-get update', 'sudo apt-get update',
@ -40,8 +52,39 @@ if ci_lib.have_apt():
'python{pv}-dev ' 'python{pv}-dev '
'libsasl2-dev ' 'libsasl2-dev '
'libldap2-dev ' 'libldap2-dev '
.format(pv=os.environ['PYTHONVERSION']) .format(pv=os.environ['PYTHONVERSION']),
'sudo ln -fs /usr/bin/python{pv} /usr/local/bin/python{pv}'
.format(pv=os.environ['PYTHONVERSION'])
])
if is_python3:
venv_steps.append('sudo apt-get -y install python{pv}-venv'.format(pv=os.environ['PYTHONVERSION']))
# TODO: somehow `Mito36CentOS6_26` has both brew and apt installed https://dev.azure.com/dw-mitogen/Mitogen/_build/results?buildId=1031&view=logs&j=7bdbcdc6-3d3e-568d-ccf8-9ddca1a9623a&t=73d379b6-4eea-540f-c97e-046a2f620483
elif is_python3 and ci_lib.have_brew():
# Mac's System Integrity Protection prevents symlinking /usr/bin
# and Azure isn't allowing disabling it apparently: https://developercommunityapi.westus.cloudapp.azure.com/idea/558702/allow-disabling-sip-on-microsoft-hosted-macos-agen.html
# so we'll use /usr/local/bin/python for everything
# /usr/local/bin/python2.7 already exists!
need_to_fix_psycopg2 = True
venv_steps.append(
'brew install python@{pv} postgresql'
.format(pv=os.environ['PYTHONVERSION'])
)
# need wheel before building virtualenv because of bdist_wheel and setuptools deps
venv_steps.append('/usr/local/bin/python{pv} -m pip install -U pip wheel setuptools'.format(pv=os.environ['PYTHONVERSION']))
if os.environ['PYTHONVERSION'].startswith('2'):
venv_steps.extend([
'/usr/local/bin/python{pv} -m pip install -U virtualenv'.format(pv=os.environ['PYTHONVERSION']),
'/usr/local/bin/python{pv} -m virtualenv /tmp/venv -p /usr/local/bin/python{pv}'.format(pv=os.environ['PYTHONVERSION'])
]) ])
else:
venv_steps.append('/usr/local/bin/python{pv} -m venv /tmp/venv'.format(pv=os.environ['PYTHONVERSION']))
# fixes https://stackoverflow.com/questions/59595649/can-not-install-psycopg2-on-macos-catalina https://github.com/Azure/azure-cli/issues/12854#issuecomment-619213863
if need_to_fix_psycopg2:
venv_steps.append('/tmp/venv/bin/pip3 install psycopg2==2.8.5 psycopg2-binary')
batches.append(venv_steps)
if ci_lib.have_docker(): if ci_lib.have_docker():

@ -0,0 +1,35 @@
#!/bin/bash
# workaround from https://stackoverflow.com/a/26082445 to handle Travis 4MB log limit
set -e
export PING_SLEEP=30s
export WORKDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
export BUILD_OUTPUT=$WORKDIR/build.out
touch $BUILD_OUTPUT
dump_output() {
echo Tailing the last 1000 lines of output:
tail -1000 $BUILD_OUTPUT
}
error_handler() {
echo ERROR: An error was encountered with the build.
dump_output
kill $PING_LOOP_PID
exit 1
}
# If an error occurs, run our error handler to output a tail of the build
trap 'error_handler' ERR
# Set up a repeating loop to send some output to Travis.
bash -c "while true; do echo \$(date) - building ...; sleep $PING_SLEEP; done" &
PING_LOOP_PID=$!
.ci/${MODE}_tests.py >> $BUILD_OUTPUT 2>&1
# The build finished without returning an error so dump a tail of the output
dump_output
# nicely terminate the ping output loop
kill $PING_LOOP_PID

@ -18,75 +18,65 @@ cache:
install: install:
- grep -Erl git-lfs\|couchdb /etc/apt | sudo xargs rm -v - grep -Erl git-lfs\|couchdb /etc/apt | sudo xargs rm -v
- pip install -U pip==20.2.1
- .ci/${MODE}_install.py - .ci/${MODE}_install.py
# Travis has a 4MB log limit (https://github.com/travis-ci/travis-ci/issues/1382), but verbose Mitogen logs run larger than that
# in order to keep verbosity to debug a build failure, will run with this workaround: https://stackoverflow.com/a/26082445
script: script:
- .ci/spawn_reverse_shell.py - .ci/spawn_reverse_shell.py
- .ci/${MODE}_tests.py - MODE=${MODE} .ci/travis.sh
# To avoid matrix explosion, just test against oldest->newest and # To avoid matrix explosion, just test against oldest->newest and
# newest->oldest in various configuartions. # newest->oldest in various configuartions.
matrix: matrix:
allow_failures:
# Python 2.4 tests are still unreliable
- language: c
env: MODE=mitogen_py24 DISTRO=centos5
include: include:
# Debops tests. # Debops tests.
# 2.9.6; 3.6 -> 2.7 # NOTE: debops tests turned off for Ansible 2.10: https://github.com/debops/debops/issues/1521
- python: "3.6" # 2.10; 3.6 -> 2.7
env: MODE=debops_common VER=2.9.6 # - python: "3.6"
# 2.8.3; 3.6 -> 2.7 # env: MODE=debops_common VER=2.10.0
- python: "3.6" # 2.10; 2.7 -> 2.7
env: MODE=debops_common VER=2.8.3 # - python: "2.7"
# 2.4.6.0; 2.7 -> 2.7 # env: MODE=debops_common VER=2.10.0
- python: "2.7"
env: MODE=debops_common VER=2.4.6.0
# Sanity check against vanilla Ansible. One job suffices. # Sanity check against vanilla Ansible. One job suffices.
- python: "2.7" # https://github.com/dw/mitogen/pull/715#issuecomment-719266420 migrating to Azure for now due to Travis 50 min time limit cap
env: MODE=ansible VER=2.8.3 DISTROS=debian STRATEGY=linear # azure lets us adjust the cap, and the current STRATEGY=linear tests take up to 1.5 hours to finish
# - python: "2.7"
# env: MODE=ansible VER=2.10.0 DISTROS=debian STRATEGY=linear
# ansible_mitogen tests. # ansible_mitogen tests.
# 2.9.6 -> {debian, centos6, centos7} # 2.10 -> {debian, centos6, centos7}
- python: "3.6"
env: MODE=ansible VER=2.9.6
# 2.8.3 -> {debian, centos6, centos7}
- python: "3.6" - python: "3.6"
env: MODE=ansible VER=2.8.3 env: MODE=ansible VER=2.10.0
# 2.8.3 -> {debian, centos6, centos7} # 2.10 -> {debian, centos6, centos7}
- python: "2.7" - python: "2.7"
env: MODE=ansible VER=2.8.3 env: MODE=ansible VER=2.10.0
# 2.10 -> {debian, centos6, centos7}
# 2.4.6.0 -> {debian, centos6, centos7} # - python: "2.6"
- python: "3.6" # env: MODE=ansible VER=2.10.0
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} # 2.10 -> {centos5}
- python: "2.6" # - python: "2.6"
env: MODE=ansible VER=2.3.3.0 DISTROS=centos5 # env: MODE=ansible DISTROS=centos5 VER=2.10.0
# Mitogen tests. # Mitogen tests.
# 2.4 -> 2.4 # 2.4 -> 2.4
- language: c # - language: c
env: MODE=mitogen_py24 DISTRO=centos5 # env: MODE=mitogen_py24 DISTROS=centos5 VER=2.10.0
# 2.7 -> 2.7 -- moved to Azure # 2.7 -> 2.7 -- moved to Azure
# 2.7 -> 2.6 # 2.7 -> 2.6
#- python: "2.7" #- python: "2.7"
#env: MODE=mitogen DISTRO=centos6 #env: MODE=mitogen DISTRO=centos6
- python: "3.6" - python: "3.6"
env: MODE=mitogen DISTRO=centos7 env: MODE=mitogen DISTROS=centos7 VER=2.10.0
# 2.6 -> 2.7 # 2.6 -> 2.7
- python: "2.6" # - python: "2.6"
env: MODE=mitogen DISTRO=centos7 # env: MODE=mitogen DISTROS=centos7 VER=2.10.0
# 2.6 -> 3.5 # 2.6 -> 3.5
- python: "2.6" # - python: "2.6"
env: MODE=mitogen DISTRO=debian-py3 # env: MODE=mitogen DISTROS=debian-py3 VER=2.10.0
# 3.6 -> 2.6 -- moved to Azure # 3.6 -> 2.6 -- moved to Azure

@ -59,4 +59,6 @@ except ImportError: # Ansible <2.4
# These are original, unwrapped implementations # These are original, unwrapped implementations
action_loader__get = action_loader.get action_loader__get = action_loader.get
connection_loader__get = connection_loader.get # NOTE: this used to be `connection_loader.get`; breaking change unless we do a hack based on
# ansible version again
connection_loader__get = connection_loader.get_with_context

@ -375,7 +375,7 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
# wait_for_connection, the `ping` test from Ansible won't pass because we lost connection # wait_for_connection, the `ping` test from Ansible won't pass because we lost connection
# clearing out context forces a reconnect # clearing out context forces a reconnect
# see https://github.com/dw/mitogen/issues/655 and Ansible's `wait_for_connection` module for more info # see https://github.com/dw/mitogen/issues/655 and Ansible's `wait_for_connection` module for more info
if module_name == 'ping' and type(self).__name__ == 'wait_for_connection': if module_name == 'ansible.legacy.ping' and type(self).__name__ == 'wait_for_connection':
self._connection.context = None self._connection.context = None
self._connection._connect() self._connection._connect()

@ -43,6 +43,7 @@ import os
import random import random
from ansible.executor import module_common from ansible.executor import module_common
from ansible.collections.list import list_collection_dirs
import ansible.errors import ansible.errors
import ansible.module_utils import ansible.module_utils
import ansible.release import ansible.release
@ -57,7 +58,8 @@ import ansible_mitogen.target
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
NO_METHOD_MSG = 'Mitogen: no invocation method found for: ' NO_METHOD_MSG = 'Mitogen: no invocation method found for: '
NO_INTERPRETER_MSG = 'module (%s) is missing interpreter line' NO_INTERPRETER_MSG = 'module (%s) is missing interpreter line'
NO_MODULE_MSG = 'The module %s was not found in configured module paths.' # NOTE: Ansible 2.10 no longer has a `.` at the end of NO_MODULE_MSG error
NO_MODULE_MSG = 'The module %s was not found in configured module paths'
_planner_by_path = {} _planner_by_path = {}
@ -99,6 +101,10 @@ class Invocation(object):
#: Initially ``{}``, but set by :func:`invoke`. Optional source to send #: Initially ``{}``, but set by :func:`invoke`. Optional source to send
#: to :func:`propagate_paths_and_modules` to fix Python3.5 relative import errors #: to :func:`propagate_paths_and_modules` to fix Python3.5 relative import errors
self._overridden_sources = {} self._overridden_sources = {}
#: Initially ``set()``, but set by :func:`invoke`. Optional source paths to send
#: to :func:`propagate_paths_and_modules` to handle loading source dependencies from
#: places outside of the main source path, such as collections
self._extra_sys_paths = set()
def get_module_source(self): def get_module_source(self):
if self._module_source is None: if self._module_source is None:
@ -478,8 +484,10 @@ def _propagate_deps(invocation, planner, context):
context=context, context=context,
paths=planner.get_push_files(), paths=planner.get_push_files(),
modules=planner.get_module_deps(), # modules=planner.get_module_deps(), TODO
overridden_sources=invocation._overridden_sources overridden_sources=invocation._overridden_sources,
# needs to be a list because can't unpickle() a set()
extra_sys_paths=list(invocation._extra_sys_paths),
) )
@ -545,18 +553,29 @@ def _fix_py35(invocation, module_source):
We replace a relative import in the setup module with the actual full file path We replace a relative import in the setup module with the actual full file path
This works in vanilla Ansible but not in Mitogen otherwise This works in vanilla Ansible but not in Mitogen otherwise
""" """
if invocation.module_name == 'setup' and \ if invocation.module_name in {'ansible.builtin.setup', 'setup'} and \
invocation.module_path not in invocation._overridden_sources: invocation.module_path not in invocation._overridden_sources:
# in-memory replacement of setup module's relative import # in-memory replacement of setup module's relative import
# would check for just python3.5 and run this then but we don't know the # would check for just python3.5 and run this then but we don't know the
# target python at this time yet # target python at this time yet
# NOTE: another ansible 2.10-specific fix: `from ..module_utils` used to be `from ...module_utils`
module_source = module_source.replace( module_source = module_source.replace(
b"from ...module_utils.basic import AnsibleModule", b"from ..module_utils.basic import AnsibleModule",
b"from ansible.module_utils.basic import AnsibleModule" b"from ansible.module_utils.basic import AnsibleModule"
) )
invocation._overridden_sources[invocation.module_path] = module_source invocation._overridden_sources[invocation.module_path] = module_source
def _load_collections(invocation):
"""
Special loader that ensures that `ansible_collections` exist as a module path for import
Goes through all collection path possibilities and stores paths to installed collections
Stores them on the current invocation to later be passed to the master service
"""
for collection_path in list_collection_dirs():
invocation._extra_sys_paths.add(collection_path.decode('utf-8'))
def invoke(invocation): def invoke(invocation):
""" """
Find a Planner subclass corresponding to `invocation` and use it to invoke Find a Planner subclass corresponding to `invocation` and use it to invoke
@ -579,6 +598,9 @@ def invoke(invocation):
invocation.module_path = mitogen.core.to_text(path) invocation.module_path = mitogen.core.to_text(path)
if invocation.module_path not in _planner_by_path: if invocation.module_path not in _planner_by_path:
if 'ansible_collections' in invocation.module_path:
_load_collections(invocation)
module_source = invocation.get_module_source() module_source = invocation.get_module_source()
_fix_py35(invocation, module_source) _fix_py35(invocation, module_source)
_planner_by_path[invocation.module_path] = _get_planner( _planner_by_path[invocation.module_path] = _get_planner(

@ -53,8 +53,10 @@ except ImportError:
Sentinel = None Sentinel = None
ANSIBLE_VERSION_MIN = (2, 3) # TODO: might be possible to lower this back to 2.3 if collection support works without hacks
ANSIBLE_VERSION_MAX = (2, 9) ANSIBLE_VERSION_MIN = (2, 10)
ANSIBLE_VERSION_MAX = (2, 10)
NEW_VERSION_MSG = ( NEW_VERSION_MSG = (
"Your Ansible version (%s) is too recent. The most recent version\n" "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" "supported by Mitogen for Ansible is %s.x. Please check the Mitogen\n"
@ -132,8 +134,7 @@ def wrap_action_loader__get(name, *args, **kwargs):
get_kwargs = {'class_only': True} get_kwargs = {'class_only': True}
if name in ('fetch',): if name in ('fetch',):
name = 'mitogen_' + name name = 'mitogen_' + name
if ansible.__version__ >= '2.8': get_kwargs['collection_list'] = kwargs.pop('collection_list', None)
get_kwargs['collection_list'] = kwargs.pop('collection_list', None)
klass = ansible_mitogen.loaders.action_loader__get(name, **get_kwargs) klass = ansible_mitogen.loaders.action_loader__get(name, **get_kwargs)
if klass: if klass:
@ -217,7 +218,9 @@ class AnsibleWrappers(object):
with references to the real functions. with references to the real functions.
""" """
ansible_mitogen.loaders.action_loader.get = wrap_action_loader__get ansible_mitogen.loaders.action_loader.get = wrap_action_loader__get
ansible_mitogen.loaders.connection_loader.get = wrap_connection_loader__get # NOTE: this used to be `connection_loader.get`; breaking change unless we do a hack based on
# ansible version again
ansible_mitogen.loaders.connection_loader.get_with_context = wrap_connection_loader__get
global worker__run global worker__run
worker__run = ansible.executor.process.worker.WorkerProcess.run worker__run = ansible.executor.process.worker.WorkerProcess.run
@ -230,7 +233,7 @@ class AnsibleWrappers(object):
ansible_mitogen.loaders.action_loader.get = ( ansible_mitogen.loaders.action_loader.get = (
ansible_mitogen.loaders.action_loader__get ansible_mitogen.loaders.action_loader__get
) )
ansible_mitogen.loaders.connection_loader.get = ( ansible_mitogen.loaders.connection_loader.get_with_context = (
ansible_mitogen.loaders.connection_loader__get ansible_mitogen.loaders.connection_loader__get
) )
ansible.executor.process.worker.WorkerProcess.run = worker__run ansible.executor.process.worker.WorkerProcess.run = worker__run

@ -89,6 +89,14 @@ except NameError:
RLOG = logging.getLogger('mitogen.ctx') RLOG = logging.getLogger('mitogen.ctx')
# there are some cases where modules are loaded in memory only, such as
# ansible collections, and the module "filename" doesn't actually exist
SPECIAL_FILE_PATHS = {
"__synthetic__",
"<ansible_synthetic_collection_package>"
}
def _stdlib_paths(): def _stdlib_paths():
""" """
Return a set of paths from which Python imports the standard library. Return a set of paths from which Python imports the standard library.
@ -138,7 +146,7 @@ def is_stdlib_path(path):
) )
def get_child_modules(path): def get_child_modules(path, fullname):
""" """
Return the suffixes of submodules directly neated beneath of the package Return the suffixes of submodules directly neated beneath of the package
directory at `path`. directory at `path`.
@ -147,12 +155,19 @@ def get_child_modules(path):
Path to the module's source code on disk, or some PEP-302-recognized Path to the module's source code on disk, or some PEP-302-recognized
equivalent. Usually this is the module's ``__file__`` attribute, but equivalent. Usually this is the module's ``__file__`` attribute, but
is specified explicitly to avoid loading the module. is specified explicitly to avoid loading the module.
:param str fullname:
Name of the package we're trying to get child modules for
:return: :return:
List of submodule name suffixes. List of submodule name suffixes.
""" """
it = pkgutil.iter_modules([os.path.dirname(path)]) mod_path = os.path.dirname(path)
return [to_text(name) for _, name, _ in it] if mod_path != '':
return [to_text(name) for _, name, _ in pkgutil.iter_modules([mod_path])]
else:
# we loaded some weird package in memory, so we'll see if it has a custom loader we can use
loader = pkgutil.find_loader(fullname)
return [to_text(name) for name, _ in loader.iter_modules(None)] if loader else []
def _looks_like_script(path): def _looks_like_script(path):
@ -177,17 +192,31 @@ def _looks_like_script(path):
def _py_filename(path): def _py_filename(path):
"""
Returns a tuple of a Python path (if the file looks Pythonic) and whether or not
the Python path is special. Special file paths/modules might only exist in memory
"""
if not path: if not path:
return None return None, False
if path[-4:] in ('.pyc', '.pyo'): if path[-4:] in ('.pyc', '.pyo'):
path = path.rstrip('co') path = path.rstrip('co')
if path.endswith('.py'): if path.endswith('.py'):
return path return path, False
if os.path.exists(path) and _looks_like_script(path): if os.path.exists(path) and _looks_like_script(path):
return path return path, False
basepath = os.path.basename(path)
if basepath in SPECIAL_FILE_PATHS:
return path, True
# return None, False means that the filename passed to _py_filename does not appear
# to be python, and code later will handle when this function returns None
# see https://github.com/dw/mitogen/pull/715#discussion_r532380528 for how this
# decision was made to handle non-python files in this manner
return None, False
def _get_core_source(): def _get_core_source():
@ -498,9 +527,13 @@ class PkgutilMethod(FinderMethod):
return return
try: try:
path = _py_filename(loader.get_filename(fullname)) path, is_special = _py_filename(loader.get_filename(fullname))
source = loader.get_source(fullname) source = loader.get_source(fullname)
is_pkg = loader.is_package(fullname) is_pkg = loader.is_package(fullname)
# workaround for special python modules that might only exist in memory
if is_special and is_pkg and not source:
source = '\n'
except (AttributeError, ImportError): except (AttributeError, ImportError):
# - Per PEP-302, get_source() and is_package() are optional, # - Per PEP-302, get_source() and is_package() are optional,
# calling them may throw AttributeError. # calling them may throw AttributeError.
@ -549,7 +582,7 @@ class SysModulesMethod(FinderMethod):
fullname, alleged_name, module) fullname, alleged_name, module)
return return
path = _py_filename(getattr(module, '__file__', '')) path, _ = _py_filename(getattr(module, '__file__', ''))
if not path: if not path:
return return
@ -639,7 +672,7 @@ class ParentEnumerationMethod(FinderMethod):
def _found_module(self, fullname, path, fp, is_pkg=False): def _found_module(self, fullname, path, fp, is_pkg=False):
try: try:
path = _py_filename(path) path, _ = _py_filename(path)
if not path: if not path:
return return
@ -971,7 +1004,7 @@ class ModuleResponder(object):
self.minify_secs += mitogen.core.now() - t0 self.minify_secs += mitogen.core.now() - t0
if is_pkg: if is_pkg:
pkg_present = get_child_modules(path) pkg_present = get_child_modules(path, fullname)
self._log.debug('%s is a package at %s with submodules %r', self._log.debug('%s is a package at %s with submodules %r',
fullname, path, pkg_present) fullname, path, pkg_present)
else: else:

@ -42,6 +42,7 @@ import heapq
import inspect import inspect
import logging import logging
import os import os
import platform
import re import re
import signal import signal
import socket import socket
@ -1434,7 +1435,10 @@ class Connection(object):
os.close(r) os.close(r)
os.close(W) os.close(W)
os.close(w) os.close(w)
if sys.platform == 'darwin' and sys.executable == '/usr/bin/python': # this doesn't apply anymore to Mac OSX 10.15+ (Darwin 19+), new interpreter looks like this:
# /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
if sys.platform == 'darwin' and sys.executable == '/usr/bin/python' and \
int(platform.release()[:2]) < 19:
sys.executable += sys.version[:3] sys.executable += sys.version[:3]
os.environ['ARGV0']=sys.executable os.environ['ARGV0']=sys.executable
os.execl(sys.executable,sys.executable+'(mitogen:CONTEXT_NAME)') os.execl(sys.executable,sys.executable+'(mitogen:CONTEXT_NAME)')

@ -691,6 +691,7 @@ class PushFileService(Service):
super(PushFileService, self).__init__(**kwargs) super(PushFileService, self).__init__(**kwargs)
self._lock = threading.Lock() self._lock = threading.Lock()
self._cache = {} self._cache = {}
self._extra_sys_paths = set()
self._waiters = {} self._waiters = {}
self._sent_by_stream = {} self._sent_by_stream = {}
@ -744,21 +745,35 @@ class PushFileService(Service):
@arg_spec({ @arg_spec({
'context': mitogen.core.Context, 'context': mitogen.core.Context,
'paths': list, 'paths': list,
'modules': list, # 'modules': list, TODO, modules was passed into this func but it's not used yet
}) })
def propagate_paths_and_modules(self, context, paths, modules, overridden_sources=None): def propagate_paths_and_modules(self, context, paths, overridden_sources=None, extra_sys_paths=None):
""" """
One size fits all method to ensure a target context has been preloaded One size fits all method to ensure a target context has been preloaded
with a set of small files and Python modules. with a set of small files and Python modules.
overridden_sources: optional dict containing source code to override path's source code overridden_sources: optional dict containing source code to override path's source code
extra_sys_paths: loads additional sys paths for use in finding modules; beneficial
in situations like loading Ansible Collections because source code
dependencies come from different file paths than where the source lives
""" """
for path in paths: for path in paths:
overridden_source = None overridden_source = None
if overridden_sources is not None and path in overridden_sources: if overridden_sources is not None and path in overridden_sources:
overridden_source = overridden_sources[path] overridden_source = overridden_sources[path]
self.propagate_to(context, mitogen.core.to_text(path), overridden_source) self.propagate_to(context, mitogen.core.to_text(path), overridden_source)
#self.router.responder.forward_modules(context, modules) TODO # self.router.responder.forward_modules(context, modules) TODO
# NOTE: could possibly be handled by the above TODO, but not sure how forward_modules works enough
# to know for sure, so for now going to pass the sys paths themselves and have `propagate_to`
# load them up in sys.path for later import
# ensure we don't add to sys.path the same path we've already seen
for extra_path in extra_sys_paths:
# store extra paths in cached set for O(1) lookup
if extra_path not in self._extra_sys_paths:
# not sure if it matters but we could prepend to sys.path instead if we need to
sys.path.append(extra_path)
self._extra_sys_paths.add(extra_path)
@expose(policy=AllowParents()) @expose(policy=AllowParents())
@arg_spec({ @arg_spec({

@ -21,5 +21,6 @@
copy: copy:
src: "{{item.src}}" src: "{{item.src}}"
dest: "/tmp/filetree.out/{{item.path}}" dest: "/tmp/filetree.out/{{item.path}}"
mode: 0644
with_filetree: /tmp/filetree.in with_filetree: /tmp/filetree.in
when: item.state == 'file' when: item.state == 'file'

@ -1,18 +1,12 @@
# Verify action plugins still set file modes correctly even though # Verify action plugins still set file modes correctly even though
# fixup_perms2() avoids setting execute bit despite being asked to. # fixup_perms2() avoids setting execute bit despite being asked to.
# As of Ansible 2.10.0, default perms vary based on OS. On debian systems it's 0644 and on centos it's 0664 based on test output
# regardless, we're testing that no execute bit is set here so either check is ok
- name: integration/action/fixup_perms2__copy.yml - name: integration/action/fixup_perms2__copy.yml
hosts: test-targets hosts: test-targets
any_errors_fatal: true any_errors_fatal: true
tasks: tasks:
- name: Get default remote file mode
shell: python -c 'import os; print("%04o" % (int("0666", 8) & ~os.umask(0)))'
register: py_umask
- name: Set default file mode
set_fact:
mode: "{{py_umask.stdout}}"
# #
# copy module (no mode). # copy module (no mode).
# #
@ -26,7 +20,7 @@
register: out register: out
- assert: - assert:
that: that:
- out.stat.mode == mode - out.stat.mode in ("0644", "0664")
# #
# copy module (explicit mode). # copy module (explicit mode).
@ -68,7 +62,7 @@
register: out register: out
- assert: - assert:
that: that:
- out.stat.mode == mode - out.stat.mode in ("0644", "0664")
# #
# copy module (existing disk files, preserve mode). # copy module (existing disk files, preserve mode).

@ -148,16 +148,7 @@
custom_python_detect_environment: custom_python_detect_environment:
register: out register: out
# v2.6 related: https://github.com/ansible/ansible/pull/39833 - name: "Verify modules get the same tmpdir as the action plugin"
- name: "Verify modules get the same tmpdir as the action plugin (<2.5)"
when: ansible_version.full < '2.5'
assert:
that:
- out.module_path.startswith(good_temp_path2)
- out.module_tmpdir == None
- name: "Verify modules get the same tmpdir as the action plugin (>2.5)"
when: ansible_version.full > '2.5'
assert: assert:
that: that:
- out.module_path.startswith(good_temp_path2) - out.module_path.startswith(good_temp_path2)

@ -40,19 +40,27 @@
# state: absent # state: absent
# become: true # become: true
- synchronize: # exception: File "/tmp/venv/lib/python2.7/site-packages/ansible/plugins/action/__init__.py", line 129, in cleanup
private_key: /tmp/synchronize-action-key # exception: self._remove_tmp_path(self._connection._shell.tmpdir)
dest: /tmp/sync-test.out # exception: AttributeError: 'get_with_context_result' object has no attribute '_shell'
src: /tmp/sync-test/ # TODO: looks like a bug on Ansible's end with 2.10? Maybe 2.10.1 will fix it
# https://github.com/dw/mitogen/issues/746
- name: do synchronize test
block:
- synchronize:
private_key: /tmp/synchronize-action-key
dest: /tmp/sync-test.out
src: /tmp/sync-test/
- slurp: - slurp:
src: /tmp/sync-test.out/item src: /tmp/sync-test.out/item
register: out register: out
- set_fact: outout="{{out.content|b64decode}}" - set_fact: outout="{{out.content|b64decode}}"
- assert: - assert:
that: outout == "item!" that: outout == "item!"
when: False
# TODO: https://github.com/dw/mitogen/issues/692 # TODO: https://github.com/dw/mitogen/issues/692
# - file: # - file:

@ -40,15 +40,14 @@
- result1.changed == True - result1.changed == True
# ansible/b72e989e1837ccad8dcdc926c43ccbc4d8cdfe44 # ansible/b72e989e1837ccad8dcdc926c43ccbc4d8cdfe44
- | - |
(ansible_version.full >= '2.8' and (ansible_version.full is version('2.8', ">=") and
result1.cmd == "echo alldone;\nsleep 1;\n") or result1.cmd == "echo alldone;\nsleep 1;\n") or
(ansible_version.full < '2.8' and (ansible_version.full is version('2.8', '<') and
result1.cmd == "echo alldone;\n sleep 1;") result1.cmd == "echo alldone;\n sleep 1;")
- result1.delta|length == 14 - result1.delta|length == 14
- result1.start|length == 26 - result1.start|length == 26
- result1.finished == 1 - result1.finished == 1
- result1.rc == 0 - result1.rc == 0
- result1.start|length == 26
- assert: - assert:
that: that:
@ -56,10 +55,9 @@
- result1.stderr_lines == [] - result1.stderr_lines == []
- result1.stdout == "alldone" - result1.stdout == "alldone"
- result1.stdout_lines == ["alldone"] - result1.stdout_lines == ["alldone"]
when: ansible_version.full > '2.8' # ansible#51393 when: ansible_version.full is version('2.8', '>') # ansible#51393
- assert: - assert:
that: that:
- result1.failed == False - result1.failed == False
when: ansible_version.full > '2.4' when: ansible_version.full is version('2.4', '>')

@ -1,12 +1,18 @@
# Ensure paramiko connections aren't grabbed. # Ensure paramiko connections aren't grabbed.
---
- name: integration/connection_loader/paramiko_unblemished.yml - name: integration/connection_loader/paramiko_unblemished.yml
hosts: test-targets hosts: test-targets
any_errors_fatal: true any_errors_fatal: true
tasks: tasks:
- custom_python_detect_environment: - debug:
connection: paramiko msg: "skipped for now"
register: out - name: this is flaky -> https://github.com/dw/mitogen/issues/747
block:
- custom_python_detect_environment:
connection: paramiko
register: out
- assert: - assert:
that: not out.mitogen_loaded that: not out.mitogen_loaded
when: False

@ -14,8 +14,8 @@
- out.rc == 1 - out.rc == 1
# ansible/62d8c8fde6a76d9c567ded381e9b34dad69afcd6 # ansible/62d8c8fde6a76d9c567ded381e9b34dad69afcd6
- | - |
(ansible_version.full < '2.7' and out.msg == "MODULE FAILURE") or (ansible_version.full is version('2.7', '<') and out.msg == "MODULE FAILURE") or
(ansible_version.full >= '2.7' and (ansible_version.full is version('2.7', '>=') and
out.msg == ( out.msg == (
"MODULE FAILURE\n" + "MODULE FAILURE\n" +
"See stdout/stderr for the exact error" "See stdout/stderr for the exact error"

@ -2,6 +2,10 @@
hosts: test-targets hosts: test-targets
any_errors_fatal: true any_errors_fatal: true
tasks: tasks:
# without Mitogen Ansible 2.10 hangs on this play
- meta: end_play
when: not is_mitogen
- custom_python_new_style_module: - custom_python_new_style_module:
foo: true foo: true
with_sequence: start=0 end={{end|default(1)}} with_sequence: start=0 end={{end|default(1)}}

@ -16,4 +16,4 @@
- assert: - assert:
that: | that: |
'The module missing_module was not found in configured module paths.' in out.stdout 'The module missing_module was not found in configured module paths' in out.stdout

@ -113,7 +113,8 @@
# ansible_become_pass & ansible_become_password set, password takes precedence # ansible_become_pass & ansible_become_password set, password used to take precedence
# but it's possible since https://github.com/ansible/ansible/pull/69629/files#r428376864, now it doesn't
- hosts: tc-become-pass-both - hosts: tc-become-pass-both
become: true become: true
tasks: tasks:
@ -124,7 +125,7 @@
- out.result|length == 2 - out.result|length == 2
- out.result[0].method == "ssh" - out.result[0].method == "ssh"
- out.result[1].method == "sudo" - out.result[1].method == "sudo"
- out.result[1].kwargs.password == "a.b.c" - out.result[1].kwargs.password == "c.b.a"
# both, mitogen_via # both, mitogen_via

@ -2,8 +2,11 @@
import sys import sys
# This is the magic marker Ansible looks for: # As of Ansible 2.10, Ansible changed new-style detection: # https://github.com/ansible/ansible/pull/61196/files#diff-5675e463b6ce1fbe274e5e7453f83cd71e61091ea211513c93e7c0b4d527d637L828-R980
# NOTE: this import works for Mitogen, and the import below matches new-style Ansible 2.10
# TODO: find out why 1 import won't work for both Mitogen and Ansible
# from ansible.module_utils. # from ansible.module_utils.
# import ansible.module_utils.
def usage(): def usage():

@ -9,6 +9,7 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import platform
import sys import sys
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
@ -23,7 +24,12 @@ def main():
result['ansible_facts'] = module.params['facts'] result['ansible_facts'] = module.params['facts']
# revert the Mitogen OSX tweak since discover_interpreter() doesn't return this info # revert the Mitogen OSX tweak since discover_interpreter() doesn't return this info
if sys.platform == 'darwin' and sys.executable != '/usr/bin/python': if sys.platform == 'darwin' and sys.executable != '/usr/bin/python':
sys.executable = sys.executable[:-3] if int(platform.release()[:2]) < 19:
sys.executable = sys.executable[:-3]
else:
# only for tests to check version of running interpreter -- Mac 10.15+ changed python2
# so it looks like it's /usr/bin/python but actually it's /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
sys.executable = "/usr/bin/python"
result['running_python_interpreter'] = sys.executable result['running_python_interpreter'] = sys.executable
module.exit_json(**result) module.exit_json(**result)

@ -26,5 +26,6 @@
copy: copy:
src: "{{item.src}}" src: "{{item.src}}"
dest: "/tmp/filetree.out/{{item.path}}" dest: "/tmp/filetree.out/{{item.path}}"
mode: 0644
with_filetree: /tmp/filetree.in with_filetree: /tmp/filetree.in
when: item.state == 'file' when: item.state == 'file'

@ -1,11 +1,9 @@
#!/usr/bin/env python #!/usr/bin/env python
# Wrap ansible-playbook, setting up some test of the test environment. # Wrap ansible-playbook, setting up some test of the test environment.
import json import json
import os import os
import sys import sys
GIT_BASEDIR = os.path.dirname( GIT_BASEDIR = os.path.dirname(
os.path.abspath( os.path.abspath(
os.path.join(__file__, '..', '..') os.path.join(__file__, '..', '..')

@ -3,7 +3,7 @@ coverage==4.5.1
Django==1.6.11 # Last version supporting 2.6. Django==1.6.11 # Last version supporting 2.6.
mock==2.0.0 mock==2.0.0
pytz==2018.5 pytz==2018.5
cffi==1.11.2 # Random pin to try and fix pyparser==2.18 not having effect cffi==1.14.3 # Random pin to try and fix pyparser==2.18 not having effect
pycparser==2.18 # Last version supporting 2.6. pycparser==2.18 # Last version supporting 2.6.
faulthandler==3.1; python_version < '3.3' # used by testlib faulthandler==3.1; python_version < '3.3' # used by testlib
pytest-catchlog==1.2.2 pytest-catchlog==1.2.2

Loading…
Cancel
Save