Repository: TomasTomecek/ansible-bender

Issue:
get python version of local ansible and exit if it's 2
This is a dupe of #76 because the problem with that issue is that ansible is buitl with python 2 and bender's callback plugin can't be imported b/c it's py 3 only code.

TODO

* [x] detect python version via `ansible-playbook --version`
  * we can't use the API because bender may be running in a venv
* [x] if the py version is < 3, halt and refuse to continue

Comments:

Patch:
diff --git a/ansible_bender/core.py b/ansible_bender/core.py
index cec269e..a01f098 100644
--- a/ansible_bender/core.py
+++ b/ansible_bender/core.py
@@ -45,7 +45,8 @@ from ansible_bender import callback_plugins
 from ansible_bender.conf import ImageMetadata, Build
 from ansible_bender.constants import TIMESTAMP_FORMAT
 from ansible_bender.exceptions import AbBuildUnsuccesful
-from ansible_bender.utils import run_cmd, ap_command_exists, random_str, graceful_get
+from ansible_bender.utils import run_cmd, ap_command_exists, random_str, graceful_get, \
+    is_ansibles_python_2
 
 logger = logging.getLogger(__name__)
 A_CFG_TEMPLATE = """\
@@ -78,6 +79,12 @@ def run_playbook(playbook_path, inventory_path, a_cfg_path, connection, extra_va
     :return: output
     """
     ap = ap_command_exists()
+    if is_ansibles_python_2(ap):
+        raise RuntimeError(
+            "ansible-bender is written in python 3 and does not work in python 2,\n"
+            f"it seems that {ap} is using python 2 - ansible-bender will not"
+            "work in such environment\n"
+        )
     cmd_args = [
         ap,
         "-c", connection,
diff --git a/ansible_bender/utils.py b/ansible_bender/utils.py
index 2f23892..ac0fac2 100644
--- a/ansible_bender/utils.py
+++ b/ansible_bender/utils.py
@@ -4,6 +4,7 @@ Utility functions. This module can't depend on anything within ab.
 import logging
 import os
 import random
+import re
 import shutil
 import string
 import subprocess
@@ -126,14 +127,14 @@ def env_get_or_fail_with(env_name, err_msg):
         raise RuntimeError(err_msg)
 
 
-def one_of_commands_exists(commands, exc_msg):
+def one_of_commands_exists(commands, exc_msg) -> str:
     """
     Verify that the provided command exists. Raise CommandDoesNotExistException in case of an
     error or if the command does not exist.
 
     :param commands: str, command to check (python 3 only)
     :param exc_msg: str, message of exception when command does not exist
-    :return: bool, True if everything's all right (otherwise exception is thrown)
+    :return: str, the command which exists
     """
     found = False
     for command in commands:
@@ -233,3 +234,29 @@ def random_str(size=10):
     :return: the string
     """
     return ''.join(random.choice(string.ascii_lowercase) for _ in range(size))
+
+
+def is_ansibles_python_2(ap_exe: str) -> bool:
+    """
+    Discover whether ansible-playbook is using python 2.
+
+    :param ap_exe: path to the python executable
+
+    :return: True if it's 2
+    """
+    out = run_cmd([ap_exe, "--version"], log_stderr=True, return_output=True)
+
+    # python version = 3.7.2
+    reg = r"python version = (\d)"
+
+    reg_grp = re.findall(reg, out)
+    try:
+        py_version = reg_grp[0]
+    except IndexError:
+        logger.warning("could not figure out which python is %s using", ap_exe)
+        return False  # we don't know, fingers crossed
+    if py_version == "2":
+        logger.info("%s is using python 2", ap_exe)
+        return True
+    logger.debug("it seems that %s is not using python 2", ap_exe)
+    return False


Test patch:
diff --git a/tests/integration/test_core.py b/tests/integration/test_core.py
new file mode 100644
index 0000000..c462889
--- /dev/null
+++ b/tests/integration/test_core.py
@@ -0,0 +1,17 @@
+"""
+Tests for ansible invocation
+"""
+import pytest
+from flexmock import flexmock
+
+from ansible_bender import utils
+from ansible_bender.core import run_playbook
+from tests.spellbook import C7_AP_VER_OUT
+
+
+def test_ansibles_python():
+    flexmock(utils, run_cmd=lambda *args, **kwargs: C7_AP_VER_OUT),
+    with pytest.raises(RuntimeError) as ex:
+        run_playbook(None, None, None, None)
+    assert str(ex.value).startswith(
+        "ansible-bender is written in python 3 and does not work in python 2,\n")
diff --git a/tests/spellbook.py b/tests/spellbook.py
index 723f4fe..1635998 100644
--- a/tests/spellbook.py
+++ b/tests/spellbook.py
@@ -31,6 +31,15 @@ change_layering_playbook = os.path.join(data_dir, "change_layering.yaml")
 bad_playbook_path = os.path.join(data_dir, "bad_playbook.yaml")
 base_image = "docker.io/library/python:3-alpine"
 
+C7_AP_VER_OUT = """\
+ansible-playbook 2.4.2.0
+  config file = /etc/ansible/ansible.cfg
+  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
+  ansible python module location = /usr/lib/python2.7/site-packages/ansible
+  executable location = /usr/bin/ansible-playbook
+  python version = 2.7.5 (default, Oct 30 2018, 23:45:53) [GCC 4.8.5 20150623 (Red Hat 4.8.5-36)]
+"""
+
 
 def random_word(length):
     # https://stackoverflow.com/a/2030081/909579
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 0eb0883..5960da9 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -1,9 +1,12 @@
 import re
 
 import pytest
+from flexmock import flexmock
 
+from ansible_bender import utils
 from ansible_bender.db import generate_working_cont_name
-from ansible_bender.utils import run_cmd, graceful_get
+from ansible_bender.utils import run_cmd, graceful_get, ap_command_exists, is_ansibles_python_2
+from tests.spellbook import C7_AP_VER_OUT
 
 
 def test_run_cmd():
@@ -39,3 +42,20 @@ def test_graceful_g_w_default():
     assert graceful_get(inp, 1, default="asd") == {2: 3}
     assert graceful_get(inp, 1, 2, default="asd") == 3
     assert graceful_get(inp, 1, 2, 4, default="asd") == "asd"
+
+
+@pytest.mark.parametrize("m,is_py2", (
+    (object, False),  # no mocking
+    (
+        lambda: flexmock(utils, run_cmd=lambda *args, **kwargs: C7_AP_VER_OUT),
+        True
+    ),
+    (
+        lambda: flexmock(utils, run_cmd=lambda *args, **kwargs: "nope"),
+        False
+    ),
+))
+def test_ansibles_python(m, is_py2):
+    m()
+    cmd = ap_command_exists()
+    assert is_ansibles_python_2(cmd) == is_py2