mirror of
https://github.com/saltstack/salt.git
synced 2025-04-10 14:51:40 +00:00
1192 lines
38 KiB
Python
1192 lines
38 KiB
Python
"""
|
|
Generate the salt thin tarball from the installed python files
|
|
"""
|
|
|
|
import contextlib
|
|
import contextvars as py_contextvars
|
|
import copy
|
|
import importlib.util
|
|
import inspect
|
|
import io
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import site
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import types
|
|
import zipfile
|
|
|
|
import distro
|
|
import idna
|
|
import jinja2
|
|
import looseversion
|
|
import msgpack
|
|
import networkx
|
|
import packaging
|
|
import requests
|
|
import tornado
|
|
import urllib3
|
|
import yaml
|
|
|
|
import salt
|
|
import salt.exceptions
|
|
import salt.utils.entrypoints
|
|
import salt.utils.files
|
|
import salt.utils.hashutils
|
|
import salt.utils.json
|
|
import salt.utils.path
|
|
import salt.utils.stringutils
|
|
import salt.version
|
|
|
|
# This is needed until we drop support for python 3.6
|
|
has_immutables = False
|
|
try:
|
|
import immutables
|
|
|
|
has_immutables = True
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
try:
|
|
import zlib
|
|
except ImportError:
|
|
zlib = None
|
|
|
|
# pylint: disable=import-error,no-name-in-module
|
|
try:
|
|
import certifi
|
|
except ImportError:
|
|
certifi = None
|
|
|
|
try:
|
|
import singledispatch
|
|
except ImportError:
|
|
singledispatch = None
|
|
|
|
try:
|
|
import singledispatch_helpers
|
|
except ImportError:
|
|
singledispatch_helpers = None
|
|
|
|
try:
|
|
import backports_abc
|
|
except ImportError:
|
|
import salt.ext.backports_abc as backports_abc
|
|
|
|
try:
|
|
# New Jinja only
|
|
import markupsafe
|
|
except ImportError:
|
|
markupsafe = None
|
|
|
|
|
|
try:
|
|
# Older python where the backport from pypi is installed
|
|
from backports import ssl_match_hostname
|
|
except ImportError:
|
|
# Other older python we use our bundled copy
|
|
try:
|
|
from salt.ext import ssl_match_hostname
|
|
except ImportError:
|
|
ssl_match_hostname = None
|
|
|
|
concurrent = None
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def import_module(name, path):
|
|
"""
|
|
Import a module from a specific path. Path can be a full or relative path
|
|
to a .py file.
|
|
|
|
:name: The name of the module to import
|
|
:path: The path of the module to import
|
|
"""
|
|
try:
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
except ValueError:
|
|
spec = None
|
|
if spec is not None:
|
|
lib = importlib.util.module_from_spec(spec)
|
|
try:
|
|
spec.loader.exec_module(lib)
|
|
except OSError:
|
|
pass
|
|
else:
|
|
return lib
|
|
|
|
|
|
def getsitepackages():
|
|
"""
|
|
Some versions of Virtualenv ship a site.py without getsitepackages. This
|
|
method will first try and return sitepackages from the default site module
|
|
if no method exists we will try importing the site module from every other
|
|
path in sys.paths until we find a getsitepackages method to return the
|
|
results from. If for some reason no gesitepackages method can be found a
|
|
RuntimeError will be raised
|
|
|
|
:return: A list containing all global site-packages directories.
|
|
"""
|
|
if hasattr(site, "getsitepackages"):
|
|
return site.getsitepackages()
|
|
for path in sys.path:
|
|
lib = import_module("site", os.path.join(path, "site.py"))
|
|
if hasattr(lib, "getsitepackages"):
|
|
return lib.getsitepackages()
|
|
raise RuntimeError("Unable to locate a getsitepackages method")
|
|
|
|
|
|
def find_site_modules(name):
|
|
"""
|
|
Finds and imports a module from site packages directories.
|
|
|
|
:name: The name of the module to import
|
|
:return: A list of imported modules, if no modules are imported an empty
|
|
list is returned.
|
|
"""
|
|
libs = []
|
|
site_paths = []
|
|
try:
|
|
site_paths = getsitepackages()
|
|
except RuntimeError:
|
|
log.debug("No site package directories found")
|
|
for site_path in site_paths:
|
|
path = os.path.join(site_path, f"{name}.py")
|
|
lib = import_module(name, path)
|
|
if lib:
|
|
libs.append(lib)
|
|
path = os.path.join(site_path, name, "__init__.py")
|
|
lib = import_module(name, path)
|
|
if lib:
|
|
libs.append(lib)
|
|
return libs
|
|
|
|
|
|
def _get_salt_call(*dirs, **namespaces):
|
|
"""
|
|
Return salt-call source, based on configuration.
|
|
This will include additional namespaces for another versions of Salt,
|
|
if needed (e.g. older interpreters etc).
|
|
|
|
:dirs: List of directories to include in the system path
|
|
:namespaces: Dictionary of namespace
|
|
:return:
|
|
"""
|
|
template = """# -*- coding: utf-8 -*-
|
|
import os
|
|
import sys
|
|
|
|
# Namespaces is a map: {namespace: major/minor version}, like {'2016.11.4': [2, 6]}
|
|
# Appears only when configured in Master configuration.
|
|
namespaces = %namespaces%
|
|
|
|
# Default system paths alongside the namespaces
|
|
syspaths = %dirs%
|
|
syspaths.append('py{0}'.format(sys.version_info[0]))
|
|
|
|
curr_ver = (sys.version_info[0], sys.version_info[1],)
|
|
|
|
namespace = ''
|
|
for ns in namespaces:
|
|
if curr_ver == tuple(namespaces[ns]):
|
|
namespace = ns
|
|
break
|
|
|
|
for base in syspaths:
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__),
|
|
namespace and os.path.join(namespace, base) or base))
|
|
|
|
if __name__ == '__main__':
|
|
from salt.scripts import salt_call
|
|
salt_call()
|
|
"""
|
|
|
|
for tgt, cnt in [("%dirs%", dirs), ("%namespaces%", namespaces)]:
|
|
template = template.replace(tgt, salt.utils.json.dumps(cnt))
|
|
|
|
return salt.utils.stringutils.to_bytes(template)
|
|
|
|
|
|
def thin_path(cachedir):
|
|
"""
|
|
Return the path to the thin tarball
|
|
"""
|
|
return os.path.join(cachedir, "thin", "thin.tgz")
|
|
|
|
|
|
def _is_shareable(mod):
|
|
"""
|
|
Return True if module is share-able between major Python versions.
|
|
|
|
:param mod:
|
|
:return:
|
|
"""
|
|
# This list is subject to change
|
|
shareable = ["salt", "jinja2", "msgpack", "certifi"]
|
|
|
|
return os.path.basename(mod) in shareable
|
|
|
|
|
|
def _add_dependency(container, obj, namespace=None):
|
|
"""
|
|
Add a dependency to the top list.
|
|
|
|
:param obj:
|
|
:param is_file:
|
|
:param namespace: Optional tuple of parent namespaces for namespace packages
|
|
:return:
|
|
"""
|
|
if os.path.basename(obj.__file__).split(".")[0] == "__init__":
|
|
container.append((os.path.dirname(obj.__file__), namespace))
|
|
else:
|
|
container.append((obj.__file__.replace(".pyc", ".py"), None))
|
|
|
|
|
|
def gte():
|
|
"""
|
|
This function is called externally from the alternative
|
|
Python interpreter from within _get_tops function.
|
|
|
|
:param extra_mods:
|
|
:param so_mods:
|
|
:return:
|
|
"""
|
|
extra = salt.utils.json.loads(sys.argv[1])
|
|
tops = get_tops(**extra)
|
|
|
|
return salt.utils.json.dumps(tops, ensure_ascii=False)
|
|
|
|
|
|
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
|
|
the user to add the dependency paths automatically.
|
|
|
|
:param py_ver:
|
|
python binary to use to detect binaries
|
|
|
|
:param exclude:
|
|
list of modules not to auto detect
|
|
|
|
:param ext_py_ver:
|
|
the py-version from the ssh_ext_alternatives config
|
|
"""
|
|
files = {}
|
|
mods = [
|
|
"jinja2",
|
|
"yaml",
|
|
"tornado",
|
|
"msgpack",
|
|
"networkx",
|
|
"requests",
|
|
"idna",
|
|
"urllib3",
|
|
"certifi",
|
|
"singledispatch",
|
|
"concurrent",
|
|
"singledispatch_helpers",
|
|
"ssl_match_hostname",
|
|
"markupsafe",
|
|
"backports_abc",
|
|
"looseversion",
|
|
"packaging",
|
|
]
|
|
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
|
|
|
|
if not salt.utils.path.which(py_ver):
|
|
log.error("%s does not exist. Could not auto detect dependencies", py_ver)
|
|
return {}
|
|
py_shell_cmd = [py_ver, "-c", "import {0}; print({0}.__file__)".format(mod)]
|
|
cmd = subprocess.Popen(py_shell_cmd, stdout=subprocess.PIPE)
|
|
stdout, _ = cmd.communicate()
|
|
mod_file = os.path.abspath(salt.utils.data.decode(stdout).rstrip("\n"))
|
|
|
|
if not stdout or not os.path.exists(mod_file):
|
|
log.error(
|
|
"Could not auto detect file location for module %s for python version %s",
|
|
mod,
|
|
py_ver,
|
|
)
|
|
continue
|
|
|
|
if os.path.basename(mod_file).split(".")[0] == "__init__":
|
|
mod_file = os.path.dirname(mod_file)
|
|
else:
|
|
mod_file = mod_file.replace("pyc", "py")
|
|
|
|
files[mod] = mod_file
|
|
return files
|
|
|
|
|
|
def get_ext_tops(config):
|
|
"""
|
|
Get top directories for the dependencies, based on external configuration.
|
|
|
|
:return:
|
|
"""
|
|
config = copy.deepcopy(config) or {}
|
|
alternatives = {}
|
|
required = ["jinja2", "yaml", "tornado", "msgpack", "networkx"]
|
|
tops = []
|
|
for ns, cfg in config.items():
|
|
alternatives[ns] = cfg
|
|
locked_py_version = cfg.get("py-version")
|
|
err_msg = None
|
|
if not locked_py_version:
|
|
err_msg = "Alternative Salt library: missing specific locked Python version"
|
|
elif not isinstance(locked_py_version, (tuple, list)):
|
|
err_msg = (
|
|
"Alternative Salt library: specific locked Python version "
|
|
"should be a list of major/minor version"
|
|
)
|
|
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")
|
|
else:
|
|
for dep in cfg.get("dependencies"):
|
|
mod = cfg["dependencies"][dep] or ""
|
|
if not mod:
|
|
log.warning("Module %s has missing configuration", dep)
|
|
continue
|
|
elif mod.endswith(".py") and not os.path.isfile(mod):
|
|
log.warning(
|
|
"Module %s configured with not a file or does not exist: %s",
|
|
dep,
|
|
mod,
|
|
)
|
|
continue
|
|
elif not mod.endswith(".py") and not os.path.isfile(
|
|
os.path.join(mod, "__init__.py")
|
|
):
|
|
log.warning(
|
|
"Module %s is not a Python importable module with %s", dep, mod
|
|
)
|
|
continue
|
|
tops.append(mod)
|
|
|
|
if dep in required:
|
|
required.pop(required.index(dep))
|
|
|
|
required = ", ".join(required)
|
|
if required:
|
|
msg = (
|
|
"Missing dependencies for the alternative version"
|
|
" in the external configuration: {}".format(required)
|
|
)
|
|
log.error(msg)
|
|
raise salt.exceptions.SaltSystemExit(msg=msg)
|
|
alternatives[ns]["dependencies"] = tops
|
|
return alternatives
|
|
|
|
|
|
def _get_ext_namespaces(config):
|
|
"""
|
|
Get namespaces from the existing configuration.
|
|
|
|
:param config:
|
|
:return:
|
|
"""
|
|
namespaces = {}
|
|
if not config:
|
|
return namespaces
|
|
|
|
for ns in config:
|
|
constraint_version = tuple(config[ns].get("py-version", []))
|
|
if not constraint_version:
|
|
raise salt.exceptions.SaltSystemExit(
|
|
"An alternative version is configured, but not defined "
|
|
"to what Python's major/minor version it should be constrained."
|
|
)
|
|
else:
|
|
namespaces[ns] = constraint_version
|
|
|
|
return namespaces
|
|
|
|
|
|
def get_tops(extra_mods="", so_mods=""):
|
|
"""
|
|
Get top directories for the dependencies, based on Python interpreter.
|
|
|
|
:param extra_mods:
|
|
:param so_mods:
|
|
:return:
|
|
"""
|
|
tops = []
|
|
mods = [
|
|
salt,
|
|
distro,
|
|
jinja2,
|
|
yaml,
|
|
tornado,
|
|
msgpack,
|
|
networkx,
|
|
certifi,
|
|
singledispatch,
|
|
concurrent,
|
|
singledispatch_helpers,
|
|
ssl_match_hostname,
|
|
markupsafe,
|
|
backports_abc,
|
|
looseversion,
|
|
packaging,
|
|
requests,
|
|
idna,
|
|
urllib3,
|
|
]
|
|
modules = find_site_modules("contextvars")
|
|
if modules:
|
|
contextvars = modules[0]
|
|
else:
|
|
contextvars = py_contextvars
|
|
log.debug("Using contextvars %r", contextvars)
|
|
mods.append(contextvars)
|
|
if has_immutables:
|
|
mods.append(immutables)
|
|
for mod in mods:
|
|
if mod:
|
|
log.debug('Adding module to the tops: "%s"', mod.__name__)
|
|
_add_dependency(tops, mod)
|
|
|
|
for mod in [m for m in extra_mods.split(",") if m]:
|
|
if mod not in locals() and mod not in globals():
|
|
try:
|
|
locals()[mod] = __import__(mod)
|
|
moddir, modname = os.path.split(locals()[mod].__file__)
|
|
base, _ = os.path.splitext(modname)
|
|
if base == "__init__":
|
|
tops.append((moddir, None))
|
|
else:
|
|
tops.append((os.path.join(moddir, base + ".py"), None))
|
|
except ImportError as err:
|
|
log.error(
|
|
'Unable to import extra-module "%s": %s', mod, err, exc_info=True
|
|
)
|
|
|
|
for mod in [m for m in so_mods.split(",") if m]:
|
|
try:
|
|
locals()[mod] = __import__(mod)
|
|
tops.append((locals()[mod].__file__, None))
|
|
except ImportError:
|
|
log.error('Unable to import so-module "%s"', mod, exc_info=True)
|
|
|
|
return tops
|
|
|
|
|
|
def _get_supported_py_config(tops, extended_cfg):
|
|
"""
|
|
Based on the Salt SSH configuration, create a YAML configuration
|
|
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: Current versions of Salt only Support Python 3, but the versions of Python
|
|
(2.7,3.0) remain to include support for ssh_ext_alternatives if user is targeting an
|
|
older version of Salt.
|
|
:return:
|
|
"""
|
|
pymap = []
|
|
for py_ver, tops in copy.deepcopy(tops).items():
|
|
py_ver = int(py_ver)
|
|
if py_ver == 2:
|
|
pymap.append("py2:2:7")
|
|
elif py_ver == 3:
|
|
pymap.append("py3:3:0")
|
|
cfg_copy = copy.deepcopy(extended_cfg) or {}
|
|
for ns, cfg in cfg_copy.items():
|
|
pymap.append("{}:{}:{}".format(ns, *cfg.get("py-version")))
|
|
pymap.append("")
|
|
|
|
return salt.utils.stringutils.to_bytes(os.linesep.join(pymap))
|
|
|
|
|
|
def _get_thintar_prefix(tarname):
|
|
"""
|
|
Make sure thintar temporary name is concurrent and secure.
|
|
|
|
:param tarname: name of the chosen tarball
|
|
:return: prefixed tarname
|
|
"""
|
|
tfd, tmp_tarname = tempfile.mkstemp(
|
|
dir=os.path.dirname(tarname),
|
|
prefix=".thin-",
|
|
suffix=os.path.splitext(tarname)[1],
|
|
)
|
|
os.close(tfd)
|
|
|
|
return tmp_tarname
|
|
|
|
|
|
def _pack_alternative(extended_cfg, digest_collector, tfp):
|
|
# Pack alternative data
|
|
config = copy.deepcopy(extended_cfg)
|
|
# Check if auto_detect is enabled and update dependencies
|
|
for ns, cfg in config.items():
|
|
if cfg.get("auto_detect"):
|
|
py_ver = "python" + str(cfg.get("py-version", [""])[0])
|
|
if cfg.get("py_bin"):
|
|
py_ver = cfg["py_bin"]
|
|
|
|
exclude = []
|
|
# get any manually set deps
|
|
deps = config[ns].get("dependencies")
|
|
if deps:
|
|
for dep in deps.keys():
|
|
exclude.append(dep)
|
|
else:
|
|
config[ns]["dependencies"] = {}
|
|
|
|
# get auto deps
|
|
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]
|
|
|
|
for ns, cfg in get_ext_tops(config).items():
|
|
tops = [cfg.get("path")] + cfg.get("dependencies")
|
|
py_ver_major, py_ver_minor = cfg.get("py-version")
|
|
|
|
for top in tops:
|
|
top = os.path.normpath(top)
|
|
base, top_dirname = os.path.basename(top), os.path.dirname(top)
|
|
os.chdir(top_dirname)
|
|
site_pkg_dir = _is_shareable(base) and "pyall" or f"py{py_ver_major}"
|
|
log.debug(
|
|
'Packing alternative "%s" to "%s/%s" destination',
|
|
base,
|
|
ns,
|
|
site_pkg_dir,
|
|
)
|
|
if not os.path.exists(top):
|
|
log.error(
|
|
"File path %s does not exist. Unable to add to salt-ssh thin", top
|
|
)
|
|
continue
|
|
if not os.path.isdir(top):
|
|
# top is a single file module
|
|
if os.path.exists(os.path.join(top_dirname, base)):
|
|
tfp.add(base, arcname=os.path.join(ns, site_pkg_dir, base))
|
|
continue
|
|
for root, dirs, files in salt.utils.path.os_walk(base, followlinks=True):
|
|
for name in files:
|
|
if not name.endswith((".pyc", ".pyo")):
|
|
digest_collector.add(os.path.join(root, name))
|
|
arcname = os.path.join(ns, site_pkg_dir, root, name)
|
|
if hasattr(tfp, "getinfo"):
|
|
try:
|
|
tfp.getinfo(os.path.join(site_pkg_dir, root, name))
|
|
arcname = None
|
|
except KeyError:
|
|
log.debug(
|
|
'ZIP: Unable to add "%s" with "getinfo"', arcname
|
|
)
|
|
if arcname:
|
|
tfp.add(os.path.join(root, name), arcname=arcname)
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _catch_entry_points_exception(entry_point):
|
|
context = types.SimpleNamespace(exception_caught=False)
|
|
try:
|
|
yield context
|
|
except Exception as exc: # pylint: disable=broad-except
|
|
context.exception_caught = True
|
|
entry_point_details = salt.utils.entrypoints.name_and_version_from_entry_point(
|
|
entry_point
|
|
)
|
|
log.error(
|
|
"Error processing Salt Extension %s(version: %s): %s",
|
|
entry_point_details.name,
|
|
entry_point_details.version,
|
|
exc,
|
|
exc_info_on_loglevel=logging.DEBUG,
|
|
)
|
|
|
|
|
|
def _get_package_root_mod(mod):
|
|
"""
|
|
Given an imported module, find the topmost module
|
|
that is not a namespace package.
|
|
Returns a tuple of (root_mod, tuple), where the
|
|
second value is a tuple of parent namespaces.
|
|
Needed for saltext discovery if the entrypoint is not
|
|
part of the root module.
|
|
"""
|
|
parts = mod.__name__.split(".")
|
|
level = 0
|
|
while level < len(parts):
|
|
root_mod_name = ".".join(parts[: level + 1])
|
|
root_mod = sys.modules[root_mod_name]
|
|
# importlib.machinery.NamespaceLoader requires Python 3.11+
|
|
if type(root_mod.__path__) is list:
|
|
return root_mod, tuple(parts[:level])
|
|
level += 1
|
|
raise RuntimeError(f"Unable to determine package root mod for {mod}")
|
|
|
|
|
|
def _discover_saltexts(allowlist=None, blocklist=None):
|
|
mods = []
|
|
loaded_saltexts = {}
|
|
blocklist = blocklist or []
|
|
|
|
for entry_point in salt.utils.entrypoints.iter_entry_points("salt.loader"):
|
|
if allowlist is not None and entry_point.dist.name not in allowlist:
|
|
log.debug(
|
|
"Skipping entry point '%s' of '%s': not in allowlist",
|
|
entry_point.name,
|
|
entry_point.dist.name,
|
|
)
|
|
continue
|
|
if entry_point.dist.name in blocklist:
|
|
log.debug(
|
|
"Skipping entry point '%s' of '%s': in blocklist",
|
|
entry_point.name,
|
|
entry_point.dist.name,
|
|
)
|
|
continue
|
|
with _catch_entry_points_exception(entry_point) as ctx:
|
|
loaded_entry_point = entry_point.load()
|
|
if ctx.exception_caught:
|
|
continue
|
|
if not isinstance(loaded_entry_point, (types.FunctionType, types.ModuleType)):
|
|
log.debug(
|
|
"Skipping entry point '%s' of '%s': Not a function/module",
|
|
entry_point.name,
|
|
entry_point.dist.name,
|
|
)
|
|
continue
|
|
if entry_point.dist.name not in loaded_saltexts:
|
|
try:
|
|
# We could get this via entry_point.dist._path.name, but that is hacky
|
|
dist_name = next(
|
|
iter(
|
|
file.parent.name
|
|
for file in entry_point.dist.files
|
|
if file.parent.suffix == ".dist-info"
|
|
)
|
|
)
|
|
except StopIteration:
|
|
# This should never happen since we have the data to arrive here
|
|
log.debug(
|
|
"Skipping entry point '%s' of '%s': Failed discovering dist-info",
|
|
entry_point.name,
|
|
entry_point.dist.name,
|
|
)
|
|
continue
|
|
loaded_saltexts[entry_point.dist.name] = {
|
|
"name": dist_name,
|
|
"entrypoints": {},
|
|
}
|
|
|
|
mod = inspect.getmodule(loaded_entry_point)
|
|
with _catch_entry_points_exception(entry_point) as ctx:
|
|
root_mod, namespace = _get_package_root_mod(mod)
|
|
if ctx.exception_caught:
|
|
continue
|
|
|
|
loaded_saltexts[entry_point.dist.name]["entrypoints"][
|
|
entry_point.name
|
|
] = entry_point.value
|
|
_add_dependency(mods, root_mod, namespace=namespace)
|
|
|
|
# We need the mods to be in a deterministic order for the hash digest later
|
|
return list(sorted(set(mods))), loaded_saltexts
|
|
|
|
|
|
def _pack_saltext_dists(saltext_dists, digest_collector, tfp):
|
|
"""
|
|
Take the output of discover_saltexts and add appropriate entry point definitions
|
|
for the loader to be able to discover the extensions.
|
|
"""
|
|
# Again, we need this to execute in a deterministic order for the hash digest
|
|
for dist in sorted(saltext_dists):
|
|
data = saltext_dists[dist]
|
|
if not data["entrypoints"]:
|
|
log.debug("No entrypoints for distribution '%s'", dist)
|
|
continue
|
|
log.debug("Packing entrypoints for distribution '%s'", dist)
|
|
defs = (
|
|
"[salt.loader]\n"
|
|
+ "\n".join(f"{name} = {val}" for name, val in data["entrypoints"].items())
|
|
+ "\n"
|
|
).encode("utf-8")
|
|
info = tarfile.TarInfo(name="py3/" + data["name"] + "/entry_points.txt")
|
|
info.size = len(defs)
|
|
tfp.addfile(tarinfo=info, fileobj=io.BytesIO(defs))
|
|
digest_collector.add_data(defs)
|
|
|
|
|
|
def gen_thin(
|
|
cachedir,
|
|
extra_mods="",
|
|
overwrite=False,
|
|
so_mods="",
|
|
absonly=True,
|
|
compress="gzip",
|
|
extended_cfg=None,
|
|
exclude_saltexts=False,
|
|
saltext_allowlist=None,
|
|
saltext_blocklist=None,
|
|
):
|
|
"""
|
|
Generate the salt-thin tarball and print the location of the tarball
|
|
Optional additional mods to include (e.g. mako) can be supplied as a comma
|
|
delimited string. Permits forcing an overwrite of the output file as well.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt-run thin.generate
|
|
salt-run thin.generate mako
|
|
salt-run thin.generate mako,wempy 1
|
|
salt-run thin.generate overwrite=1
|
|
"""
|
|
if sys.version_info < (3,):
|
|
raise salt.exceptions.SaltSystemExit(
|
|
'The minimum required python version to run salt-ssh is "3".'
|
|
)
|
|
if compress not in ["gzip", "zip"]:
|
|
log.warning(
|
|
'Unknown compression type: "%s". Falling back to "gzip" compression.',
|
|
compress,
|
|
)
|
|
compress = "gzip"
|
|
|
|
thindir = os.path.join(cachedir, "thin")
|
|
if not os.path.isdir(thindir):
|
|
os.makedirs(thindir)
|
|
thintar = os.path.join(thindir, "thin." + (compress == "gzip" and "tgz" or "zip"))
|
|
thinver = os.path.join(thindir, "version")
|
|
pythinver = os.path.join(thindir, ".thin-gen-py-version")
|
|
salt_call = os.path.join(thindir, "salt-call")
|
|
pymap_cfg = os.path.join(thindir, "supported-versions")
|
|
code_checksum = os.path.join(thindir, "code-checksum")
|
|
digest_collector = salt.utils.hashutils.DigestCollector()
|
|
|
|
with salt.utils.files.fopen(salt_call, "wb") as fp_:
|
|
fp_.write(_get_salt_call("pyall", **_get_ext_namespaces(extended_cfg)))
|
|
|
|
if os.path.isfile(thintar):
|
|
if not overwrite:
|
|
if os.path.isfile(thinver):
|
|
with salt.utils.files.fopen(thinver) as fh_:
|
|
overwrite = fh_.read() != salt.version.__version__
|
|
if overwrite is False and os.path.isfile(pythinver):
|
|
with salt.utils.files.fopen(pythinver) as fh_:
|
|
overwrite = fh_.read() != str(sys.version_info[0])
|
|
else:
|
|
overwrite = True
|
|
|
|
if overwrite:
|
|
try:
|
|
log.debug("Removing %s archive file", thintar)
|
|
os.remove(thintar)
|
|
except OSError as exc:
|
|
log.error("Error while removing %s file: %s", thintar, exc)
|
|
if os.path.exists(thintar):
|
|
raise salt.exceptions.SaltSystemExit(
|
|
"Unable to remove {} file. See logs for details.".format(
|
|
thintar
|
|
)
|
|
)
|
|
else:
|
|
return thintar
|
|
|
|
tops_py_version_mapping = {}
|
|
tops = get_tops(extra_mods=extra_mods, so_mods=so_mods)
|
|
if not exclude_saltexts:
|
|
if compress != "gzip":
|
|
# The reason being that we're generating the filtered entrypoints
|
|
# and adding them from memory - if this is deemed as unnecessary,
|
|
# we would need the absolute path to the entry_points.txt file for
|
|
# the distribution, which is only available as a protected attribute.
|
|
# Salt-SSH never overrides `compress` from gzip though.
|
|
log.warning("Cannot include saltexts in thin when compression is not gzip")
|
|
exclude_saltexts = True
|
|
else:
|
|
mods, saltext_dists = _discover_saltexts(
|
|
allowlist=saltext_allowlist, blocklist=saltext_blocklist
|
|
)
|
|
# Deduplicate in case some saltexts were passed in thin_extra_modules
|
|
tops.extend(mod for mod in mods if mod not in tops)
|
|
|
|
tops_py_version_mapping[sys.version_info.major] = tops
|
|
|
|
with salt.utils.files.fopen(pymap_cfg, "wb") as fp_:
|
|
fp_.write(
|
|
_get_supported_py_config(
|
|
tops=tops_py_version_mapping, extended_cfg=extended_cfg
|
|
)
|
|
)
|
|
|
|
tmp_thintar = _get_thintar_prefix(thintar)
|
|
if compress == "gzip":
|
|
tfp = tarfile.open(tmp_thintar, "w:gz", dereference=True)
|
|
elif compress == "zip":
|
|
tfp = zipfile.ZipFile(
|
|
tmp_thintar,
|
|
"w",
|
|
compression=zlib and zipfile.ZIP_DEFLATED or zipfile.ZIP_STORED,
|
|
)
|
|
tfp.add = tfp.write
|
|
try: # cwd may not exist if it was removed but salt was run from it
|
|
start_dir = os.getcwd()
|
|
except OSError:
|
|
start_dir = None
|
|
tempdir = None
|
|
|
|
# Pack default data
|
|
log.debug("Packing default libraries based on current Salt version")
|
|
for py_ver, tops in tops_py_version_mapping.items():
|
|
for top, namespace in tops:
|
|
if absonly and not os.path.isabs(top):
|
|
continue
|
|
base = os.path.basename(top)
|
|
top_dirname = os.path.dirname(top)
|
|
if os.path.isdir(top_dirname):
|
|
os.chdir(top_dirname)
|
|
else:
|
|
# This is likely a compressed python .egg
|
|
tempdir = tempfile.mkdtemp()
|
|
egg = zipfile.ZipFile(top_dirname)
|
|
egg.extractall(tempdir) # nosec
|
|
top = os.path.join(tempdir, base)
|
|
os.chdir(tempdir)
|
|
|
|
site_pkg_dir = _is_shareable(base) and "pyall" or f"py{py_ver}"
|
|
|
|
log.debug('Packing "%s" to "%s" destination', base, site_pkg_dir)
|
|
if not os.path.isdir(top):
|
|
# top is a single file module
|
|
if os.path.exists(os.path.join(top_dirname, base)):
|
|
tfp.add(base, arcname=os.path.join(site_pkg_dir, base))
|
|
continue
|
|
for root, dirs, files in salt.utils.path.os_walk(base, followlinks=True):
|
|
for name in files:
|
|
if not name.endswith((".pyc", ".pyo")):
|
|
digest_collector.add(os.path.join(root, name))
|
|
arcname = os.path.join(
|
|
site_pkg_dir, *(namespace or ()), root, name
|
|
)
|
|
if hasattr(tfp, "getinfo"):
|
|
try:
|
|
# This is a little slow but there's no clear way to detect duplicates
|
|
tfp.getinfo(os.path.join(site_pkg_dir, root, name))
|
|
arcname = None
|
|
except KeyError:
|
|
log.debug(
|
|
'ZIP: Unable to add "%s" with "getinfo"', arcname
|
|
)
|
|
if arcname:
|
|
tfp.add(os.path.join(root, name), arcname=arcname)
|
|
|
|
if tempdir is not None:
|
|
shutil.rmtree(tempdir)
|
|
tempdir = None
|
|
|
|
if not exclude_saltexts:
|
|
log.debug("Packing saltext distribution entrypoints")
|
|
_pack_saltext_dists(saltext_dists, digest_collector, tfp)
|
|
if extended_cfg:
|
|
log.debug("Packing libraries based on alternative Salt versions")
|
|
_pack_alternative(extended_cfg, digest_collector, tfp)
|
|
|
|
os.chdir(thindir)
|
|
with salt.utils.files.fopen(thinver, "w+") as fp_:
|
|
fp_.write(salt.version.__version__)
|
|
with salt.utils.files.fopen(pythinver, "w+") as fp_:
|
|
fp_.write(str(sys.version_info.major))
|
|
with salt.utils.files.fopen(code_checksum, "w+") as fp_:
|
|
fp_.write(digest_collector.digest())
|
|
os.chdir(os.path.dirname(thinver))
|
|
|
|
for fname in [
|
|
"version",
|
|
".thin-gen-py-version",
|
|
"salt-call",
|
|
"supported-versions",
|
|
"code-checksum",
|
|
]:
|
|
tfp.add(fname)
|
|
|
|
if start_dir and os.access(start_dir, os.R_OK) and os.access(start_dir, os.X_OK):
|
|
os.chdir(start_dir)
|
|
tfp.close()
|
|
|
|
shutil.move(tmp_thintar, thintar)
|
|
|
|
return thintar
|
|
|
|
|
|
def thin_sum(cachedir, form="sha1"):
|
|
"""
|
|
Return the checksum of the current thin tarball
|
|
"""
|
|
thintar = gen_thin(cachedir)
|
|
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 = f"'{fh.read().strip()}'"
|
|
else:
|
|
code_checksum = "'0'"
|
|
|
|
return code_checksum, salt.utils.hashutils.get_hash(thintar, form)
|
|
|
|
|
|
def gen_min(
|
|
cachedir,
|
|
extra_mods="",
|
|
overwrite=False,
|
|
so_mods="",
|
|
):
|
|
"""
|
|
Generate the salt-min tarball and print the location of the tarball
|
|
Optional additional mods to include (e.g. mako) can be supplied as a comma
|
|
delimited string. Permits forcing an overwrite of the output file as well.
|
|
|
|
CLI Example:
|
|
|
|
.. code-block:: bash
|
|
|
|
salt-run min.generate
|
|
salt-run min.generate mako
|
|
salt-run min.generate mako,wempy 1
|
|
salt-run min.generate overwrite=1
|
|
"""
|
|
mindir = os.path.join(cachedir, "min")
|
|
if not os.path.isdir(mindir):
|
|
os.makedirs(mindir)
|
|
mintar = os.path.join(mindir, "min.tgz")
|
|
minver = os.path.join(mindir, "version")
|
|
pyminver = os.path.join(mindir, ".min-gen-py-version")
|
|
salt_call = os.path.join(mindir, "salt-call")
|
|
with salt.utils.files.fopen(salt_call, "wb") as fp_:
|
|
fp_.write(_get_salt_call())
|
|
if os.path.isfile(mintar):
|
|
if not overwrite:
|
|
if os.path.isfile(minver):
|
|
with salt.utils.files.fopen(minver) as fh_:
|
|
overwrite = fh_.read() != salt.version.__version__
|
|
if overwrite is False and os.path.isfile(pyminver):
|
|
with salt.utils.files.fopen(pyminver) as fh_:
|
|
overwrite = fh_.read() != str(sys.version_info[0])
|
|
else:
|
|
overwrite = True
|
|
|
|
if overwrite:
|
|
try:
|
|
os.remove(mintar)
|
|
except OSError:
|
|
pass
|
|
else:
|
|
return mintar
|
|
|
|
tops_py_version_mapping = {}
|
|
tops = get_tops(extra_mods=extra_mods, so_mods=so_mods)
|
|
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
|
|
start_dir = os.getcwd()
|
|
except OSError:
|
|
start_dir = None
|
|
tempdir = None
|
|
|
|
# This is the absolute minimum set of files required to run salt-call
|
|
min_files = (
|
|
"salt/__init__.py",
|
|
"salt/utils",
|
|
"salt/utils/__init__.py",
|
|
"salt/utils/atomicfile.py",
|
|
"salt/utils/validate",
|
|
"salt/utils/validate/__init__.py",
|
|
"salt/utils/validate/path.py",
|
|
"salt/utils/decorators",
|
|
"salt/utils/decorators/__init__.py",
|
|
"salt/utils/cache.py",
|
|
"salt/utils/xdg.py",
|
|
"salt/utils/odict.py",
|
|
"salt/utils/minions.py",
|
|
"salt/utils/dicttrim.py",
|
|
"salt/utils/sdb.py",
|
|
"salt/utils/migrations.py",
|
|
"salt/utils/files.py",
|
|
"salt/utils/parsers.py",
|
|
"salt/utils/locales.py",
|
|
"salt/utils/lazy.py",
|
|
"salt/utils/s3.py",
|
|
"salt/utils/dictupdate.py",
|
|
"salt/utils/verify.py",
|
|
"salt/utils/args.py",
|
|
"salt/utils/kinds.py",
|
|
"salt/utils/xmlutil.py",
|
|
"salt/utils/debug.py",
|
|
"salt/utils/jid.py",
|
|
"salt/utils/openstack",
|
|
"salt/utils/openstack/__init__.py",
|
|
"salt/utils/openstack/swift.py",
|
|
"salt/utils/asynchronous.py",
|
|
"salt/utils/process.py",
|
|
"salt/utils/jinja.py",
|
|
"salt/utils/rsax931.py",
|
|
"salt/utils/requisite.py",
|
|
"salt/utils/context.py",
|
|
"salt/utils/minion.py",
|
|
"salt/utils/error.py",
|
|
"salt/utils/aws.py",
|
|
"salt/utils/timed_subprocess.py",
|
|
"salt/utils/zeromq.py",
|
|
"salt/utils/schedule.py",
|
|
"salt/utils/url.py",
|
|
"salt/utils/yamlencoding.py",
|
|
"salt/utils/network.py",
|
|
"salt/utils/http.py",
|
|
"salt/utils/gzip_util.py",
|
|
"salt/utils/vt.py",
|
|
"salt/utils/templates.py",
|
|
"salt/utils/aggregation.py",
|
|
"salt/utils/yaml.py",
|
|
"salt/utils/yamldumper.py",
|
|
"salt/utils/yamlloader.py",
|
|
"salt/utils/event.py",
|
|
"salt/utils/state.py",
|
|
"salt/serializers",
|
|
"salt/serializers/__init__.py",
|
|
"salt/serializers/yamlex.py",
|
|
"salt/template.py",
|
|
"salt/_compat.py",
|
|
"salt/loader.py",
|
|
"salt/client",
|
|
"salt/client/__init__.py",
|
|
"salt/ext",
|
|
"salt/ext/__init__.py",
|
|
"salt/ext/ipaddress.py",
|
|
"salt/version.py",
|
|
"salt/syspaths.py",
|
|
"salt/defaults",
|
|
"salt/defaults/__init__.py",
|
|
"salt/defaults/exitcodes.py",
|
|
"salt/renderers",
|
|
"salt/renderers/__init__.py",
|
|
"salt/renderers/jinja.py",
|
|
"salt/renderers/yaml.py",
|
|
"salt/modules",
|
|
"salt/modules/__init__.py",
|
|
"salt/modules/test.py",
|
|
"salt/modules/selinux.py",
|
|
"salt/modules/cmdmod.py",
|
|
"salt/modules/saltutil.py",
|
|
"salt/minion.py",
|
|
"salt/pillar",
|
|
"salt/pillar/__init__.py",
|
|
"salt/utils/textformat.py",
|
|
"salt/log_handlers",
|
|
"salt/log_handlers/__init__.py",
|
|
"salt/_logging/__init__.py",
|
|
"salt/_logging/handlers.py",
|
|
"salt/_logging/impl.py",
|
|
"salt/_logging/mixins.py",
|
|
"salt/cli",
|
|
"salt/cli/__init__.py",
|
|
"salt/cli/caller.py",
|
|
"salt/cli/daemons.py",
|
|
"salt/cli/salt.py",
|
|
"salt/cli/call.py",
|
|
"salt/fileserver",
|
|
"salt/fileserver/__init__.py",
|
|
"salt/channel",
|
|
"salt/channel/__init__.py",
|
|
"salt/channel/client.py",
|
|
"salt/transport", # XXX Are the transport imports still needed?
|
|
"salt/transport/__init__.py",
|
|
"salt/transport/client.py",
|
|
"salt/exceptions.py",
|
|
"salt/grains",
|
|
"salt/grains/__init__.py",
|
|
"salt/grains/extra.py",
|
|
"salt/scripts.py",
|
|
"salt/state.py",
|
|
"salt/fileclient.py",
|
|
"salt/crypt.py",
|
|
"salt/config.py",
|
|
"salt/beacons",
|
|
"salt/beacons/__init__.py",
|
|
"salt/payload.py",
|
|
"salt/output",
|
|
"salt/output/__init__.py",
|
|
"salt/output/nested.py",
|
|
)
|
|
|
|
for py_ver, tops in tops_py_version_mapping.items():
|
|
for top in tops:
|
|
base = os.path.basename(top)
|
|
top_dirname = os.path.dirname(top)
|
|
if os.path.isdir(top_dirname):
|
|
os.chdir(top_dirname)
|
|
else:
|
|
# This is likely a compressed python .egg
|
|
tempdir = tempfile.mkdtemp()
|
|
egg = zipfile.ZipFile(top_dirname)
|
|
egg.extractall(tempdir) # nosec
|
|
top = os.path.join(tempdir, base)
|
|
os.chdir(tempdir)
|
|
if not os.path.isdir(top):
|
|
# top is a single file module
|
|
tfp.add(base, arcname=os.path.join(f"py{py_ver}", base))
|
|
continue
|
|
for root, dirs, files in salt.utils.path.os_walk(base, followlinks=True):
|
|
for name in files:
|
|
if name.endswith((".pyc", ".pyo")):
|
|
continue
|
|
if (
|
|
root.startswith("salt")
|
|
and os.path.join(root, name) not in min_files
|
|
):
|
|
continue
|
|
tfp.add(
|
|
os.path.join(root, name),
|
|
arcname=os.path.join(f"py{py_ver}", root, name),
|
|
)
|
|
if tempdir is not None:
|
|
shutil.rmtree(tempdir)
|
|
tempdir = None
|
|
|
|
os.chdir(mindir)
|
|
tfp.add("salt-call")
|
|
with salt.utils.files.fopen(minver, "w+") as fp_:
|
|
fp_.write(salt.version.__version__)
|
|
with salt.utils.files.fopen(pyminver, "w+") as fp_:
|
|
fp_.write(str(sys.version_info[0]))
|
|
os.chdir(os.path.dirname(minver))
|
|
tfp.add("version")
|
|
tfp.add(".min-gen-py-version")
|
|
if start_dir:
|
|
os.chdir(start_dir)
|
|
tfp.close()
|
|
return mintar
|
|
|
|
|
|
def min_sum(cachedir, form="sha1"):
|
|
"""
|
|
Return the checksum of the current thin tarball
|
|
"""
|
|
mintar = gen_min(cachedir)
|
|
return salt.utils.hashutils.get_hash(mintar, form)
|