796 lines
30 KiB
Diff
796 lines
30 KiB
Diff
From 3fd6c0c6793632c819fb5f8fb3b3538463eaaccc Mon Sep 17 00:00:00 2001
|
|
From: Victor Zhestkov <vzhestkov@suse.com>
|
|
Date: Thu, 24 Feb 2022 16:52:24 +0300
|
|
Subject: [PATCH] Add salt-ssh support with venv-salt-minion - 3004
|
|
(#493)
|
|
|
|
* Add salt-ssh support with venv-salt-minion
|
|
|
|
* Add some comments and drop the commented line
|
|
|
|
* Fix return in check_venv_hash_file
|
|
|
|
* Convert all script parameters to strings
|
|
|
|
* Reduce the size of minion response
|
|
|
|
Minion response contains SSH_PY_CODE wrapped to base64.
|
|
This fix reduces the size of the response in DEBUG logging
|
|
|
|
* Make VENV_HASH_FILE global
|
|
|
|
* Pass the context to roster modules
|
|
|
|
* Avoid race condition on loading roster modules
|
|
|
|
* Prevent simultaneous to salt-ssh minion
|
|
|
|
* Make ssh session grace time configurable
|
|
|
|
* Prevent possible segfault by GC
|
|
|
|
* Revert "Avoid race condition on loading roster modules"
|
|
|
|
This reverts commit 8ff822a162cc494d3528184aef983ad20e09f4e2.
|
|
|
|
* Prevent deadlocks with importlib on using LazyLoader
|
|
|
|
* Make logging on salt-ssh errors more informative
|
|
|
|
* Add comments about using salt.loader.LOAD_LOCK
|
|
|
|
* Fix test_loader test
|
|
|
|
* Prevent deadlocks on using logging
|
|
|
|
* Use collections.deque instead of list for salt-ssh
|
|
|
|
Suggested by @agraul
|
|
|
|
* Get proper exitstatus from salt.utils.vt.Terminal
|
|
|
|
to prevent empty event returns due to improperly detecting
|
|
the child process as failed
|
|
|
|
* Do not run pre flight script for raw_shell
|
|
---
|
|
salt/_logging/impl.py | 55 +++++++-----
|
|
salt/client/ssh/__init__.py | 157 ++++++++++++++++++++++++++++-----
|
|
salt/client/ssh/client.py | 7 +-
|
|
salt/client/ssh/shell.py | 8 ++
|
|
salt/client/ssh/ssh_py_shim.py | 108 +++++++++++++----------
|
|
salt/loader/__init__.py | 31 ++++++-
|
|
salt/netapi/__init__.py | 3 +-
|
|
salt/roster/__init__.py | 6 +-
|
|
tests/unit/test_loader.py | 2 +-
|
|
9 files changed, 278 insertions(+), 99 deletions(-)
|
|
|
|
diff --git a/salt/_logging/impl.py b/salt/_logging/impl.py
|
|
index cc18f49a9e..e050f43caf 100644
|
|
--- a/salt/_logging/impl.py
|
|
+++ b/salt/_logging/impl.py
|
|
@@ -14,6 +14,7 @@ import re
|
|
import socket
|
|
import sys
|
|
import traceback
|
|
+import threading
|
|
import types
|
|
import urllib.parse
|
|
|
|
@@ -104,6 +105,10 @@ DFLT_LOG_DATEFMT_LOGFILE = "%Y-%m-%d %H:%M:%S"
|
|
DFLT_LOG_FMT_CONSOLE = "[%(levelname)-8s] %(message)s"
|
|
DFLT_LOG_FMT_LOGFILE = "%(asctime)s,%(msecs)03d [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(process)d] %(message)s"
|
|
|
|
+# LOG_LOCK is used to prevent deadlocks on using logging
|
|
+# in combination with multiprocessing with salt-api
|
|
+LOG_LOCK = threading.Lock()
|
|
+
|
|
|
|
class SaltLogRecord(logging.LogRecord):
|
|
def __init__(self, *args, **kwargs):
|
|
@@ -270,27 +275,35 @@ class SaltLoggingClass(LOGGING_LOGGER_CLASS, metaclass=LoggingMixinMeta):
|
|
else:
|
|
extra["exc_info_on_loglevel"] = exc_info_on_loglevel
|
|
|
|
- if sys.version_info < (3, 8):
|
|
- LOGGING_LOGGER_CLASS._log(
|
|
- self,
|
|
- level,
|
|
- msg,
|
|
- args,
|
|
- exc_info=exc_info,
|
|
- extra=extra,
|
|
- stack_info=stack_info,
|
|
- )
|
|
- else:
|
|
- LOGGING_LOGGER_CLASS._log(
|
|
- self,
|
|
- level,
|
|
- msg,
|
|
- args,
|
|
- exc_info=exc_info,
|
|
- extra=extra,
|
|
- stack_info=stack_info,
|
|
- stacklevel=stacklevel,
|
|
- )
|
|
+ try:
|
|
+ LOG_LOCK.acquire()
|
|
+ if sys.version_info < (3,):
|
|
+ LOGGING_LOGGER_CLASS._log(
|
|
+ self, level, msg, args, exc_info=exc_info, extra=extra
|
|
+ )
|
|
+ elif sys.version_info < (3, 8):
|
|
+ LOGGING_LOGGER_CLASS._log(
|
|
+ self,
|
|
+ level,
|
|
+ msg,
|
|
+ args,
|
|
+ exc_info=exc_info,
|
|
+ extra=extra,
|
|
+ stack_info=stack_info,
|
|
+ )
|
|
+ else:
|
|
+ LOGGING_LOGGER_CLASS._log(
|
|
+ self,
|
|
+ level,
|
|
+ msg,
|
|
+ args,
|
|
+ exc_info=exc_info,
|
|
+ extra=extra,
|
|
+ stack_info=stack_info,
|
|
+ stacklevel=stacklevel,
|
|
+ )
|
|
+ finally:
|
|
+ LOG_LOCK.release()
|
|
|
|
def makeRecord(
|
|
self,
|
|
diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py
|
|
index 19089ce8ad..e6837df4e5 100644
|
|
--- a/salt/client/ssh/__init__.py
|
|
+++ b/salt/client/ssh/__init__.py
|
|
@@ -6,11 +6,13 @@ import base64
|
|
import binascii
|
|
import copy
|
|
import datetime
|
|
+import gc
|
|
import getpass
|
|
import hashlib
|
|
import logging
|
|
import multiprocessing
|
|
import os
|
|
+import psutil
|
|
import queue
|
|
import re
|
|
import shlex
|
|
@@ -20,6 +22,7 @@ import tarfile
|
|
import tempfile
|
|
import time
|
|
import uuid
|
|
+from collections import deque
|
|
|
|
import salt.client.ssh.shell
|
|
import salt.client.ssh.wrapper
|
|
@@ -47,6 +50,7 @@ import salt.utils.url
|
|
import salt.utils.verify
|
|
from salt._logging import LOG_LEVELS
|
|
from salt._logging.mixins import MultiprocessingStateMixin
|
|
+from salt._logging.impl import LOG_LOCK
|
|
from salt.template import compile_template
|
|
from salt.utils.process import Process
|
|
from salt.utils.zeromq import zmq
|
|
@@ -146,15 +150,26 @@ if [ "$SUDO" ] && [ "$SUDO_USER" ]
|
|
then SUDO="$SUDO -u $SUDO_USER"
|
|
fi
|
|
EX_PYTHON_INVALID={EX_THIN_PYTHON_INVALID}
|
|
-PYTHON_CMDS="python3 /usr/libexec/platform-python python27 python2.7 python26 python2.6 python2 python"
|
|
+set +x
|
|
+SSH_PY_CODE='import base64;
|
|
+ exec(base64.b64decode("""{{SSH_PY_CODE}}""").decode("utf-8"))'
|
|
+if [ -n "$DEBUG" ]
|
|
+ then set -x
|
|
+fi
|
|
+PYTHON_CMDS="/var/tmp/venv-salt-minion/bin/python python3 /usr/libexec/platform-python python27 python2.7 python26 python2.6 python2 python"
|
|
for py_cmd in $PYTHON_CMDS
|
|
do
|
|
if command -v "$py_cmd" >/dev/null 2>&1 && "$py_cmd" -c "import sys; sys.exit(not (sys.version_info >= (2, 6)));"
|
|
then
|
|
py_cmd_path=`"$py_cmd" -c 'from __future__ import print_function;import sys; print(sys.executable);'`
|
|
cmdpath=`command -v $py_cmd 2>/dev/null || which $py_cmd 2>/dev/null`
|
|
+ cmdpath=`readlink -f $cmdpath`
|
|
if file $cmdpath | grep "shell script" > /dev/null
|
|
then
|
|
+ if echo $cmdpath | grep venv-salt-minion > /dev/null
|
|
+ then
|
|
+ exec $SUDO "$cmdpath" -c "$SSH_PY_CODE"
|
|
+ fi
|
|
ex_vars="'PATH', 'LD_LIBRARY_PATH', 'MANPATH', \
|
|
'XDG_DATA_DIRS', 'PKG_CONFIG_PATH'"
|
|
export `$py_cmd -c \
|
|
@@ -166,13 +181,9 @@ do
|
|
exec $SUDO PATH=$PATH LD_LIBRARY_PATH=$LD_LIBRARY_PATH \
|
|
MANPATH=$MANPATH XDG_DATA_DIRS=$XDG_DATA_DIRS \
|
|
PKG_CONFIG_PATH=$PKG_CONFIG_PATH \
|
|
- "$py_cmd_path" -c \
|
|
- 'import base64;
|
|
- exec(base64.b64decode("""{{SSH_PY_CODE}}""").decode("utf-8"))'
|
|
+ "$py_cmd_path" -c "$SSH_PY_CODE"
|
|
else
|
|
- exec $SUDO "$py_cmd_path" -c \
|
|
- 'import base64;
|
|
- exec(base64.b64decode("""{{SSH_PY_CODE}}""").decode("utf-8"))'
|
|
+ exec $SUDO "$py_cmd_path" -c "$SSH_PY_CODE"
|
|
fi
|
|
exit 0
|
|
else
|
|
@@ -189,6 +200,9 @@ EOF'''.format(
|
|
]
|
|
)
|
|
|
|
+# The file on a salt-ssh minion used to identify if Salt Bundle was deployed
|
|
+VENV_HASH_FILE = "/var/tmp/venv-salt-minion/venv-hash.txt"
|
|
+
|
|
if not salt.utils.platform.is_windows() and not salt.utils.platform.is_junos():
|
|
shim_file = os.path.join(os.path.dirname(__file__), "ssh_py_shim.py")
|
|
if not os.path.exists(shim_file):
|
|
@@ -209,7 +223,7 @@ class SSH(MultiprocessingStateMixin):
|
|
|
|
ROSTER_UPDATE_FLAG = "#__needs_update"
|
|
|
|
- def __init__(self, opts):
|
|
+ def __init__(self, opts, context=None):
|
|
self.__parsed_rosters = {SSH.ROSTER_UPDATE_FLAG: True}
|
|
pull_sock = os.path.join(opts["sock_dir"], "master_event_pull.ipc")
|
|
if os.path.exists(pull_sock) and zmq:
|
|
@@ -236,7 +250,9 @@ class SSH(MultiprocessingStateMixin):
|
|
else "glob"
|
|
)
|
|
self._expand_target()
|
|
- self.roster = salt.roster.Roster(self.opts, self.opts.get("roster", "flat"))
|
|
+ self.roster = salt.roster.Roster(
|
|
+ self.opts, self.opts.get("roster", "flat"), context=context
|
|
+ )
|
|
self.targets = self.roster.targets(self.opts["tgt"], self.tgt_type)
|
|
if not self.targets:
|
|
self._update_targets()
|
|
@@ -316,6 +332,13 @@ class SSH(MultiprocessingStateMixin):
|
|
extended_cfg=self.opts.get("ssh_ext_alternatives"),
|
|
)
|
|
self.mods = mod_data(self.fsclient)
|
|
+ self.cache = salt.cache.Cache(self.opts)
|
|
+ self.master_id = self.opts["id"]
|
|
+ self.max_pid_wait = int(self.opts.get("ssh_max_pid_wait", 600))
|
|
+ self.session_flock_file = os.path.join(
|
|
+ self.opts["cachedir"], "salt-ssh.session.lock"
|
|
+ )
|
|
+ self.ssh_session_grace_time = int(self.opts.get("ssh_session_grace_time", 3))
|
|
|
|
# __setstate__ and __getstate__ are only used on spawning platforms.
|
|
def __setstate__(self, state):
|
|
@@ -546,6 +569,8 @@ class SSH(MultiprocessingStateMixin):
|
|
"""
|
|
Run the routine in a "Thread", put a dict on the queue
|
|
"""
|
|
+ LOG_LOCK.release()
|
|
+ salt.loader.LOAD_LOCK.release()
|
|
opts = copy.deepcopy(opts)
|
|
single = Single(
|
|
opts,
|
|
@@ -585,7 +610,7 @@ class SSH(MultiprocessingStateMixin):
|
|
"""
|
|
que = multiprocessing.Queue()
|
|
running = {}
|
|
- target_iter = self.targets.__iter__()
|
|
+ targets_queue = deque(self.targets.keys())
|
|
returned = set()
|
|
rets = set()
|
|
init = False
|
|
@@ -594,11 +619,43 @@ class SSH(MultiprocessingStateMixin):
|
|
log.error("No matching targets found in roster.")
|
|
break
|
|
if len(running) < self.opts.get("ssh_max_procs", 25) and not init:
|
|
- try:
|
|
- host = next(target_iter)
|
|
- except StopIteration:
|
|
+ if targets_queue:
|
|
+ host = targets_queue.popleft()
|
|
+ else:
|
|
init = True
|
|
continue
|
|
+ with salt.utils.files.flopen(self.session_flock_file, "w"):
|
|
+ cached_session = self.cache.fetch("salt-ssh/session", host)
|
|
+ if cached_session is not None and "ts" in cached_session:
|
|
+ prev_session_running = time.time() - cached_session["ts"]
|
|
+ if (
|
|
+ "pid" in cached_session
|
|
+ and cached_session.get("master_id", self.master_id)
|
|
+ == self.master_id
|
|
+ ):
|
|
+ pid_running = (
|
|
+ False
|
|
+ if cached_session["pid"] == 0
|
|
+ else psutil.pid_exists(cached_session["pid"])
|
|
+ )
|
|
+ if (
|
|
+ pid_running and prev_session_running < self.max_pid_wait
|
|
+ ) or (
|
|
+ not pid_running
|
|
+ and prev_session_running < self.ssh_session_grace_time
|
|
+ ):
|
|
+ targets_queue.append(host)
|
|
+ time.sleep(0.3)
|
|
+ continue
|
|
+ self.cache.store(
|
|
+ "salt-ssh/session",
|
|
+ host,
|
|
+ {
|
|
+ "pid": 0,
|
|
+ "master_id": self.master_id,
|
|
+ "ts": time.time(),
|
|
+ },
|
|
+ )
|
|
for default in self.defaults:
|
|
if default not in self.targets[host]:
|
|
self.targets[host][default] = self.defaults[default]
|
|
@@ -630,8 +687,38 @@ class SSH(MultiprocessingStateMixin):
|
|
mine,
|
|
)
|
|
routine = Process(target=self.handle_routine, args=args)
|
|
- routine.start()
|
|
+ # Explicitly call garbage collector to prevent possible segfault
|
|
+ # in salt-api child process. (bsc#1188607)
|
|
+ gc.collect()
|
|
+ try:
|
|
+ # salt.loader.LOAD_LOCK is used to prevent deadlock
|
|
+ # with importlib in combination with using multiprocessing (bsc#1182851)
|
|
+ # If the salt-api child process is creating while LazyLoader instance
|
|
+ # is loading module, new child process gets the lock for this module acquired.
|
|
+ # Touching this module with importlib inside child process leads to deadlock.
|
|
+ #
|
|
+ # salt.loader.LOAD_LOCK is used to prevent salt-api child process creation
|
|
+ # while creating new instance of LazyLoader
|
|
+ # salt.loader.LOAD_LOCK must be released explicitly in self.handle_routine
|
|
+ salt.loader.LOAD_LOCK.acquire()
|
|
+ # The same solution applied to fix logging deadlock
|
|
+ # LOG_LOCK must be released explicitly in self.handle_routine
|
|
+ LOG_LOCK.acquire()
|
|
+ routine.start()
|
|
+ finally:
|
|
+ LOG_LOCK.release()
|
|
+ salt.loader.LOAD_LOCK.release()
|
|
running[host] = {"thread": routine}
|
|
+ with salt.utils.files.flopen(self.session_flock_file, "w"):
|
|
+ self.cache.store(
|
|
+ "salt-ssh/session",
|
|
+ host,
|
|
+ {
|
|
+ "pid": routine.pid,
|
|
+ "master_id": self.master_id,
|
|
+ "ts": time.time(),
|
|
+ },
|
|
+ )
|
|
continue
|
|
ret = {}
|
|
try:
|
|
@@ -662,12 +749,27 @@ class SSH(MultiprocessingStateMixin):
|
|
)
|
|
ret = {"id": host, "ret": error}
|
|
log.error(error)
|
|
+ log.error(
|
|
+ "PID %s did not return any data for host '%s'",
|
|
+ running[host]["thread"].pid,
|
|
+ host,
|
|
+ )
|
|
yield {ret["id"]: ret["ret"]}
|
|
running[host]["thread"].join()
|
|
rets.add(host)
|
|
for host in rets:
|
|
if host in running:
|
|
running.pop(host)
|
|
+ with salt.utils.files.flopen(self.session_flock_file, "w"):
|
|
+ self.cache.store(
|
|
+ "salt-ssh/session",
|
|
+ host,
|
|
+ {
|
|
+ "pid": 0,
|
|
+ "master_id": self.master_id,
|
|
+ "ts": time.time(),
|
|
+ },
|
|
+ )
|
|
if len(rets) >= len(self.targets):
|
|
break
|
|
# Sleep when limit or all threads started
|
|
@@ -1036,14 +1138,24 @@ class Single:
|
|
return False
|
|
return True
|
|
|
|
+ def check_venv_hash_file(self):
|
|
+ """
|
|
+ check if the venv exists on the remote machine
|
|
+ """
|
|
+ stdout, stderr, retcode = self.shell.exec_cmd(
|
|
+ "test -f {}".format(VENV_HASH_FILE)
|
|
+ )
|
|
+ return retcode == 0
|
|
+
|
|
def deploy(self):
|
|
"""
|
|
Deploy salt-thin
|
|
"""
|
|
- self.shell.send(
|
|
- self.thin,
|
|
- os.path.join(self.thin_dir, "salt-thin.tgz"),
|
|
- )
|
|
+ if not self.check_venv_hash_file():
|
|
+ self.shell.send(
|
|
+ self.thin,
|
|
+ os.path.join(self.thin_dir, "salt-thin.tgz"),
|
|
+ )
|
|
self.deploy_ext()
|
|
return True
|
|
|
|
@@ -1071,8 +1183,9 @@ class Single:
|
|
Returns tuple of (stdout, stderr, retcode)
|
|
"""
|
|
stdout = stderr = retcode = None
|
|
+ raw_shell = self.opts.get("raw_shell", False)
|
|
|
|
- if self.ssh_pre_flight:
|
|
+ if self.ssh_pre_flight and not raw_shell:
|
|
if not self.opts.get("ssh_run_pre_flight", False) and self.check_thin_dir():
|
|
log.info(
|
|
"%s thin dir already exists. Not running ssh_pre_flight script",
|
|
@@ -1086,14 +1199,16 @@ class Single:
|
|
stdout, stderr, retcode = self.run_ssh_pre_flight()
|
|
if retcode != 0:
|
|
log.error(
|
|
- "Error running ssh_pre_flight script %s", self.ssh_pre_file
|
|
+ "Error running ssh_pre_flight script %s for host '%s'",
|
|
+ self.ssh_pre_file,
|
|
+ self.target["host"],
|
|
)
|
|
return stdout, stderr, retcode
|
|
log.info(
|
|
"Successfully ran the ssh_pre_flight script: %s", self.ssh_pre_file
|
|
)
|
|
|
|
- if self.opts.get("raw_shell", False):
|
|
+ if raw_shell:
|
|
cmd_str = " ".join([self._escape_arg(arg) for arg in self.argv])
|
|
stdout, stderr, retcode = self.shell.exec_cmd(cmd_str)
|
|
|
|
diff --git a/salt/client/ssh/client.py b/salt/client/ssh/client.py
|
|
index be9247cb15..0b67598fc6 100644
|
|
--- a/salt/client/ssh/client.py
|
|
+++ b/salt/client/ssh/client.py
|
|
@@ -108,7 +108,7 @@ class SSHClient:
|
|
return sane_kwargs
|
|
|
|
def _prep_ssh(
|
|
- self, tgt, fun, arg=(), timeout=None, tgt_type="glob", kwarg=None, **kwargs
|
|
+ self, tgt, fun, arg=(), timeout=None, tgt_type="glob", kwarg=None, context=None, **kwargs
|
|
):
|
|
"""
|
|
Prepare the arguments
|
|
@@ -123,7 +123,7 @@ class SSHClient:
|
|
opts["selected_target_option"] = tgt_type
|
|
opts["tgt"] = tgt
|
|
opts["arg"] = arg
|
|
- return salt.client.ssh.SSH(opts)
|
|
+ return salt.client.ssh.SSH(opts, context=context)
|
|
|
|
def cmd_iter(
|
|
self,
|
|
@@ -160,7 +160,7 @@ class SSHClient:
|
|
final.update(ret)
|
|
return final
|
|
|
|
- def cmd_sync(self, low):
|
|
+ def cmd_sync(self, low, context=None):
|
|
"""
|
|
Execute a salt-ssh call synchronously.
|
|
|
|
@@ -193,6 +193,7 @@ class SSHClient:
|
|
low.get("timeout"),
|
|
low.get("tgt_type"),
|
|
low.get("kwarg"),
|
|
+ context=context,
|
|
**kwargs
|
|
)
|
|
|
|
diff --git a/salt/client/ssh/shell.py b/salt/client/ssh/shell.py
|
|
index cfa82d13c2..bc1ad034df 100644
|
|
--- a/salt/client/ssh/shell.py
|
|
+++ b/salt/client/ssh/shell.py
|
|
@@ -464,6 +464,14 @@ class Shell:
|
|
if stdout:
|
|
old_stdout = stdout
|
|
time.sleep(0.01)
|
|
+ if term.exitstatus is None:
|
|
+ try:
|
|
+ term.wait()
|
|
+ except: # pylint: disable=broad-except
|
|
+ # It's safe to put the broad exception handling here
|
|
+ # as we just need to ensure the child process in term finished
|
|
+ # to get proper term.exitstatus instead of None
|
|
+ pass
|
|
return ret_stdout, ret_stderr, term.exitstatus
|
|
finally:
|
|
term.close(terminate=True, kill=True)
|
|
diff --git a/salt/client/ssh/ssh_py_shim.py b/salt/client/ssh/ssh_py_shim.py
|
|
index b77749f495..293ea1b7fa 100644
|
|
--- a/salt/client/ssh/ssh_py_shim.py
|
|
+++ b/salt/client/ssh/ssh_py_shim.py
|
|
@@ -279,56 +279,72 @@ def main(argv): # pylint: disable=W0613
|
|
"""
|
|
Main program body
|
|
"""
|
|
- thin_path = os.path.join(OPTIONS.saltdir, THIN_ARCHIVE)
|
|
- if os.path.isfile(thin_path):
|
|
- if OPTIONS.checksum != get_hash(thin_path, OPTIONS.hashfunc):
|
|
- need_deployment()
|
|
- unpack_thin(thin_path)
|
|
- # Salt thin now is available to use
|
|
- else:
|
|
- if not sys.platform.startswith("win"):
|
|
- scpstat = subprocess.Popen(["/bin/sh", "-c", "command -v scp"]).wait()
|
|
- if scpstat != 0:
|
|
- sys.exit(EX_SCP_NOT_FOUND)
|
|
-
|
|
- if os.path.exists(OPTIONS.saltdir) and not os.path.isdir(OPTIONS.saltdir):
|
|
- sys.stderr.write(
|
|
- 'ERROR: salt path "{0}" exists but is not a directory\n'.format(
|
|
- OPTIONS.saltdir
|
|
+
|
|
+ virt_env = os.getenv("VIRTUAL_ENV", None)
|
|
+ # VIRTUAL_ENV environment variable is defined by venv-salt-minion wrapper
|
|
+ # it's used to check if the shim is running under this wrapper
|
|
+ venv_salt_call = None
|
|
+ if virt_env and "venv-salt-minion" in virt_env:
|
|
+ venv_salt_call = os.path.join(virt_env, "bin", "salt-call")
|
|
+ if not os.path.exists(venv_salt_call):
|
|
+ venv_salt_call = None
|
|
+ elif not os.path.exists(OPTIONS.saltdir):
|
|
+ os.makedirs(OPTIONS.saltdir)
|
|
+ cache_dir = os.path.join(OPTIONS.saltdir, "running_data", "var", "cache")
|
|
+ os.makedirs(os.path.join(cache_dir, "salt"))
|
|
+ os.symlink("salt", os.path.relpath(os.path.join(cache_dir, "venv-salt-minion")))
|
|
+
|
|
+ if venv_salt_call is None:
|
|
+ # Use Salt thin only if Salt Bundle (venv-salt-minion) is not available
|
|
+ thin_path = os.path.join(OPTIONS.saltdir, THIN_ARCHIVE)
|
|
+ if os.path.isfile(thin_path):
|
|
+ if OPTIONS.checksum != get_hash(thin_path, OPTIONS.hashfunc):
|
|
+ need_deployment()
|
|
+ unpack_thin(thin_path)
|
|
+ # Salt thin now is available to use
|
|
+ else:
|
|
+ if not sys.platform.startswith("win"):
|
|
+ scpstat = subprocess.Popen(["/bin/sh", "-c", "command -v scp"]).wait()
|
|
+ if scpstat != 0:
|
|
+ sys.exit(EX_SCP_NOT_FOUND)
|
|
+
|
|
+ if os.path.exists(OPTIONS.saltdir) and not os.path.isdir(OPTIONS.saltdir):
|
|
+ sys.stderr.write(
|
|
+ 'ERROR: salt path "{0}" exists but is'
|
|
+ " not a directory\n".format(OPTIONS.saltdir)
|
|
)
|
|
- )
|
|
- sys.exit(EX_CANTCREAT)
|
|
+ sys.exit(EX_CANTCREAT)
|
|
|
|
- if not os.path.exists(OPTIONS.saltdir):
|
|
- need_deployment()
|
|
+ if not os.path.exists(OPTIONS.saltdir):
|
|
+ need_deployment()
|
|
|
|
- code_checksum_path = os.path.normpath(
|
|
- os.path.join(OPTIONS.saltdir, "code-checksum")
|
|
- )
|
|
- if not os.path.exists(code_checksum_path) or not os.path.isfile(
|
|
- code_checksum_path
|
|
- ):
|
|
- sys.stderr.write(
|
|
- "WARNING: Unable to locate current code checksum: {0}.\n".format(
|
|
- code_checksum_path
|
|
- )
|
|
+ code_checksum_path = os.path.normpath(
|
|
+ os.path.join(OPTIONS.saltdir, "code-checksum")
|
|
)
|
|
- need_deployment()
|
|
- with open(code_checksum_path, "r") as vpo:
|
|
- cur_code_cs = vpo.readline().strip()
|
|
- if cur_code_cs != OPTIONS.code_checksum:
|
|
- sys.stderr.write(
|
|
- "WARNING: current code checksum {0} is different to {1}.\n".format(
|
|
- cur_code_cs, OPTIONS.code_checksum
|
|
+ if not os.path.exists(code_checksum_path) or not os.path.isfile(
|
|
+ code_checksum_path
|
|
+ ):
|
|
+ sys.stderr.write(
|
|
+ "WARNING: Unable to locate current code checksum: {0}.\n".format(
|
|
+ code_checksum_path
|
|
+ )
|
|
)
|
|
- )
|
|
- need_deployment()
|
|
- # Salt thin exists and is up-to-date - fall through and use it
|
|
+ need_deployment()
|
|
+ with open(code_checksum_path, "r") as vpo:
|
|
+ cur_code_cs = vpo.readline().strip()
|
|
+ if cur_code_cs != OPTIONS.code_checksum:
|
|
+ sys.stderr.write(
|
|
+ "WARNING: current code checksum {0} is different to {1}.\n".format(
|
|
+ cur_code_cs, OPTIONS.code_checksum
|
|
+ )
|
|
+ )
|
|
+ need_deployment()
|
|
+ # Salt thin exists and is up-to-date - fall through and use it
|
|
|
|
- salt_call_path = os.path.join(OPTIONS.saltdir, "salt-call")
|
|
- if not os.path.isfile(salt_call_path):
|
|
- sys.stderr.write('ERROR: thin is missing "{0}"\n'.format(salt_call_path))
|
|
- need_deployment()
|
|
+ salt_call_path = os.path.join(OPTIONS.saltdir, "salt-call")
|
|
+ if not os.path.isfile(salt_call_path):
|
|
+ sys.stderr.write('ERROR: thin is missing "{0}"\n'.format(salt_call_path))
|
|
+ need_deployment()
|
|
|
|
with open(os.path.join(OPTIONS.saltdir, "minion"), "w") as config:
|
|
config.write(OPTIONS.config + "\n")
|
|
@@ -351,8 +367,8 @@ def main(argv): # pylint: disable=W0613
|
|
argv_prepared = ARGS
|
|
|
|
salt_argv = [
|
|
- get_executable(),
|
|
- salt_call_path,
|
|
+ sys.executable if venv_salt_call is not None else get_executable(),
|
|
+ venv_salt_call if venv_salt_call is not None else salt_call_path,
|
|
"--retcode-passthrough",
|
|
"--local",
|
|
"--metadata",
|
|
diff --git a/salt/loader/__init__.py b/salt/loader/__init__.py
|
|
index 72a5e54401..32f8a7702c 100644
|
|
--- a/salt/loader/__init__.py
|
|
+++ b/salt/loader/__init__.py
|
|
@@ -9,6 +9,7 @@ import inspect
|
|
import logging
|
|
import os
|
|
import re
|
|
+import threading
|
|
import time
|
|
import types
|
|
|
|
@@ -31,7 +32,7 @@ from salt.exceptions import LoaderError
|
|
from salt.template import check_render_pipe_str
|
|
from salt.utils import entrypoints
|
|
|
|
-from .lazy import SALT_BASE_PATH, FilterDictWrapper, LazyLoader
|
|
+from .lazy import SALT_BASE_PATH, FilterDictWrapper, LazyLoader as _LazyLoader
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
@@ -81,6 +82,18 @@ SALT_INTERNAL_LOADERS_PATHS = (
|
|
str(SALT_BASE_PATH / "wheel"),
|
|
)
|
|
|
|
+LOAD_LOCK = threading.Lock()
|
|
+
|
|
+
|
|
+def LazyLoader(*args, **kwargs):
|
|
+ # This wrapper is used to prevent deadlocks with importlib (bsc#1182851)
|
|
+ # LOAD_LOCK is also used directly in salt.client.ssh.SSH
|
|
+ try:
|
|
+ LOAD_LOCK.acquire()
|
|
+ return _LazyLoader(*args, **kwargs)
|
|
+ finally:
|
|
+ LOAD_LOCK.release()
|
|
+
|
|
|
|
def static_loader(
|
|
opts,
|
|
@@ -725,7 +738,7 @@ def fileserver(opts, backends, loaded_base_name=None):
|
|
)
|
|
|
|
|
|
-def roster(opts, runner=None, utils=None, whitelist=None, loaded_base_name=None):
|
|
+def roster(opts, runner=None, utils=None, whitelist=None, loaded_base_name=None, context=None):
|
|
"""
|
|
Returns the roster modules
|
|
|
|
@@ -736,12 +749,15 @@ def roster(opts, runner=None, utils=None, whitelist=None, loaded_base_name=None)
|
|
:param str loaded_base_name: The imported modules namespace when imported
|
|
by the salt loader.
|
|
"""
|
|
+ if context is None:
|
|
+ context = {}
|
|
+
|
|
return LazyLoader(
|
|
_module_dirs(opts, "roster"),
|
|
opts,
|
|
tag="roster",
|
|
whitelist=whitelist,
|
|
- pack={"__runner__": runner, "__utils__": utils},
|
|
+ pack={"__runner__": runner, "__utils__": utils, "__context__": context},
|
|
extra_module_dirs=utils.module_dirs if utils else None,
|
|
loaded_base_name=loaded_base_name,
|
|
)
|
|
@@ -933,7 +949,14 @@ def render(
|
|
)
|
|
rend = FilterDictWrapper(ret, ".render")
|
|
|
|
- if not check_render_pipe_str(
|
|
+ def _check_render_pipe_str(pipestr, renderers, blacklist, whitelist):
|
|
+ try:
|
|
+ LOAD_LOCK.acquire()
|
|
+ return check_render_pipe_str(pipestr, renderers, blacklist, whitelist)
|
|
+ finally:
|
|
+ LOAD_LOCK.release()
|
|
+
|
|
+ if not _check_render_pipe_str(
|
|
opts["renderer"], rend, opts["renderer_blacklist"], opts["renderer_whitelist"]
|
|
):
|
|
err = (
|
|
diff --git a/salt/netapi/__init__.py b/salt/netapi/__init__.py
|
|
index a89c1a19af..8a28c48460 100644
|
|
--- a/salt/netapi/__init__.py
|
|
+++ b/salt/netapi/__init__.py
|
|
@@ -79,6 +79,7 @@ class NetapiClient:
|
|
self.loadauth = salt.auth.LoadAuth(apiopts)
|
|
self.key = salt.daemons.masterapi.access_keys(apiopts)
|
|
self.ckminions = salt.utils.minions.CkMinions(apiopts)
|
|
+ self.context = {}
|
|
|
|
def _is_master_running(self):
|
|
"""
|
|
@@ -245,7 +246,7 @@ class NetapiClient:
|
|
with salt.client.ssh.client.SSHClient(
|
|
mopts=self.opts, disable_custom_roster=True
|
|
) as client:
|
|
- return client.cmd_sync(kwargs)
|
|
+ return client.cmd_sync(kwargs, context=self.context)
|
|
|
|
def runner(self, fun, timeout=None, full_return=False, **kwargs):
|
|
"""
|
|
diff --git a/salt/roster/__init__.py b/salt/roster/__init__.py
|
|
index fc7339d785..ea23d550d7 100644
|
|
--- a/salt/roster/__init__.py
|
|
+++ b/salt/roster/__init__.py
|
|
@@ -59,7 +59,7 @@ class Roster:
|
|
minion aware
|
|
"""
|
|
|
|
- def __init__(self, opts, backends="flat"):
|
|
+ def __init__(self, opts, backends="flat", context=None):
|
|
self.opts = opts
|
|
if isinstance(backends, list):
|
|
self.backends = backends
|
|
@@ -71,7 +71,9 @@ class Roster:
|
|
self.backends = ["flat"]
|
|
utils = salt.loader.utils(self.opts)
|
|
runner = salt.loader.runner(self.opts, utils=utils)
|
|
- self.rosters = salt.loader.roster(self.opts, runner=runner, utils=utils)
|
|
+ self.rosters = salt.loader.roster(
|
|
+ self.opts, runner=runner, utils=utils, context=context
|
|
+ )
|
|
|
|
def _gen_back(self):
|
|
"""
|
|
diff --git a/tests/unit/test_loader.py b/tests/unit/test_loader.py
|
|
index cf33903320..1b616375b3 100644
|
|
--- a/tests/unit/test_loader.py
|
|
+++ b/tests/unit/test_loader.py
|
|
@@ -1697,7 +1697,7 @@ class LazyLoaderRefreshFileMappingTest(TestCase):
|
|
cls.funcs = salt.loader.minion_mods(cls.opts, utils=cls.utils, proxy=cls.proxy)
|
|
|
|
def setUp(self):
|
|
- class LazyLoaderMock(salt.loader.LazyLoader):
|
|
+ class LazyLoaderMock(salt.loader._LazyLoader):
|
|
pass
|
|
|
|
self.LOADER_CLASS = LazyLoaderMock
|
|
--
|
|
2.39.2
|
|
|
|
|