mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Do not allow python2 to be added to salt-ssh tar
This commit is contained in:
parent
bcde0376b1
commit
af7000f9dd
8 changed files with 209 additions and 308 deletions
1
changelog/57647.fixed
Normal file
1
changelog/57647.fixed
Normal file
|
@ -0,0 +1 @@
|
|||
Do not allow python2 to be added to salt-ssh tar since Salt deprecated Python 2.
|
|
@ -102,10 +102,10 @@ Alternatively ssh agent forwarding can be used by setting the priv to agent-forw
|
|||
Calling Salt SSH
|
||||
================
|
||||
|
||||
.. note:: ``salt-ssh`` on RHEL/CentOS 5
|
||||
.. note:: ``salt-ssh`` on target hosts without Python 3
|
||||
|
||||
The ``salt-ssh`` command requires at least python 2.6, which is not
|
||||
installed by default on RHEL/CentOS 5. An easy workaround in this
|
||||
The ``salt-ssh`` command requires at least python 3, which is not
|
||||
installed by default on some target hosts. An easy workaround in this
|
||||
situation is to use the ``-r`` option to run a raw shell command that
|
||||
installs python26:
|
||||
|
||||
|
@ -322,4 +322,5 @@ is being dropped we have provided multiple ways to work around this with Salt-SS
|
|||
can use the following options:
|
||||
|
||||
* :ref:`ssh_pre_flight <ssh_pre_flight>`
|
||||
* :ref:`SSH ext alternatives <ssh-ext-alternatives>`
|
||||
* Using the Salt-SSH raw shell calls to install Python3.
|
||||
* Use an older version of Salt on the target host that still supports Python 2 using the feature :ref:`SSH ext alternatives <ssh-ext-alternatives>`
|
||||
|
|
|
@ -5,10 +5,10 @@ SSH Ext Alternatives
|
|||
====================
|
||||
|
||||
In the 2019.2.0 release the ``ssh_ext_alternatives`` feature was added.
|
||||
This allows salt-ssh to work across different python versions. You will
|
||||
This allows salt-ssh to work across different supported python versions. You will
|
||||
need to ensure you have the following:
|
||||
|
||||
- Salt is installed, with all required dependnecies for both Python2 and Python3
|
||||
- Salt is installed, with all required dependnecies for the Python version.
|
||||
- Everything needs to be importable from the respective Python environment.
|
||||
|
||||
To enable using this feature you will need to edit the master configuration similar
|
||||
|
@ -31,6 +31,14 @@ to below:
|
|||
markupsafe: /opt/markupsafe
|
||||
backports_abc: /opt/backports_abc.py
|
||||
|
||||
.. warning::
|
||||
When using Salt versions >= 3001 and Python 2 is your ``py-version``
|
||||
you need to use an older version of Salt that supports Python 2.
|
||||
For example, if using Salt-SSH version 3001 and you do not want
|
||||
to install Python 3 on your target host you can use ``ssh_ext_alternatives``'s
|
||||
``path`` option. This option needs to point to a 2019.2.3 Salt installation directory
|
||||
on your Salt-SSH host, which still supports Python 2.
|
||||
|
||||
auto_detect
|
||||
-----------
|
||||
|
||||
|
|
|
@ -1527,30 +1527,6 @@ ARGS = {arguments}\n'''.format(
|
|||
"Permissions problem, target user may need " "to be root or use sudo:\n {0}"
|
||||
)
|
||||
|
||||
def _version_mismatch_error():
|
||||
messages = {
|
||||
2: {
|
||||
6: "Install Python 2.7 / Python 3 Salt dependencies on the Salt SSH master \n"
|
||||
"to interact with Python 2.7 / Python 3 targets",
|
||||
7: "Install Python 2.6 / Python 3 Salt dependencies on the Salt SSH master \n"
|
||||
"to interact with Python 2.6 / Python 3 targets",
|
||||
},
|
||||
3: {
|
||||
"default": "- Install Python 2.6/2.7 Salt dependencies on the Salt SSH \n"
|
||||
" master to interact with Python 2.6/2.7 targets\n"
|
||||
"- Install Python 3 on the target machine(s)",
|
||||
},
|
||||
"default": "Matching major/minor Python release (>=2.6) needed both on the Salt SSH \n"
|
||||
"master and target machine",
|
||||
}
|
||||
major, minor = sys.version_info[:2]
|
||||
help_msg = (
|
||||
messages.get(major, {}).get(minor)
|
||||
or messages.get(major, {}).get("default")
|
||||
or messages["default"]
|
||||
)
|
||||
return "Python version error. Recommendation(s) follow:\n" + help_msg
|
||||
|
||||
errors = [
|
||||
(
|
||||
(),
|
||||
|
@ -1560,7 +1536,9 @@ ARGS = {arguments}\n'''.format(
|
|||
(
|
||||
(salt.defaults.exitcodes.EX_THIN_PYTHON_INVALID,),
|
||||
"Python interpreter is too old",
|
||||
_version_mismatch_error(),
|
||||
"Python version error. Recommendation(s) follow:\n"
|
||||
"- Install Python 3 on the target machine(s)\n"
|
||||
"- You can use ssh_pre_flight or raw shell (-r) to install Python 3",
|
||||
),
|
||||
(
|
||||
(salt.defaults.exitcodes.EX_THIN_CHECKSUM,),
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Generate the salt thin tarball from the installed python files
|
||||
"""
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
@ -14,7 +12,6 @@ import tarfile
|
|||
import tempfile
|
||||
import zipfile
|
||||
|
||||
import distro
|
||||
import jinja2
|
||||
import msgpack
|
||||
import salt
|
||||
|
@ -72,11 +69,13 @@ except ImportError:
|
|||
except ImportError:
|
||||
ssl_match_hostname = None
|
||||
# pylint: enable=import-error,no-name-in-module
|
||||
|
||||
|
||||
if _six.PY2:
|
||||
import concurrent
|
||||
|
||||
distro = None
|
||||
else:
|
||||
import distro
|
||||
|
||||
concurrent = None
|
||||
|
||||
|
||||
|
@ -177,7 +176,7 @@ def gte():
|
|||
return salt.utils.json.dumps(tops, ensure_ascii=False)
|
||||
|
||||
|
||||
def get_tops_python(py_ver, exclude=None):
|
||||
def get_tops_python(py_ver, exclude=None, ext_py_ver=None):
|
||||
"""
|
||||
Get top directories for the ssh_ext_alternatives dependencies
|
||||
automatically for the given python version. This allows
|
||||
|
@ -188,10 +187,12 @@ def get_tops_python(py_ver, exclude=None):
|
|||
|
||||
:param exclude:
|
||||
list of modules not to auto detect
|
||||
|
||||
:param ext_py_ver:
|
||||
the py-version from the ssh_ext_alternatives config
|
||||
"""
|
||||
files = {}
|
||||
for mod in [
|
||||
"distro",
|
||||
mods = [
|
||||
"jinja2",
|
||||
"yaml",
|
||||
"tornado",
|
||||
|
@ -203,7 +204,11 @@ def get_tops_python(py_ver, exclude=None):
|
|||
"ssl_match_hostname",
|
||||
"markupsafe",
|
||||
"backports_abc",
|
||||
]:
|
||||
]
|
||||
if ext_py_ver and tuple(ext_py_ver) >= (3, 0):
|
||||
mods.append("distro")
|
||||
|
||||
for mod in mods:
|
||||
if exclude and mod in exclude:
|
||||
continue
|
||||
|
||||
|
@ -242,7 +247,7 @@ def get_ext_tops(config):
|
|||
"""
|
||||
config = copy.deepcopy(config)
|
||||
alternatives = {}
|
||||
required = ["jinja2", "yaml", "tornado", "msgpack", "distro"]
|
||||
required = ["jinja2", "yaml", "tornado", "msgpack"]
|
||||
tops = []
|
||||
for ns, cfg in salt.ext.six.iteritems(config or {}):
|
||||
alternatives[ns] = cfg
|
||||
|
@ -258,6 +263,9 @@ def get_ext_tops(config):
|
|||
if err_msg:
|
||||
raise salt.exceptions.SaltSystemExit(err_msg)
|
||||
|
||||
if tuple(locked_py_version) >= (3, 0) and "distro" not in required:
|
||||
required.append("distro")
|
||||
|
||||
if cfg.get("dependencies") == "inherit":
|
||||
# TODO: implement inheritance of the modules from _here_
|
||||
raise NotImplementedError("This feature is not yet implemented")
|
||||
|
@ -293,7 +301,7 @@ def get_ext_tops(config):
|
|||
" in the external configuration: {}".format(required)
|
||||
)
|
||||
log.error(msg)
|
||||
raise salt.exceptions.SaltSystemExit(msg)
|
||||
raise salt.exceptions.SaltSystemExit(msg=msg)
|
||||
alternatives[ns]["dependencies"] = tops
|
||||
return alternatives
|
||||
|
||||
|
@ -381,7 +389,7 @@ def _get_supported_py_config(tops, extended_cfg):
|
|||
for the supported Python interpreter versions. This is then written into the thin.tgz
|
||||
archive and then verified by salt.client.ssh.ssh_py_shim.get_executable()
|
||||
|
||||
Note: Minimum default of 2.x versions is 2.7 and 3.x is 3.0, unless specified in namespaces.
|
||||
Note: Minimum default of 3.x is 3.0, unless specified in namespaces.
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
@ -437,7 +445,9 @@ def _pack_alternative(extended_cfg, digest_collector, tfp):
|
|||
config[ns]["dependencies"] = {}
|
||||
|
||||
# get auto deps
|
||||
auto_deps = get_tops_python(py_ver, exclude=exclude)
|
||||
auto_deps = get_tops_python(
|
||||
py_ver, exclude=exclude, ext_py_ver=cfg["py-version"]
|
||||
)
|
||||
for dep in auto_deps:
|
||||
config[ns]["dependencies"][dep] = auto_deps[dep]
|
||||
|
||||
|
@ -450,7 +460,7 @@ def _pack_alternative(extended_cfg, digest_collector, tfp):
|
|||
base, top_dirname = os.path.basename(top), os.path.dirname(top)
|
||||
os.chdir(top_dirname)
|
||||
site_pkg_dir = (
|
||||
_is_shareable(base) and "pyall" or "py{0}".format(py_ver_major)
|
||||
_is_shareable(base) and "pyall" or "py{}".format(py_ver_major)
|
||||
)
|
||||
log.debug(
|
||||
'Packing alternative "%s" to "%s/%s" destination',
|
||||
|
@ -510,9 +520,14 @@ def gen_thin(
|
|||
salt-run thin.generate mako,wempy 1
|
||||
salt-run thin.generate overwrite=1
|
||||
"""
|
||||
if sys.version_info < (2, 6):
|
||||
if python2_bin != "python2" or python3_bin != "python3":
|
||||
salt.utils.versions.warn_until(
|
||||
"Silicon",
|
||||
"python2_bin and python3_bin are no longer used, please update your call to gen_thin",
|
||||
)
|
||||
if sys.version_info < (3,):
|
||||
raise salt.exceptions.SaltSystemExit(
|
||||
'The minimum required python version to run salt-ssh is "2.6".'
|
||||
'The minimum required python version to run salt-ssh is "3".'
|
||||
)
|
||||
if compress not in ["gzip", "zip"]:
|
||||
log.warning(
|
||||
|
@ -562,93 +577,12 @@ def gen_thin(
|
|||
)
|
||||
else:
|
||||
return thintar
|
||||
if _six.PY3:
|
||||
# Let's check for the minimum python 2 version requirement, 2.6
|
||||
if not salt.utils.path.which(python2_bin):
|
||||
log.debug(
|
||||
"%s binary does not exist. Will not detect Python 2 version",
|
||||
python2_bin,
|
||||
)
|
||||
else:
|
||||
py_shell_cmd = "{} -c 'import sys;sys.stdout.write(\"%s.%s\\n\" % sys.version_info[:2]);'".format(
|
||||
python2_bin
|
||||
)
|
||||
cmd = subprocess.Popen(py_shell_cmd, stdout=subprocess.PIPE, shell=True)
|
||||
stdout, _ = cmd.communicate()
|
||||
if cmd.returncode == 0:
|
||||
py2_version = tuple(
|
||||
int(n) for n in stdout.decode("utf-8").strip().split(".")
|
||||
)
|
||||
if py2_version < (2, 6):
|
||||
raise salt.exceptions.SaltSystemExit(
|
||||
'The minimum required python version to run salt-ssh is "2.6".'
|
||||
'The version reported by "{0}" is "{1}". Please try "salt-ssh '
|
||||
'--python2-bin=<path-to-python-2.6-binary-or-higher>".'.format(
|
||||
python2_bin, stdout.strip()
|
||||
)
|
||||
)
|
||||
else:
|
||||
log.debug("Unable to detect %s version", python2_bin)
|
||||
log.debug(stdout)
|
||||
|
||||
tops_failure_msg = "Failed %s tops for Python binary %s."
|
||||
python_check_msg = (
|
||||
"%s binary does not exist. Will not attempt to generate tops for Python %s"
|
||||
)
|
||||
tops_py_version_mapping = {}
|
||||
tops = get_tops(extra_mods=extra_mods, so_mods=so_mods)
|
||||
tops_py_version_mapping[sys.version_info.major] = tops
|
||||
|
||||
# Collect tops, alternative to 2.x version
|
||||
if _six.PY2 and sys.version_info.major == 2:
|
||||
# Get python 3 tops
|
||||
if not salt.utils.path.which(python3_bin):
|
||||
log.debug(python_check_msg, python3_bin, "3")
|
||||
else:
|
||||
py_shell_cmd = "{0} -c 'import salt.utils.thin as t;print(t.gte())' '{1}'".format(
|
||||
python3_bin,
|
||||
salt.utils.json.dumps({"extra_mods": extra_mods, "so_mods": so_mods}),
|
||||
)
|
||||
cmd = subprocess.Popen(
|
||||
py_shell_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
|
||||
)
|
||||
stdout, stderr = cmd.communicate()
|
||||
if cmd.returncode == 0:
|
||||
try:
|
||||
tops = salt.utils.json.loads(stdout)
|
||||
tops_py_version_mapping["3"] = tops
|
||||
except ValueError as err:
|
||||
log.error(tops_failure_msg, "parsing", python3_bin)
|
||||
log.exception(err)
|
||||
else:
|
||||
log.debug(tops_failure_msg, "collecting", python3_bin)
|
||||
log.debug(stderr)
|
||||
|
||||
# Collect tops, alternative to 3.x version
|
||||
if _six.PY3 and sys.version_info.major == 3:
|
||||
# Get python 2 tops
|
||||
if not salt.utils.path.which(python2_bin):
|
||||
log.debug(python_check_msg, python2_bin, "2")
|
||||
else:
|
||||
py_shell_cmd = "{0} -c 'import salt.utils.thin as t;print(t.gte())' '{1}'".format(
|
||||
python2_bin,
|
||||
salt.utils.json.dumps({"extra_mods": extra_mods, "so_mods": so_mods}),
|
||||
)
|
||||
cmd = subprocess.Popen(
|
||||
py_shell_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
|
||||
)
|
||||
stdout, stderr = cmd.communicate()
|
||||
if cmd.returncode == 0:
|
||||
try:
|
||||
tops = salt.utils.json.loads(stdout.decode("utf-8"))
|
||||
tops_py_version_mapping["2"] = tops
|
||||
except ValueError as err:
|
||||
log.error(tops_failure_msg, "parsing", python2_bin)
|
||||
log.exception(err)
|
||||
else:
|
||||
log.debug(tops_failure_msg, "collecting", python2_bin)
|
||||
log.debug(stderr)
|
||||
|
||||
with salt.utils.files.fopen(pymap_cfg, "wb") as fp_:
|
||||
fp_.write(
|
||||
_get_supported_py_config(
|
||||
|
@ -760,7 +694,7 @@ def thin_sum(cachedir, form="sha1"):
|
|||
code_checksum_path = os.path.join(cachedir, "thin", "code-checksum")
|
||||
if os.path.isfile(code_checksum_path):
|
||||
with salt.utils.files.fopen(code_checksum_path, "r") as fh:
|
||||
code_checksum = "'{0}'".format(fh.read().strip())
|
||||
code_checksum = "'{}'".format(fh.read().strip())
|
||||
else:
|
||||
code_checksum = "'0'"
|
||||
|
||||
|
@ -789,6 +723,11 @@ def gen_min(
|
|||
salt-run min.generate mako,wempy 1
|
||||
salt-run min.generate overwrite=1
|
||||
"""
|
||||
if python2_bin != "python2" or python3_bin != "python3":
|
||||
salt.utils.versions.warn_until(
|
||||
"Silicon",
|
||||
"python2_bin and python3_bin are no longer used, please update your call to gen_min",
|
||||
)
|
||||
mindir = os.path.join(cachedir, "min")
|
||||
if not os.path.isdir(mindir):
|
||||
os.makedirs(mindir)
|
||||
|
@ -818,81 +757,10 @@ def gen_min(
|
|||
pass
|
||||
else:
|
||||
return mintar
|
||||
if _six.PY3:
|
||||
# Let's check for the minimum python 2 version requirement, 2.6
|
||||
py_shell_cmd = (
|
||||
python2_bin + " -c 'from __future__ import print_function; import sys; "
|
||||
'print("{0}.{1}".format(*(sys.version_info[:2])));\''
|
||||
)
|
||||
cmd = subprocess.Popen(py_shell_cmd, stdout=subprocess.PIPE, shell=True)
|
||||
stdout, _ = cmd.communicate()
|
||||
if cmd.returncode == 0:
|
||||
py2_version = tuple(
|
||||
int(n) for n in stdout.decode("utf-8").strip().split(".")
|
||||
)
|
||||
if py2_version < (2, 6):
|
||||
# Bail!
|
||||
raise salt.exceptions.SaltSystemExit(
|
||||
'The minimum required python version to run salt-ssh is "2.6".'
|
||||
'The version reported by "{0}" is "{1}". Please try "salt-ssh '
|
||||
'--python2-bin=<path-to-python-2.6-binary-or-higher>".'.format(
|
||||
python2_bin, stdout.strip()
|
||||
)
|
||||
)
|
||||
elif sys.version_info < (2, 6):
|
||||
# Bail! Though, how did we reached this far in the first place.
|
||||
raise salt.exceptions.SaltSystemExit(
|
||||
'The minimum required python version to run salt-ssh is "2.6".'
|
||||
)
|
||||
|
||||
tops_py_version_mapping = {}
|
||||
tops = get_tops(extra_mods=extra_mods, so_mods=so_mods)
|
||||
if _six.PY2:
|
||||
tops_py_version_mapping["2"] = tops
|
||||
else:
|
||||
tops_py_version_mapping["3"] = tops
|
||||
|
||||
# TODO: Consider putting known py2 and py3 compatible libs in its own sharable directory.
|
||||
# This would reduce the min size.
|
||||
if _six.PY2 and sys.version_info[0] == 2:
|
||||
# Get python 3 tops
|
||||
py_shell_cmd = (
|
||||
python3_bin + " -c 'import sys; import json; import salt.utils.thin; "
|
||||
"print(json.dumps(salt.utils.thin.get_tops(**(json.loads(sys.argv[1]))), ensure_ascii=False)); exit(0);' "
|
||||
"'{0}'".format(
|
||||
salt.utils.json.dumps({"extra_mods": extra_mods, "so_mods": so_mods})
|
||||
)
|
||||
)
|
||||
cmd = subprocess.Popen(
|
||||
py_shell_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
|
||||
)
|
||||
stdout, stderr = cmd.communicate()
|
||||
if cmd.returncode == 0:
|
||||
try:
|
||||
tops = salt.utils.json.loads(stdout)
|
||||
tops_py_version_mapping["3"] = tops
|
||||
except ValueError:
|
||||
pass
|
||||
if _six.PY3 and sys.version_info[0] == 3:
|
||||
# Get python 2 tops
|
||||
py_shell_cmd = (
|
||||
python2_bin + " -c 'from __future__ import print_function; "
|
||||
"import sys; import json; import salt.utils.thin; "
|
||||
"print(json.dumps(salt.utils.thin.get_tops(**(json.loads(sys.argv[1]))), ensure_ascii=False)); exit(0);' "
|
||||
"'{0}'".format(
|
||||
salt.utils.json.dumps({"extra_mods": extra_mods, "so_mods": so_mods})
|
||||
)
|
||||
)
|
||||
cmd = subprocess.Popen(
|
||||
py_shell_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
|
||||
)
|
||||
stdout, stderr = cmd.communicate()
|
||||
if cmd.returncode == 0:
|
||||
try:
|
||||
tops = salt.utils.json.loads(stdout.decode("utf-8"))
|
||||
tops_py_version_mapping["2"] = tops
|
||||
except ValueError:
|
||||
pass
|
||||
tops_py_version_mapping["3"] = tops
|
||||
|
||||
tfp = tarfile.open(mintar, "w:gz", dereference=True)
|
||||
try: # cwd may not exist if it was removed but salt was run from it
|
||||
|
@ -1038,7 +906,7 @@ def gen_min(
|
|||
os.chdir(tempdir)
|
||||
if not os.path.isdir(top):
|
||||
# top is a single file module
|
||||
tfp.add(base, arcname=os.path.join("py{0}".format(py_ver), base))
|
||||
tfp.add(base, arcname=os.path.join("py{}".format(py_ver), base))
|
||||
continue
|
||||
for root, dirs, files in salt.utils.path.os_walk(base, followlinks=True):
|
||||
for name in files:
|
||||
|
@ -1051,7 +919,7 @@ def gen_min(
|
|||
continue
|
||||
tfp.add(
|
||||
os.path.join(root, name),
|
||||
arcname=os.path.join("py{0}".format(py_ver), root, name),
|
||||
arcname=os.path.join("py{}".format(py_ver), root, name),
|
||||
)
|
||||
if tempdir is not None:
|
||||
shutil.rmtree(tempdir)
|
||||
|
|
58
tests/pytests/unit/client/test_ssh.py
Normal file
58
tests/pytests/unit/client/test_ssh.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
import pytest
|
||||
import salt.utils.msgpack
|
||||
from salt.client import ssh
|
||||
from tests.support.mock import MagicMock, patch
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def ssh_target(tmpdir):
|
||||
argv = [
|
||||
"ssh.set_auth_key",
|
||||
"root",
|
||||
"hobn+amNAXSBTiOXEqlBjGB...rsa root@master",
|
||||
]
|
||||
|
||||
opts = {
|
||||
"argv": argv,
|
||||
"__role": "master",
|
||||
"cachedir": tmpdir.strpath,
|
||||
"extension_modules": tmpdir.join("extmods"),
|
||||
}
|
||||
target = {
|
||||
"passwd": "abc123",
|
||||
"ssh_options": None,
|
||||
"sudo": False,
|
||||
"identities_only": False,
|
||||
"host": "login1",
|
||||
"user": "root",
|
||||
"timeout": 65,
|
||||
"remote_port_forwards": None,
|
||||
"sudo_user": "",
|
||||
"port": "22",
|
||||
"priv": "/etc/salt/pki/master/ssh/salt-ssh.rsa",
|
||||
}
|
||||
return opts, target
|
||||
|
||||
|
||||
def test_cmd_block_python_version_error(ssh_target):
|
||||
opts = ssh_target[0]
|
||||
target = ssh_target[1]
|
||||
|
||||
single = ssh.Single(
|
||||
opts,
|
||||
opts["argv"],
|
||||
"localhost",
|
||||
mods={},
|
||||
fsclient=None,
|
||||
thin=salt.utils.thin.thin_path(opts["cachedir"]),
|
||||
mine=False,
|
||||
winrm=False,
|
||||
**target
|
||||
)
|
||||
mock_shim = MagicMock(
|
||||
return_value=(("", "ERROR: Unable to locate appropriate python command\n", 10))
|
||||
)
|
||||
patch_shim = patch("salt.client.ssh.Single.shim_cmd", mock_shim)
|
||||
with patch_shim:
|
||||
ret = single.cmd_block()
|
||||
assert "ERROR: Python version error. Recommendation(s) follow:" in ret[0]
|
77
tests/pytests/unit/utils/test_thin.py
Normal file
77
tests/pytests/unit/utils/test_thin.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
import pytest
|
||||
import salt.exceptions
|
||||
import salt.utils.stringutils
|
||||
import salt.utils.thin
|
||||
from tests.support.mock import MagicMock, patch
|
||||
|
||||
|
||||
def _mock_popen(return_value=None, side_effect=None, returncode=0):
|
||||
proc = MagicMock()
|
||||
proc.communicate = MagicMock(return_value=return_value, side_effect=side_effect)
|
||||
proc.returncode = returncode
|
||||
popen = MagicMock(return_value=proc)
|
||||
return popen
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", [[2, 7], [3, 0], [3, 7]])
|
||||
def test_get_tops_python(version):
|
||||
"""
|
||||
Tests 'distro' is only included when targeting
|
||||
python 3 in get_tops_python
|
||||
"""
|
||||
python3 = False
|
||||
if tuple(version) >= (3, 0):
|
||||
python3 = True
|
||||
|
||||
mods = ["jinja2"]
|
||||
if python3:
|
||||
mods.append("distro")
|
||||
|
||||
popen_ret = tuple(salt.utils.stringutils.to_bytes(x) for x in ("", ""))
|
||||
mock_popen = _mock_popen(return_value=popen_ret)
|
||||
patch_proc = patch("salt.utils.thin.subprocess.Popen", mock_popen)
|
||||
|
||||
with patch_proc:
|
||||
salt.utils.thin.get_tops_python("python2", ext_py_ver=version)
|
||||
cmds = [x[0][0] for x in mock_popen.call_args_list]
|
||||
assert [x for x in cmds if "jinja2" in x]
|
||||
if python3:
|
||||
assert [x for x in cmds if "distro" in x]
|
||||
else:
|
||||
assert not [x for x in cmds if "distro" in x]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", [[2, 7], [3, 0], [3, 7]])
|
||||
def test_get_ext_tops(version):
|
||||
"""
|
||||
Tests 'distro' is only included when targeting
|
||||
python 3 in get_ext_tops
|
||||
"""
|
||||
python3 = False
|
||||
if tuple(version) >= (3, 0):
|
||||
python3 = True
|
||||
|
||||
cfg = {
|
||||
"namespace": {
|
||||
"path": "/foo",
|
||||
"py-version": version,
|
||||
"dependencies": {
|
||||
"jinja2": "/jinja/foo.py",
|
||||
"yaml": "/yaml/",
|
||||
"tornado": "/tornado/tornado.py",
|
||||
"msgpack": "msgpack.py",
|
||||
},
|
||||
}
|
||||
}
|
||||
with patch("salt.utils.thin.os.path.isfile", MagicMock(return_value=True)):
|
||||
if python3:
|
||||
with pytest.raises(salt.exceptions.SaltSystemExit) as err:
|
||||
salt.utils.thin.get_ext_tops(cfg)
|
||||
else:
|
||||
ret = salt.utils.thin.get_ext_tops(cfg)
|
||||
|
||||
if python3:
|
||||
assert "distro" in err.value.code
|
||||
else:
|
||||
assert not [x for x in ret["namespace"]["dependencies"] if "distro" in x]
|
||||
assert [x for x in ret["namespace"]["dependencies"] if "msgpack" in x]
|
|
@ -137,7 +137,6 @@ class SSHThinTestCase(TestCase):
|
|||
|
||||
return tf
|
||||
|
||||
@patch("salt.exceptions.SaltSystemExit", Exception)
|
||||
@patch("salt.utils.thin.log", MagicMock())
|
||||
@patch("salt.utils.thin.os.path.isfile", MagicMock(return_value=False))
|
||||
def test_get_ext_tops_cfg_missing_dependencies(self):
|
||||
|
@ -148,7 +147,7 @@ class SSHThinTestCase(TestCase):
|
|||
"""
|
||||
cfg = {"namespace": {"py-version": [0, 0], "path": "/foo", "dependencies": []}}
|
||||
|
||||
with pytest.raises(Exception) as err:
|
||||
with pytest.raises(salt.exceptions.SaltSystemExit) as err:
|
||||
thin.get_ext_tops(cfg)
|
||||
self.assertIn("Missing dependencies", str(err.value))
|
||||
self.assertTrue(thin.log.error.called)
|
||||
|
@ -223,7 +222,6 @@ class SSHThinTestCase(TestCase):
|
|||
== thin.log.warning.mock_calls[0][1][0] % "test"
|
||||
)
|
||||
|
||||
@patch("salt.exceptions.SaltSystemExit", Exception)
|
||||
@patch("salt.utils.thin.log", MagicMock())
|
||||
@patch("salt.utils.thin.os.path.isfile", MagicMock(return_value=False))
|
||||
def test_get_ext_tops_dependency_config_check(self):
|
||||
|
@ -664,72 +662,10 @@ class SSHThinTestCase(TestCase):
|
|||
thin.sys.exc_clear = lambda: None
|
||||
thin.gen_thin("")
|
||||
self.assertIn(
|
||||
"The minimum required python version to run salt-ssh is " '"2.6"',
|
||||
"The minimum required python version to run salt-ssh is " '"3"',
|
||||
str(err.value),
|
||||
)
|
||||
|
||||
@skipIf(
|
||||
salt.utils.platform.is_windows() and thin._six.PY2, "Dies on Python2 on Windows"
|
||||
)
|
||||
@patch("salt.exceptions.SaltSystemExit", Exception)
|
||||
@patch("salt.utils.thin.os.makedirs", MagicMock())
|
||||
@patch("salt.utils.files.fopen", MagicMock())
|
||||
@patch("salt.utils.thin._get_salt_call", MagicMock())
|
||||
@patch("salt.utils.thin._get_ext_namespaces", MagicMock())
|
||||
@patch("salt.utils.thin.get_tops", MagicMock(return_value=["/foo3", "/bar3"]))
|
||||
@patch("salt.utils.thin.get_ext_tops", MagicMock(return_value={}))
|
||||
@patch("salt.utils.thin.os.path.isfile", MagicMock())
|
||||
@patch("salt.utils.thin.os.path.isdir", MagicMock(return_value=True))
|
||||
@patch("salt.utils.thin.os.remove", MagicMock())
|
||||
@patch("salt.utils.thin.os.path.exists", MagicMock())
|
||||
@patch("salt.utils.path.os_walk", MagicMock(return_value=[]))
|
||||
@patch(
|
||||
"salt.utils.thin.subprocess.Popen",
|
||||
_popen(
|
||||
None,
|
||||
side_effect=[(bts("2.7"), bts("")), (bts('["/foo27", "/bar27"]'), bts(""))],
|
||||
),
|
||||
)
|
||||
@patch("salt.utils.thin.tarfile", MagicMock())
|
||||
@patch("salt.utils.thin.zipfile", MagicMock())
|
||||
@patch("salt.utils.thin.os.getcwd", MagicMock())
|
||||
@patch("salt.utils.thin.os.access", MagicMock(return_value=True))
|
||||
@patch("salt.utils.thin.os.chdir", MagicMock())
|
||||
@patch("salt.utils.thin.tempfile.mkdtemp", MagicMock())
|
||||
@patch(
|
||||
"salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
|
||||
)
|
||||
@patch("salt.utils.thin.shutil", MagicMock())
|
||||
@patch("salt.utils.path.which", MagicMock(return_value=""))
|
||||
@patch("salt.utils.thin._get_thintar_prefix", MagicMock())
|
||||
def test_gen_thin_python_exist_or_not(self):
|
||||
"""
|
||||
Test thin.gen_thin function if the opposite python
|
||||
binary does not exist
|
||||
"""
|
||||
with TstSuiteLoggingHandler() as handler:
|
||||
thin.gen_thin("")
|
||||
salt.utils.thin.subprocess.Popen.assert_not_called()
|
||||
|
||||
if salt.ext.six.PY2:
|
||||
self.assertIn(
|
||||
"DEBUG:python3 binary does not exist. Will not attempt to generate "
|
||||
"tops for Python 3",
|
||||
handler.messages,
|
||||
)
|
||||
|
||||
if salt.ext.six.PY3:
|
||||
self.assertIn(
|
||||
"DEBUG:python2 binary does not exist. Will not "
|
||||
"detect Python 2 version",
|
||||
handler.messages,
|
||||
)
|
||||
self.assertIn(
|
||||
"DEBUG:python2 binary does not exist. Will not attempt to generate "
|
||||
"tops for Python 2",
|
||||
handler.messages,
|
||||
)
|
||||
|
||||
@skipIf(
|
||||
salt.utils.platform.is_windows() and thin._six.PY2, "Dies on Python2 on Windows"
|
||||
)
|
||||
|
@ -765,8 +701,6 @@ class SSHThinTestCase(TestCase):
|
|||
"salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
|
||||
)
|
||||
@patch("salt.utils.thin.shutil", MagicMock())
|
||||
@patch("salt.utils.thin._six.PY3", True)
|
||||
@patch("salt.utils.thin._six.PY2", False)
|
||||
@patch("salt.utils.thin.sys.version_info", _version_info(None, 3, 6))
|
||||
@patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/python"))
|
||||
def test_gen_thin_compression_fallback_py3(self):
|
||||
|
@ -818,15 +752,11 @@ class SSHThinTestCase(TestCase):
|
|||
"salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
|
||||
)
|
||||
@patch("salt.utils.thin.shutil", MagicMock())
|
||||
@patch("salt.utils.thin._six.PY3", True)
|
||||
@patch("salt.utils.thin._six.PY2", False)
|
||||
@patch("salt.utils.thin.sys.version_info", _version_info(None, 3, 6))
|
||||
@patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/python"))
|
||||
def test_gen_thin_control_files_written_py3(self):
|
||||
"""
|
||||
Test thin.gen_thin function if control files are written (version, salt-call etc).
|
||||
NOTE: Py2 version of this test is not required, as code shares the same spot across the versions.
|
||||
|
||||
:return:
|
||||
"""
|
||||
thin.gen_thin("")
|
||||
|
@ -836,7 +766,7 @@ class SSHThinTestCase(TestCase):
|
|||
for idx, fname in enumerate(
|
||||
["version", ".thin-gen-py-version", "salt-call", "supported-versions"]
|
||||
):
|
||||
name = thin.tarfile.open().method_calls[idx + 4][1][0]
|
||||
name = thin.tarfile.open().method_calls[idx + 2][1][0]
|
||||
self.assertEqual(name, fname)
|
||||
thin.tarfile.open().close.assert_called()
|
||||
|
||||
|
@ -862,13 +792,6 @@ class SSHThinTestCase(TestCase):
|
|||
)
|
||||
),
|
||||
)
|
||||
@patch(
|
||||
"salt.utils.thin.subprocess.Popen",
|
||||
_popen(
|
||||
None,
|
||||
side_effect=[(bts("2.7"), bts("")), (bts('["/foo27", "/bar27"]'), bts(""))],
|
||||
),
|
||||
)
|
||||
@patch("salt.utils.thin.tarfile", _tarfile(None))
|
||||
@patch("salt.utils.thin.zipfile", MagicMock())
|
||||
@patch(
|
||||
|
@ -882,8 +805,6 @@ class SSHThinTestCase(TestCase):
|
|||
"salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
|
||||
)
|
||||
@patch("salt.utils.thin.shutil", MagicMock())
|
||||
@patch("salt.utils.thin._six.PY3", True)
|
||||
@patch("salt.utils.thin._six.PY2", False)
|
||||
@patch("salt.utils.thin.sys.version_info", _version_info(None, 3, 6))
|
||||
@patch("salt.utils.hashutils.DigestCollector", MagicMock())
|
||||
@patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/python"))
|
||||
|
@ -896,7 +817,7 @@ class SSHThinTestCase(TestCase):
|
|||
"""
|
||||
thin.gen_thin("")
|
||||
files = []
|
||||
for py in ("py2", "py2", "py3", "pyall"):
|
||||
for py in ("py3", "pyall"):
|
||||
for i in range(1, 4):
|
||||
files.append(os.path.join(py, "root", "r{}".format(i)))
|
||||
for i in range(4, 7):
|
||||
|
@ -919,7 +840,7 @@ class SSHThinTestCase(TestCase):
|
|||
MagicMock(
|
||||
return_value={
|
||||
"namespace": {
|
||||
"py-version": [2, 7],
|
||||
"py-version": [3, 0],
|
||||
"path": "/opt/2015.8/salt",
|
||||
"dependencies": ["/opt/certifi", "/opt/whatever"],
|
||||
}
|
||||
|
@ -940,13 +861,6 @@ class SSHThinTestCase(TestCase):
|
|||
)
|
||||
),
|
||||
)
|
||||
@patch(
|
||||
"salt.utils.thin.subprocess.Popen",
|
||||
_popen(
|
||||
None,
|
||||
side_effect=[(bts("2.7"), bts("")), (bts('["/foo27", "/bar27"]'), bts(""))],
|
||||
),
|
||||
)
|
||||
@patch("salt.utils.thin.tarfile", _tarfile(None))
|
||||
@patch("salt.utils.thin.zipfile", MagicMock())
|
||||
@patch(
|
||||
|
@ -960,21 +874,17 @@ class SSHThinTestCase(TestCase):
|
|||
"salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
|
||||
)
|
||||
@patch("salt.utils.thin.shutil", MagicMock())
|
||||
@patch("salt.utils.thin._six.PY3", True)
|
||||
@patch("salt.utils.thin._six.PY2", False)
|
||||
@patch("salt.utils.thin.sys.version_info", _version_info(None, 3, 6))
|
||||
@patch("salt.utils.hashutils.DigestCollector", MagicMock())
|
||||
@patch("salt.utils.path.which", MagicMock(return_value="/usr/bin/python"))
|
||||
def test_gen_thin_ext_alternative_content_files_written_py3(self):
|
||||
"""
|
||||
Test thin.gen_thin function if external alternative content files are written.
|
||||
NOTE: Py2 version of this test is not required, as code shares the same spot across the versions.
|
||||
|
||||
:return:
|
||||
"""
|
||||
ext_conf = {
|
||||
"namespace": {
|
||||
"py-version": [2, 7],
|
||||
"py-version": [3, 0],
|
||||
"path": "/opt/2015.8/salt",
|
||||
"dependencies": {
|
||||
"certifi": "/opt/certifi",
|
||||
|
@ -985,13 +895,13 @@ class SSHThinTestCase(TestCase):
|
|||
|
||||
thin.gen_thin("", extended_cfg=ext_conf)
|
||||
files = []
|
||||
for py in ("pyall", "pyall", "py2"):
|
||||
for py in ("pyall", "pyall", "py3"):
|
||||
for i in range(1, 4):
|
||||
files.append(os.path.join("namespace", py, "root", "r{}".format(i)))
|
||||
for i in range(4, 7):
|
||||
files.append(os.path.join("namespace", py, "root2", "r{}".format(i)))
|
||||
|
||||
for idx, cl in enumerate(thin.tarfile.open().method_calls[12:-6]):
|
||||
for idx, cl in enumerate(thin.tarfile.open().method_calls[:-6]):
|
||||
arcname = cl[2].get("arcname")
|
||||
self.assertIn(arcname, files)
|
||||
files.pop(files.index(arcname))
|
||||
|
@ -1077,8 +987,6 @@ class SSHThinTestCase(TestCase):
|
|||
"salt.utils.thin.tempfile.mkstemp", MagicMock(return_value=(3, ".temporary"))
|
||||
)
|
||||
@patch("salt.utils.thin.shutil", MagicMock())
|
||||
@patch("salt.utils.thin._six.PY3", True)
|
||||
@patch("salt.utils.thin._six.PY2", False)
|
||||
@patch("salt.utils.thin.sys.version_info", _version_info(None, 3, 6))
|
||||
def test_gen_thin_control_files_written_access_denied_cwd(self):
|
||||
"""
|
||||
|
@ -1111,7 +1019,6 @@ class SSHThinTestCase(TestCase):
|
|||
self._popen(
|
||||
None,
|
||||
side_effect=[
|
||||
(bts("distro.py"), bts("")),
|
||||
(bts("jinja2/__init__.py"), bts("")),
|
||||
(bts("yaml/__init__.py"), bts("")),
|
||||
(bts("tornado/__init__.py"), bts("")),
|
||||
|
@ -1123,6 +1030,7 @@ class SSHThinTestCase(TestCase):
|
|||
(bts(""), bts("")),
|
||||
(bts(""), bts("")),
|
||||
(bts(""), bts("")),
|
||||
(bts("distro.py"), bts("")),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -1132,7 +1040,7 @@ class SSHThinTestCase(TestCase):
|
|||
with patch_proc, patch_os, patch_which:
|
||||
with TstSuiteLoggingHandler() as log_handler:
|
||||
exp_ret = copy.deepcopy(self.exp_ret)
|
||||
ret = thin.get_tops_python("python2.7")
|
||||
ret = thin.get_tops_python("python3.7", ext_py_ver=[3, 7])
|
||||
if salt.utils.platform.is_windows():
|
||||
for key, value in ret.items():
|
||||
ret[key] = str(pathlib.Path(value).resolve(strict=False))
|
||||
|
@ -1140,7 +1048,7 @@ class SSHThinTestCase(TestCase):
|
|||
exp_ret[key] = str(pathlib.Path(value).resolve(strict=False))
|
||||
assert ret == exp_ret
|
||||
assert (
|
||||
"ERROR:Could not auto detect file location for module concurrent for python version python2.7"
|
||||
"ERROR:Could not auto detect file location for module concurrent for python version python3.7"
|
||||
in log_handler.messages
|
||||
)
|
||||
|
||||
|
@ -1153,7 +1061,6 @@ class SSHThinTestCase(TestCase):
|
|||
self._popen(
|
||||
None,
|
||||
side_effect=[
|
||||
(bts("distro.py"), bts("")),
|
||||
(bts("tornado/__init__.py"), bts("")),
|
||||
(bts("msgpack/__init__.py"), bts("")),
|
||||
(bts("certifi/__init__.py"), bts("")),
|
||||
|
@ -1163,6 +1070,7 @@ class SSHThinTestCase(TestCase):
|
|||
(bts(""), bts("")),
|
||||
(bts(""), bts("")),
|
||||
(bts(""), bts("")),
|
||||
(bts("distro.py"), bts("")),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -1173,7 +1081,9 @@ class SSHThinTestCase(TestCase):
|
|||
patch_os = patch("os.path.exists", return_value=True)
|
||||
patch_which = patch("salt.utils.path.which", return_value=True)
|
||||
with patch_proc, patch_os, patch_which:
|
||||
ret = thin.get_tops_python("python2.7", exclude=self.exc_libs)
|
||||
ret = thin.get_tops_python(
|
||||
"python3.7", exclude=self.exc_libs, ext_py_ver=[3, 7]
|
||||
)
|
||||
if salt.utils.platform.is_windows():
|
||||
for key, value in ret.items():
|
||||
ret[key] = str(pathlib.Path(value).resolve(strict=False))
|
||||
|
|
Loading…
Add table
Reference in a new issue