File fixed-gitfs-cachedir_basename-to-avoid-hash-collisio.patch of Package salt

From 7051f86bb48dbd618a7422d469f3aae4c6f18008 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
 <psuarezhernandez@suse.com>
Date: Thu, 31 Aug 2023 10:41:53 +0100
Subject: [PATCH] Fixed gitfs cachedir_basename to avoid hash collisions
 (#599)

(bsc#1193948, bsc#1214797, CVE-2023-20898)

Fix gitfs tests

It's `gitfs` not `gtfs`, plus some code fixes and cleanup

Signed-off-by: Pedro Algarvio <palgarvio@vmware.com>

fix doc

wrap sha in base64

clean up cache name

stop branch collision

run pre

Co-authored-by: cmcmarrow <charles.mcmarrow.4@gmail.com>
---
 changelog/cve-2023-20898.security.md   |   1 +
 salt/utils/gitfs.py                    |  83 ++++++-
 tests/pytests/unit/utils/test_gitfs.py | 255 +++++++++++++++++++++
 tests/unit/utils/test_gitfs.py         | 305 ++++++-------------------
 4 files changed, 403 insertions(+), 241 deletions(-)
 create mode 100644 changelog/cve-2023-20898.security.md
 create mode 100644 tests/pytests/unit/utils/test_gitfs.py

diff --git a/changelog/cve-2023-20898.security.md b/changelog/cve-2023-20898.security.md
new file mode 100644
index 0000000000..44f1729192
--- /dev/null
+++ b/changelog/cve-2023-20898.security.md
@@ -0,0 +1 @@
+Fixed gitfs cachedir_basename to avoid hash collisions. Added MP Lock to gitfs. These changes should stop race conditions.
diff --git a/salt/utils/gitfs.py b/salt/utils/gitfs.py
index 38e84f38aa..af61aa0dda 100644
--- a/salt/utils/gitfs.py
+++ b/salt/utils/gitfs.py
@@ -3,6 +3,7 @@ Classes which provide the shared base for GitFS, git_pillar, and winrepo
 """
 
 
+import base64
 import contextlib
 import copy
 import errno
@@ -11,10 +12,12 @@ import glob
 import hashlib
 import io
 import logging
+import multiprocessing
 import os
 import shlex
 import shutil
 import stat
+import string
 import subprocess
 import time
 import weakref
@@ -22,6 +25,7 @@ from datetime import datetime
 
 import salt.ext.tornado.ioloop
 import salt.fileserver
+import salt.syspaths
 import salt.utils.configparser
 import salt.utils.data
 import salt.utils.files
@@ -34,7 +38,6 @@ import salt.utils.stringutils
 import salt.utils.url
 import salt.utils.user
 import salt.utils.versions
-import salt.syspaths
 from salt.config import DEFAULT_MASTER_OPTS as _DEFAULT_MASTER_OPTS
 from salt.exceptions import FileserverConfigError, GitLockError, get_error_message
 from salt.utils.event import tagify
@@ -226,6 +229,10 @@ class GitProvider:
     invoking the parent class' __init__.
     """
 
+    # master lock should only be locked for very short periods of times "seconds"
+    # the master lock should be used when ever git provider reads or writes to one if it locks
+    _master_lock = multiprocessing.Lock()
+
     def __init__(
         self,
         opts,
@@ -452,13 +459,44 @@ class GitProvider:
             failhard(self.role)
 
         hash_type = getattr(hashlib, self.opts.get("hash_type", "md5"))
+        # Generate full id.
+        # Full id helps decrease the chances of collections in the gitfs cache.
+        try:
+            target = str(self.get_checkout_target())
+        except AttributeError:
+            target = ""
+        self._full_id = "-".join(
+            [
+                getattr(self, "name", ""),
+                self.id,
+                getattr(self, "env", ""),
+                getattr(self, "_root", ""),
+                self.role,
+                getattr(self, "base", ""),
+                getattr(self, "branch", ""),
+                target,
+            ]
+        )
         # We loaded this data from yaml configuration files, so, its safe
         # to use UTF-8
-        self.hash = hash_type(self.id.encode("utf-8")).hexdigest()
-        self.cachedir_basename = getattr(self, "name", self.hash)
+        base64_hash = str(
+            base64.b64encode(hash_type(self._full_id.encode("utf-8")).digest()),
+            encoding="ascii",  # base64 only outputs ascii
+        ).replace(
+            "/", "_"
+        )  # replace "/" with "_" to not cause trouble with file system
+
+        # limit name length to 19, so we don't eat up all the path length for windows
+        # this is due to pygit2 limitations
+        # replace any unknown char with "_" to not cause trouble with file system
+        name_chars = string.ascii_letters + string.digits + "-"
+        cache_name = "".join(
+            c if c in name_chars else "_" for c in getattr(self, "name", "")[:19]
+        )
+
+        self.cachedir_basename = f"{cache_name}-{base64_hash}"
         self.cachedir = salt.utils.path.join(cache_root, self.cachedir_basename)
         self.linkdir = salt.utils.path.join(cache_root, "links", self.cachedir_basename)
-
         if not os.path.isdir(self.cachedir):
             os.makedirs(self.cachedir)
 
@@ -473,6 +511,12 @@ class GitProvider:
             log.critical(msg, exc_info=True)
             failhard(self.role)
 
+    def full_id(self):
+        return self._full_id
+
+    def get_cachedir_basename(self):
+        return self.cachedir_basename
+
     def _get_envs_from_ref_paths(self, refs):
         """
         Return the names of remote refs (stripped of the remote name) and tags
@@ -663,6 +707,19 @@ class GitProvider:
         """
         Clear update.lk
         """
+        if self.__class__._master_lock.acquire(timeout=60) is False:
+            # if gitfs works right we should never see this timeout error.
+            log.error("gitfs master lock timeout!")
+            raise TimeoutError("gitfs master lock timeout!")
+        try:
+            return self._clear_lock(lock_type)
+        finally:
+            self.__class__._master_lock.release()
+
+    def _clear_lock(self, lock_type="update"):
+        """
+        Clear update.lk without MultiProcessing locks
+        """
         lock_file = self._get_lock_file(lock_type=lock_type)
 
         def _add_error(errlist, exc):
@@ -838,6 +895,20 @@ class GitProvider:
         """
         Place a lock file if (and only if) it does not already exist.
         """
+        if self.__class__._master_lock.acquire(timeout=60) is False:
+            # if gitfs works right we should never see this timeout error.
+            log.error("gitfs master lock timeout!")
+            raise TimeoutError("gitfs master lock timeout!")
+        try:
+            return self.__lock(lock_type, failhard)
+        finally:
+            self.__class__._master_lock.release()
+
+    def __lock(self, lock_type="update", failhard=False):
+        """
+        Place a lock file if (and only if) it does not already exist.
+        Without MultiProcessing locks.
+        """
         try:
             fh_ = os.open(
                 self._get_lock_file(lock_type), os.O_CREAT | os.O_EXCL | os.O_WRONLY
@@ -904,9 +975,9 @@ class GitProvider:
                             lock_type,
                             lock_file,
                         )
-                    success, fail = self.clear_lock()
+                    success, fail = self._clear_lock()
                     if success:
-                        return self._lock(lock_type="update", failhard=failhard)
+                        return self.__lock(lock_type="update", failhard=failhard)
                     elif failhard:
                         raise
                     return
diff --git a/tests/pytests/unit/utils/test_gitfs.py b/tests/pytests/unit/utils/test_gitfs.py
new file mode 100644
index 0000000000..e9915de412
--- /dev/null
+++ b/tests/pytests/unit/utils/test_gitfs.py
@@ -0,0 +1,255 @@
+import os
+import string
+import time
+
+import pytest
+
+import salt.fileserver.gitfs
+import salt.utils.gitfs
+from salt.exceptions import FileserverConfigError
+from tests.support.helpers import patched_environ
+from tests.support.mock import MagicMock, patch
+
+try:
+    HAS_PYGIT2 = (
+        salt.utils.gitfs.PYGIT2_VERSION
+        and salt.utils.gitfs.PYGIT2_VERSION >= salt.utils.gitfs.PYGIT2_MINVER
+        and salt.utils.gitfs.LIBGIT2_VERSION
+        and salt.utils.gitfs.LIBGIT2_VERSION >= salt.utils.gitfs.LIBGIT2_MINVER
+    )
+except AttributeError:
+    HAS_PYGIT2 = False
+
+
+if HAS_PYGIT2:
+    import pygit2
+
+
+@pytest.mark.parametrize(
+    "role_name,role_class",
+    (
+        ("gitfs", salt.utils.gitfs.GitFS),
+        ("git_pillar", salt.utils.gitfs.GitPillar),
+        ("winrepo", salt.utils.gitfs.WinRepo),
+    ),
+)
+def test_provider_case_insensitive_gitfs_provider(minion_opts, role_name, role_class):
+    """
+    Ensure that both lowercase and non-lowercase values are supported
+    """
+    provider = "GitPython"
+    key = "{}_provider".format(role_name)
+    with patch.object(role_class, "verify_gitpython", MagicMock(return_value=True)):
+        with patch.object(role_class, "verify_pygit2", MagicMock(return_value=False)):
+            args = [minion_opts, {}]
+            kwargs = {"init_remotes": False}
+            if role_name == "winrepo":
+                kwargs["cache_root"] = "/tmp/winrepo-dir"
+            with patch.dict(minion_opts, {key: provider}):
+                # Try to create an instance with uppercase letters in
+                # provider name. If it fails then a
+                # FileserverConfigError will be raised, so no assert is
+                # necessary.
+                role_class(*args, **kwargs)
+            # Now try to instantiate an instance with all lowercase
+            # letters. Again, no need for an assert here.
+            role_class(*args, **kwargs)
+
+
+@pytest.mark.parametrize(
+    "role_name,role_class",
+    (
+        ("gitfs", salt.utils.gitfs.GitFS),
+        ("git_pillar", salt.utils.gitfs.GitPillar),
+        ("winrepo", salt.utils.gitfs.WinRepo),
+    ),
+)
+def test_valid_provider_gitfs_provider(minion_opts, role_name, role_class):
+    """
+    Ensure that an invalid provider is not accepted, raising a
+    FileserverConfigError.
+    """
+
+    def _get_mock(verify, provider):
+        """
+        Return a MagicMock with the desired return value
+        """
+        return MagicMock(return_value=verify.endswith(provider))
+
+    key = "{}_provider".format(role_name)
+    for provider in salt.utils.gitfs.GIT_PROVIDERS:
+        verify = "verify_gitpython"
+        mock1 = _get_mock(verify, provider)
+        with patch.object(role_class, verify, mock1):
+            verify = "verify_pygit2"
+            mock2 = _get_mock(verify, provider)
+            with patch.object(role_class, verify, mock2):
+                args = [minion_opts, {}]
+                kwargs = {"init_remotes": False}
+                if role_name == "winrepo":
+                    kwargs["cache_root"] = "/tmp/winrepo-dir"
+                with patch.dict(minion_opts, {key: provider}):
+                    role_class(*args, **kwargs)
+                with patch.dict(minion_opts, {key: "foo"}):
+                    # Set the provider name to a known invalid provider
+                    # and make sure it raises an exception.
+                    with pytest.raises(FileserverConfigError):
+                        role_class(*args, **kwargs)
+
+
+@pytest.fixture
+def _prepare_remote_repository_pygit2(tmp_path):
+    remote = os.path.join(tmp_path, "pygit2-repo")
+    filecontent = "This is an empty README file"
+    filename = "README"
+    signature = pygit2.Signature(
+        "Dummy Commiter", "dummy@dummy.com", int(time.time()), 0
+    )
+    repository = pygit2.init_repository(remote, False)
+    builder = repository.TreeBuilder()
+    tree = builder.write()
+    commit = repository.create_commit(
+        "HEAD", signature, signature, "Create master branch", tree, []
+    )
+    repository.create_reference("refs/tags/simple_tag", commit)
+    with salt.utils.files.fopen(
+        os.path.join(repository.workdir, filename), "w"
+    ) as file:
+        file.write(filecontent)
+    blob = repository.create_blob_fromworkdir(filename)
+    builder = repository.TreeBuilder()
+    builder.insert(filename, blob, pygit2.GIT_FILEMODE_BLOB)
+    tree = builder.write()
+    repository.index.read()
+    repository.index.add(filename)
+    repository.index.write()
+    commit = repository.create_commit(
+        "HEAD",
+        signature,
+        signature,
+        "Added a README",
+        tree,
+        [repository.head.target],
+    )
+    repository.create_tag(
+        "annotated_tag", commit, pygit2.GIT_OBJ_COMMIT, signature, "some message"
+    )
+    return remote
+
+
+@pytest.fixture
+def _prepare_provider(tmp_path, minion_opts, _prepare_remote_repository_pygit2):
+    cache = tmp_path / "pygit2-repo-cache"
+    minion_opts.update(
+        {
+            "cachedir": str(cache),
+            "gitfs_disable_saltenv_mapping": False,
+            "gitfs_base": "master",
+            "gitfs_insecure_auth": False,
+            "gitfs_mountpoint": "",
+            "gitfs_passphrase": "",
+            "gitfs_password": "",
+            "gitfs_privkey": "",
+            "gitfs_provider": "pygit2",
+            "gitfs_pubkey": "",
+            "gitfs_ref_types": ["branch", "tag", "sha"],
+            "gitfs_refspecs": [
+                "+refs/heads/*:refs/remotes/origin/*",
+                "+refs/tags/*:refs/tags/*",
+            ],
+            "gitfs_root": "",
+            "gitfs_saltenv_blacklist": [],
+            "gitfs_saltenv_whitelist": [],
+            "gitfs_ssl_verify": True,
+            "gitfs_update_interval": 3,
+            "gitfs_user": "",
+            "verified_gitfs_provider": "pygit2",
+        }
+    )
+    per_remote_defaults = {
+        "base": "master",
+        "disable_saltenv_mapping": False,
+        "insecure_auth": False,
+        "ref_types": ["branch", "tag", "sha"],
+        "passphrase": "",
+        "mountpoint": "",
+        "password": "",
+        "privkey": "",
+        "pubkey": "",
+        "refspecs": [
+            "+refs/heads/*:refs/remotes/origin/*",
+            "+refs/tags/*:refs/tags/*",
+        ],
+        "root": "",
+        "saltenv_blacklist": [],
+        "saltenv_whitelist": [],
+        "ssl_verify": True,
+        "update_interval": 60,
+        "user": "",
+    }
+    per_remote_only = ("all_saltenvs", "name", "saltenv")
+    override_params = tuple(per_remote_defaults)
+    cache_root = cache / "gitfs"
+    role = "gitfs"
+    provider = salt.utils.gitfs.Pygit2(
+        minion_opts,
+        _prepare_remote_repository_pygit2,
+        per_remote_defaults,
+        per_remote_only,
+        override_params,
+        str(cache_root),
+        role,
+    )
+    return provider
+
+
+@pytest.mark.skipif(not HAS_PYGIT2, reason="This host lacks proper pygit2 support")
+@pytest.mark.skip_on_windows(
+    reason="Skip Pygit2 on windows, due to pygit2 access error on windows"
+)
+def test_checkout_pygit2(_prepare_provider):
+    provider = _prepare_provider
+    provider.remotecallbacks = None
+    provider.credentials = None
+    provider.init_remote()
+    provider.fetch()
+    provider.branch = "master"
+    assert provider.cachedir in provider.checkout()
+    provider.branch = "simple_tag"
+    assert provider.cachedir in provider.checkout()
+    provider.branch = "annotated_tag"
+    assert provider.cachedir in provider.checkout()
+    provider.branch = "does_not_exist"
+    assert provider.checkout() is None
+
+
+@pytest.mark.skipif(not HAS_PYGIT2, reason="This host lacks proper pygit2 support")
+@pytest.mark.skip_on_windows(
+    reason="Skip Pygit2 on windows, due to pygit2 access error on windows"
+)
+def test_checkout_pygit2_with_home_env_unset(_prepare_provider):
+    provider = _prepare_provider
+    provider.remotecallbacks = None
+    provider.credentials = None
+    with patched_environ(__cleanup__=["HOME"]):
+        assert "HOME" not in os.environ
+        provider.init_remote()
+        provider.fetch()
+        assert "HOME" in os.environ
+
+
+def test_full_id_pygit2(_prepare_provider):
+    assert _prepare_provider.full_id().startswith("-")
+    assert _prepare_provider.full_id().endswith("/pygit2-repo---gitfs-master--")
+
+
+@pytest.mark.skipif(not HAS_PYGIT2, reason="This host lacks proper pygit2 support")
+@pytest.mark.skip_on_windows(
+    reason="Skip Pygit2 on windows, due to pygit2 access error on windows"
+)
+def test_get_cachedir_basename_pygit2(_prepare_provider):
+    basename = _prepare_provider.get_cachedir_basename()
+    assert len(basename) == 45
+    assert basename[0] == "-"
+    # check that a valid base64 is given '/' -> '_'
+    assert all(c in string.ascii_letters + string.digits + "+_=" for c in basename[1:])
diff --git a/tests/unit/utils/test_gitfs.py b/tests/unit/utils/test_gitfs.py
index 7c400b69af..6d8e97a239 100644
--- a/tests/unit/utils/test_gitfs.py
+++ b/tests/unit/utils/test_gitfs.py
@@ -2,37 +2,20 @@
 These only test the provider selection and verification logic, they do not init
 any remotes.
 """
-import os
-import shutil
-from time import time
+
+import tempfile
 
 import pytest
 
+import salt.ext.tornado.ioloop
 import salt.fileserver.gitfs
 import salt.utils.files
 import salt.utils.gitfs
+import salt.utils.path
 import salt.utils.platform
-import tests.support.paths
-from salt.exceptions import FileserverConfigError
-from tests.support.helpers import patched_environ
 from tests.support.mixins import AdaptedConfigurationTestCaseMixin
-from tests.support.mock import MagicMock, patch
 from tests.support.unit import TestCase
 
-try:
-    HAS_PYGIT2 = (
-        salt.utils.gitfs.PYGIT2_VERSION
-        and salt.utils.gitfs.PYGIT2_VERSION >= salt.utils.gitfs.PYGIT2_MINVER
-        and salt.utils.gitfs.LIBGIT2_VERSION
-        and salt.utils.gitfs.LIBGIT2_VERSION >= salt.utils.gitfs.LIBGIT2_MINVER
-    )
-except AttributeError:
-    HAS_PYGIT2 = False
-
-
-if HAS_PYGIT2:
-    import pygit2
-
 
 def _clear_instance_map():
     try:
@@ -45,6 +28,9 @@ def _clear_instance_map():
 
 class TestGitBase(TestCase, AdaptedConfigurationTestCaseMixin):
     def setUp(self):
+        self._tmp_dir = tempfile.TemporaryDirectory()
+        tmp_name = self._tmp_dir.name
+
         class MockedProvider(
             salt.utils.gitfs.GitProvider
         ):  # pylint: disable=abstract-method
@@ -71,6 +57,7 @@ class TestGitBase(TestCase, AdaptedConfigurationTestCaseMixin):
                 )
 
             def init_remote(self):
+                self.gitdir = salt.utils.path.join(tmp_name, ".git")
                 self.repo = True
                 new = False
                 return new
@@ -107,6 +94,7 @@ class TestGitBase(TestCase, AdaptedConfigurationTestCaseMixin):
         for remote in self.main_class.remotes:
             remote.fetched = False
         del self.main_class
+        self._tmp_dir.cleanup()
 
     def test_update_all(self):
         self.main_class.update()
@@ -126,226 +114,73 @@ class TestGitBase(TestCase, AdaptedConfigurationTestCaseMixin):
         self.assertTrue(self.main_class.remotes[0].fetched)
         self.assertFalse(self.main_class.remotes[1].fetched)
 
-
-class TestGitFSProvider(TestCase):
-    def setUp(self):
-        self.opts = {"cachedir": "/tmp/gitfs-test-cache"}
-
-    def tearDown(self):
-        self.opts = None
-
-    def test_provider_case_insensitive(self):
-        """
-        Ensure that both lowercase and non-lowercase values are supported
-        """
-        provider = "GitPython"
-        for role_name, role_class in (
-            ("gitfs", salt.utils.gitfs.GitFS),
-            ("git_pillar", salt.utils.gitfs.GitPillar),
-            ("winrepo", salt.utils.gitfs.WinRepo),
-        ):
-
-            key = "{}_provider".format(role_name)
-            with patch.object(
-                role_class, "verify_gitpython", MagicMock(return_value=True)
-            ):
-                with patch.object(
-                    role_class, "verify_pygit2", MagicMock(return_value=False)
-                ):
-                    args = [self.opts, {}]
-                    kwargs = {"init_remotes": False}
-                    if role_name == "winrepo":
-                        kwargs["cache_root"] = "/tmp/winrepo-dir"
-                    with patch.dict(self.opts, {key: provider}):
-                        # Try to create an instance with uppercase letters in
-                        # provider name. If it fails then a
-                        # FileserverConfigError will be raised, so no assert is
-                        # necessary.
-                        role_class(*args, **kwargs)
-                    # Now try to instantiate an instance with all lowercase
-                    # letters. Again, no need for an assert here.
-                    role_class(*args, **kwargs)
-
-    def test_valid_provider(self):
-        """
-        Ensure that an invalid provider is not accepted, raising a
-        FileserverConfigError.
-        """
-
-        def _get_mock(verify, provider):
-            """
-            Return a MagicMock with the desired return value
-            """
-            return MagicMock(return_value=verify.endswith(provider))
-
-        for role_name, role_class in (
-            ("gitfs", salt.utils.gitfs.GitFS),
-            ("git_pillar", salt.utils.gitfs.GitPillar),
-            ("winrepo", salt.utils.gitfs.WinRepo),
-        ):
-            key = "{}_provider".format(role_name)
-            for provider in salt.utils.gitfs.GIT_PROVIDERS:
-                verify = "verify_gitpython"
-                mock1 = _get_mock(verify, provider)
-                with patch.object(role_class, verify, mock1):
-                    verify = "verify_pygit2"
-                    mock2 = _get_mock(verify, provider)
-                    with patch.object(role_class, verify, mock2):
-                        args = [self.opts, {}]
-                        kwargs = {"init_remotes": False}
-                        if role_name == "winrepo":
-                            kwargs["cache_root"] = "/tmp/winrepo-dir"
-
-                        with patch.dict(self.opts, {key: provider}):
-                            role_class(*args, **kwargs)
-
-                        with patch.dict(self.opts, {key: "foo"}):
-                            # Set the provider name to a known invalid provider
-                            # and make sure it raises an exception.
-                            self.assertRaises(
-                                FileserverConfigError, role_class, *args, **kwargs
-                            )
-
-
-@pytest.mark.skipif(not HAS_PYGIT2, reason="This host lacks proper pygit2 support")
-@pytest.mark.skip_on_windows(
-    reason="Skip Pygit2 on windows, due to pygit2 access error on windows"
-)
-class TestPygit2(TestCase):
-    def _prepare_remote_repository(self, path):
-        shutil.rmtree(path, ignore_errors=True)
-
-        filecontent = "This is an empty README file"
-        filename = "README"
-
-        signature = pygit2.Signature(
-            "Dummy Commiter", "dummy@dummy.com", int(time()), 0
+    def test_full_id(self):
+        self.assertEqual(
+            self.main_class.remotes[0].full_id(), "-file://repo1.git---gitfs-master--"
         )
 
-        repository = pygit2.init_repository(path, False)
-        builder = repository.TreeBuilder()
-        tree = builder.write()
-        commit = repository.create_commit(
-            "HEAD", signature, signature, "Create master branch", tree, []
+    def test_full_id_with_name(self):
+        self.assertEqual(
+            self.main_class.remotes[1].full_id(),
+            "repo2-file://repo2.git---gitfs-master--",
         )
-        repository.create_reference("refs/tags/simple_tag", commit)
 
-        with salt.utils.files.fopen(
-            os.path.join(repository.workdir, filename), "w"
-        ) as file:
-            file.write(filecontent)
-
-        blob = repository.create_blob_fromworkdir(filename)
-        builder = repository.TreeBuilder()
-        builder.insert(filename, blob, pygit2.GIT_FILEMODE_BLOB)
-        tree = builder.write()
-
-        repository.index.read()
-        repository.index.add(filename)
-        repository.index.write()
-
-        commit = repository.create_commit(
-            "HEAD",
-            signature,
-            signature,
-            "Added a README",
-            tree,
-            [repository.head.target],
-        )
-        repository.create_tag(
-            "annotated_tag", commit, pygit2.GIT_OBJ_COMMIT, signature, "some message"
+    def test_get_cachedir_basename(self):
+        self.assertEqual(
+            self.main_class.remotes[0].get_cachedir_basename(),
+            "-jXhnbGDemchtZwTwaD2s6VOaVvs98a7w+AtiYlmOVb0=",
         )
 
-    def _prepare_cache_repository(self, remote, cache):
-        opts = {
-            "cachedir": cache,
-            "__role": "minion",
-            "gitfs_disable_saltenv_mapping": False,
-            "gitfs_base": "master",
-            "gitfs_insecure_auth": False,
-            "gitfs_mountpoint": "",
-            "gitfs_passphrase": "",
-            "gitfs_password": "",
-            "gitfs_privkey": "",
-            "gitfs_provider": "pygit2",
-            "gitfs_pubkey": "",
-            "gitfs_ref_types": ["branch", "tag", "sha"],
-            "gitfs_refspecs": [
-                "+refs/heads/*:refs/remotes/origin/*",
-                "+refs/tags/*:refs/tags/*",
-            ],
-            "gitfs_root": "",
-            "gitfs_saltenv_blacklist": [],
-            "gitfs_saltenv_whitelist": [],
-            "gitfs_ssl_verify": True,
-            "gitfs_update_interval": 3,
-            "gitfs_user": "",
-            "verified_gitfs_provider": "pygit2",
-        }
-        per_remote_defaults = {
-            "base": "master",
-            "disable_saltenv_mapping": False,
-            "insecure_auth": False,
-            "ref_types": ["branch", "tag", "sha"],
-            "passphrase": "",
-            "mountpoint": "",
-            "password": "",
-            "privkey": "",
-            "pubkey": "",
-            "refspecs": [
-                "+refs/heads/*:refs/remotes/origin/*",
-                "+refs/tags/*:refs/tags/*",
-            ],
-            "root": "",
-            "saltenv_blacklist": [],
-            "saltenv_whitelist": [],
-            "ssl_verify": True,
-            "update_interval": 60,
-            "user": "",
-        }
-        per_remote_only = ("all_saltenvs", "name", "saltenv")
-        override_params = tuple(per_remote_defaults.keys())
-        cache_root = os.path.join(cache, "gitfs")
-        role = "gitfs"
-        shutil.rmtree(cache_root, ignore_errors=True)
-        provider = salt.utils.gitfs.Pygit2(
-            opts,
-            remote,
-            per_remote_defaults,
-            per_remote_only,
-            override_params,
-            cache_root,
-            role,
+    def test_get_cachedir_base_with_name(self):
+        self.assertEqual(
+            self.main_class.remotes[1].get_cachedir_basename(),
+            "repo2-nuezpiDtjQRFC0ZJDByvi+F6Vb8ZhfoH41n_KFxTGsU=",
         )
-        return provider
 
-    def test_checkout(self):
-        remote = os.path.join(tests.support.paths.TMP, "pygit2-repo")
-        cache = os.path.join(tests.support.paths.TMP, "pygit2-repo-cache")
-        self._prepare_remote_repository(remote)
-        provider = self._prepare_cache_repository(remote, cache)
-        provider.remotecallbacks = None
-        provider.credentials = None
-        provider.init_remote()
-        provider.fetch()
-        provider.branch = "master"
-        self.assertIn(provider.cachedir, provider.checkout())
-        provider.branch = "simple_tag"
-        self.assertIn(provider.cachedir, provider.checkout())
-        provider.branch = "annotated_tag"
-        self.assertIn(provider.cachedir, provider.checkout())
-        provider.branch = "does_not_exist"
-        self.assertIsNone(provider.checkout())
+    def test_git_provider_mp_lock(self):
+        """
+        Check that lock is released after provider.lock()
+        """
+        provider = self.main_class.remotes[0]
+        provider.lock()
+        # check that lock has been released
+        self.assertTrue(provider._master_lock.acquire(timeout=5))
+        provider._master_lock.release()
 
-    def test_checkout_with_home_env_unset(self):
-        remote = os.path.join(tests.support.paths.TMP, "pygit2-repo")
-        cache = os.path.join(tests.support.paths.TMP, "pygit2-repo-cache")
-        self._prepare_remote_repository(remote)
-        provider = self._prepare_cache_repository(remote, cache)
-        provider.remotecallbacks = None
-        provider.credentials = None
-        with patched_environ(__cleanup__=["HOME"]):
-            self.assertTrue("HOME" not in os.environ)
-            provider.init_remote()
-            provider.fetch()
-            self.assertTrue("HOME" in os.environ)
+    def test_git_provider_mp_clear_lock(self):
+        """
+        Check that lock is released after provider.clear_lock()
+        """
+        provider = self.main_class.remotes[0]
+        provider.clear_lock()
+        # check that lock has been released
+        self.assertTrue(provider._master_lock.acquire(timeout=5))
+        provider._master_lock.release()
+
+    @pytest.mark.slow_test
+    def test_git_provider_mp_lock_timeout(self):
+        """
+        Check that lock will time out if master lock is locked.
+        """
+        provider = self.main_class.remotes[0]
+        # Hijack the lock so git provider is fooled into thinking another instance is doing somthing.
+        self.assertTrue(provider._master_lock.acquire(timeout=5))
+        try:
+            # git provider should raise timeout error to avoid lock race conditions
+            self.assertRaises(TimeoutError, provider.lock)
+        finally:
+            provider._master_lock.release()
+
+    @pytest.mark.slow_test
+    def test_git_provider_mp_clear_lock_timeout(self):
+        """
+        Check that clear lock will time out if master lock is locked.
+        """
+        provider = self.main_class.remotes[0]
+        # Hijack the lock so git provider is fooled into thinking another instance is doing somthing.
+        self.assertTrue(provider._master_lock.acquire(timeout=5))
+        try:
+            # git provider should raise timeout error to avoid lock race conditions
+            self.assertRaises(TimeoutError, provider.clear_lock)
+        finally:
+            provider._master_lock.release()
-- 
2.41.0


openSUSE Build Service is sponsored by