ansible/molecule

Ansible Interpreter discovery not working with pyenv/poetry/molecule/docker

timblaktu opened this issue · 4 comments

Issue Type

  • Bug report

Molecule and Ansible details

> ansible --version && molecule --version
ansible [core 2.12.6]
  config file = /home/tim/src/timblaktu-cm/ansible/ansible.cfg
  configured module search path = ['/home/tim/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/lib/python3.10/site-packages/ansible
  ansible collection location = /home/tim/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/bin/ansible
  python version = 3.10.0 (default, Oct 12 2021, 11:25:19) [GCC 8.3.0]
  jinja version = 3.1.2
  libyaml = True
molecule 4.0.0 using python 3.10
    ansible:2.12.6
    delegated:4.0.0 from molecule
    docker:1.1.0 from molecule_docker requiring collections: community.docker>=1.9.1

Molecule installation method (one of):

  • poetry

Ansible installation method (one of):

  • poetry

Summary

As noted over in ansible-community/molecule-docker#151 and ansible/ansible#78090, ansible is not able to find the python interpreter when run from a simple "deb10-headless" molecule scenario using docker driver:

---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance
    image: debian:buster
    pre_build_image: true
provisioner:
  name: ansible
verifier:
  name: ansible  # TODO: pytest-testinfra

After successfully creating the container using molecule create --scenario-name deb10-headless, and confirming success with:

> molecule list --scenario-name deb10-headless
INFO     Running deb10-headless > list
                ╷             ╷                  ╷                ╷         ╷
  Instance Name │ Driver Name │ Provisioner Name │ Scenario Name  │ Created │ Converged
╶───────────────┼─────────────┼──────────────────┼────────────────┼─────────┼───────────╴
  instance      │ docker      │ ansible          │ deb10-headless │ true    │ false
                ╵             ╵                  ╵                ╵         ╵

> docker container ls
CONTAINER ID   IMAGE           COMMAND                  CREATED       STATUS       PORTS     NAMES
252bf11e0d1d   debian:buster   "bash -c 'while true…"   2 hours ago   Up 2 hours             instance

I get error output from molecule --verbose converge --scenario-name deb10-headless:

TASK [Gathering Facts] *********************************************************
task path: /home/tim/src/timblaktu-cm/ansible/tblack-deb10-headless.yml:8
<instance> ESTABLISH DOCKER CONNECTION FOR USER: root
<instance> EXEC ['/usr/bin/docker', '-H=unix:///mnt/wsl/shared-docker/docker.sock', b'exec', b'-i', 'instance', '/bin/sh', '-c', "/bin/sh -c 'echo ~ && sleep 0'"]
<instance> EXEC ['/usr/bin/docker', '-H=unix:///mnt/wsl/shared-docker/docker.sock', b'exec', b'-i', 'instance', '/bin/sh', '-c', '/bin/sh -c \'( umask 77 && mkdir -p "` echo /root/.ansible/tmp `"&& mkdir "` echo /root/.ansible/tmp/ansible-tmp-1655759232.9442172-20889-87937448235310 `" && echo ansible-tmp-1655759232.9442172-20889-87937448235310="` echo /root/.ansible/tmp/ansible-tmp-1655759232.9442172-20889-87937448235310 `" ) && sleep 0\'']
Using module file /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/lib/python3.10/site-packages/ansible/modules/setup.py
<instance> PUT /home/tim/.ansible/tmp/ansible-local-2088466y_h2kc/tmppuqtmyv1 TO /root/.ansible/tmp/ansible-tmp-1655759232.9442172-20889-87937448235310/AnsiballZ_setup.py
<instance> EXEC ['/usr/bin/docker', '-H=unix:///mnt/wsl/shared-docker/docker.sock', b'exec', b'-i', 'instance', '/bin/sh', '-c', "/bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1655759232.9442172-20889-87937448235310/ /root/.ansible/tmp/ansible-tmp-1655759232.9442172-20889-87937448235310/AnsiballZ_setup.py && sleep 0'"]
<instance> EXEC ['/usr/bin/docker', '-H=unix:///mnt/wsl/shared-docker/docker.sock', b'exec', b'-i', 'instance', '/bin/sh', '-c', "/bin/sh -c '/home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/bin/python /root/.ansible/tmp/ansible-tmp-1655759232.9442172-20889-87937448235310/AnsiballZ_setup.py && sleep 0'"]
<instance> EXEC ['/usr/bin/docker', '-H=unix:///mnt/wsl/shared-docker/docker.sock', b'exec', b'-i', 'instance', '/bin/sh', '-c', "/bin/sh -c 'rm -f -r /root/.ansible/tmp/ansible-tmp-1655759232.9442172-20889-87937448235310/ > /dev/null 2>&1 && sleep 0'"]
fatal: [instance]: FAILED! => {
    "ansible_facts": {},
    "changed": false,
    "failed_modules": {
        "ansible.legacy.setup": {
            "failed": true,
            "module_stderr": "/bin/sh: 1: /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/bin/python: not found\n",
            "module_stdout": "",
            "msg": "The module failed to execute correctly, you probably need to set the interpreter.\nSee stdout/stderr for the exact error",
            "rc": 127
        }
    },
    "msg": "The following modules failed to execute: ansible.legacy.setup\n"
}

PLAY RECAP *********************************************************************
instance                   : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

WARNING  Retrying execution failure 2 of: ansible-playbook --inventory /home/tim/.cache/molecule/ansible/deb10-headless/inventory --skip-tags molecule-notest,notest /home/tim/src/timblaktu-cm/ansible/molecule/deb10-headless/converge.yml
CRITICAL Ansible return code was 2, command was: ['ansible-playbook', '--inventory', '/home/tim/.cache/molecule/ansible/deb10-headless/inventory', '--skip-tags', 'molecule-notest,notest', '/home/tim/src/timblaktu-cm/ansible/molecule/deb10-headless/converge.yml']

Even though I'm telling ansible explicitly this path using:

export ANSIBLE_PYTHON_INTERPRETER="$(which python)"

(and if I don't specify this env var, the error still happens but it complains about the path /usr/bin/python.)

The active python interpreter on my path is a poetry venv shim:

> which python
/home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/bin/python

.. which points to a pyenv-installed interpreter binary:

> ll /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/bin/python
lrwxrwxrwx 1 tim tim 47 Jun 18 11:21 /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/bin/python -> /home/tim/.pyenv/versions/3.10.0/bin/python3.10*

I suspect this is a molecule issue (or perhaps me misusing molecule) because I can successfully run ansible in this environment to gather localhost facts:

> ansible -m setup localhost                                                                            (1 results) [1047/64866][WARNING]: No inventory was parsed, only implicit localhost is available
localhost | SUCCESS => {
    "ansible_facts": {
    .
    .

I understand from ansible/ansible#74045 that this is thought to be caused by an implicit localhost, but since I'm specifying ANSIBLE_PYTHON_INTERPRETER explicitly, and I'm using the molecule-docker driver which generates an inventory file to pass to the playbook, and since the error I'm getting clearly indicates ansible is using this path, I believe this to be another problem.

Ansible Version

$ ansible --version
ansible [core 2.12.6]
  config file = /home/tim/src/timblaktu-cm/ansible/ansible.cfg
  configured module search path = ['/home/tim/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/lib/python3.10/site-packages/ansible
  ansible collection location = /home/tim/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/bin/ansible
  python version = 3.10.0 (default, Oct 12 2021, 11:25:19) [GCC 8.3.0]
  jinja version = 3.1.2
  libyaml = True

Ansible Configuration

# if using a version older than ansible-core 2.12 you should omit the '-t all'
$ ansible-config dump --only-changed -t all
DEFAULT_FORKS(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = 20
DEFAULT_LOG_PATH(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = /home/tim/src/timblaktu-cm/ansible/ansible.log
DEFAULT_MANAGED_STR(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = #Ansible managed by user {uid} on {host} from template file:

DEFAULT_POLL_INTERVAL(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = 5
DEFAULT_ROLES_PATH(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = ['/etc/ansible/roles', '/home/tim/src/timblaktu-cm/ansible/roles', '/home/tim/.ansible/collections/ansib>DEFAULT_STDOUT_CALLBACK(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = debug
HOST_KEY_CHECKING(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = False
INTERPRETER_PYTHON(env: ANSIBLE_PYTHON_INTERPRETER) = /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/bin/python
RETRY_FILES_ENABLED(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = False

BECOME:
======

CACHE:
=====

CALLBACK:
========

CLICONF:
=======

CONNECTION:
==========

paramiko_ssh:
____________
host_key_checking(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = False

ssh:
___
host_key_checking(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = False
pipelining(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = False
ssh_args(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no

HTTPAPI:
=======

INVENTORY:
=========

LOOKUP:
======

NETCONF:
=======

SHELL:
=====

sh:
__
world_readable_temp(/home/tim/src/timblaktu-cm/ansible/ansible.cfg) = True

VARS:
====

OS / Environment

Debian 11.2 in WSL

Virtual Environment Versions:

> poetry show
ansible            5.9.0     Radically simple IT automation
ansible-compat     2.1.0     Ansible compatibility goodies
ansible-core       2.12.6    Radically simple IT automation
arrow              1.2.2     Better dates & times for Python
attrs              21.4.0    Classes Without Boilerplate
binaryornot        0.4.4     Ultra-lightweight pure Python package to check if a file is binary or text.
cerberus           1.3.2     Lightweight, extensible schema and data validation tool for Python dictionaries.
certifi            2022.6.15 Python package for providing Mozilla's CA Bundle.
cffi               1.15.0    Foreign Function Interface for Python calling C code.
chardet            4.0.0     Universal encoding detector for Python 2 and 3
charset-normalizer 2.0.12    The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet.
click              8.1.3     Composable command line interface toolkit
click-help-colors  0.9.1     Colorization of help messages in Click
commonmark         0.9.1     Python parser for the CommonMark Markdown spec
cookiecutter       2.1.1     A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project te...
cryptography       37.0.2    cryptography is a package which provides cryptographic recipes and primitives to Python developers.
distro             1.7.0     Distro - an OS platform information API
docker             5.0.3     A Python library for the Docker Engine API.
enrich             1.2.7     enrich
idna               3.3       Internationalized Domain Names in Applications (IDNA)
iniconfig          1.1.1     iniconfig: brain-dead simple config-ini parsing
jinja2             3.1.2     A very fast and expressive template engine.
jinja2-time        0.2.0     Jinja2 Extension for Dates and Times
jsonschema         4.6.0     An implementation of JSON Schema validation for Python
markupsafe         2.1.1     Safely add untrusted strings to HTML/XML markup.
molecule           4.0.0     Molecule aids in the development and testing of Ansible roles
molecule-docker    1.1.0     Molecule aids in the development and testing of Ansible roles
packaging          21.3      Core utilities for Python packages
pluggy             1.0.0     plugin and hook calling mechanisms for python
py                 1.11.0    library with cross-python path, ini-parsing, io, code, log facilities
pycparser          2.21      C parser in Python
pygments           2.12.0    Pygments is a syntax highlighting package written in Python.
pyparsing          3.0.9     pyparsing module - Classes and methods to define and execute parsing grammars
pyrsistent         0.18.1    Persistent/Functional/Immutable data structures
pytest             7.1.2     pytest: simple powerful testing with Python
python-dateutil    2.8.2     Extensions to the standard Python datetime module
python-slugify     6.1.2     A Python slugify application that also handles Unicode
pyyaml             6.0       YAML parser and emitter for Python
requests           2.28.0    Python HTTP for Humans.
resolvelib         0.5.5     Resolve abstract dependencies into concrete ones
rich               12.4.4    Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal
selinux            0.2.1     shim selinux module
six                1.16.0    Python 2 and 3 compatibility utilities
subprocess-tee     0.3.5     subprocess-tee
text-unidecode     1.3       The most basic Text::Unidecode port
tomli              2.0.1     A lil' TOML parser
urllib3            1.26.9    HTTP library with thread-safe connection pooling, file post, and more.
websocket-client   1.3.2     WebSocket client for Python with low level API options

Steps to Reproduce

Inside my poetry shell, run molecule converge on the molecule scenario containing this test playbook:

---
- name: Converge
  hosts: all
  tasks:
    - name: "Import Main Playbook to be Tested"
      import_playbook:
        name: "../../some-other-playbook-to-test.yml"

Expected Results

I expected ansible to be able to find and run the python interpreter pointer to by ANSIBLE_INTERPRETER_PATH, which i set explicitly using:

export ANSIBLE_PYTHON_INTERPRETER="$(which python)"

which works just fine manually in the same shell I ran molecule from:

> /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/bin/python
Python 3.10.0 (default, Oct 12 2021, 11:25:19) [GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Actual Results

TASK [Gathering Facts] *********************************************************
fatal: [instance]: FAILED! => {"ansible_facts": {}, "changed": false, "failed_modules": {"ansible.legacy.setup": {"failed": true, "module_stderr": "/bin/sh: 1: /home/tim/.cache/pypoetry/virtualenvs/timblaktu-cm-ansible-X34RCiD6-py3.10/bin/python: not found\n", "module_stdout": "", "msg": "The module failed to execute correctly, you probably need to set the interpreter.\nSee stdout/stderr for the exact error", "rc": 127}}, "msg": "The following modules failed to execute: ansible.legacy.setup\n"}

@timblaktu

If you want to import a playbook, files with a list of plays can only be included at the top level.

Please note the indent.

You cannot use this action inside a play.

Here is an example.

- hosts: localhost
  tasks:
    - debug:
        msg: play1

- name: Include a play after another play
  import_playbook: otherplays.yaml

- name: Set variables on an imported playbook
  import_playbook: otherplays.yml
  vars:
    service: httpd

- name: This DOES NOT WORK
  hosts: all
  tasks:
    - debug:
        msg: task1

    - name: This fails because I'm inside a play already
      import_playbook: stuff.yaml

How to test

~/src/tmp/my_new_role_docker 11s                                                                                      23:17:52
v-397-test ❯ cat molecule/default/converge.yml
---
- name: Converge
  hosts: all
  tasks:
    - name: "Include acme.my_new_role_docker"
      include_role:
        name: "acme.my_new_role_docker"

- name: Import Main Playbook to be Tested
  import_playbook: ../../a.yml
~/src/tmp/my_new_role_docker                                                                                          23:23:16
v-397-test ❯ cat a.yml
---
- name: Converge
  hosts: all
  tasks:
    - name: "Include acme.my_new_role_docker"
      debug:
        msg: "{{ molecule_ephemeral_directory | default(5) }}"
~/src/tmp/my_new_role_docker                                                                                          23:23:23
v-397-test ❯ cat a.yml
~/src/tmp/my_new_role_docker                                                                                          23:23:26
v-397-test ❯ molecule converge
[WARNING]: You are running the development version of Ansible. You should only
run Ansible from "devel" if you are modifying the Ansible engine, or trying out
features under development. This is a rapidly changing source of code and can
become unstable at any point.
INFO     default scenario test matrix: dependency, create, prepare, converge
INFO     Performing prerun with role_name_check=0...
INFO     Set ANSIBLE_LIBRARY=/Users/jackzhang/.cache/ansible-compat/81a7e6/modules:/Users/jackzhang/.ansible/plugins/modules:/usr/share/ansible/plugins/modules
INFO     Set ANSIBLE_COLLECTIONS_PATH=/Users/jackzhang/.cache/ansible-compat/81a7e6/collections:/Users/jackzhang/.ansible/collections:/usr/share/ansible/collections
INFO     Set ANSIBLE_ROLES_PATH=/Users/jackzhang/.cache/ansible-compat/81a7e6/roles:/Users/jackzhang/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles
INFO     Using /Users/jackzhang/.cache/ansible-compat/81a7e6/roles/acme.my_new_role_docker symlink to current repository in order to enable Ansible to find the role using its expected full name.
INFO     Running default > dependency
WARNING  Skipping, missing the requirements file.
WARNING  Skipping, missing the requirements file.
INFO     Running default > create
WARNING  Skipping, instances already created.
INFO     Running default > prepare
WARNING  Skipping, prepare playbook not configured.
INFO     Running default > converge
INFO     Sanity checks: 'docker'
[WARNING]: You are running the development version of Ansible. You should only
run Ansible from "devel" if you are modifying the Ansible engine, or trying out
features under development. This is a rapidly changing source of code and can
become unstable at any point.

PLAY [Converge] ****************************************************************

TASK [Gathering Facts] *********************************************************
ok: [instance]

TASK [Include acme.my_new_role_docker] *****************************************

PLAY [Converge] ****************************************************************

TASK [Gathering Facts] *********************************************************
ok: [instance]

TASK [Include acme.my_new_role_docker] *****************************************
ok: [instance] => {
    "msg": "/Users/jackzhang/.cache/molecule/my_new_role_docker/default"
}

PLAY RECAP *********************************************************************
instance                   : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Thanks @zhan9san, I will fix my playbook syntax as you've noted. However I can't imagine that this would be the cause of the error I reported. It appears that molecule isn't even able to find and/or execute ansible in the virtual python environment I'm using. This is the same virtual environment that molecule is installed into and running in.

@zhan9san changing my deb10-headless molecule scenario's converge.yml playbook to correctly invoke the desired playbook:

---
- name: Import Main Playbook to be Tested
  import_playbook: ../../tblack-deb10-headless.yml

has no effect on the reported error.

News flash

It just occurred to me that this error means that ansible is not finding python in the target container.

I then removed ANSIBLE_PYTHON_INTERPRETER from my environment, and the started focusing on why ansible is not able to discover python at /usr/bin/python in the container. This was cleared up pretty quickly when I shelled into the container and saw no python.

So, I have confirmed this was cockpit error on my part, and confirmed it's discovering python interpreter after changing the docker driver config in molecule.yml to use the python:buster image. I can now see that there are going to be other pre-reqs that my playbooks will require, and I can use a prepare step to ensure I satisfy these.

UPDATE: I eventually came to learn, and not from reading the molecule docs, that there is a Dockerfile.j2 template placed in the scenario dir when you initialize it using docker driver type. I had initially initialized my scenario using default driver, and manually changed the driver in the molecule.yml, so I had no idea where the Dockerfile.j2 came from until I found it in a search of the molecule repo. The only mention of this template is on this page which discussed how to modify the template, but nothing about where it comes from.