We have some news to share for the request index beta feature. We’ve added more options to sort your requests, counters to the individual filters and documentation for the search functionality. Checkout the blog post for more details.

File add-salt-ssh-support-with-venv-salt-minion-3004-493.patch of Package salt

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


openSUSE Build Service is sponsored by