Merge remote-tracking branch 'origin/master' into issue1086

pull/1094/head
timansky 2 days ago
commit 88eb64745e

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

@ -35,13 +35,13 @@ ci_lib.check_stray_processes(interesting)
with ci_lib.Fold('docker_setup'): with ci_lib.Fold('docker_setup'):
containers = ci_lib.container_specs(ci_lib.DISTROS) containers = ci_lib.container_specs(ci_lib.DISTRO_SPECS.split())
ci_lib.start_containers(containers) ci_lib.start_containers(containers)
with ci_lib.Fold('job_setup'): with ci_lib.Fold('job_setup'):
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', 8))
ci_lib.run("mkdir %s", HOSTS_DIR) ci_lib.run("mkdir %s", HOSTS_DIR)
for path in glob.glob(TESTS_DIR + '/hosts/*'): for path in glob.glob(TESTS_DIR + '/hosts/*'):

@ -1,105 +0,0 @@
# Each step entry runs a task (Azure Pipelines analog of an Ansible module).
# https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/?view=azure-pipelines&viewFallbackFrom=azure-devops#tool
# `{script: ...}` is shorthand for `{task: CmdLine@<mumble>, inputs: {script: ...}}`.
# The shell is bash.
# https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/steps-script?view=azure-pipelines
# https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/cmd-line-v2?view=azure-pipelines
steps:
- task: UsePythonVersion@0
displayName: Install python
inputs:
githubToken: '$(GITHUB_PYVER_TOKEN)'
versionSpec: '$(python.version)'
condition: ne(variables['python.version'], '')
- script: |
set -o errexit
set -o nounset
set -o pipefail
aws ecr-public get-login-password | docker login --username AWS --password-stdin public.ecr.aws
displayName: Authenticate to container registry
condition: eq(variables['Agent.OS'], 'Linux')
env:
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
AWS_DEFAULT_REGION: $(AWS_DEFAULT_REGION)
- script: |
set -o errexit
set -o nounset
set -o pipefail
sudo apt-get update
sudo apt-get install -y python2-dev python3-pip virtualenv
displayName: Install build deps
condition: and(eq(variables['python.version'], ''), eq(variables['Agent.OS'], 'Linux'))
- script: |
set -o errexit
set -o nounset
set -o pipefail
# macOS builders lack a realpath command
type python && python -c"import os.path;print(os.path.realpath('$(type -p python)'))" && python --version
type python2 && python2 -c"import os.path;print(os.path.realpath('$(type -p python2)'))" && python2 --version
type python3 && python3 -c"import os.path;print(os.path.realpath('$(type -p python3)'))" && python3 --version
echo
if [ -e /usr/bin/python ]; then
echo "/usr/bin/python: sys.executable: $(/usr/bin/python -c 'import sys; print(sys.executable)')"
fi
if [ -e /usr/bin/python2 ]; then
echo "/usr/bin/python2: sys.executable: $(/usr/bin/python2 -c 'import sys; print(sys.executable)')"
fi
if [ -e /usr/bin/python2.7 ]; then
echo "/usr/bin/python2.7: sys.executable: $(/usr/bin/python2.7 -c 'import sys; print(sys.executable)')"
fi
displayName: Show python versions
- script: |
set -o errexit
set -o nounset
set -o pipefail
# Tox environment name (e.g. py312-mode_mitogen) -> Python executable name (e.g. python3.12)
PYTHON=$(python -c 'import re; print(re.sub(r"^py([23])([0-9]{1,2}).*", r"python\1.\2", "$(tox.env)"))')
if [[ -z $PYTHON ]]; then
echo 1>&2 "Python interpreter could not be determined"
exit 1
fi
if [[ $PYTHON == "python2.7" && $(uname) == "Darwin" ]]; then
"$PYTHON" -m ensurepip --user --altinstall --no-default-pip
"$PYTHON" -m pip install --user -r "tests/requirements-tox.txt"
elif [[ $PYTHON == "python2.7" ]]; then
curl "https://bootstrap.pypa.io/pip/2.7/get-pip.py" --output "get-pip.py"
"$PYTHON" get-pip.py --user --no-python-version-warning
# Avoid Python 2.x pip masking system pip
rm -f ~/.local/bin/{easy_install,pip,wheel}
"$PYTHON" -m pip install --user -r "tests/requirements-tox.txt"
else
"$PYTHON" -m pip install -r "tests/requirements-tox.txt"
fi
displayName: Install tooling
- script: |
set -o errexit
set -o nounset
set -o pipefail
# Tox environment name (e.g. py312-mode_mitogen) -> Python executable name (e.g. python3.12)
PYTHON=$(python -c 'import re; print(re.sub(r"^py([23])([0-9]{1,2}).*", r"python\1.\2", "$(tox.env)"))')
if [[ -z $PYTHON ]]; then
echo 1>&2 "Python interpreter could not be determined"
exit 1
fi
"$PYTHON" -m tox -e "$(tox.env)"
displayName: "Run tests"

@ -1,157 +0,0 @@
# Python package
# Create and test a Python package on multiple Python versions.
# Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/python
# User defined variables are also injected as environment variables
# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables#environment-variables
#variables:
#ANSIBLE_VERBOSITY: 3
trigger:
branches:
include:
- "*"
exclude:
- docs-master
jobs:
- job: mac12
# vanilla Ansible is really slow
timeoutInMinutes: 120
steps:
- template: azure-pipelines-steps.yml
pool:
# https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md
vmImage: macOS-12
strategy:
matrix:
Mito_312:
tox.env: py312-mode_mitogen
Loc_312_10:
tox.env: py312-mode_localhost-ansible10
Van_312_10:
tox.env: py312-mode_localhost-ansible10-strategy_linear
- job: Linux
pool:
# https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2004-Readme.md
vmImage: ubuntu-20.04
steps:
- template: azure-pipelines-steps.yml
strategy:
matrix:
Mito_27_centos6:
tox.env: py27-mode_mitogen-distro_centos6
Mito_27_centos7:
tox.env: py27-mode_mitogen-distro_centos7
Mito_27_centos8:
tox.env: py27-mode_mitogen-distro_centos8
Mito_27_debian9:
tox.env: py27-mode_mitogen-distro_debian9
Mito_27_debian10:
tox.env: py27-mode_mitogen-distro_debian10
Mito_27_debian11:
tox.env: py27-mode_mitogen-distro_debian11
Mito_27_ubuntu1604:
tox.env: py27-mode_mitogen-distro_ubuntu1604
Mito_27_ubuntu1804:
tox.env: py27-mode_mitogen-distro_ubuntu1804
Mito_27_ubuntu2004:
tox.env: py27-mode_mitogen-distro_ubuntu2004
Mito_36_centos6:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_centos6
Mito_36_centos7:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_centos7
Mito_36_centos8:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_centos8
Mito_36_debian9:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_debian9
Mito_36_debian10:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_debian10
Mito_36_debian11:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_debian11
Mito_36_ubuntu1604:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_ubuntu1604
Mito_36_ubuntu1804:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_ubuntu1804
Mito_36_ubuntu2004:
python.version: '3.6'
tox.env: py36-mode_mitogen-distro_ubuntu2004
Mito_312_centos6:
python.version: '3.12'
tox.env: py312-mode_mitogen-distro_centos6
Mito_312_centos7:
python.version: '3.12'
tox.env: py312-mode_mitogen-distro_centos7
Mito_312_centos8:
python.version: '3.12'
tox.env: py312-mode_mitogen-distro_centos8
Mito_312_debian9:
python.version: '3.12'
tox.env: py312-mode_mitogen-distro_debian9
Mito_312_debian10:
python.version: '3.12'
tox.env: py312-mode_mitogen-distro_debian10
Mito_312_debian11:
python.version: '3.12'
tox.env: py312-mode_mitogen-distro_debian11
Mito_312_ubuntu1604:
python.version: '3.12'
tox.env: py312-mode_mitogen-distro_ubuntu1604
Mito_312_ubuntu1804:
python.version: '3.12'
tox.env: py312-mode_mitogen-distro_ubuntu1804
Mito_312_ubuntu2004:
python.version: '3.12'
tox.env: py312-mode_mitogen-distro_ubuntu2004
Ans_27_210:
tox.env: py27-mode_ansible-ansible2.10
Ans_27_4:
tox.env: py27-mode_ansible-ansible4
Ans_36_210:
python.version: '3.6'
tox.env: py36-mode_ansible-ansible2.10
Ans_36_4:
python.version: '3.6'
tox.env: py36-mode_ansible-ansible4
Ans_311_210:
python.version: '3.11'
tox.env: py311-mode_ansible-ansible2.10
Ans_311_3:
python.version: '3.11'
tox.env: py311-mode_ansible-ansible3
Ans_311_4:
python.version: '3.11'
tox.env: py311-mode_ansible-ansible4
Ans_311_5:
python.version: '3.11'
tox.env: py311-mode_ansible-ansible5
Ans_312_6:
python.version: '3.12'
tox.env: py312-mode_ansible-ansible6
Ans_312_7:
python.version: '3.12'
tox.env: py312-mode_ansible-ansible7
Ans_312_8:
python.version: '3.12'
tox.env: py312-mode_ansible-ansible8
Ans_312_9:
python.version: '3.12'
tox.env: py312-mode_ansible-ansible9
Ans_312_10:
python.version: '3.12'
tox.env: py312-mode_ansible-ansible10

@ -28,6 +28,10 @@ os.chdir(
) )
DISTRO_SPECS = os.environ.get(
'MITOGEN_TEST_DISTRO_SPECS',
'centos6 centos8 debian9 debian11 ubuntu1604 ubuntu2004',
)
IMAGE_TEMPLATE = os.environ.get( IMAGE_TEMPLATE = os.environ.get(
'MITOGEN_TEST_IMAGE_TEMPLATE', 'MITOGEN_TEST_IMAGE_TEMPLATE',
'public.ecr.aws/n5z0e8q9/%(distro)s-test', 'public.ecr.aws/n5z0e8q9/%(distro)s-test',
@ -196,10 +200,6 @@ class Fold(object):
GIT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) GIT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
# Used only when MODE=mitogen
DISTRO = os.environ.get('DISTRO', 'debian9')
# Used only when MODE=ansible
DISTROS = os.environ.get('DISTROS', 'centos6 centos8 debian9 debian11 ubuntu1604 ubuntu2004').split()
TMP = TempDir().path TMP = TempDir().path

@ -3,8 +3,6 @@
from __future__ import print_function from __future__ import print_function
import getpass
import io
import os import os
import subprocess import subprocess
import sys import sys
@ -53,22 +51,13 @@ with ci_lib.Fold('machine_prep'):
subprocess.check_call('sudo chmod 700 ~root/.ssh', shell=True) subprocess.check_call('sudo chmod 700 ~root/.ssh', shell=True)
subprocess.check_call('sudo chmod 600 ~root/.ssh/authorized_keys', shell=True) subprocess.check_call('sudo chmod 600 ~root/.ssh/authorized_keys', shell=True)
os.chdir(IMAGE_PREP_DIR)
ci_lib.run("ansible-playbook -c local -i localhost, macos_localhost.yml")
if os.path.expanduser('~mitogen__user1') == '~mitogen__user1': if os.path.expanduser('~mitogen__user1') == '~mitogen__user1':
os.chdir(IMAGE_PREP_DIR) os.chdir(IMAGE_PREP_DIR)
ci_lib.run("ansible-playbook -c local -i localhost, _user_accounts.yml") ci_lib.run("ansible-playbook -c local -i localhost, _user_accounts.yml")
# FIXME Don't hardcode https://github.com/mitogen-hq/mitogen/issues/1022
# and os.environ['USER'] is not populated on Azure macOS runners.
os.chdir(HOSTS_DIR)
with io.open('default.hosts', 'r+', encoding='utf-8') as f:
user = getpass.getuser()
content = f.read()
content = content.replace("{{ lookup('pipe', 'whoami') }}", user)
f.seek(0)
f.write(content)
f.truncate()
ci_lib.dump_file('default.hosts')
cmd = ';'.join([ cmd = ';'.join([
'from __future__ import print_function', 'from __future__ import print_function',
'import os, sys', 'import os, sys',

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

@ -6,7 +6,6 @@ import os
import ci_lib import ci_lib
os.environ.update({ os.environ.update({
'MITOGEN_TEST_DISTRO': ci_lib.DISTRO,
'MITOGEN_LOG_LEVEL': 'debug', 'MITOGEN_LOG_LEVEL': 'debug',
'SKIP_ANSIBLE': '1', 'SKIP_ANSIBLE': '1',
}) })

@ -48,99 +48,33 @@ jobs:
- name: Ans_311_5 - name: Ans_311_5
python_version: '3.11' python_version: '3.11'
tox_env: py311-mode_ansible-ansible5 tox_env: py311-mode_ansible-ansible5
- name: Ans_312_6 - name: Ans_313_6
python_version: '3.12' python_version: '3.13'
tox_env: py312-mode_ansible-ansible6 tox_env: py313-mode_ansible-ansible6
- name: Ans_312_7 - name: Ans_313_7
python_version: '3.12' python_version: '3.13'
tox_env: py312-mode_ansible-ansible7 tox_env: py313-mode_ansible-ansible7
- name: Ans_312_8 - name: Ans_313_8
python_version: '3.12' python_version: '3.13'
tox_env: py312-mode_ansible-ansible8 tox_env: py313-mode_ansible-ansible8
- name: Ans_312_9 - name: Ans_313_9
python_version: '3.12' python_version: '3.13'
tox_env: py312-mode_ansible-ansible9 tox_env: py313-mode_ansible-ansible9
- name: Ans_312_10 - name: Ans_313_10
python_version: '3.12' python_version: '3.13'
tox_env: py312-mode_ansible-ansible10 tox_env: py313-mode_ansible-ansible10
- name: Van_312_10 - name: Van_313_10
python_version: '3.12' python_version: '3.13'
tox_env: py312-mode_ansible-ansible10-strategy_linear tox_env: py313-mode_ansible-ansible10-strategy_linear
- name: Mito_27_centos6 - name: Mito_27
tox_env: py27-mode_mitogen-distro_centos6 tox_env: py27-mode_mitogen
- name: Mito_27_centos7 - name: Mito_36
tox_env: py27-mode_mitogen-distro_centos7
- name: Mito_27_centos8
tox_env: py27-mode_mitogen-distro_centos8
- name: Mito_27_debian9
tox_env: py27-mode_mitogen-distro_debian9
- name: Mito_27_debian10
tox_env: py27-mode_mitogen-distro_debian10
- name: Mito_27_debian11
tox_env: py27-mode_mitogen-distro_debian11
- name: Mito_27_ubuntu1604
tox_env: py27-mode_mitogen-distro_ubuntu1604
- name: Mito_27_ubuntu1804
tox_env: py27-mode_mitogen-distro_ubuntu1804
- name: Mito_27_ubuntu2004
tox_env: py27-mode_mitogen-distro_ubuntu2004
- name: Mito_36_centos6
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_centos6
- name: Mito_36_centos7
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_centos7
- name: Mito_36_centos8
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_centos8
- name: Mito_36_debian9
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_debian9
- name: Mito_36_debian10
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_debian10
- name: Mito_36_debian11
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_debian11
- name: Mito_36_ubuntu1604
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_ubuntu1604
- name: Mito_36_ubuntu1804
python_version: '3.6'
tox_env: py36-mode_mitogen-distro_ubuntu1804
- name: Mito_36_ubuntu2004
python_version: '3.6' python_version: '3.6'
tox_env: py36-mode_mitogen-distro_ubuntu2004 tox_env: py36-mode_mitogen
- name: Mito_313
- name: Mito_312_centos6 python_version: '3.13'
python_version: '3.12' tox_env: py313-mode_mitogen
tox_env: py312-mode_mitogen-distro_centos6
- name: Mito_312_centos7
python_version: '3.12'
tox_env: py312-mode_mitogen-distro_centos7
- name: Mito_312_centos8
python_version: '3.12'
tox_env: py312-mode_mitogen-distro_centos8
- name: Mito_312_debian9
python_version: '3.12'
tox_env: py312-mode_mitogen-distro_debian9
- name: Mito_312_debian10
python_version: '3.12'
tox_env: py312-mode_mitogen-distro_debian10
- name: Mito_312_debian11
python_version: '3.12'
tox_env: py312-mode_mitogen-distro_debian11
- name: Mito_312_ubuntu1604
python_version: '3.12'
tox_env: py312-mode_mitogen-distro_ubuntu1604
- name: Mito_312_ubuntu1804
python_version: '3.12'
tox_env: py312-mode_mitogen-distro_ubuntu1804
- name: Mito_312_ubuntu2004
python_version: '3.12'
tox_env: py312-mode_mitogen-distro_ubuntu2004
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -222,28 +156,22 @@ jobs:
"$PYTHON" -m tox -e "${{ matrix.tox_env }}" "$PYTHON" -m tox -e "${{ matrix.tox_env }}"
macos: macos:
# https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md # https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md
runs-on: macos-12 runs-on: macos-13
timeout-minutes: 120 timeout-minutes: 120
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- name: Mito_27 - name: Mito_313
tox_env: py27-mode_mitogen tox_env: py313-mode_mitogen
- name: Mito_312
tox_env: py312-mode_mitogen
- name: Loc_27_210 - name: Loc_313_10
tox_env: py27-mode_localhost-ansible2.10 tox_env: py313-mode_localhost-ansible10
- name: Loc_312_10
tox_env: py312-mode_localhost-ansible10
- name: Van_27_210 - name: Van_313_10
tox_env: py27-mode_localhost-ansible2.10-strategy_linear tox_env: py313-mode_localhost-ansible10-strategy_linear
- name: Van_312_10
tox_env: py312-mode_localhost-ansible10-strategy_linear
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -324,3 +252,15 @@ jobs:
fi fi
"$PYTHON" -m tox -e "${{ matrix.tox_env }}" "$PYTHON" -m tox -e "${{ matrix.tox_env }}"
# https://github.com/marketplace/actions/alls-green
check:
if: always()
needs:
- linux
- macos
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

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

@ -159,6 +159,7 @@ def _connect_ssh(spec):
} }
} }
def _connect_buildah(spec): def _connect_buildah(spec):
""" """
Return ContextService arguments for a Buildah connection. Return ContextService arguments for a Buildah connection.
@ -174,6 +175,7 @@ def _connect_buildah(spec):
} }
} }
def _connect_docker(spec): def _connect_docker(spec):
""" """
Return ContextService arguments for a Docker connection. Return ContextService arguments for a Docker connection.
@ -277,6 +279,7 @@ def _connect_podman(spec):
} }
} }
def _connect_setns(spec, kind=None): def _connect_setns(spec, kind=None):
""" """
Return ContextService arguments for a mitogen_setns connection. Return ContextService arguments for a mitogen_setns connection.
@ -812,7 +815,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self.context = dct['context'] self.context = dct['context']
self.chain = CallChain(self, self.context, pipelined=True) self.chain = CallChain(self, self.context, pipelined=True)
if self._play_context.become: if self.become:
self.login_context = dct['via'] self.login_context = dct['via']
else: else:
self.login_context = self.context self.login_context = self.context
@ -888,6 +891,29 @@ class Connection(ansible.plugins.connection.ConnectionBase):
self.binding.close() self.binding.close()
self.binding = None self.binding = None
def _mitogen_var_options(self, templar):
# Workaround for https://github.com/ansible/ansible/issues/84238
var_names = C.config.get_plugin_vars('connection', self._load_name)
variables = templar.available_variables
var_options = {
var_name: templar.template(variables[var_name])
for var_name in var_names
if var_name in variables
}
if self.allow_extras:
extras_var_prefix = 'ansible_%s_' % self.extras_prefix
var_options['_extras'] = {
var_name: templar.template(variables[var_name])
for var_name in variables
if var_name not in var_options
and var_name.startswith(extras_var_prefix)
}
else:
var_options['_extras'] = {}
return var_options
reset_compat_msg = ( reset_compat_msg = (
'Mitogen only supports "reset_connection" on Ansible 2.5.6 or later' 'Mitogen only supports "reset_connection" on Ansible 2.5.6 or later'
) )
@ -920,11 +946,24 @@ class Connection(ansible.plugins.connection.ConnectionBase):
shared_loader_obj=0 shared_loader_obj=0
) )
# Workaround for https://github.com/ansible/ansible/issues/84238
try:
task, templar = self._play_context.vars.pop(
'_mitogen.smuggled.reset_connection',
)
except KeyError:
pass
else:
self.set_options(
task_keys=task.dump_attrs(),
var_options=self._mitogen_var_options(templar),
)
# Clear out state in case we were ever connected. # Clear out state in case we were ever connected.
self.close() self.close()
inventory_name, stack = self._build_stack() inventory_name, stack = self._build_stack()
if self._play_context.become: if self.become:
stack = stack[:-1] stack = stack[:-1]
worker_model = ansible_mitogen.process.get_worker_model() worker_model = ansible_mitogen.process.get_worker_model()

@ -36,8 +36,6 @@ import random
import traceback import traceback
import ansible import ansible
import ansible.constants
import ansible.plugins
import ansible.plugins.action import ansible.plugins.action
import ansible.utils.unsafe_proxy import ansible.utils.unsafe_proxy
import ansible.vars.clean import ansible.vars.clean
@ -296,7 +294,7 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
if not path.startswith('~'): if not path.startswith('~'):
# /home/foo -> /home/foo # /home/foo -> /home/foo
return path return path
if sudoable or not self._play_context.become: if sudoable or not self._connection.become:
if path == '~': if path == '~':
# ~ -> /home/dmw # ~ -> /home/dmw
return self._connection.homedir return self._connection.homedir

@ -32,35 +32,20 @@ __metaclass__ = type
import os.path import os.path
import sys import sys
from ansible.plugins.connection.ssh import (
DOCUMENTATION as _ansible_ssh_DOCUMENTATION,
)
DOCUMENTATION = """ DOCUMENTATION = """
name: mitogen_ssh
author: David Wilson <dw@botanicus.net> author: David Wilson <dw@botanicus.net>
connection: mitogen_ssh
short_description: Connect over SSH via Mitogen short_description: Connect over SSH via Mitogen
description: description:
- This connects using an OpenSSH client controlled by the Mitogen for - This connects using an OpenSSH client controlled by the Mitogen for
Ansible extension. It accepts every option the vanilla ssh plugin Ansible extension. It accepts every option the vanilla ssh plugin
accepts. accepts.
version_added: "2.5"
options: options:
ssh_args: """ + _ansible_ssh_DOCUMENTATION.partition('options:\n')[2]
type: str
vars:
- name: ssh_args
- name: ansible_ssh_args
- name: ansible_mitogen_ssh_args
ssh_common_args:
type: str
vars:
- name: ssh_args
- name: ansible_ssh_common_args
- name: ansible_mitogen_ssh_common_args
ssh_extra_args:
type: str
vars:
- name: ssh_args
- name: ansible_ssh_extra_args
- name: ansible_mitogen_ssh_extra_args
"""
try: try:
import ansible_mitogen import ansible_mitogen

@ -45,6 +45,7 @@ import ansible_mitogen.mixins
import ansible_mitogen.process import ansible_mitogen.process
import ansible.executor.process.worker import ansible.executor.process.worker
import ansible.template
import ansible.utils.sentinel import ansible.utils.sentinel
@ -326,3 +327,24 @@ class StrategyMixin(object):
self._worker_model.on_strategy_complete() self._worker_model.on_strategy_complete()
finally: finally:
ansible_mitogen.process.set_worker_model(None) ansible_mitogen.process.set_worker_model(None)
def _smuggle_to_connction_reset(self, task, play_context, iterator, target_host):
# Workaround for https://github.com/ansible/ansible/issues/84238
variables = self._variable_manager.get_vars(
play=iterator._play, host=target_host, task=task,
_hosts=self._hosts_cache, _hosts_all=self._hosts_cache_all,
)
templar = ansible.template.Templar(
loader=self._loader, variables=variables,
)
play_context.vars.update({
'_mitogen.smuggled.reset_connection': (task, templar),
})
def _execute_meta(self, task, play_context, iterator, target_host):
if task.args['_raw_params'] == 'reset_connection':
self._smuggle_to_connction_reset(task, play_context, iterator, target_host)
return super(StrategyMixin, self)._execute_meta(
task, play_context, iterator, target_host,
)

@ -62,7 +62,9 @@ from __future__ import unicode_literals
__metaclass__ = type __metaclass__ = type
import abc import abc
import logging
import os import os
import ansible.utils.shlex import ansible.utils.shlex
import ansible.constants as C import ansible.constants as C
import ansible.executor.interpreter_discovery import ansible.executor.interpreter_discovery
@ -74,6 +76,9 @@ from ansible.module_utils.parsing.convert_bool import boolean
import mitogen.core import mitogen.core
LOG = logging.getLogger(__name__)
def run_interpreter_discovery_if_necessary(s, task_vars, action, rediscover_python): def run_interpreter_discovery_if_necessary(s, task_vars, action, rediscover_python):
""" """
Triggers ansible python interpreter discovery if requested. Triggers ansible python interpreter discovery if requested.
@ -84,12 +89,12 @@ def run_interpreter_discovery_if_necessary(s, task_vars, action, rediscover_pyth
# keep trying different interpreters until we don't error # keep trying different interpreters until we don't error
if action._finding_python_interpreter: if action._finding_python_interpreter:
return action._possible_python_interpreter return action._possible_python_interpreter
if s in ['auto', 'auto_legacy', 'auto_silent', 'auto_legacy_silent']: if s in ['auto', 'auto_legacy', 'auto_silent', 'auto_legacy_silent']:
# python is the only supported interpreter_name as of Ansible 2.8.8 # python is the only supported interpreter_name as of Ansible 2.8.8
interpreter_name = 'python' interpreter_name = 'python'
discovered_interpreter_config = u'discovered_interpreter_%s' % interpreter_name discovered_interpreter_config = u'discovered_interpreter_%s' % interpreter_name
if task_vars.get('ansible_facts') is None: if task_vars.get('ansible_facts') is None:
task_vars['ansible_facts'] = {} task_vars['ansible_facts'] = {}
@ -130,7 +135,7 @@ def run_interpreter_discovery_if_necessary(s, task_vars, action, rediscover_pyth
def parse_python_path(s, task_vars, action, rediscover_python): def parse_python_path(s, task_vars, action, rediscover_python):
""" """
Given the string set for ansible_python_interpeter, parse it using shell Given the string set for ansible_python_interpeter, parse it using shell
syntax and return an appropriate argument vector. If the value detected is syntax and return an appropriate argument vector. If the value detected is
one of interpreter discovery then run that first. Caches python interpreter one of interpreter discovery then run that first. Caches python interpreter
discovery value in `facts_from_task_vars` like how Ansible handles this. discovery value in `facts_from_task_vars` like how Ansible handles this.
""" """
@ -208,6 +213,12 @@ class Spec(with_metaclass(abc.ABCMeta, object)):
:data:`True` if privilege escalation should be active. :data:`True` if privilege escalation should be active.
""" """
@abc.abstractmethod
def become_flags(self):
"""
The command line arguments passed to the become executable.
"""
@abc.abstractmethod @abc.abstractmethod
def become_method(self): def become_method(self):
""" """
@ -285,10 +296,9 @@ class Spec(with_metaclass(abc.ABCMeta, object)):
@abc.abstractmethod @abc.abstractmethod
def sudo_args(self): def sudo_args(self):
""" """
The list of additional arguments that should be included in a become The list of additional arguments that should be included in a sudo
invocation. invocation.
""" """
# TODO: split out into sudo_args/become_args.
@abc.abstractmethod @abc.abstractmethod
def mitogen_via(self): def mitogen_via(self):
@ -412,6 +422,43 @@ class PlayContextSpec(Spec):
# used to run interpreter discovery # used to run interpreter discovery
self._action = connection._action self._action = connection._action
def _become_option(self, name):
plugin = self._connection.become
try:
return plugin.get_option(name, self._task_vars, self._play_context)
except AttributeError:
# A few ansible_mitogen connection plugins look more like become
# plugins. They don't quite fit Ansible's plugin.get_option() API.
# https://github.com/mitogen-hq/mitogen/issues/1173
fallback_plugins = {'mitogen_doas', 'mitogen_sudo', 'mitogen_su'}
if self._connection.transport not in fallback_plugins:
raise
fallback_options = {
'become_exe',
'become_flags',
}
if name not in fallback_options:
raise
LOG.info(
'Used fallback=PlayContext.%s for plugin=%r, option=%r',
name, self._connection, name,
)
return getattr(self._play_context, name)
def _connection_option(self, name, fallback_attr=None):
try:
return self._connection.get_option(name, hostvars=self._task_vars)
except KeyError:
if fallback_attr is None:
fallback_attr = name
LOG.info(
'Used fallback=PlayContext.%s for plugin=%r, option=%r',
fallback_attr, self._connection, name,
)
return getattr(self._play_context, fallback_attr)
def transport(self): def transport(self):
return self._transport return self._transport
@ -419,40 +466,31 @@ class PlayContextSpec(Spec):
return self._inventory_name return self._inventory_name
def remote_addr(self): def remote_addr(self):
return self._play_context.remote_addr return self._connection_option('host', fallback_attr='remote_addr')
def remote_user(self): def remote_user(self):
return self._play_context.remote_user return self._connection_option('remote_user')
def become(self): def become(self):
return self._play_context.become return self._connection.become
def become_flags(self):
return self._become_option('become_flags')
def become_method(self): def become_method(self):
return self._play_context.become_method return self._connection.become.name
def become_user(self): def become_user(self):
return self._play_context.become_user return self._become_option('become_user')
def become_pass(self): def become_pass(self):
# become_pass is owned/provided by the active become plugin. However return optional_secret(self._become_option('become_pass'))
# PlayContext is intertwined with it. Known complications
# - ansible_become_password is higher priority than ansible_become_pass,
# `play_context.become_pass` doesn't obey this (atleast with Mitgeon).
# - `meta: reset_connection` runs `connection.reset()` but
# `ansible_mitogen.connection.Connection.reset()` recreates the
# connection object, setting `connection.become = None`.
become_plugin = self._connection.become
try:
become_pass = become_plugin.get_option('become_pass', playcontext=self._play_context)
except AttributeError:
become_pass = self._play_context.become_pass
return optional_secret(become_pass)
def password(self): def password(self):
return optional_secret(self._play_context.password) return optional_secret(self._connection_option('password'))
def port(self): def port(self):
return self._play_context.port return self._connection_option('port')
def python_path(self, rediscover_python=False): def python_path(self, rediscover_python=False):
s = self._connection.get_task_var('ansible_python_interpreter') s = self._connection.get_task_var('ansible_python_interpreter')
@ -466,18 +504,13 @@ class PlayContextSpec(Spec):
rediscover_python=rediscover_python) rediscover_python=rediscover_python)
def host_key_checking(self): def host_key_checking(self):
def candidates(): return self._connection_option('host_key_checking')
yield self._connection.get_task_var('ansible_ssh_host_key_checking')
yield self._connection.get_task_var('ansible_host_key_checking')
yield C.HOST_KEY_CHECKING
val = next((v for v in candidates() if v is not None), True)
return boolean(val)
def private_key_file(self): def private_key_file(self):
return self._play_context.private_key_file return self._connection_option('private_key_file')
def ssh_executable(self): def ssh_executable(self):
return C.config.get_config_value("ssh_executable", plugin_type="connection", plugin_name="ssh", variables=self._task_vars.get("vars", {})) return self._connection_option('ssh_executable')
def timeout(self): def timeout(self):
return self._play_context.timeout return self._play_context.timeout
@ -490,42 +523,21 @@ class PlayContextSpec(Spec):
) )
def ssh_args(self): def ssh_args(self):
local_vars = self._task_vars.get("hostvars", {}).get(self._inventory_name, {})
return [ return [
mitogen.core.to_text(term) mitogen.core.to_text(term)
for s in ( for s in (
C.config.get_config_value("ssh_args", plugin_type="connection", plugin_name="ssh", variables=local_vars), self._connection_option('ssh_args'),
C.config.get_config_value("ssh_common_args", plugin_type="connection", plugin_name="ssh", variables=local_vars), self._connection_option('ssh_common_args'),
C.config.get_config_value("ssh_extra_args", plugin_type="connection", plugin_name="ssh", variables=local_vars) self._connection_option('ssh_extra_args'),
) )
for term in ansible.utils.shlex.shlex_split(s or '') for term in ansible.utils.shlex.shlex_split(s or '')
] ]
def become_exe(self): def become_exe(self):
# In Ansible 2.8, PlayContext.become_exe always has a default value due return self._become_option('become_exe')
# 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): def sudo_args(self):
return [ return ansible.utils.shlex.shlex_split(self.become_flags() or '')
mitogen.core.to_text(term)
for term in ansible.utils.shlex.shlex_split(
first_true((
self._play_context.become_flags,
# Ansible <=2.7.
getattr(self._play_context, 'sudo_flags', ''),
# Ansible <=2.3.
getattr(C, 'DEFAULT_BECOME_FLAGS', ''),
getattr(C, 'DEFAULT_SUDO_FLAGS', '')
), default='')
)
]
def mitogen_via(self): def mitogen_via(self):
return self._connection.get_task_var('mitogen_via') return self._connection.get_task_var('mitogen_via')
@ -660,6 +672,9 @@ class MitogenViaSpec(Spec):
def become(self): def become(self):
return bool(self._become_user) return bool(self._become_user)
def become_flags(self):
return self._host_vars.get('ansible_become_flags')
def become_method(self): def become_method(self):
return ( return (
self._become_method or self._become_method or
@ -678,6 +693,7 @@ class MitogenViaSpec(Spec):
def password(self): def password(self):
return optional_secret( return optional_secret(
self._host_vars.get('ansible_ssh_password') or
self._host_vars.get('ansible_ssh_pass') or self._host_vars.get('ansible_ssh_pass') or
self._host_vars.get('ansible_password') self._host_vars.get('ansible_password')
) )
@ -754,7 +770,7 @@ class MitogenViaSpec(Spec):
mitogen.core.to_text(term) mitogen.core.to_text(term)
for s in ( for s in (
self._host_vars.get('ansible_sudo_flags') or '', self._host_vars.get('ansible_sudo_flags') or '',
self._host_vars.get('ansible_become_flags') or '', self.become_flags() or '',
) )
for term in ansible.utils.shlex.shlex_split(s) for term in ansible.utils.shlex.shlex_split(s)
] ]

@ -132,13 +132,13 @@ Noteworthy Differences
| 5 | 3.8 - 3.11 | | 5 | 3.8 - 3.11 |
+-----------------+-----------------+ +-----------------+-----------------+
| 6 | | | 6 | |
+-----------------+ 3.8 - 3.12 | +-----------------+ 3.8 - 3.13 |
| 7 | | | 7 | |
+-----------------+-----------------+ +-----------------+-----------------+
| 8 | 3.9 - 3.12 | | 8 | 3.9 - 3.13 |
+-----------------+-----------------+ +-----------------+-----------------+
| 9 | | | 9 | |
+-----------------+ 3.10 - 3.12 | +-----------------+ 3.10 - 3.13 |
| 10 | | | 10 | |
+-----------------+-----------------+ +-----------------+-----------------+
@ -306,7 +306,8 @@ container.
* Intermediary machines cannot use login and become passwords that were * Intermediary machines cannot use login and become passwords that were
supplied to Ansible interactively. If an intermediary requires a supplied to Ansible interactively. If an intermediary requires a
password, it must be supplied via ``ansible_ssh_pass``, password, it must be supplied via ``ansible_ssh_pass``,
``ansible_password``, or ``ansible_become_pass`` inventory variables. ``ansible_ssh_password``, ``ansible_password``, or
``ansible_become_pass`` inventory variables.
* Automatic tunnelling of SSH-dependent actions, such as the * Automatic tunnelling of SSH-dependent actions, such as the
``synchronize`` module, is not yet supported. This will be addressed in a ``synchronize`` module, is not yet supported. This will be addressed in a
@ -1011,7 +1012,8 @@ Like the :ans:conn:`ssh` except connection delegation is supported.
* ``ansible_port``, ``ssh_port`` * ``ansible_port``, ``ssh_port``
* ``ansible_ssh_executable``, ``ssh_executable`` * ``ansible_ssh_executable``, ``ssh_executable``
* ``ansible_ssh_private_key_file`` * ``ansible_ssh_private_key_file``
* ``ansible_ssh_pass``, ``ansible_password`` (default: assume passwordless) * ``ansible_ssh_pass``, ``ansible_ssh_password``, ``ansible_password``
(default: assume passwordless)
* ``ssh_args``, ``ssh_common_args``, ``ssh_extra_args`` * ``ssh_args``, ``ssh_common_args``, ``ssh_extra_args``
* ``mitogen_mask_remote_name``: if :data:`True`, mask the identity of the * ``mitogen_mask_remote_name``: if :data:`True`, mask the identity of the
Ansible controller process on remote machines. To simplify diagnostics, Ansible controller process on remote machines. To simplify diagnostics,
@ -1273,7 +1275,7 @@ on each process whose name begins with ``mitogen:``::
[pid 29858] futex(0x55ea9be52f60, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 0, NULL, 0xffffffff [pid 29858] futex(0x55ea9be52f60, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 0, NULL, 0xffffffff
^C ^C
$ $
This shows one thread waiting on IO (``poll``) and two more waiting on the same This shows one thread waiting on IO (``poll``) and two more waiting on the same
lock. It is taken from a real example of a deadlock due to a forking bug. lock. It is taken from a real example of a deadlock due to a forking bug.

@ -18,7 +18,89 @@ To avail of fixes in an unreleased version, please download a ZIP file
`directly from GitHub <https://github.com/mitogen-hq/mitogen/>`_. `directly from GitHub <https://github.com/mitogen-hq/mitogen/>`_.
v0.3.11 (2024-10-30) In progress (unreleased)
------------------------
v0.3.18 (2024-11-07)
--------------------
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated become method
(e.g. ``ansible_become_method``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated become flag
(e.g. ``ansible_become_method``, ``become`` keyword).
v0.3.17 (2024-11-07)
--------------------
* :gh:issue:`1182` CI: Fix incorrect world readable/writable file permissions
on SSH key ``mitogen__has_sudo_pubkey.key`` during Ansible tests.
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated SSH private key file
(e.g. ``ansible_private_key_file``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated SSH host key checking
(e.g. ``ansible_host_key_checking``, ``ansible_ssh_host_key_checking``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated host address
(e.g. ``ansible_host``, ``ansible_ssh_host``)
* :gh:issue:`1184` Test templated SSH host key checking in task vars
v0.3.16 (2024-11-05)
--------------------
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated become executable
(e.g. ``become_exe``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated become executable
arguments (e.g. ``become_flags``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated ssh executable
(``ansible_ssh_executable``).
* :gh:issue:`1083` :mod:`ansible_mitogen`: Fixed templated connection options
during a ``meta: reset_connection`` task.
* :gh:issue:`1129` CI: Migrated macOS 12 runners to macOS 13, due to EOL.
v0.3.15 (2024-10-28)
--------------------
* :gh:issue:`905` :mod:`ansible_mitogen`: Support templated SSH command
arguments (e.g. ``ansible_ssh_args``, ``ansible_ssh_extra_args``).
* :gh:issue:`692` tests: Fix and re-enable several sudo tests
* :gh:issue:`1083` :mod:`ansible_mitogen`: Support templated become password
(e.g. ``ansible_become_pass``, ``ansible_sudo_pass``)
v0.3.14 (2024-10-16)
--------------------
* :gh:issue:`1159` CI: Reduce number of Jobs by parameterizing Mitogen Docker
SSH tests
* :gh:issue:`1083` :mod:`ansible_mitogen`: Support templated become username.
v0.3.13 (2024-10-09)
--------------------
* :gh:issue:`1138` CI: Complete migration from Azure DevOps Pipelines to
GitHub Actions
* :gh:issue:`1116` :mod:`ansible_mitogen`: Support for templated variable
`ansible_ssh_user`.
* :gh:issue:`978` :mod:`ansible_mitogen`: Support templated Ansible SSH port.
* :gh:issue:`1073` Python 3.13 support
v0.3.12 (2024-10-07)
--------------------
* :gh:issue:`1106` :mod:`ansible_mitogen`: Support for `ansible_ssh_password`
connection variable, and templated SSH connection password.
* :gh:issue:`1136` tests: Improve Ansible fail_msg formatting.
* :gh:issue:`1137` tests: Ignore inventory files of inactive tests & benchmarks
* :gh:issue:`1138` CI: Add re-actors/alls-green GitHub Actions job to simplify
branch protections configuration.
v0.3.11 (2024-09-30)
-------------------- --------------------
* :gh:issue:`1127` :mod:`mitogen`: Consolidate mitogen backward compatibility * :gh:issue:`1127` :mod:`mitogen`: Consolidate mitogen backward compatibility
@ -101,7 +183,7 @@ v0.3.4 (2023-07-02)
* :gh:issue:`929` Support Ansible 6 and ansible-core 2.13 * :gh:issue:`929` Support Ansible 6 and ansible-core 2.13
* :gh:issue:`832` Fix runtime error when using the ansible.builtin.dnf module multiple times * :gh:issue:`832` Fix runtime error when using the ansible.builtin.dnf module multiple times
* :gh:issue:`925` :class:`ansible_mitogen.connection.Connection` no longer tries to close the * :gh:issue:`925` :class:`ansible_mitogen.connection.Connection` no longer tries to close the
connection on destruction. This is expected to reduce cases of `mitogen.core.Error: An attempt connection on destruction. This is expected to reduce cases of `mitogen.core.Error: An attempt
was made to enqueue a message with a Broker that has already exitted`. However it may result in was made to enqueue a message with a Broker that has already exitted`. However it may result in
resource leaks. resource leaks.

@ -127,11 +127,14 @@ sponsorship and outstanding future-thinking of its early adopters.
<li>jgadling</li> <li>jgadling</li>
<li>John F Wall &mdash; <em>Making Ansible Great with Massive Parallelism</em></li> <li>John F Wall &mdash; <em>Making Ansible Great with Massive Parallelism</em></li>
<li><a href="https://github.com/jrosser">Jonathan Rosser</a></li> <li><a href="https://github.com/jrosser">Jonathan Rosser</a></li>
<li><a href="https://github.com/jmkeyes">Joshua M. Keyes</a></li>
<li>KennethC</li> <li>KennethC</li>
<li><a href="https://github.com/lberruti">Luca Berruti</li> <li><a href="https://github.com/lberruti">Luca Berruti</li>
<li>Lewis Bellwood &mdash; <em>Happy to be apart of a great project.</em></li> <li>Lewis Bellwood &mdash; <em>Happy to be apart of a great project.</em></li>
<li>luto</li> <li>luto</li>
<li><a href="https://mayeu.me/">Mayeu a.k.a Matthieu Maury</a></li> <li><a href="https://mayeu.me/">Mayeu a.k.a Matthieu Maury</a></li>
<li><a href="https://github.com/madsi1m">Michael D'Silva</a></li>
<li><a href="https://github.com/mordekasg">mordek</a></li>
<li><a href="https://twitter.com/nathanhruby">@nathanhruby</a></li> <li><a href="https://twitter.com/nathanhruby">@nathanhruby</a></li>
<li><a href="https://github.com/opoplawski">Orion Poplawski</a></li> <li><a href="https://github.com/opoplawski">Orion Poplawski</a></li>
<li><a href="https://github.com/philfry">Philippe Kueck</a></li> <li><a href="https://github.com/philfry">Philippe Kueck</a></li>

@ -1038,7 +1038,7 @@ receive items in the order they are requested, as they become available.
Mitogen enables SSH compression by default, there are circumstances where Mitogen enables SSH compression by default, there are circumstances where
disabling SSH compression is desirable, and many scenarios for future disabling SSH compression is desirable, and many scenarios for future
connection methods where transport-layer compression is not supported at connection methods where transport-layer compression is not supported at
all. all.
.. [#f2] Compression may seem redundant, however it is basically free and reducing IO .. [#f2] Compression may seem redundant, however it is basically free and reducing IO
is always a good idea. The 33% / 200 byte saving may mean the presence or is always a good idea. The 33% / 200 byte saving may mean the presence or

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

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

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

@ -35,7 +35,7 @@ be expected. On the slave, it is built dynamically during startup.
#: Library version as a tuple. #: Library version as a tuple.
__version__ = (0, 3, 11) __version__ = (0, 3, 19, 'dev')
#: This is :data:`False` in slave contexts. Previously it was used to prevent #: This is :data:`False` in slave contexts. Previously it was used to prevent

@ -3290,7 +3290,7 @@ class Router(object):
This can be used from any thread, but its output is only meaningful This can be used from any thread, but its output is only meaningful
from the context of the :class:`Broker` thread, as disconnection or from the context of the :class:`Broker` thread, as disconnection or
replacement could happen in parallel on the broker thread at any replacement could happen in parallel on the broker thread at any
moment. moment.
""" """
return ( return (
self._stream_by_id.get(dst_id) or self._stream_by_id.get(dst_id) or

@ -652,7 +652,7 @@ class ParentImpEnumerationMethod(FinderMethod):
insane) parent package, and if no insane parents exist, simply use insane) parent package, and if no insane parents exist, simply use
:mod:`sys.path` to search for it from scratch on the filesystem using the :mod:`sys.path` to search for it from scratch on the filesystem using the
normal Python lookup mechanism. normal Python lookup mechanism.
This is required for older versions of :mod:`ansible.compat.six`, This is required for older versions of :mod:`ansible.compat.six`,
:mod:`plumbum.colors`, Ansible 2.8 :mod:`ansible.module_utils.distro` and :mod:`plumbum.colors`, Ansible 2.8 :mod:`ansible.module_utils.distro` and
its submodule :mod:`ansible.module_utils.distro._distro`. its submodule :mod:`ansible.module_utils.distro._distro`.

@ -631,7 +631,7 @@ class TimerList(object):
def get_timeout(self): def get_timeout(self):
""" """
Return the floating point seconds until the next event is due. Return the floating point seconds until the next event is due.
:returns: :returns:
Floating point delay, or 0.0, or :data:`None` if no events are Floating point delay, or 0.0, or :data:`None` if no events are
scheduled. scheduled.

@ -79,6 +79,7 @@ setup(
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: CPython',
'Topic :: System :: Distributed Computing', 'Topic :: System :: Distributed Computing',
'Topic :: System :: Systems Administration', 'Topic :: System :: Systems Administration',

@ -7,7 +7,7 @@ started in September 2017. Pull requests in this area are very welcome!
## Running The Tests ## Running The Tests
[![Build Status](https://dev.azure.com/mitogen-hq/mitogen/_apis/build/status/mitogen-hq.mitogen?branchName=master)](https://dev.azure.com/mitogen-hq/mitogen/_build/latest?definitionId=1&branchName=master) [![Build Status](https://img.shields.io/github/actions/workflow/status/mitogen-hq/mitogen/tests.yml?branch=master)](https://github.com/mitogen-hq/mitogen/actions?query=branch%3Amaster)
Your computer should have an Internet connection, and the ``docker`` command Your computer should have an Internet connection, and the ``docker`` command
line tool should be able to connect to a working Docker daemon (localhost or line tool should be able to connect to a working Docker daemon (localhost or
@ -30,11 +30,19 @@ and run the tests there.
1. Run ``test`` 1. Run ``test``
# Selecting a target distribution # Selecting target distributions
Docker target images exist for testing against CentOS and Debian, with the Linux container images for testing are available at
default being Debian. To select CentOS, specify `MITOGEN_TEST_DISTRO=centos` in
the environment. - https://github.com/orgs/mitogen-hq/packages
- https://public.ecr.aws/n5z0e8q9
The images used are determined by two environment variables
- `MITOGEN_TEST_DISTRO_SPECS`
- `MITOGEN_TEST_IMAGE_TEMPLATE`
Defaults for these can be found in `.ci/ci_lib.py` & `tests/testlib.py`
# User Accounts # User Accounts

@ -48,6 +48,7 @@ host_key_checking = False
[inventory] [inventory]
any_unparsed_is_failed = true any_unparsed_is_failed = true
host_pattern_mismatch = error host_pattern_mismatch = error
ignore_extensions = ~, .bak, .disabled
[callback_profile_tasks] [callback_profile_tasks]
task_output_limit = 10 task_output_limit = 10

@ -4,8 +4,7 @@
# When running the tests outside CI, make a single 'target' host which is the # When running the tests outside CI, make a single 'target' host which is the
# local machine. The ansible_user override is necessary since some tests want a # local machine. The ansible_user override is necessary since some tests want a
# fixed ansible.cfg remote_user setting to test against. # fixed ansible.cfg remote_user setting to test against.
# FIXME Hardcoded by replacement in some CI runs https://github.com/mitogen-hq/mitogen/issues/1022 # os.environ['USER'] is an empty string on GitHub Actions macOS runners.
# and os.environ['USER'] is not populated on Azure macOS runners.
target ansible_host=localhost ansible_user="{{ lookup('pipe', 'whoami') }}" target ansible_host=localhost ansible_user="{{ lookup('pipe', 'whoami') }}"
[test-targets] [test-targets]
@ -18,4 +17,35 @@ ssh-common-args ansible_host=localhost ansible_user="{{ lookup('pipe', 'whoami')
[issue905:vars] [issue905:vars]
ansible_ssh_common_args=-o PermitLocalCommand=yes -o LocalCommand="touch {{ ssh_args_canary_file }}" ansible_ssh_common_args=-o PermitLocalCommand=yes -o LocalCommand="touch {{ ssh_args_canary_file }}"
ssh_args_canary_file=/tmp/ssh_args_{{ inventory_hostname }} ssh_args_canary_file=/tmp/ssh_args_by_inv_{{ inventory_hostname }}
[tt_targets_bare]
tt-bare
[tt_become_bare]
tt-become-bare
[tt_become_bare:vars]
ansible_host=localhost
ansible_user="{{ lookup('pipe', 'whoami') }}"
[tt_become_by_inv]
tt-become ansible_become="{{ 'true' | trim }}" ansible_become_user=root
tt-become-exe ansible_become=true ansible_become_exe="{{ 'sudo' | trim }}" ansible_become_user=root
tt-become-flags ansible_become=true ansible_become_flags="{{ '--set-home --stdin --non-interactive' | trim }}" ansible_become_user=root
tt-become-method ansible_become=true ansible_become_method="{{ 'sudo' | trim }}" ansible_become_user=root
tt-become-pass ansible_become=true ansible_become_pass="{{ 'pw_required_password' | trim }}" ansible_become_user=mitogen__pw_required
tt-become-user ansible_become=true ansible_become_user="{{ 'root' | trim }}"
[tt_become_by_inv:vars]
ansible_host=localhost
ansible_user="{{ lookup('pipe', 'whoami') }}"
[tt_targets_inventory]
tt-host ansible_host="{{ 'localhost' | trim }}" ansible_password=has_sudo_nopw_password ansible_user=mitogen__has_sudo_nopw
tt-host-key-checking ansible_host=localhost ansible_host_key_checking="{{ 'false' | trim }}" ansible_password=has_sudo_nopw_password ansible_user=mitogen__has_sudo_nopw
tt-password ansible_host=localhost ansible_password="{{ 'has_sudo_nopw_password' | trim }}" ansible_user=mitogen__has_sudo_nopw
tt-port ansible_host=localhost ansible_password=has_sudo_nopw_password ansible_port="{{ 22 | int }}" ansible_user=mitogen__has_sudo_nopw
tt-private-key-file ansible_host=localhost ansible_private_key_file="{{ git_basedir }}/tests/data/docker/mitogen__has_sudo_pubkey.key" ansible_user=mitogen__has_sudo_pubkey
tt-remote-user ansible_host=localhost ansible_password=has_sudo_nopw_password ansible_user="{{ 'mitogen__has_sudo_nopw' | trim }}"
tt-ssh-executable ansible_host=localhost ansible_password=has_sudo_nopw_password ansible_ssh_executable="{{ 'ssh' | trim }}" ansible_user=mitogen__has_sudo_nopw

@ -0,0 +1,18 @@
# Ansible removed its default SSH port in May 2021, defering to the SSH
# implementation.
# https://github.com/ansible/ansible/commit/45618a6f3856f7332df8afe4adc40d85649a70da
# Careful templating is needed to preseve the type(s) of expected_ssh_port,
# particularly in combination with the assert_equal action plugin.
# Do: {{ expected_ssh_port }}
# Don't: {{ expected_ssh_port | any_filter }}
# Don't: {% if ...%}{{ expected_ssh_port }}{% else %}...{% endif %}
# https://stackoverflow.com/questions/66102524/ansible-set-fact-type-cast/66104814#66104814
- set_fact:
expected_ssh_port: null
when: ansible_version.full is version('2.11.1', '>=', strict=True)
- set_fact:
expected_ssh_port: 22
when: ansible_version.full is version('2.11.1', '<', strict=True)

@ -77,7 +77,8 @@
that: that:
- item.stat.checksum == item.item.expected_checksum - item.stat.checksum == item.item.expected_checksum
quiet: true # Avoid spamming stdout with 400 kB of item.item.content quiet: true # Avoid spamming stdout with 400 kB of item.item.content
fail_msg: item={{ item }} fail_msg: |
item={{ item }}
with_items: "{{ stat.results }}" with_items: "{{ stat.results }}"
loop_control: loop_control:
label: "{{ item.stat.path }}" label: "{{ item.stat.path }}"

@ -16,7 +16,8 @@
- assert: - assert:
that: that:
- out.stat.mode in ("0644", "0664") - out.stat.mode in ("0644", "0664")
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: "Copy files from content: arg" - name: "Copy files from content: arg"
copy: copy:
@ -29,7 +30,8 @@
- assert: - assert:
that: that:
- out.stat.mode == "0400" - out.stat.mode == "0400"
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: Cleanup local weird mode file - name: Cleanup local weird mode file
file: file:
@ -55,7 +57,8 @@
- assert: - assert:
that: that:
- out.stat.mode in ("0644", "0664") - out.stat.mode in ("0644", "0664")
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: Copy file with weird mode, preserving mode - name: Copy file with weird mode, preserving mode
copy: copy:
@ -69,7 +72,8 @@
- assert: - assert:
that: that:
- out.stat.mode == "1462" - out.stat.mode == "1462"
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: Copy file with weird mode, explicit mode - name: Copy file with weird mode, explicit mode
copy: copy:
@ -84,7 +88,8 @@
- assert: - assert:
that: that:
- out.stat.mode == "1461" - out.stat.mode == "1461"
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: Cleanup - name: Cleanup
file: file:

@ -15,7 +15,8 @@
- 'raw.rc == 0' - 'raw.rc == 0'
- 'raw.stdout_lines[-1]|to_text == "2"' - 'raw.stdout_lines[-1]|to_text == "2"'
- 'raw.stdout[-1]|to_text == "2"' - 'raw.stdout[-1]|to_text == "2"'
fail_msg: raw={{raw}} fail_msg: |
raw={{ raw }}
- name: Run raw module with sudo - name: Run raw module with sudo
become: true become: true
@ -39,6 +40,7 @@
["root\r\n"], ["root\r\n"],
["root"], ["root"],
) )
fail_msg: raw={{raw}} fail_msg: |
raw={{ raw }}
tags: tags:
- low_level_execute_command - low_level_execute_command

@ -42,13 +42,17 @@
assert: assert:
that: that:
- good_temp_path == good_temp_path2 - good_temp_path == good_temp_path2
fail_msg: good_temp_path={{good_temp_path}} good_temp_path2={{good_temp_path2}} fail_msg: |
good_temp_path={{ good_temp_path }}
good_temp_path2={{ good_temp_path2 }}
- name: "Verify different subdir for both tasks" - name: "Verify different subdir for both tasks"
assert: assert:
that: that:
- tmp_path.path != tmp_path2.path - tmp_path.path != tmp_path2.path
fail_msg: tmp_path={{tmp_path}} tmp_path2={{tmp_path2}} fail_msg: |
tmp_path={{ tmp_path }}
tmp_path2={{ tmp_path2 }}
# #
# Verify subdirectory removal. # Verify subdirectory removal.
@ -69,7 +73,9 @@
that: that:
- not stat1.stat.exists - not stat1.stat.exists
- not stat2.stat.exists - not stat2.stat.exists
fail_msg: stat1={{stat1}} stat2={{stat2}} fail_msg: |
stat1={{ stat1 }}
stat2={{ stat2 }}
# #
# Verify good directory persistence. # Verify good directory persistence.
@ -84,7 +90,8 @@
assert: assert:
that: that:
- stat.stat.exists - stat.stat.exists
fail_msg: stat={{stat}} fail_msg: |
stat={{ stat }}
# #
# Write some junk into the temp path. # Write some junk into the temp path.
@ -107,7 +114,8 @@
- assert: - assert:
that: that:
- not out.stat.exists - not out.stat.exists
fail_msg: out={{out}} fail_msg: |
out={{ out }}
# #
# root # root
@ -126,23 +134,24 @@
that: that:
- tmp_path2.path != tmp_path_root.path - tmp_path2.path != tmp_path_root.path
- tmp_path2.path|dirname != tmp_path_root.path|dirname - tmp_path2.path|dirname != tmp_path_root.path|dirname
fail_msg: tmp_path_root={{tmp_path_root}} tmp_path2={{tmp_path2}} fail_msg: |
tmp_path_root={{ tmp_path_root }}
tmp_path2={{ tmp_path2 }}
# #
# readonly homedir # readonly homedir
# #
# TODO: https://github.com/dw/mitogen/issues/692 - name: Try writing to temp directory for the readonly_homedir user
# - name: "Try writing to temp directory for the readonly_homedir user" become: true
# become: true become_user: mitogen__readonly_homedir
# become_user: mitogen__readonly_homedir custom_python_run_script:
# custom_python_run_script: script: |
# script: | from ansible.module_utils.basic import get_module_path
# from ansible.module_utils.basic import get_module_path path = get_module_path() + '/foo.txt'
# path = get_module_path() + '/foo.txt' result['path'] = path
# result['path'] = path open(path, 'w').write("bar")
# open(path, 'w').write("bar") register: tmp_path
# register: tmp_path
# #
# modules get the same base dir # modules get the same base dir
@ -157,7 +166,8 @@
that: that:
- out.module_path.startswith(good_temp_path2) - out.module_path.startswith(good_temp_path2)
- out.module_tmpdir.startswith(good_temp_path2) - out.module_tmpdir.startswith(good_temp_path2)
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- make_tmp_path - make_tmp_path
- mitogen_only - mitogen_only

@ -26,7 +26,8 @@
register: out register: out
- assert: - assert:
that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo' that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: "Expand ~/foo with become active. ~ is become_user's home." - name: "Expand ~/foo with become active. ~ is become_user's home."
action_passthrough: action_passthrough:
@ -49,7 +50,8 @@
register: out register: out
- assert: - assert:
that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo' that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: "Expanding $HOME/foo has no effect." - name: "Expanding $HOME/foo has no effect."
action_passthrough: action_passthrough:
@ -60,7 +62,8 @@
register: out register: out
- assert: - assert:
that: out.result == '$HOME/foo' that: out.result == '$HOME/foo'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
# ------------------------ # ------------------------
@ -73,7 +76,8 @@
register: out register: out
- assert: - assert:
that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo' that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: "sudoable; Expand ~/foo with become active. ~ is become_user's home." - name: "sudoable; Expand ~/foo with become active. ~ is become_user's home."
action_passthrough: action_passthrough:
@ -97,7 +101,8 @@
register: out register: out
- assert: - assert:
that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo' that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: "sudoable; Expanding $HOME/foo has no effect." - name: "sudoable; Expanding $HOME/foo has no effect."
action_passthrough: action_passthrough:
@ -108,6 +113,7 @@
register: out register: out
- assert: - assert:
that: out.result == '$HOME/foo' that: out.result == '$HOME/foo'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- remote_expand_user - remote_expand_user

@ -12,7 +12,8 @@
register: out register: out
- assert: - assert:
that: out.result == False that: out.result == False
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: Ensure does-exist does - name: Ensure does-exist does
copy: copy:
@ -24,7 +25,8 @@
register: out register: out
- assert: - assert:
that: out.result == True that: out.result == True
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: Cleanup - name: Cleanup
file: file:

@ -23,7 +23,8 @@
- assert: - assert:
that: that:
- not out2.stat.exists - not out2.stat.exists
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- stat: - stat:
path: "{{out.src|dirname}}" path: "{{out.src|dirname}}"
@ -32,7 +33,8 @@
- assert: - assert:
that: that:
- not out2.stat.exists - not out2.stat.exists
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- file: - file:
path: /tmp/remove_tmp_path_test path: /tmp/remove_tmp_path_test

@ -40,11 +40,11 @@
delegate_to: localhost delegate_to: localhost
run_once: true run_once: true
# TODO: https://github.com/dw/mitogen/issues/692 - name: Ensure clean slate
# - file: become: true
# path: /tmp/sync-test.out file:
# state: absent path: /tmp/sync-test.out
# become: true state: absent
# exception: File "/tmp/venv/lib/python2.7/site-packages/ansible/plugins/action/__init__.py", line 129, in cleanup # exception: File "/tmp/venv/lib/python2.7/site-packages/ansible/plugins/action/__init__.py", line 129, in cleanup
# exception: self._remove_tmp_path(self._connection._shell.tmpdir) # exception: self._remove_tmp_path(self._connection._shell.tmpdir)
@ -66,17 +66,18 @@
- assert: - assert:
that: outout == "item!" that: outout == "item!"
fail_msg: outout={{outout}} fail_msg: |
outout={{ outout }}
when: False when: False
# TODO: https://github.com/dw/mitogen/issues/692 - name: Cleanup
# - file: become: true
# path: "{{item}}" file:
# state: absent path: "{{ item }}"
# become: true state: absent
# with_items: with_items:
# - /tmp/synchronize-action-key - /tmp/synchronize-action-key
# - /tmp/sync-test - /tmp/sync-test
# - /tmp/sync-test.out - /tmp/sync-test.out
tags: tags:
- synchronize - synchronize

@ -22,7 +22,8 @@
- assert: - assert:
that: | that: |
out.content|b64decode == '{"I am JSON": true}' out.content|b64decode == '{"I am JSON": true}'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: Create text transfer data - name: Create text transfer data
action_passthrough: action_passthrough:
@ -37,7 +38,8 @@
- assert: - assert:
that: that:
out.content|b64decode == 'I am text.' out.content|b64decode == 'I am text.'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: Cleanup transfer data - name: Cleanup transfer data
file: file:

@ -33,6 +33,7 @@
- out.results[1].stdout == 'hi-from-job-2' - out.results[1].stdout == 'hi-from-job-2'
- out.results[1].rc == 0 - out.results[1].rc == 0
- out.results[1].delta > '0:00:05' - out.results[1].delta > '0:00:05'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- multiple_items_loop - multiple_items_loop

@ -27,7 +27,8 @@
(job.started == 1) and (job.started == 1) and
(job.changed == True) and (job.changed == True) and
(job.finished == 0) (job.finished == 0)
fail_msg: job={{job}} fail_msg: |
job={{ job }}
- name: busy-poll up to 100000 times - name: busy-poll up to 100000 times
async_status: async_status:
@ -52,7 +53,8 @@
- async_out.failed == False - async_out.failed == False
- async_out.msg == "Hello, world." - async_out.msg == "Hello, world."
- 'async_out.stderr == "binary_producing_json: oh noes\n"' - 'async_out.stderr == "binary_producing_json: oh noes\n"'
fail_msg: async_out={{async_out}} fail_msg: |
async_out={{ async_out }}
vars: vars:
async_out: "{{result.content|b64decode|from_json}}" async_out: "{{result.content|b64decode|from_json}}"
tags: tags:

@ -38,7 +38,8 @@
- async_out.msg.startswith("Traceback") - async_out.msg.startswith("Traceback")
- '"ValueError: No start of json char found\n" in async_out.msg' - '"ValueError: No start of json char found\n" in async_out.msg'
- 'async_out.stderr == "binary_producing_junk: oh noes\n"' - 'async_out.stderr == "binary_producing_junk: oh noes\n"'
fail_msg: async_out={{async_out}} fail_msg: |
async_out={{ async_out }}
vars: vars:
async_out: "{{result.content|b64decode|from_json}}" async_out: "{{result.content|b64decode|from_json}}"
tags: tags:

@ -42,14 +42,16 @@
- async_out.start.startswith("20") - async_out.start.startswith("20")
- async_out.stderr == "there" - async_out.stderr == "there"
- async_out.stdout == "hi" - async_out.stdout == "hi"
fail_msg: async_out={{async_out}} fail_msg: |
async_out={{ async_out }}
vars: vars:
async_out: "{{result.content|b64decode|from_json}}" async_out: "{{result.content|b64decode|from_json}}"
- assert: - assert:
that: that:
- async_out.invocation.module_args.stdin == None - async_out.invocation.module_args.stdin == None
fail_msg: async_out={{async_out}} fail_msg: |
async_out={{ async_out }}
when: when:
- ansible_version.full is version('2.4', '>=', strict=True) - ansible_version.full is version('2.4', '>=', strict=True)
vars: vars:

@ -15,7 +15,9 @@
- assert: - assert:
that: that:
- sync_proc1.pid == sync_proc2.pid - sync_proc1.pid == sync_proc2.pid
fail_msg: sync_proc1={{sync_proc1}} sync_proc2={{sync_proc2}} fail_msg: |
sync_proc1={{ sync_proc1 }}
sync_proc2={{ sync_proc2 }}
when: is_mitogen when: is_mitogen
- name: get async process ID. - name: get async process ID.
@ -52,7 +54,9 @@
- sync_proc1.pid == sync_proc2.pid - sync_proc1.pid == sync_proc2.pid
- async_result1.pid != sync_proc1.pid - async_result1.pid != sync_proc1.pid
- async_result1.pid != async_result2.pid - async_result1.pid != async_result2.pid
fail_msg: async_result1={{async_result1}} async_result2={{async_result2}} fail_msg: |
async_result1={{ async_result1 }}
async_result2={{ async_result2 }}
when: is_mitogen when: is_mitogen
tags: tags:
- runner_new_process - runner_new_process

@ -23,7 +23,8 @@
(job1.started == 1) and (job1.started == 1) and
(job1.changed == True) and (job1.changed == True) and
(job1.finished == 0) (job1.finished == 0)
fail_msg: job1={{job1}} fail_msg: |
job1={{ job1 }}
- name: busy-poll up to 100000 times - name: busy-poll up to 100000 times
async_status: async_status:
@ -48,7 +49,8 @@
- result1.start|length == 26 - result1.start|length == 26
- result1.finished == 1 - result1.finished == 1
- result1.rc == 0 - result1.rc == 0
fail_msg: result1={{result1}} fail_msg: |
result1={{ result1 }}
- assert: - assert:
that: that:
@ -56,14 +58,16 @@
- result1.stderr_lines == [] - result1.stderr_lines == []
- result1.stdout == "alldone" - result1.stdout == "alldone"
- result1.stdout_lines == ["alldone"] - result1.stdout_lines == ["alldone"]
fail_msg: result1={{result1}} fail_msg: |
result1={{ result1 }}
when: when:
- ansible_version.full is version('2.8', '>', strict=True) # ansible#51393 - ansible_version.full is version('2.8', '>', strict=True) # ansible#51393
- assert: - assert:
that: that:
- result1.failed == False - result1.failed == False
fail_msg: result1={{result1}} fail_msg: |
result1={{ result1 }}
when: when:
- ansible_version.full is version('2.4', '>', strict=True) - ansible_version.full is version('2.4', '>', strict=True)
tags: tags:

@ -29,7 +29,8 @@
- result.failed == 1 - result.failed == 1
- result.finished == 1 - result.finished == 1
- result.msg == "Job reached maximum time limit of 1 seconds." - result.msg == "Job reached maximum time limit of 1 seconds."
fail_msg: result={{result}} fail_msg: |
result={{ result }}
tags: tags:
- mitogen_only - mitogen_only
- runner_timeout_then_polling - runner_timeout_then_polling

@ -56,12 +56,15 @@
that: that:
- result1.rc == 0 - result1.rc == 0
- result2.rc == 0 - result2.rc == 0
fail_msg: result1={{result1}} result2={{result2}} fail_msg: |
result1={{ result1 }}
result2={{ result2 }}
- assert: - assert:
that: that:
- result2.stdout == 'im_alive' - result2.stdout == 'im_alive'
fail_msg: result2={{result2}} fail_msg: |
result2={{ result2 }}
when: when:
- ansible_version.full is version('2.8', '>=', strict=True) # ansible#51393 - ansible_version.full is version('2.8', '>=', strict=True) # ansible#51393
tags: tags:

@ -21,6 +21,7 @@
job1.msg == "async task did not complete within the requested time" or job1.msg == "async task did not complete within the requested time" or
job1.msg == "async task did not complete within the requested time - 1s" or job1.msg == "async task did not complete within the requested time - 1s" or
job1.msg == "Job reached maximum time limit of 1 seconds." job1.msg == "Job reached maximum time limit of 1 seconds."
fail_msg: job1={{job1}} fail_msg: |
job1={{ job1 }}
tags: tags:
- runner_with_polling_and_timeout - runner_with_polling_and_timeout

@ -5,3 +5,7 @@
- import_playbook: sudo_nopassword.yml - import_playbook: sudo_nopassword.yml
- import_playbook: sudo_password.yml - import_playbook: sudo_password.yml
- import_playbook: sudo_requiretty.yml - import_playbook: sudo_requiretty.yml
- import_playbook: templated_by_inv.yml
- import_playbook: templated_by_play_keywords.yml
- import_playbook: templated_by_play_vars.yml
- import_playbook: templated_by_task_keywords.yml

@ -20,7 +20,8 @@
('password is required' in out.msg) or ('password is required' in out.msg) or
('password is required' in out.module_stderr) ('password is required' in out.module_stderr)
) )
fail_msg: out={{out}} fail_msg: |
out={{ out }}
when: is_mitogen when: is_mitogen
@ -40,7 +41,8 @@
('Incorrect su password' in out.msg) or ('Incorrect su password' in out.msg) or
('su password is incorrect' in out.msg) ('su password is incorrect' in out.msg)
) )
fail_msg: out={{out}} fail_msg: |
out={{ out }}
when: is_mitogen when: is_mitogen
- name: Ensure password su with chdir succeeds - name: Ensure password su with chdir succeeds
@ -62,7 +64,8 @@
- assert: - assert:
that: that:
- out.stdout == 'mitogen__user1' - out.stdout == 'mitogen__user1'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
when: when:
# CI containers lack `setfacl` for unpriv -> unpriv # CI containers lack `setfacl` for unpriv -> unpriv
# https://github.com/mitogen-hq/mitogen/issues/1118 # https://github.com/mitogen-hq/mitogen/issues/1118
@ -87,7 +90,8 @@
- assert: - assert:
that: that:
- out.stdout == 'mitogen__user1' - out.stdout == 'mitogen__user1'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
when: when:
# CI containers lack `setfacl` for unpriv -> unpriv # CI containers lack `setfacl` for unpriv -> unpriv
# https://github.com/mitogen-hq/mitogen/issues/1118 # https://github.com/mitogen-hq/mitogen/issues/1118

@ -20,7 +20,8 @@
or out.module_stderr is match("sudo: invalid option -- '-'") or out.module_stderr is match("sudo: invalid option -- '-'")
or out.module_stdout is match("sudo: unrecognized option [`']--derps'") or out.module_stdout is match("sudo: unrecognized option [`']--derps'")
or out.module_stderr is match("sudo: unrecognized option [`']--derps'") or out.module_stderr is match("sudo: unrecognized option [`']--derps'")
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- sudo - sudo
- sudo_flags_failure - sudo_flags_failure

@ -23,7 +23,8 @@
- >- - >-
(out.module_stderr | default(out.module_stdout, true) | default(out.msg, true)) is search('sudo: unknown user:? slartibartfast') (out.module_stderr | default(out.module_stdout, true) | default(out.msg, true)) is search('sudo: unknown user:? slartibartfast')
or (ansible_facts.os_family == 'RedHat' and ansible_facts.distribution_version == '6.10') or (ansible_facts.os_family == 'RedHat' and ansible_facts.distribution_version == '6.10')
fail_msg: out={{out}} fail_msg: |
out={{ out }}
when: when:
# https://github.com/ansible/ansible/pull/70785 # https://github.com/ansible/ansible/pull/70785
- ansible_facts.distribution not in ["MacOSX"] - ansible_facts.distribution not in ["MacOSX"]

@ -11,7 +11,8 @@
- assert: - assert:
that: that:
- out.stdout != 'root' - out.stdout != 'root'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: Ensure passwordless sudo to root succeeds. - name: Ensure passwordless sudo to root succeeds.
shell: whoami shell: whoami
@ -22,7 +23,8 @@
- assert: - assert:
that: that:
- out.stdout == 'root' - out.stdout == 'root'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- sudo - sudo
- sudo_nopassword - sudo_nopassword

@ -5,10 +5,12 @@
tasks: tasks:
- name: Ensure sudo password absent but required. - name: Ensure sudo password absent but required.
shell: whoami
become: true become: true
become_user: mitogen__pw_required become_user: mitogen__pw_required
command:
cmd: whoami
register: out register: out
changed_when: false
ignore_errors: true ignore_errors: true
when: when:
# https://github.com/ansible/ansible/pull/70785 # https://github.com/ansible/ansible/pull/70785
@ -23,7 +25,8 @@
('Missing sudo password' in out.msg) or ('Missing sudo password' in out.msg) or
('password is required' in out.module_stderr) ('password is required' in out.module_stderr)
) )
fail_msg: out={{out}} fail_msg: |
out={{ out }}
when: when:
# https://github.com/ansible/ansible/pull/70785 # https://github.com/ansible/ansible/pull/70785
- ansible_facts.distribution not in ["MacOSX"] - ansible_facts.distribution not in ["MacOSX"]
@ -31,10 +34,12 @@
or is_mitogen or is_mitogen
- name: Ensure password sudo incorrect. - name: Ensure password sudo incorrect.
shell: whoami
become: true become: true
become_user: mitogen__pw_required become_user: mitogen__pw_required
command:
cmd: whoami
register: out register: out
changed_when: false
vars: vars:
ansible_become_pass: nopes ansible_become_pass: nopes
ignore_errors: true ignore_errors: true
@ -50,25 +55,35 @@
('Incorrect sudo password' in out.msg) or ('Incorrect sudo password' in out.msg) or
('sudo password is incorrect' in out.msg) ('sudo password is incorrect' in out.msg)
) )
fail_msg: out={{out}} fail_msg: |
out={{ out }}
when: when:
# https://github.com/ansible/ansible/pull/70785 # https://github.com/ansible/ansible/pull/70785
- ansible_facts.distribution not in ["MacOSX"] - ansible_facts.distribution not in ["MacOSX"]
or ansible_version.full is version("2.11", ">=", strict=True) or ansible_version.full is version("2.11", ">=", strict=True)
or is_mitogen or is_mitogen
# TODO: https://github.com/dw/mitogen/issues/692 - block:
# - name: Ensure password sudo succeeds. - name: Ensure password sudo succeeds
# shell: whoami become: true
# become: true become_user: mitogen__pw_required
# become_user: mitogen__pw_required vars:
# register: out ansible_become_pass: pw_required_password
# vars: command:
# ansible_become_pass: pw_required_password cmd: whoami
register: sudo_password_success_whoami
changed_when: false
# - assert: - assert:
# that: that:
# - out.stdout == 'mitogen__pw_required' - sudo_password_success_whoami.stdout == 'mitogen__pw_required'
fail_msg: |
sudo_password_success_whoami={{ sudo_password_success_whoami }}
when:
# https://github.com/ansible/ansible/pull/70785
- ansible_facts.distribution not in ["MacOSX"]
or ansible_version.full is version("2.11", ">=", strict=True)
or is_mitogen
tags: tags:
- sudo - sudo
- sudo_password - sudo_password

@ -3,34 +3,38 @@
- name: integration/become/sudo_requiretty.yml - name: integration/become/sudo_requiretty.yml
hosts: test-targets hosts: test-targets
tasks: tasks:
# - include_tasks: ../_mitogen_only.yml # AIUI Vanilla Ansible cannot do sudo when requiretty configured
- include_tasks: ../_mitogen_only.yml
# TODO: https://github.com/dw/mitogen/issues/692 - name: Verify we can login to a non-passworded requiretty account
# - name: Verify we can login to a non-passworded requiretty account become: true
# shell: whoami become_user: mitogen__require_tty
# become: true command:
# become_user: mitogen__require_tty cmd: whoami
# register: out changed_when: false
register: sudo_require_tty_whoami
# - assert: - assert:
# that: that:
# - out.stdout == 'mitogen__require_tty' - sudo_require_tty_whoami.stdout == 'mitogen__require_tty'
fail_msg: |
sudo_require_tty_whoami={{ sudo_require_tty_whoami }}
- name: Verify we can login to a passworded requiretty account
become: true
become_user: mitogen__require_tty_pw_required
vars:
ansible_become_pass: require_tty_pw_required_password
command:
cmd: whoami
changed_when: false
register: sudo_require_tty_password_whoami
# --------------- - assert:
that:
# TODO: https://github.com/dw/mitogen/issues/692 - sudo_require_tty_password_whoami.stdout == 'mitogen__require_tty_pw_required'
# - name: Verify we can login to a passworded requiretty account fail_msg: |
# shell: whoami sudo_require_tty_password_whoami={{ sudo_require_tty_password_whoami }}
# become: true
# become_user: mitogen__require_tty_pw_required
# vars:
# ansible_become_pass: require_tty_pw_required_password
# register: out
# - assert:
# that:
# - out.stdout == 'mitogen__require_tty_pw_required'
tags: tags:
- mitogen_only - mitogen_only
- sudo - sudo

@ -0,0 +1,34 @@
- name: integration/become/templated_by_inv.yml
hosts: tt_become_by_inv
gather_facts: false
tasks:
- name: Gather facts (avoiding any unprivileged become)
vars:
ansible_become: false
setup:
- meta: reset_connection
- name: Templated become in inventory
vars:
expected_become_users:
tt-become: root
tt-become-exe: root
tt-become-flags: root
tt-become-method: root
tt-become-pass: mitogen__pw_required
tt-become-user: root
command:
cmd: whoami
changed_when: false
check_mode: false
register: become_templated_by_inv_whoami
failed_when:
- become_templated_by_inv_whoami is failed
or become_templated_by_inv_whoami.stdout != expected_become_users[inventory_hostname]
when:
# https://github.com/ansible/ansible/pull/70785
- ansible_become_user in ['root']
or ansible_facts.distribution not in ["MacOSX"]
or ansible_version.full is version("2.11", ">=", strict=True)
or is_mitogen

@ -0,0 +1,53 @@
- name: integration/become/templated_by_play_keywords.yml
hosts: tt_become_bare
gather_facts: false
become: "{{ 'true' | trim }}"
become_exe: "{{ 'sudo' | trim }}"
become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}"
become_method: "{{ 'sudo' | trim }}"
become_user: "{{ 'root' | trim }}"
tasks:
- meta: reset_connection
- name: Templated become by play keywords, no password
command:
cmd: whoami
changed_when: false
check_mode: false
register: become_templated_by_play_keywords_whoami
failed_when:
- become_templated_by_play_keywords_whoami is failed
or become_templated_by_play_keywords_whoami.stdout != 'root'
- name: integration/become/templated_by_play_keywords.yml
hosts: tt_become_bare
gather_facts: false
become: "{{ 'true' | trim }}"
become_exe: "{{ 'sudo' | trim }}"
become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}"
become_method: "{{ 'sudo' | trim }}"
become_user: "{{ 'mitogen__pw_required' | trim }}"
vars:
ansible_become_pass: "{{ 'pw_required_password' | trim }}"
tasks:
- name: Gather facts (avoiding any unprivileged become)
vars:
ansible_become: false
setup:
- meta: reset_connection
- name: Templated become by play keywords, password
command:
cmd: whoami
changed_when: false
check_mode: false
register: become_templated_by_play_keywords_password_whoami
failed_when:
- become_templated_by_play_keywords_password_whoami is failed
or become_templated_by_play_keywords_password_whoami.stdout != 'mitogen__pw_required'
when:
# https://github.com/ansible/ansible/pull/70785
- ansible_facts.distribution not in ["MacOSX"]
or ansible_version.full is version("2.11", ">=", strict=True)
or is_mitogen

@ -0,0 +1,52 @@
- name: integration/become/templated_by_play_vars.yml
hosts: tt_become_bare
gather_facts: false
vars:
ansible_become: true
ansible_become_exe: "{{ 'sudo' | trim }}"
ansible_become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}"
ansible_become_method: "{{ 'sudo' | trim }}"
ansible_become_user: "{{ 'root' | trim }}"
tasks:
- name: Templated become by play vars, no password
command:
cmd: whoami
changed_when: false
check_mode: false
register: become_templated_by_play_vars_whoami
failed_when:
- become_templated_by_play_vars_whoami is failed
or become_templated_by_play_vars_whoami.stdout != 'root'
- name: integration/become/templated_by_play_vars.yml
hosts: tt_become_bare
gather_facts: false
vars:
ansible_become: true
ansible_become_exe: "{{ 'sudo' | trim }}"
ansible_become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}"
ansible_become_method: "{{ 'sudo' | trim }}"
ansible_become_pass: "{{ 'pw_required_password' | trim }}"
ansible_become_user: "{{ 'mitogen__pw_required' | trim }}"
tasks:
- name: Gather facts (avoiding any unprivileged become)
vars:
ansible_become: false
setup:
- meta: reset_connection
- name: Templated become by play vars, password
command:
cmd: whoami
changed_when: false
check_mode: false
register: become_templated_by_play_vars_password_whoami
failed_when:
- become_templated_by_play_vars_password_whoami is failed
or become_templated_by_play_vars_password_whoami.stdout != 'mitogen__pw_required'
when:
# https://github.com/ansible/ansible/pull/70785
- ansible_facts.distribution not in ["MacOSX"]
or ansible_version.full is version("2.11", ">=", strict=True)
or is_mitogen

@ -0,0 +1,83 @@
- name: integration/become/templated_by_task_keywords.yml
hosts: tt_become_bare
gather_facts: false
# FIXME Resetting the connection shouldn't require credentials
# https://github.com/mitogen-hq/mitogen/issues/1132
become: "{{ 'true' | trim }}"
become_exe: "{{ 'sudo' | trim }}"
become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}"
become_method: "{{ 'sudo' | trim }}"
become_user: "{{ 'root' | trim }}"
tasks:
- name: Reset connection to target that will be delegate_to
meta: reset_connection
- name: Test connection template by task keywords, with delegate_to
hosts: test-targets[0]
gather_facts: false
tasks:
- name: Templated become by task keywords, with delegate_to
become: "{{ 'true' | trim }}"
become_exe: "{{ 'sudo' | trim }}"
become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}"
become_method: "{{ 'sudo' | trim }}"
become_user: "{{ 'root' | trim }}"
delegate_to: "{{ groups.tt_become_bare[0] }}"
command:
cmd: whoami
changed_when: false
check_mode: false
register: become_templated_by_task_with_delegate_to_whoami
failed_when:
- become_templated_by_task_with_delegate_to_whoami is failed
or become_templated_by_task_with_delegate_to_whoami.stdout != 'root'
- name: integration/become/templated_by_task_keywords.yml
hosts: tt_become_bare
gather_facts: false
# FIXME Resetting the connection shouldn't require credentials
# https://github.com/mitogen-hq/mitogen/issues/1132
become: "{{ 'true' | trim }}"
become_exe: "{{ 'sudo' | trim }}"
become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}"
become_method: "{{ 'sudo' | trim }}"
become_user: "{{ 'mitogen__pw_required' | trim }}"
vars:
ansible_become_pass: "{{ 'pw_required_password' | trim }}"
tasks:
- name: Reset connection to target that will be delegate_to
meta: reset_connection
- name: Test connection template by task keywords, with delegate_to
hosts: test-targets[0]
gather_facts: false
tasks:
- name: Gather facts (avoiding any unprivileged become)
delegate_to: "{{ groups.tt_become_bare[0] }}"
vars:
ansible_become: false
setup:
- name: Templated become by task keywords, with delegate_to
become: "{{ 'true' | trim }}"
become_exe: "{{ 'sudo' | trim }}"
become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}"
become_method: "{{ 'sudo' | trim }}"
become_user: "{{ 'mitogen__pw_required' | trim }}"
delegate_to: "{{ groups.tt_become_bare[0] }}"
vars:
ansible_become_pass: "{{ 'pw_required_password' | trim }}"
command:
cmd: whoami
changed_when: false
check_mode: false
register: become_templated_by_task_with_delegate_to_password_whoami
failed_when:
- become_templated_by_task_with_delegate_to_password_whoami is failed
or become_templated_by_task_with_delegate_to_password_whoami.stdout != 'mitogen__pw_required'
when:
# https://github.com/ansible/ansible/pull/70785
- ansible_facts.distribution not in ["MacOSX"]
or ansible_version.full is version("2.11", ">=", strict=True)
or is_mitogen

@ -26,4 +26,6 @@
- original.stat.checksum == copied.stat.checksum - original.stat.checksum == copied.stat.checksum
# Upstream does not preserve timestamps at al. # Upstream does not preserve timestamps at al.
#- (not is_mitogen) or (original.stat.mtime|int == copied.stat.mtime|int) #- (not is_mitogen) or (original.stat.mtime|int == copied.stat.mtime|int)
fail_msg: original={{original}} copied={{copied}} fail_msg: |
original={{ original }}
copied={{ copied }}

@ -18,7 +18,8 @@
- out.result[0].method == "ssh" - out.result[0].method == "ssh"
- out.result[0].kwargs.username == "joe" - out.result[0].kwargs.username == "joe"
- out.result|length == 1 # no sudo - out.result|length == 1 # no sudo
fail_msg: out={{out}} fail_msg: |
out={{ out }}
# Now try with a different account. # Now try with a different account.
@ -34,7 +35,8 @@
- out.result[1].method == "sudo" - out.result[1].method == "sudo"
- out.result[1].kwargs.username == "james" - out.result[1].kwargs.username == "james"
- out.result|length == 2 # no sudo - out.result|length == 2 # no sudo
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- become_same_user - become_same_user
- mitogen_only - mitogen_only

@ -29,7 +29,8 @@
that: that:
- out.rc == 4 - out.rc == 4
- "'Mitogen was disconnected from the remote environment while a call was in-progress.' in out.stdout" - "'Mitogen was disconnected from the remote environment while a call was in-progress.' in out.stdout"
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- disconnect - disconnect
- disconnect_during_module - disconnect_during_module

@ -16,6 +16,7 @@
- out.result[0] == 0 - out.result[0] == 0
- out.result[1].decode() == "hello, world\r\n" - out.result[1].decode() == "hello, world\r\n"
- out.result[2].decode().startswith("Shared connection to ") - out.result[2].decode().startswith("Shared connection to ")
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- exec_command - exec_command

@ -44,7 +44,11 @@
# sudo PID has changed. # sudo PID has changed.
- out_become.ppid != out_become2.ppid - out_become.ppid != out_become2.ppid
fail_msg: out={{out}} out2={{out2}} out_become={{out_become}} out_become2={{out_become2}} fail_msg: |
out={{ out }}
out2={{ out2 }}
out_become={{ out_become }}
out_become2={{ out_become2 }}
tags: tags:
- mitogen_only - mitogen_only
- reset - reset

@ -27,7 +27,9 @@
assert: assert:
that: that:
- become_acct.pid != login_acct.pid - become_acct.pid != login_acct.pid
fail_msg: become_acct={{become_acct}} login_acct={{login_acct}} fail_msg: |
become_acct={{ become_acct }}
login_acct={{ login_acct }}
- name: reset the connection - name: reset the connection
meta: reset_connection meta: reset_connection
@ -40,7 +42,9 @@
assert: assert:
that: that:
- become_acct.pid != new_become_acct.pid - become_acct.pid != new_become_acct.pid
fail_msg: become_acct={{become_acct}} new_become_acct={{new_become_acct}} fail_msg: |
become_acct={{ become_acct }}
new_become_acct={{ new_become_acct }}
- name: save new pid of login acct - name: save new pid of login acct
become: false become: false
@ -51,6 +55,8 @@
assert: assert:
that: that:
- login_acct.pid != new_login_acct.pid - login_acct.pid != new_login_acct.pid
fail_msg: login_acct={{login_acct}} new_login_acct={{new_login_acct}} fail_msg: |
login_acct={{ login_acct }}
new_login_acct={{ new_login_acct }}
tags: tags:
- reset_become - reset_become

@ -11,11 +11,11 @@
- name: integration/connection_delegation/delegate_to_template.yml - name: integration/connection_delegation/delegate_to_template.yml
vars: vars:
physical_host: "cd-normal-alias" physical_host: "cd-normal-alias"
physical_hosts: ["cd-normal-alias", "cd-normal-normal"]
hosts: test-targets hosts: test-targets
gather_facts: no gather_facts: no
tasks: tasks:
- include_tasks: ../_mitogen_only.yml - include_tasks: ../_mitogen_only.yml
- include_tasks: ../_expected_ssh_port.yml
- meta: end_play - meta: end_play
when: when:
@ -67,7 +67,7 @@
'keepalive_interval': 30, 'keepalive_interval': 30,
'keepalive_count': 10, 'keepalive_count': 10,
'password': null, 'password': null,
'port': null, 'port': '{{ expected_ssh_port }}',
'python_path': ['python3000'], 'python_path': ['python3000'],
'remote_name': null, 'remote_name': null,
'ssh_args': [ 'ssh_args': [

@ -55,6 +55,7 @@
- hosts: cd-normal - hosts: cd-normal
tasks: tasks:
- include_tasks: ../_mitogen_only.yml - include_tasks: ../_mitogen_only.yml
- include_tasks: ../_expected_ssh_port.yml
- mitogen_get_stack: - mitogen_get_stack:
delegate_to: cd-alias delegate_to: cd-alias
register: out register: out
@ -72,7 +73,7 @@
'keepalive_interval': 30, 'keepalive_interval': 30,
'keepalive_count': 10, 'keepalive_count': 10,
'password': null, 'password': null,
'port': null, 'port': '{{ expected_ssh_port }}',
"python_path": ["python3000"], "python_path": ["python3000"],
'remote_name': null, 'remote_name': null,
'ssh_args': [ 'ssh_args': [
@ -98,6 +99,7 @@
- hosts: cd-alias - hosts: cd-alias
tasks: tasks:
- include_tasks: ../_mitogen_only.yml - include_tasks: ../_mitogen_only.yml
- include_tasks: ../_expected_ssh_port.yml
- mitogen_get_stack: - mitogen_get_stack:
register: out register: out
- assert_equal: - assert_equal:
@ -114,7 +116,7 @@
'keepalive_interval': 30, 'keepalive_interval': 30,
'keepalive_count': 10, 'keepalive_count': 10,
'password': null, 'password': null,
'port': null, 'port': '{{ expected_ssh_port }}',
"python_path": ["python3000"], "python_path": ["python3000"],
'remote_name': null, 'remote_name': null,
'ssh_args': [ 'ssh_args': [
@ -140,6 +142,7 @@
- hosts: cd-normal-normal - hosts: cd-normal-normal
tasks: tasks:
- include_tasks: ../_mitogen_only.yml - include_tasks: ../_mitogen_only.yml
- include_tasks: ../_expected_ssh_port.yml
- mitogen_get_stack: - mitogen_get_stack:
register: out register: out
- assert_equal: - assert_equal:
@ -167,7 +170,7 @@
'keepalive_interval': 30, 'keepalive_interval': 30,
'keepalive_count': 10, 'keepalive_count': 10,
'password': null, 'password': null,
'port': null, 'port': '{{ expected_ssh_port }}',
"python_path": ["python3000"], "python_path": ["python3000"],
'remote_name': null, 'remote_name': null,
'ssh_args': [ 'ssh_args': [
@ -193,6 +196,7 @@
- hosts: cd-normal-alias - hosts: cd-normal-alias
tasks: tasks:
- include_tasks: ../_mitogen_only.yml - include_tasks: ../_mitogen_only.yml
- include_tasks: ../_expected_ssh_port.yml
- mitogen_get_stack: - mitogen_get_stack:
register: out register: out
- assert_equal: - assert_equal:
@ -237,7 +241,7 @@
'keepalive_interval': 30, 'keepalive_interval': 30,
'keepalive_count': 10, 'keepalive_count': 10,
'password': null, 'password': null,
'port': null, 'port': '{{ expected_ssh_port }}',
"python_path": ["python3000"], "python_path": ["python3000"],
'remote_name': null, 'remote_name': null,
'ssh_args': [ 'ssh_args': [
@ -262,6 +266,7 @@
- hosts: cd-newuser-normal-normal - hosts: cd-newuser-normal-normal
tasks: tasks:
- include_tasks: ../_mitogen_only.yml - include_tasks: ../_mitogen_only.yml
- include_tasks: ../_expected_ssh_port.yml
- mitogen_get_stack: - mitogen_get_stack:
register: out register: out
- assert_equal: - assert_equal:
@ -289,7 +294,7 @@
'keepalive_interval': 30, 'keepalive_interval': 30,
'keepalive_count': 10, 'keepalive_count': 10,
'password': null, 'password': null,
'port': null, 'port': '{{ expected_ssh_port }}',
"python_path": ["python3000"], "python_path": ["python3000"],
'remote_name': null, 'remote_name': null,
'ssh_args': [ 'ssh_args': [
@ -315,6 +320,7 @@
- hosts: cd-newuser-normal-normal - hosts: cd-newuser-normal-normal
tasks: tasks:
- include_tasks: ../_mitogen_only.yml - include_tasks: ../_mitogen_only.yml
- include_tasks: ../_expected_ssh_port.yml
- mitogen_get_stack: - mitogen_get_stack:
delegate_to: cd-alias delegate_to: cd-alias
register: out register: out
@ -332,7 +338,7 @@
'keepalive_interval': 30, 'keepalive_interval': 30,
'keepalive_count': 10, 'keepalive_count': 10,
'password': null, 'password': null,
'port': null, 'port': '{{ expected_ssh_port }}',
"python_path": ["python3000"], "python_path": ["python3000"],
'remote_name': null, 'remote_name': null,
'ssh_args': [ 'ssh_args': [

@ -11,7 +11,8 @@
- assert: - assert:
that: (not not out.mitogen_loaded) == (not not is_mitogen) that: (not not out.mitogen_loaded) == (not not is_mitogen)
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- local - local
- local_blemished - local_blemished

@ -14,7 +14,8 @@
- assert: - assert:
that: not out.mitogen_loaded that: not out.mitogen_loaded
fail_msg: out={{out}} fail_msg: |
out={{ out }}
when: False when: False
tags: tags:
- paramiko - paramiko

@ -11,7 +11,8 @@
- assert: - assert:
that: (not not out.mitogen_loaded) == (not not is_mitogen) that: (not not out.mitogen_loaded) == (not not is_mitogen)
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- ssh - ssh
- ssh_blemished - ssh_blemished

@ -31,6 +31,8 @@
- assert: - assert:
that: that:
- old_become_env.pid != new_become_env.pid - old_become_env.pid != new_become_env.pid
fail_msg: old_become_env={{old_become_env}} new_become_env={{new_become_env}} fail_msg: |
old_become_env={{ old_become_env }}
new_become_env={{ new_become_env }}
tags: tags:
- reconnection - reconnection

@ -17,7 +17,8 @@
- assert: - assert:
that: that:
- out.stdout is match('.*python([0-9.]+)?\(mitogen:[a-z]+@[^:]+:[0-9]+\)') - out.stdout is match('.*python([0-9.]+)?\(mitogen:[a-z]+@[^:]+:[0-9]+\)')
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- name: Get cmdline, with mitogen_mask_remote_name - name: Get cmdline, with mitogen_mask_remote_name
shell: 'cat /proc/$PPID/cmdline | tr \\0 \\n' shell: 'cat /proc/$PPID/cmdline | tr \\0 \\n'
@ -29,7 +30,8 @@
- assert: - assert:
that: that:
- out.stdout is match('.*python([0-9.]+)?\(mitogen:ansible\)') - out.stdout is match('.*python([0-9.]+)?\(mitogen:ansible\)')
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- mitogen_only - mitogen_only
- remote_name - remote_name

@ -46,7 +46,8 @@
- out.failed - out.failed
- '"Name or service not known" in out.msg or - '"Name or service not known" in out.msg or
"Temporary failure in name resolution" in out.msg' "Temporary failure in name resolution" in out.msg'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
when: when:
- ansible_facts.virtualization_type == "docker" - ansible_facts.virtualization_type == "docker"
- ansible_facts.python.version_info[:2] >= [2, 5] - ansible_facts.python.version_info[:2] >= [2, 5]

@ -135,7 +135,8 @@
assert: assert:
that: that:
- legacy.deprecations | default([]) | length > 0 - legacy.deprecations | default([]) | length > 0
fail_msg: legacy={{legacy}} fail_msg: |
legacy={{ legacy }}
# only check for a dep warning if legacy returned /usr/bin/python and auto didn't # only check for a dep warning if legacy returned /usr/bin/python and auto didn't
when: when:
- legacy.ansible_facts.discovered_interpreter_python == '/usr/bin/python' - legacy.ansible_facts.discovered_interpreter_python == '/usr/bin/python'
@ -146,7 +147,8 @@
assert: assert:
that: that:
- legacy.warnings | default([]) | length > 0 - legacy.warnings | default([]) | length > 0
fail_msg: legacy={{legacy}} fail_msg: |
legacy={{ legacy }}
# only check for a warning if legacy returned /usr/bin/python and auto didn't # only check for a warning if legacy returned /usr/bin/python and auto didn't
when: when:
- legacy.ansible_facts.discovered_interpreter_python == '/usr/bin/python' - legacy.ansible_facts.discovered_interpreter_python == '/usr/bin/python'
@ -168,7 +170,8 @@
that: that:
- auto_silent_out.warnings is not defined - auto_silent_out.warnings is not defined
- auto_silent_out.ansible_facts.discovered_interpreter_python == auto_out.ansible_facts.discovered_interpreter_python - auto_silent_out.ansible_facts.discovered_interpreter_python == auto_out.ansible_facts.discovered_interpreter_python
fail_msg: auto_silent_out={{auto_silent_out}} fail_msg: |
auto_silent_out={{ auto_silent_out }}
- name: test that auto_legacy_silent never warns and got the same answer as auto_legacy - name: test that auto_legacy_silent never warns and got the same answer as auto_legacy
@ -186,7 +189,8 @@
that: that:
- legacy_silent.warnings is not defined - legacy_silent.warnings is not defined
- legacy_silent.ansible_facts.discovered_interpreter_python == legacy.ansible_facts.discovered_interpreter_python - legacy_silent.ansible_facts.discovered_interpreter_python == legacy.ansible_facts.discovered_interpreter_python
fail_msg: legacy_silent={{legacy_silent}} fail_msg: |
legacy_silent={{ legacy_silent }}
- name: ensure modules can't set discovered_interpreter_X or ansible_X_interpreter - name: ensure modules can't set discovered_interpreter_X or ansible_X_interpreter
block: block:
@ -212,7 +216,7 @@
assert: assert:
that: that:
- auto_out.ansible_facts.discovered_interpreter_python == discovered_interpreter_expected - auto_out.ansible_facts.discovered_interpreter_python == discovered_interpreter_expected
fail_msg: >- fail_msg: |
distro={{ distro }} distro={{ distro }}
distro_major= {{ distro_major }} distro_major= {{ distro_major }}
system={{ system }} system={{ system }}

@ -52,7 +52,7 @@
{% if "1" == "1" %} {% if "1" == "1" %}
{{ special_python }} {{ special_python }}
{% else %} {% else %}
python python
{% endif %} {% endif %}
tags: tags:
- complex_args - complex_args

@ -19,6 +19,7 @@
- assert: - assert:
that: stat.stat.exists that: stat.stat.exists
fail_msg: stat={{stat}} fail_msg: |
stat={{ stat }}
tags: tags:
- cwd_prseserved - cwd_prseserved

@ -12,6 +12,7 @@
that: that:
- out.external1_path == "ansible/integration/module_utils/module_utils/external1.py" - out.external1_path == "ansible/integration/module_utils/module_utils/external1.py"
- out.external2_path == "ansible/lib/module_utils/external2.py" - out.external2_path == "ansible/lib/module_utils/external2.py"
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- adjacent_to_playbook - adjacent_to_playbook

@ -11,6 +11,7 @@
that: that:
- out.external1_path == "ansible/lib/module_utils/external1.py" - out.external1_path == "ansible/lib/module_utils/external1.py"
- out.external2_path == "ansible/lib/module_utils/external2.py" - out.external2_path == "ansible/lib/module_utils/external2.py"
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- from_config_path - from_config_path

@ -10,6 +10,7 @@
- assert: - assert:
that: that:
- out.extmod_path == "ansible/lib/module_utils/externalpkg/extmod.py" - out.extmod_path == "ansible/lib/module_utils/externalpkg/extmod.py"
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- from_config_path - from_config_path

@ -7,4 +7,5 @@
that: that:
- out.external3_path == "integration/module_utils/roles/modrole/module_utils/external3.py" - out.external3_path == "integration/module_utils/roles/modrole/module_utils/external3.py"
- out.external2_path == "integration/module_utils/roles/modrole/module_utils/external2.py" - out.external2_path == "integration/module_utils/roles/modrole/module_utils/external2.py"
fail_msg: out={{out}} fail_msg: |
out={{ out }}

@ -6,4 +6,5 @@
- assert: - assert:
that: that:
- out.path == "ansible/integration/module_utils/roles/override_modrole/module_utils/known_hosts.py" - out.path == "ansible/integration/module_utils/roles/override_modrole/module_utils/known_hosts.py"
fail_msg: out={{out}} fail_msg: |
out={{ out }}

@ -13,7 +13,8 @@
- assert: - assert:
that: "out.stdout == ''" that: "out.stdout == ''"
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- become_flags - become_flags
@ -29,6 +30,7 @@
- assert: - assert:
that: "out2.stdout == '2'" that: "out2.stdout == '2'"
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- become_flags - become_flags

@ -11,6 +11,7 @@
- assert: - assert:
that: "result.stdout == '123'" that: "result.stdout == '123'"
fail_msg: result={{result}} fail_msg: |
result={{ result }}
tags: tags:
- environment - environment

@ -3,30 +3,39 @@
- name: integration/playbook_semantics/with_items.yml - name: integration/playbook_semantics/with_items.yml
hosts: test-targets hosts: test-targets
gather_facts: true
tasks: tasks:
- block:
- name: Spin up a few interpreters
become: true
vars:
ansible_become_user: "mitogen__user{{ item }}"
command:
cmd: whoami
with_sequence: start=1 end=3
register: first_run
changed_when: false
# TODO: https://github.com/dw/mitogen/issues/692 - name: Reuse them
# - name: Spin up a few interpreters become: true
# shell: whoami vars:
# become: true ansible_become_user: "mitogen__user{{ item }}"
# vars: command:
# ansible_become_user: "mitogen__user{{item}}" cmd: whoami
# with_sequence: start=1 end=3 with_sequence: start=1 end=3
# register: first_run register: second_run
changed_when: false
# - name: Reuse them - name: Verify first and second run matches expected username.
# shell: whoami vars:
# become: true user_expected: "mitogen__user{{ item | int + 1 }}"
# vars: assert:
# ansible_become_user: "mitogen__user{{item}}" that:
# with_sequence: start=1 end=3 - first_run.results[item | int].stdout == user_expected
# register: second_run - second_run.results[item | int].stdout == user_expected
with_sequence: start=0 end=2
# - name: Verify first and second run matches expected username. when:
# assert: # https://github.com/ansible/ansible/pull/70785
# that: - ansible_facts.distribution not in ["MacOSX"]
# - first_run.results[item|int].stdout == ("mitogen__user%d" % (item|int + 1)) or ansible_version.full is version("2.11", ">=", strict=True)
# - first_run.results[item|int].stdout == second_run.results[item|int].stdout or is_mitogen
# with_sequence: start=0 end=2
tags:
- custom_python_new_style_module

@ -12,7 +12,8 @@
- assert: - assert:
that: echo.stdout == "" that: echo.stdout == ""
fail_msg: echo={{echo}} fail_msg: |
echo={{ echo }}
- name: Create /etc/environment - name: Create /etc/environment
copy: copy:
@ -32,7 +33,8 @@
- assert: - assert:
that: echo.stdout == "555" that: echo.stdout == "555"
fail_msg: echo={{echo}} fail_msg: |
echo={{ echo }}
- name: Cleanup /etc/environment - name: Cleanup /etc/environment
file: file:
@ -51,4 +53,5 @@
- assert: - assert:
that: echo.stdout == "" that: echo.stdout == ""
fail_msg: echo={{echo}} fail_msg: |
echo={{ echo }}

@ -10,7 +10,8 @@
- assert: - assert:
that: echo.stdout == "" that: echo.stdout == ""
fail_msg: echo={{echo}} fail_msg: |
echo={{ echo }}
- name: Copy pam_environment - name: Copy pam_environment
copy: copy:
@ -23,7 +24,8 @@
- assert: - assert:
that: echo.stdout == "321" that: echo.stdout == "321"
fail_msg: echo={{echo}} fail_msg: |
echo={{ echo }}
- name: Cleanup pam_environment - name: Cleanup pam_environment
file: file:
@ -35,4 +37,5 @@
- assert: - assert:
that: echo.stdout == "" that: echo.stdout == ""
fail_msg: echo={{echo}} fail_msg: |
echo={{ echo }}

@ -25,6 +25,7 @@
- assert: - assert:
that: that:
- not out.stat.exists - not out.stat.exists
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- atexit - atexit

@ -16,6 +16,7 @@
out.results[0].item == '1' and out.results[0].item == '1' and
out.results[0].rc == 0 and out.results[0].rc == 0 and
(out.results[0].stdout == ansible_nodename) (out.results[0].stdout == ansible_nodename)
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- builtin_command_module - builtin_command_module

@ -22,6 +22,7 @@
- (out.module_stdout == "" and out.module_stderr is search(tb_pattern)) - (out.module_stdout == "" and out.module_stderr is search(tb_pattern))
or or
(out.module_stdout is search(tb_pattern) and out.module_stderr is match("Shared connection to localhost closed.")) (out.module_stdout is search(tb_pattern) and out.module_stderr is match("Shared connection to localhost closed."))
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- crashy_new_style_module - crashy_new_style_module

@ -22,6 +22,7 @@
(not out.results[0].changed) and (not out.results[0].changed) and
out.results[0].msg == 'Here is my input' and out.results[0].msg == 'Here is my input' and
out.results[0].run_via_env == "yes" out.results[0].run_via_env == "yes"
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- custom_bash_hashbang_argument - custom_bash_hashbang_argument

@ -12,6 +12,7 @@
(not out.changed) and (not out.changed) and
(not out.results[0].changed) and (not out.results[0].changed) and
out.results[0].msg == 'Here is my input' out.results[0].msg == 'Here is my input'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- custom_bash_old_style_module - custom_bash_old_style_module

@ -11,6 +11,7 @@
(not out.changed) and (not out.changed) and
(not out.results[0].changed) and (not out.results[0].changed) and
out.results[0].msg == 'Here is my input' out.results[0].msg == 'Here is my input'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- custom_bash_want_json_module - custom_bash_want_json_module

@ -23,6 +23,7 @@
out.changed and out.changed and
out.results[0].changed and out.results[0].changed and
out.results[0].msg == 'Hello, world.' out.results[0].msg == 'Hello, world.'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- custom_binary_producing_json - custom_binary_producing_json

@ -31,6 +31,7 @@
- out.results[0].failed - out.results[0].failed
- out.results[0].msg.startswith('MODULE FAILURE') - out.results[0].msg.startswith('MODULE FAILURE')
- out.results[0].rc == 0 - out.results[0].rc == 0
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- custom_binary_producing_junk - custom_binary_producing_junk

@ -28,7 +28,8 @@
'custom_binary_single_null: cannot execute binary file: Exec format error\r\n', 'custom_binary_single_null: cannot execute binary file: Exec format error\r\n',
)) ))
or (ansible_facts.distribution == 'Ubuntu' and ansible_facts.distribution_version == '16.04') or (ansible_facts.distribution == 'Ubuntu' and ansible_facts.distribution_version == '16.04')
fail_msg: out={{out}} fail_msg: |
out={{ out }}
tags: tags:
- custom_binary_single_null - custom_binary_single_null

@ -10,13 +10,15 @@
that: that:
- out.results[0].input.foo - out.results[0].input.foo
- out.results[0].message == 'I am a perl script! Here is my input.' - out.results[0].message == 'I am a perl script! Here is my input.'
fail_msg: out={{out}} fail_msg: |
out={{ out }}
- assert: - assert:
that: that:
- (not out.changed) - (not out.changed)
- (not out.results[0].changed) - (not out.results[0].changed)
fail_msg: out={{out}} fail_msg: |
out={{ out }}
when: when:
- ansible_version.full is version('2.4', '>=', strict=True) - ansible_version.full is version('2.4', '>=', strict=True)
tags: tags:

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

Loading…
Cancel
Save