Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package salt for openSUSE:Factory checked in 
at 2022-10-10 18:43:22
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/salt (Old)
 and      /work/SRC/openSUSE:Factory/.salt.new.2275 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "salt"

Mon Oct 10 18:43:22 2022 rev:131 rq:1008560 version:3004

Changes:
--------
--- /work/SRC/openSUSE:Factory/salt/salt.changes        2022-07-12 
11:12:22.719693673 +0200
+++ /work/SRC/openSUSE:Factory/.salt.new.2275/salt.changes      2022-10-10 
18:43:27.382725800 +0200
@@ -1,0 +2,40 @@
+Thu Oct  6 10:10:16 UTC 2022 - Pablo Su??rez Hern??ndez 
<[email protected]>
+
+- Make pass renderer configurable and fix detected issues
+- Workaround fopen line buffering for binary mode (bsc#1203834)
+- Handle non-UTF-8 bytes in core grains generation (bsc#1202165)
+- Fix Syndic authentication errors (bsc#1199562)
+
+- Added:
+  * make-pass-renderer-configurable-other-fixes-532.patch
+  * ignore-non-utf8-characters-while-reading-files-with-.patch
+  * fopen-workaround-bad-buffering-for-binary-mode-563.patch
+  * backport-syndic-auth-fixes.patch
+
+-------------------------------------------------------------------
+Thu Sep  1 12:43:39 UTC 2022 - Victor Zhestkov <[email protected]>
+
+- Add Amazon EC2 detection for virtual grains (bsc#1195624)
+- Fix the regression in schedule module releasded in 3004 (bsc#1202631)
+- Fix state.apply in test mode with file state module
+  on user/group checking (bsc#1202167)
+- Change the delimeters to prevent possible tracebacks
+  on some packages with dpkg_lowpkg
+- Make zypperpkg to retry if RPM lock is temporarily unavailable (bsc#1200596)
+
+- Added:
+  * fix-the-regression-in-schedule-module-releasded-in-3.patch
+  * retry-if-rpm-lock-is-temporarily-unavailable-547.patch
+  * change-the-delimeters-to-prevent-possible-tracebacks.patch
+  * add-amazon-ec2-detection-for-virtual-grains-bsc-1195.patch
+  * fix-state.apply-in-test-mode-with-file-state-module-.patch
+
+-------------------------------------------------------------------
+Tue Jul 12 12:37:51 UTC 2022 - Alexander Graul <[email protected]>
+
+- Fix test_ipc unit test
+
+- Added:
+  * fix-test_ipc-unit-tests.patch
+
+-------------------------------------------------------------------

New:
----
  add-amazon-ec2-detection-for-virtual-grains-bsc-1195.patch
  backport-syndic-auth-fixes.patch
  change-the-delimeters-to-prevent-possible-tracebacks.patch
  fix-state.apply-in-test-mode-with-file-state-module-.patch
  fix-test_ipc-unit-tests.patch
  fix-the-regression-in-schedule-module-releasded-in-3.patch
  fopen-workaround-bad-buffering-for-binary-mode-563.patch
  ignore-non-utf8-characters-while-reading-files-with-.patch
  make-pass-renderer-configurable-other-fixes-532.patch
  retry-if-rpm-lock-is-temporarily-unavailable-547.patch

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ salt.spec ++++++
--- /var/tmp/diff_new_pack.oKYii9/_old  2022-10-10 18:43:29.286729898 +0200
+++ /var/tmp/diff_new_pack.oKYii9/_new  2022-10-10 18:43:29.294729915 +0200
@@ -330,6 +330,26 @@
 Patch91:        fix-jinja2-contextfuntion-base-on-version-bsc-119874.patch
 # PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62209
 Patch92:        add-support-for-gpgautoimport-539.patch
+# PATCH-FIX_OPENSUSE: 
https://github.com/openSUSE/salt/commit/2b486d0484c51509e9972e581d97655f4f87852e
+Patch93:        fix-test_ipc-unit-tests.patch
+# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62204
+Patch94:        retry-if-rpm-lock-is-temporarily-unavailable-547.patch
+# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62519
+Patch95:        change-the-delimeters-to-prevent-possible-tracebacks.patch
+# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/61847
+Patch96:        fix-state.apply-in-test-mode-with-file-state-module-.patch
+# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/61423
+Patch97:        fix-the-regression-in-schedule-module-releasded-in-3.patch
+# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62539
+Patch98:        add-amazon-ec2-detection-for-virtual-grains-bsc-1195.patch
+# PATCH-FIX_UPSTREAM: 
https://github.com/saltstack/salt/commit/643bd4b572ca97466e085ecd1d84da45b1684332
+Patch99:        backport-syndic-auth-fixes.patch
+# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62633
+Patch100:       ignore-non-utf8-characters-while-reading-files-with-.patch
+# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62817
+Patch101:       fopen-workaround-bad-buffering-for-binary-mode-563.patch
+# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62120
+Patch102:       make-pass-renderer-configurable-other-fixes-532.patch
 
 BuildRoot:      %{_tmppath}/%{name}-%{version}-build
 BuildRequires:  logrotate

++++++ _lastrevision ++++++
--- /var/tmp/diff_new_pack.oKYii9/_old  2022-10-10 18:43:29.358730053 +0200
+++ /var/tmp/diff_new_pack.oKYii9/_new  2022-10-10 18:43:29.362730062 +0200
@@ -1,3 +1,3 @@
-e07459bfeea39239f6b446f40f6502e72dea488f
+e04acec89d982e3bd465742afffe6ae5ec82620b
 (No newline at EOF)
 

++++++ _service ++++++
--- /var/tmp/diff_new_pack.oKYii9/_old  2022-10-10 18:43:29.378730096 +0200
+++ /var/tmp/diff_new_pack.oKYii9/_new  2022-10-10 18:43:29.382730105 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/openSUSE/salt-packaging.git</param>
     <param name="subdir">salt</param>
     <param name="filename">package</param>
-    <param name="revision">3004</param>
+    <param name="revision">release/3004</param>
     <param name="scm">git</param>
   </service>
   <service name="extract_file" mode="disabled">

++++++ add-amazon-ec2-detection-for-virtual-grains-bsc-1195.patch ++++++
>From 77e90c4925a4268c5975cf1ce0bb0e4c457618c1 Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <[email protected]>
Date: Thu, 1 Sep 2022 14:46:24 +0300
Subject: [PATCH] Add Amazon EC2 detection for virtual grains
 (bsc#1195624)

* Add ignore_retcode to quiet run functions

* Implement Amazon EC2 detection for virtual grains

* Add test for virtual grain detection of Amazon EC2

* Also detect the product of Amazon EC2 instance

* Add changelog entry
---
 changelog/62539.added                  |   1 +
 salt/grains/core.py                    |  18 ++++
 salt/modules/cmdmod.py                 |   4 +
 tests/pytests/unit/grains/test_core.py | 117 +++++++++++++++++++++++++
 4 files changed, 140 insertions(+)
 create mode 100644 changelog/62539.added

diff --git a/changelog/62539.added b/changelog/62539.added
new file mode 100644
index 0000000000..5f402d61c2
--- /dev/null
+++ b/changelog/62539.added
@@ -0,0 +1 @@
+Implementation of Amazon EC2 instance detection and setting `virtual_subtype` 
grain accordingly including the product if possible to identify.
diff --git a/salt/grains/core.py b/salt/grains/core.py
index c5d996d1bb..9530a43fc5 100644
--- a/salt/grains/core.py
+++ b/salt/grains/core.py
@@ -1173,6 +1173,24 @@ def _virtual(osdata):
     if grains.get("virtual_subtype") and grains["virtual"] == "physical":
         grains["virtual"] = "virtual"
 
+    # Try to detect if the instance is running on Amazon EC2
+    if grains["virtual"] in ("qemu", "kvm", "xen"):
+        dmidecode = salt.utils.path.which("dmidecode")
+        if dmidecode:
+            ret = __salt__["cmd.run_all"](
+                [dmidecode, "-t", "system"], ignore_retcode=True
+            )
+            output = ret["stdout"]
+            if "Manufacturer: Amazon EC2" in output:
+                grains["virtual_subtype"] = "Amazon EC2"
+                product = re.match(
+                    r".*Product Name: ([^\r\n]*).*", output, flags=re.DOTALL
+                )
+                if product:
+                    grains["virtual_subtype"] = "Amazon EC2 
({})".format(product[1])
+            elif re.match(r".*Version: [^\r\n]+\.amazon.*", output, 
flags=re.DOTALL):
+                grains["virtual_subtype"] = "Amazon EC2"
+
     for command in failed_commands:
         log.info(
             "Although '%s' was found in path, the current user "
diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py
index 61b328b13b..cd42e2cda0 100644
--- a/salt/modules/cmdmod.py
+++ b/salt/modules/cmdmod.py
@@ -907,6 +907,7 @@ def _run_quiet(
     success_retcodes=None,
     success_stdout=None,
     success_stderr=None,
+    ignore_retcode=None,
 ):
     """
     Helper for running commands quietly for minion startup
@@ -933,6 +934,7 @@ def _run_quiet(
         success_retcodes=success_retcodes,
         success_stdout=success_stdout,
         success_stderr=success_stderr,
+        ignore_retcode=ignore_retcode,
     )["stdout"]
 
 
@@ -955,6 +957,7 @@ def _run_all_quiet(
     success_retcodes=None,
     success_stdout=None,
     success_stderr=None,
+    ignore_retcode=None,
 ):
 
     """
@@ -987,6 +990,7 @@ def _run_all_quiet(
         success_retcodes=success_retcodes,
         success_stdout=success_stdout,
         success_stderr=success_stderr,
+        ignore_retcode=ignore_retcode,
     )
 
 
diff --git a/tests/pytests/unit/grains/test_core.py 
b/tests/pytests/unit/grains/test_core.py
index bc3947fa1b..84dd97d62f 100644
--- a/tests/pytests/unit/grains/test_core.py
+++ b/tests/pytests/unit/grains/test_core.py
@@ -2720,3 +2720,120 @@ def test_get_server_id():
 
     with patch.dict(core.__opts__, {"id": "otherid"}):
         assert core.get_server_id() != expected
+
+
[email protected]_unless_on_linux
+def test_virtual_set_virtual_ec2():
+    osdata = {}
+
+    (
+        osdata["kernel"],
+        osdata["nodename"],
+        osdata["kernelrelease"],
+        osdata["kernelversion"],
+        osdata["cpuarch"],
+        _,
+    ) = platform.uname()
+
+    which_mock = MagicMock(
+        side_effect=[
+            # Check with virt-what
+            "/usr/sbin/virt-what",
+            "/usr/sbin/virt-what",
+            None,
+            "/usr/sbin/dmidecode",
+            # Check with systemd-detect-virt
+            None,
+            "/usr/bin/systemd-detect-virt",
+            None,
+            "/usr/sbin/dmidecode",
+            # Check with systemd-detect-virt when no dmidecode available
+            None,
+            "/usr/bin/systemd-detect-virt",
+            None,
+            None,
+        ]
+    )
+    cmd_run_all_mock = MagicMock(
+        side_effect=[
+            # Check with virt-what
+            {"retcode": 0, "stderr": "", "stdout": "xen"},
+            {
+                "retcode": 0,
+                "stderr": "",
+                "stdout": "\n".join(
+                    [
+                        "dmidecode 3.2",
+                        "Getting SMBIOS data from sysfs.",
+                        "SMBIOS 2.7 present.",
+                        "",
+                        "Handle 0x0100, DMI type 1, 27 bytes",
+                        "System Information",
+                        "      Manufacturer: Xen",
+                        "      Product Name: HVM domU",
+                        "      Version: 4.11.amazon",
+                        "      Serial Number: 
12345678-abcd-4321-dcba-0123456789ab",
+                        "      UUID: 01234567-dcba-1234-abcd-abcdef012345",
+                        "      Wake-up Type: Power Switch",
+                        "      SKU Number: Not Specified",
+                        "      Family: Not Specified",
+                        "",
+                        "Handle 0x2000, DMI type 32, 11 bytes",
+                        "System Boot Information",
+                        "      Status: No errors detected",
+                    ]
+                ),
+            },
+            # Check with systemd-detect-virt
+            {"retcode": 0, "stderr": "", "stdout": "kvm"},
+            {
+                "retcode": 0,
+                "stderr": "",
+                "stdout": "\n".join(
+                    [
+                        "dmidecode 3.2",
+                        "Getting SMBIOS data from sysfs.",
+                        "SMBIOS 2.7 present.",
+                        "",
+                        "Handle 0x0001, DMI type 1, 27 bytes",
+                        "System Information",
+                        "      Manufacturer: Amazon EC2",
+                        "      Product Name: m5.large",
+                        "      Version: Not Specified",
+                        "      Serial Number: 
01234567-dcba-1234-abcd-abcdef012345",
+                        "      UUID: 12345678-abcd-4321-dcba-0123456789ab",
+                        "      Wake-up Type: Power Switch",
+                        "      SKU Number: Not Specified",
+                        "      Family: Not Specified",
+                    ]
+                ),
+            },
+            # Check with systemd-detect-virt when no dmidecode available
+            {"retcode": 0, "stderr": "", "stdout": "kvm"},
+        ]
+    )
+
+    with patch("salt.utils.path.which", which_mock), patch.dict(
+        core.__salt__,
+        {
+            "cmd.run": salt.modules.cmdmod.run,
+            "cmd.run_all": cmd_run_all_mock,
+            "cmd.retcode": salt.modules.cmdmod.retcode,
+            "smbios.get": salt.modules.smbios.get,
+        },
+    ):
+
+        virtual_grains = core._virtual(osdata.copy())
+
+        assert virtual_grains["virtual"] == "xen"
+        assert virtual_grains["virtual_subtype"] == "Amazon EC2"
+
+        virtual_grains = core._virtual(osdata.copy())
+
+        assert virtual_grains["virtual"] == "kvm"
+        assert virtual_grains["virtual_subtype"] == "Amazon EC2 (m5.large)"
+
+        virtual_grains = core._virtual(osdata.copy())
+
+        assert virtual_grains["virtual"] == "kvm"
+        assert "virtual_subtype" not in virtual_grains
-- 
2.37.2



++++++ backport-syndic-auth-fixes.patch ++++++
>From 54ab69e74beb83710d0bf6049039d13e260d5517 Mon Sep 17 00:00:00 2001
From: Alexander Graul <[email protected]>
Date: Tue, 13 Sep 2022 11:26:21 +0200
Subject: [PATCH] Backport Syndic auth fixes

[3004.2] Syndic Fixes

(cherry picked from commit 643bd4b572ca97466e085ecd1d84da45b1684332)

Co-authored-by: Megan Wilhite <[email protected]>
---
 changelog/61868.fixed                       |   1 +
 salt/transport/mixins/auth.py               |   2 +-
 salt/transport/tcp.py                       |   2 +-
 salt/transport/zeromq.py                    |   2 +-
 tests/pytests/unit/transport/test_tcp.py    | 149 +++++++++++++++++++-
 tests/pytests/unit/transport/test_zeromq.py |  73 +++++++++-
 6 files changed, 224 insertions(+), 5 deletions(-)
 create mode 100644 changelog/61868.fixed

diff --git a/changelog/61868.fixed b/changelog/61868.fixed
new file mode 100644
index 0000000000..0169c48e99
--- /dev/null
+++ b/changelog/61868.fixed
@@ -0,0 +1 @@
+Make sure the correct key is being used when verifying or validating 
communication, eg. when a Salt syndic is involved use syndic_master.pub and 
when a Salt minion is involved use minion_master.pub.
diff --git a/salt/transport/mixins/auth.py b/salt/transport/mixins/auth.py
index 1e2e8e6b7b..e5c6a5345f 100644
--- a/salt/transport/mixins/auth.py
+++ b/salt/transport/mixins/auth.py
@@ -43,7 +43,7 @@ class AESPubClientMixin:
                 )
 
             # Verify that the signature is valid
-            master_pubkey_path = os.path.join(self.opts["pki_dir"], 
"minion_master.pub")
+            master_pubkey_path = os.path.join(self.opts["pki_dir"], 
self.auth.mpub)
             if not salt.crypt.verify_signature(
                 master_pubkey_path, payload["load"], payload.get("sig")
             ):
diff --git a/salt/transport/tcp.py b/salt/transport/tcp.py
index f00b3c40eb..2821be82c7 100644
--- a/salt/transport/tcp.py
+++ b/salt/transport/tcp.py
@@ -295,7 +295,7 @@ class AsyncTCPReqChannel(salt.transport.client.ReqChannel):
         signed_msg = pcrypt.loads(ret[dictkey])
 
         # Validate the master's signature.
-        master_pubkey_path = os.path.join(self.opts["pki_dir"], 
"minion_master.pub")
+        master_pubkey_path = os.path.join(self.opts["pki_dir"], self.auth.mpub)
         if not salt.crypt.verify_signature(
             master_pubkey_path, signed_msg["data"], signed_msg["sig"]
         ):
diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py
index aa06298ee1..8199378239 100644
--- a/salt/transport/zeromq.py
+++ b/salt/transport/zeromq.py
@@ -255,7 +255,7 @@ class 
AsyncZeroMQReqChannel(salt.transport.client.ReqChannel):
         signed_msg = pcrypt.loads(ret[dictkey])
 
         # Validate the master's signature.
-        master_pubkey_path = os.path.join(self.opts["pki_dir"], 
"minion_master.pub")
+        master_pubkey_path = os.path.join(self.opts["pki_dir"], self.auth.mpub)
         if not salt.crypt.verify_signature(
             master_pubkey_path, signed_msg["data"], signed_msg["sig"]
         ):
diff --git a/tests/pytests/unit/transport/test_tcp.py 
b/tests/pytests/unit/transport/test_tcp.py
index 3b6e175472..e41edcc37e 100644
--- a/tests/pytests/unit/transport/test_tcp.py
+++ b/tests/pytests/unit/transport/test_tcp.py
@@ -1,13 +1,53 @@
 import contextlib
+import os
 import socket
 
 import attr
 import pytest
 import salt.exceptions
+import salt.transport.mixins.auth
 import salt.transport.tcp
 from salt.ext.tornado import concurrent, gen, ioloop
 from saltfactories.utils.ports import get_unused_localhost_port
-from tests.support.mock import MagicMock, patch
+from tests.support.mock import MagicMock, PropertyMock, create_autospec, patch
+
+
[email protected]
+def fake_keys():
+    with patch("salt.crypt.AsyncAuth.get_keys", autospec=True):
+        yield
+
+
[email protected]
+def fake_crypto():
+    with patch("salt.transport.tcp.PKCS1_OAEP", create=True) as fake_crypto:
+        yield fake_crypto
+
+
[email protected]
+def fake_authd():
+    @salt.ext.tornado.gen.coroutine
+    def return_nothing():
+        raise salt.ext.tornado.gen.Return()
+
+    with patch(
+        "salt.crypt.AsyncAuth.authenticated", new_callable=PropertyMock
+    ) as mock_authed, patch(
+        "salt.crypt.AsyncAuth.authenticate",
+        autospec=True,
+        return_value=return_nothing(),
+    ), patch(
+        "salt.crypt.AsyncAuth.gen_token", autospec=True, return_value=42
+    ):
+        mock_authed.return_value = False
+        yield
+
+
[email protected]
+def fake_crypticle():
+    with patch("salt.crypt.Crypticle") as fake_crypticle:
+        fake_crypticle.generate_key_string.return_value = "fakey fake"
+        yield fake_crypticle
 
 
 @pytest.fixture
@@ -405,3 +445,110 @@ def test_client_reconnect_backoff(client_socket):
             client.io_loop.run_sync(client._connect)
     finally:
         client.close()
+
+
+async def 
test_when_async_req_channel_with_syndic_role_should_use_syndic_master_pub_file_to_verify_master_sig(
+    fake_keys, fake_crypto, fake_crypticle
+):
+    # Syndics use the minion pki dir, but they also create a syndic_master.pub
+    # file for comms with the Salt master
+    expected_pubkey_path = os.path.join("/etc/salt/pki/minion", 
"syndic_master.pub")
+    fake_crypto.new.return_value.decrypt.return_value = 
"decrypted_return_value"
+    mockloop = MagicMock()
+    opts = {
+        "master_uri": "tcp://127.0.0.1:4506",
+        "interface": "127.0.0.1",
+        "ret_port": 4506,
+        "ipv6": False,
+        "sock_dir": ".",
+        "pki_dir": "/etc/salt/pki/minion",
+        "id": "syndic",
+        "__role": "syndic",
+        "keysize": 4096,
+    }
+    client = salt.transport.tcp.AsyncTCPReqChannel(opts, io_loop=mockloop)
+
+    dictkey = "pillar"
+    target = "minion"
+
+    # Mock auth and message client.
+    client.auth._authenticate_future = MagicMock()
+    client.auth._authenticate_future.done.return_value = True
+    client.auth._authenticate_future.exception.return_value = None
+    client.auth._crypticle = MagicMock()
+    client.message_client = create_autospec(client.message_client)
+
+    @salt.ext.tornado.gen.coroutine
+    def mocksend(msg, timeout=60, tries=3):
+        raise salt.ext.tornado.gen.Return({"pillar": "data", "key": "value"})
+
+    client.message_client.send = mocksend
+
+    # Note the 'ver' value in 'load' does not represent the the 'version' sent
+    # in the top level of the transport's message.
+    load = {
+        "id": target,
+        "grains": {},
+        "saltenv": "base",
+        "pillarenv": "base",
+        "pillar_override": True,
+        "extra_minion_data": {},
+        "ver": "2",
+        "cmd": "_pillar",
+    }
+    fake_nonce = 42
+    with patch(
+        "salt.crypt.verify_signature", autospec=True, return_value=True
+    ) as fake_verify, patch(
+        "salt.payload.loads",
+        autospec=True,
+        return_value={"key": "value", "nonce": fake_nonce, "pillar": "data"},
+    ), patch(
+        "uuid.uuid4", autospec=True
+    ) as fake_uuid:
+        fake_uuid.return_value.hex = fake_nonce
+        ret = await client.crypted_transfer_decode_dictentry(
+            load,
+            dictkey="pillar",
+        )
+
+        assert fake_verify.mock_calls[0].args[0] == expected_pubkey_path
+
+
+async def test_mixin_should_use_correct_path_when_syndic(
+    fake_keys, fake_authd, fake_crypticle
+):
+    mockloop = MagicMock()
+    expected_pubkey_path = os.path.join("/etc/salt/pki/minion", 
"syndic_master.pub")
+    opts = {
+        "master_uri": "tcp://127.0.0.1:4506",
+        "interface": "127.0.0.1",
+        "ret_port": 4506,
+        "ipv6": False,
+        "sock_dir": ".",
+        "pki_dir": "/etc/salt/pki/minion",
+        "id": "syndic",
+        "__role": "syndic",
+        "keysize": 4096,
+        "sign_pub_messages": True,
+    }
+
+    with patch(
+        "salt.crypt.verify_signature", autospec=True, return_value=True
+    ) as fake_verify, patch(
+        "salt.utils.msgpack.loads",
+        autospec=True,
+        return_value={"enc": "aes", "load": "", "sig": "fake_signature"},
+    ):
+        client = salt.transport.tcp.AsyncTCPPubChannel(opts, io_loop=mockloop)
+        client.message_client = MagicMock()
+        client.message_client.on_recv.side_effect = lambda x: x(b"some_data")
+        await client.connect()
+        client.auth._crypticle = fake_crypticle
+
+        @client.on_recv
+        def test_recv_function(*args, **kwargs):
+            ...
+
+        await test_recv_function
+        assert fake_verify.mock_calls[0].args[0] == expected_pubkey_path
diff --git a/tests/pytests/unit/transport/test_zeromq.py 
b/tests/pytests/unit/transport/test_zeromq.py
index 1f0515c91a..c3093f4b19 100644
--- a/tests/pytests/unit/transport/test_zeromq.py
+++ b/tests/pytests/unit/transport/test_zeromq.py
@@ -23,7 +23,7 @@ import salt.utils.process
 import salt.utils.stringutils
 from salt.master import SMaster
 from salt.transport.zeromq import AsyncReqMessageClientPool
-from tests.support.mock import MagicMock, patch
+from tests.support.mock import MagicMock, create_autospec, patch
 
 try:
     from M2Crypto import RSA
@@ -608,6 +608,7 @@ async def test_req_chan_decode_data_dict_entry_v2(pki_dir):
     auth = client.auth
     auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY)
     client.auth = MagicMock()
+    client.auth.mpub = auth.mpub
     client.auth.authenticated = True
     client.auth.get_keys = auth.get_keys
     client.auth.crypticle.dumps = auth.crypticle.dumps
@@ -672,6 +673,7 @@ async def 
test_req_chan_decode_data_dict_entry_v2_bad_nonce(pki_dir):
     auth = client.auth
     auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY)
     client.auth = MagicMock()
+    client.auth.mpub = auth.mpub
     client.auth.authenticated = True
     client.auth.get_keys = auth.get_keys
     client.auth.crypticle.dumps = auth.crypticle.dumps
@@ -735,6 +737,7 @@ async def 
test_req_chan_decode_data_dict_entry_v2_bad_signature(pki_dir):
     auth = client.auth
     auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY)
     client.auth = MagicMock()
+    client.auth.mpub = auth.mpub
     client.auth.authenticated = True
     client.auth.get_keys = auth.get_keys
     client.auth.crypticle.dumps = auth.crypticle.dumps
@@ -814,6 +817,7 @@ async def 
test_req_chan_decode_data_dict_entry_v2_bad_key(pki_dir):
     auth = client.auth
     auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY)
     client.auth = MagicMock()
+    client.auth.mpub = auth.mpub
     client.auth.authenticated = True
     client.auth.get_keys = auth.get_keys
     client.auth.crypticle.dumps = auth.crypticle.dumps
@@ -1273,3 +1277,70 @@ async def 
test_req_chan_auth_v2_new_minion_without_master_pub(pki_dir, io_loop):
     assert "sig" in ret
     ret = client.auth.handle_signin_response(signin_payload, ret)
     assert ret == "retry"
+
+
+async def 
test_when_async_req_channel_with_syndic_role_should_use_syndic_master_pub_file_to_verify_master_sig(
+    pki_dir,
+):
+    # Syndics use the minion pki dir, but they also create a syndic_master.pub
+    # file for comms with the Salt master
+    expected_pubkey_path = 
str(pki_dir.join("minion").join("syndic_master.pub"))
+    mockloop = MagicMock()
+    opts = {
+        "master_uri": "tcp://127.0.0.1:4506",
+        "interface": "127.0.0.1",
+        "ret_port": 4506,
+        "ipv6": False,
+        "sock_dir": ".",
+        "pki_dir": str(pki_dir.join("minion")),
+        "id": "syndic",
+        "__role": "syndic",
+        "keysize": 4096,
+    }
+    master_opts = dict(opts, pki_dir=str(pki_dir.join("master")))
+    server = salt.transport.zeromq.ZeroMQReqServerChannel(master_opts)
+    client = salt.transport.zeromq.AsyncZeroMQReqChannel(opts, 
io_loop=mockloop)
+
+    dictkey = "pillar"
+    target = "minion"
+    pillar_data = {"pillar1": "data1"}
+
+    # Mock auth and message client.
+    client.auth._authenticate_future = MagicMock()
+    client.auth._authenticate_future.done.return_value = True
+    client.auth._authenticate_future.exception.return_value = None
+    client.auth._crypticle = salt.crypt.Crypticle(opts, AES_KEY)
+    client.message_client = create_autospec(client.message_client)
+
+    @salt.ext.tornado.gen.coroutine
+    def mocksend(msg, timeout=60, tries=3):
+        client.message_client.msg = msg
+        load = client.auth.crypticle.loads(msg["load"])
+        ret = server._encrypt_private(
+            pillar_data, dictkey, target, nonce=load["nonce"], 
sign_messages=True
+        )
+        raise salt.ext.tornado.gen.Return(ret)
+
+    client.message_client.send = mocksend
+
+    # Note the 'ver' value in 'load' does not represent the the 'version' sent
+    # in the top level of the transport's message.
+    load = {
+        "id": target,
+        "grains": {},
+        "saltenv": "base",
+        "pillarenv": "base",
+        "pillar_override": True,
+        "extra_minion_data": {},
+        "ver": "2",
+        "cmd": "_pillar",
+    }
+    with patch(
+        "salt.crypt.verify_signature", autospec=True, return_value=True
+    ) as fake_verify:
+        ret = await client.crypted_transfer_decode_dictentry(
+            load,
+            dictkey="pillar",
+        )
+
+        assert fake_verify.mock_calls[0].args[0] == expected_pubkey_path
-- 
2.37.3


++++++ change-the-delimeters-to-prevent-possible-tracebacks.patch ++++++
>From e28385eb37932809a11ec81c81834a51e094f507 Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <[email protected]>
Date: Thu, 1 Sep 2022 14:42:24 +0300
Subject: [PATCH] Change the delimeters to prevent possible tracebacks
 on some packages with dpkg_lowpkg

* Use another separator on query to dpkg-query

* Fix the test test_dpkg_lowpkg::test_info
---
 salt/modules/dpkg_lowpkg.py            | 13 ++++++++-----
 tests/unit/modules/test_dpkg_lowpkg.py |  4 ++--
 2 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/salt/modules/dpkg_lowpkg.py b/salt/modules/dpkg_lowpkg.py
index 2c25b1fb2a..fc93d99549 100644
--- a/salt/modules/dpkg_lowpkg.py
+++ b/salt/modules/dpkg_lowpkg.py
@@ -309,9 +309,8 @@ def _get_pkg_info(*packages, **kwargs):
         "origin:${Origin}\\n"
         "homepage:${Homepage}\\n"
         "status:${db:Status-Abbrev}\\n"
-        "======\\n"
         "description:${Description}\\n"
-        "------\\n'"
+        "\\n*/~^\\\\*\\n'"
     )
     cmd += " {}".format(" ".join(packages))
     cmd = cmd.strip()
@@ -325,9 +324,13 @@ def _get_pkg_info(*packages, **kwargs):
         else:
             return ret
 
-    for pkg_info in [elm for elm in re.split(r"------", call["stdout"]) if 
elm.strip()]:
+    for pkg_info in [
+        elm
+        for elm in re.split(r"\r?\n\*/~\^\\\*(\r?\n|)", call["stdout"])
+        if elm.strip()
+    ]:
         pkg_data = {}
-        pkg_info, pkg_descr = re.split(r"======", pkg_info)
+        pkg_info, pkg_descr = pkg_info.split("\ndescription:", 1)
         for pkg_info_line in [
             el.strip() for el in pkg_info.split(os.linesep) if el.strip()
         ]:
@@ -344,7 +347,7 @@ def _get_pkg_info(*packages, **kwargs):
         if build_date:
             pkg_data["build_date"] = build_date
             pkg_data["build_date_time_t"] = build_date_t
-        pkg_data["description"] = pkg_descr.split(":", 1)[-1]
+        pkg_data["description"] = pkg_descr
         ret.append(pkg_data)
 
     return ret
diff --git a/tests/unit/modules/test_dpkg_lowpkg.py 
b/tests/unit/modules/test_dpkg_lowpkg.py
index d00fc46c66..a97519f489 100644
--- a/tests/unit/modules/test_dpkg_lowpkg.py
+++ b/tests/unit/modules/test_dpkg_lowpkg.py
@@ -290,7 +290,6 @@ class DpkgTestCase(TestCase, LoaderModuleMockMixin):
                         "origin:",
                         
"homepage:http://tiswww.case.edu/php/chet/bash/bashtop.html";,
                         "status:ii ",
-                        "======",
                         "description:GNU Bourne Again SHell",
                         " Bash is an sh-compatible command language 
interpreter that"
                         " executes",
@@ -307,7 +306,8 @@ class DpkgTestCase(TestCase, LoaderModuleMockMixin):
                         " The Programmable Completion Code, by Ian Macdonald, 
is now"
                         " found in",
                         " the bash-completion package.",
-                        "------",
+                        "",
+                        "*/~^\\*",  # pylint: disable=W1401
                     ]
                 ),
             }
-- 
2.37.2



++++++ fix-state.apply-in-test-mode-with-file-state-module-.patch ++++++
>From ed567e5f339f7bf95d4361ac47e67427db71714c Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <[email protected]>
Date: Thu, 1 Sep 2022 14:44:26 +0300
Subject: [PATCH] Fix state.apply in test mode with file state module
 on user/group checking (bsc#1202167)

* Do not fail on checking user/group in test mode

* fixes saltstack/salt#61846 reporting of errors in test mode

Co-authored-by: nicholasmhughes <[email protected]>

* Add tests for _check_user usage

Co-authored-by: nicholasmhughes <[email protected]>
---
 changelog/61846.fixed                         |  1 +
 salt/states/file.py                           |  5 ++
 tests/pytests/unit/states/file/test_copy.py   | 35 ++++++++++++
 .../unit/states/file/test_directory.py        | 55 +++++++++++++++++++
 .../unit/states/file/test_filestate.py        | 42 ++++++++++++++
 .../pytests/unit/states/file/test_managed.py  | 31 +++++++++++
 6 files changed, 169 insertions(+)
 create mode 100644 changelog/61846.fixed

diff --git a/changelog/61846.fixed b/changelog/61846.fixed
new file mode 100644
index 0000000000..c4024efe9f
--- /dev/null
+++ b/changelog/61846.fixed
@@ -0,0 +1 @@
+Fix the reporting of errors for file.directory in test mode
diff --git a/salt/states/file.py b/salt/states/file.py
index a6288025e5..39cf83b78e 100644
--- a/salt/states/file.py
+++ b/salt/states/file.py
@@ -379,6 +379,11 @@ def _check_user(user, group):
         gid = __salt__["file.group_to_gid"](group)
         if gid == "":
             err += "Group {} is not available".format(group)
+    if err and __opts__["test"]:
+        # Write the warning with error message, but prevent failing,
+        # in case of applying the state in test mode.
+        log.warning(err)
+        return ""
     return err
 
 
diff --git a/tests/pytests/unit/states/file/test_copy.py 
b/tests/pytests/unit/states/file/test_copy.py
index ce7161f02d..a11adf5ae0 100644
--- a/tests/pytests/unit/states/file/test_copy.py
+++ b/tests/pytests/unit/states/file/test_copy.py
@@ -205,3 +205,38 @@ def test_copy(tmp_path):
                     )
                     res = filestate.copy_(name, source, group=group, 
preserve=False)
                     assert res == ret
+
+
+def test_copy_test_mode_user_group_not_present():
+    """
+    Test file copy in test mode with no user or group existing
+    """
+    source = "/tmp/src_copy_no_user_group_test_mode"
+    filename = "/tmp/copy_no_user_group_test_mode"
+    with patch.dict(
+        filestate.__salt__,
+        {
+            "file.group_to_gid": MagicMock(side_effect=["1234", "", ""]),
+            "file.user_to_uid": MagicMock(side_effect=["", "4321", ""]),
+            "file.get_mode": MagicMock(return_value="0644"),
+        },
+    ), patch.dict(filestate.__opts__, {"test": True}), patch.object(
+        os.path, "exists", return_value=True
+    ):
+        ret = filestate.copy_(
+            source, filename, group="nonexistinggroup", user="nonexistinguser"
+        )
+        assert ret["result"] is not False
+        assert "is not available" not in ret["comment"]
+
+        ret = filestate.copy_(
+            source, filename, group="nonexistinggroup", user="nonexistinguser"
+        )
+        assert ret["result"] is not False
+        assert "is not available" not in ret["comment"]
+
+        ret = filestate.copy_(
+            source, filename, group="nonexistinggroup", user="nonexistinguser"
+        )
+        assert ret["result"] is not False
+        assert "is not available" not in ret["comment"]
diff --git a/tests/pytests/unit/states/file/test_directory.py 
b/tests/pytests/unit/states/file/test_directory.py
index 0e15e1d3ca..1287609c6a 100644
--- a/tests/pytests/unit/states/file/test_directory.py
+++ b/tests/pytests/unit/states/file/test_directory.py
@@ -291,3 +291,58 @@ def test_directory():
                             assert (
                                 filestate.directory(name, user=user, 
group=group) == ret
                             )
+
+
+def test_directory_test_mode_user_group_not_present():
+    name = "/etc/testdir"
+    user = "salt"
+    group = "saltstack"
+    if salt.utils.platform.is_windows():
+        name = name.replace("/", "\\")
+
+    ret = {
+        "name": name,
+        "result": None,
+        "comment": "",
+        "changes": {name: {"directory": "new"}},
+    }
+
+    if salt.utils.platform.is_windows():
+        comt = 'The directory "{}" will be changed' "".format(name)
+    else:
+        comt = "The following files will be changed:\n{}:" " directory - 
new\n".format(
+            name
+        )
+    ret["comment"] = comt
+
+    mock_f = MagicMock(return_value=False)
+    mock_uid = MagicMock(
+        side_effect=[
+            "",
+            "U12",
+            "",
+        ]
+    )
+    mock_gid = MagicMock(
+        side_effect=[
+            "G12",
+            "",
+            "",
+        ]
+    )
+    mock_error = CommandExecutionError
+    with patch.dict(
+        filestate.__salt__,
+        {
+            "file.user_to_uid": mock_uid,
+            "file.group_to_gid": mock_gid,
+            "file.stats": mock_f,
+        },
+    ), patch("salt.utils.win_dacl.get_sid", mock_error), patch.object(
+        os.path, "isdir", mock_f
+    ), patch.dict(
+        filestate.__opts__, {"test": True}
+    ):
+        assert filestate.directory(name, user=user, group=group) == ret
+        assert filestate.directory(name, user=user, group=group) == ret
+        assert filestate.directory(name, user=user, group=group) == ret
diff --git a/tests/pytests/unit/states/file/test_filestate.py 
b/tests/pytests/unit/states/file/test_filestate.py
index 2f9f369fb2..c373cb3449 100644
--- a/tests/pytests/unit/states/file/test_filestate.py
+++ b/tests/pytests/unit/states/file/test_filestate.py
@@ -577,3 +577,45 @@ def test_mod_run_check_cmd():
         assert filestate.mod_run_check_cmd(cmd, filename) == ret
 
         assert filestate.mod_run_check_cmd(cmd, filename)
+
+
+def test_recurse_test_mode_user_group_not_present():
+    """
+    Test file recurse in test mode with no user or group existing
+    """
+    filename = "/tmp/recurse_no_user_group_test_mode"
+    source = "salt://tmp/src_recurse_no_user_group_test_mode"
+    mock_l = MagicMock(return_value=[])
+    mock_emt = 
MagicMock(return_value=["tmp/src_recurse_no_user_group_test_mode"])
+    with patch.dict(
+        filestate.__salt__,
+        {
+            "file.group_to_gid": MagicMock(side_effect=["1234", "", ""]),
+            "file.user_to_uid": MagicMock(side_effect=["", "4321", ""]),
+            "file.get_mode": MagicMock(return_value="0644"),
+            "file.source_list": MagicMock(return_value=[source, ""]),
+            "cp.list_master_dirs": mock_emt,
+            "cp.list_master": mock_l,
+        },
+    ), patch.dict(filestate.__opts__, {"test": True}), patch.object(
+        os.path, "exists", return_value=True
+    ), patch.object(
+        os.path, "isdir", return_value=True
+    ):
+        ret = filestate.recurse(
+            filename, source, group="nonexistinggroup", user="nonexistinguser"
+        )
+        assert ret["result"] is not False
+        assert "is not available" not in ret["comment"]
+
+        ret = filestate.recurse(
+            filename, source, group="nonexistinggroup", user="nonexistinguser"
+        )
+        assert ret["result"] is not False
+        assert "is not available" not in ret["comment"]
+
+        ret = filestate.recurse(
+            filename, source, group="nonexistinggroup", user="nonexistinguser"
+        )
+        assert ret["result"] is not False
+        assert "is not available" not in ret["comment"]
diff --git a/tests/pytests/unit/states/file/test_managed.py 
b/tests/pytests/unit/states/file/test_managed.py
index 9d9fb17717..0b341e09a9 100644
--- a/tests/pytests/unit/states/file/test_managed.py
+++ b/tests/pytests/unit/states/file/test_managed.py
@@ -373,3 +373,34 @@ def test_managed():
                                         filestate.managed(name, user=user, 
group=group)
                                         == ret
                                     )
+
+
+def test_managed_test_mode_user_group_not_present():
+    """
+    Test file managed in test mode with no user or group existing
+    """
+    filename = "/tmp/managed_no_user_group_test_mode"
+    with patch.dict(
+        filestate.__salt__,
+        {
+            "file.group_to_gid": MagicMock(side_effect=["1234", "", ""]),
+            "file.user_to_uid": MagicMock(side_effect=["", "4321", ""]),
+        },
+    ), patch.dict(filestate.__opts__, {"test": True}):
+        ret = filestate.managed(
+            filename, group="nonexistinggroup", user="nonexistinguser"
+        )
+        assert ret["result"] is not False
+        assert "is not available" not in ret["comment"]
+
+        ret = filestate.managed(
+            filename, group="nonexistinggroup", user="nonexistinguser"
+        )
+        assert ret["result"] is not False
+        assert "is not available" not in ret["comment"]
+
+        ret = filestate.managed(
+            filename, group="nonexistinggroup", user="nonexistinguser"
+        )
+        assert ret["result"] is not False
+        assert "is not available" not in ret["comment"]
-- 
2.37.2



++++++ fix-test_ipc-unit-tests.patch ++++++
>From 61d9b5e4ceaa0f5feb7fc364c9089cb624006812 Mon Sep 17 00:00:00 2001
From: Alexander Graul <[email protected]>
Date: Tue, 12 Jul 2022 14:02:58 +0200
Subject: [PATCH] Fix test_ipc unit tests

---
 tests/unit/transport/test_ipc.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/unit/transport/test_ipc.py b/tests/unit/transport/test_ipc.py
index 79b49f9406..7177b7f6c4 100644
--- a/tests/unit/transport/test_ipc.py
+++ b/tests/unit/transport/test_ipc.py
@@ -107,8 +107,8 @@ class 
IPCMessagePubSubCase(salt.ext.tornado.testing.AsyncTestCase):
                 self.stop()
 
         # Now let both waiting data at once
-        client1.read_async(handler)
-        client2.read_async(handler)
+        client1.read_async()
+        client2.read_async()
         self.pub_channel.publish("TEST")
         self.wait()
         self.assertEqual(len(call_cnt), 2)
@@ -150,7 +150,7 @@ class 
IPCMessagePubSubCase(salt.ext.tornado.testing.AsyncTestCase):
             pass
 
         try:
-            ret1 = yield client1.read_async(handler)
+            ret1 = yield client1.read_async()
             self.wait()
         except StreamClosedError as ex:
             assert False, "StreamClosedError was raised inside the Future"
-- 
2.36.1



++++++ fix-the-regression-in-schedule-module-releasded-in-3.patch ++++++
++++ 821 lines (skipped)

++++++ fopen-workaround-bad-buffering-for-binary-mode-563.patch ++++++
>From 6c1c81aba71711632a14b725426077f9183065e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
 <[email protected]>
Date: Thu, 6 Oct 2022 10:55:50 +0100
Subject: [PATCH] fopen: Workaround bad buffering for binary mode
 (#563)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A lot of code assumes Python 2.x behavior for buffering, in which 1 is a
special value meaning line buffered.

Python 3 makes this value unusable, so fallback to the default buffering
size, and report these calls to be fixed.

Fixes: https://github.com/saltstack/salt/issues/57584

Do not drop buffering from kwargs to avoid errors

Add unit test around linebuffering in binary mode

Add changelog file

Co-authored-by: Pablo Su??rez Hern??ndez <[email protected]>

Co-authored-by: Ismael Luceno <[email protected]>
---
 changelog/62817.fixed                  |  1 +
 salt/utils/files.py                    |  8 ++++++++
 tests/pytests/unit/utils/test_files.py | 13 ++++++++++++-
 3 files changed, 21 insertions(+), 1 deletion(-)
 create mode 100644 changelog/62817.fixed

diff --git a/changelog/62817.fixed b/changelog/62817.fixed
new file mode 100644
index 0000000000..ff335f2916
--- /dev/null
+++ b/changelog/62817.fixed
@@ -0,0 +1 @@
+Prevent annoying RuntimeWarning message about line buffering (buffering=1) not 
being supported in binary mode
diff --git a/salt/utils/files.py b/salt/utils/files.py
index 1cf636a753..3c57cce713 100644
--- a/salt/utils/files.py
+++ b/salt/utils/files.py
@@ -6,6 +6,7 @@ Functions for working with files
 import codecs
 import contextlib
 import errno
+import io
 import logging
 import os
 import re
@@ -382,6 +383,13 @@ def fopen(*args, **kwargs):
     if not binary and not kwargs.get("newline", None):
         kwargs["newline"] = ""
 
+    # Workaround callers with bad buffering setting for binary files
+    if kwargs.get("buffering") == 1 and "b" in kwargs.get("mode", ""):
+        log.debug(
+            "Line buffering (buffering=1) isn't supported in binary mode, the 
default buffer size will be used"
+        )
+        kwargs["buffering"] = io.DEFAULT_BUFFER_SIZE
+
     f_handle = open(*args, **kwargs)  # pylint: disable=resource-leakage
 
     if is_fcntl_available():
diff --git a/tests/pytests/unit/utils/test_files.py 
b/tests/pytests/unit/utils/test_files.py
index fd88167b16..bd18bc5750 100644
--- a/tests/pytests/unit/utils/test_files.py
+++ b/tests/pytests/unit/utils/test_files.py
@@ -4,11 +4,12 @@ Unit Tests for functions located in salt/utils/files.py
 
 
 import copy
+import io
 import os
 
 import pytest
 import salt.utils.files
-from tests.support.mock import patch
+from tests.support.mock import MagicMock, patch
 
 
 def test_safe_rm():
@@ -74,6 +75,16 @@ def test_fopen_with_disallowed_fds():
             )
 
 
+def test_fopen_binary_line_buffering(tmp_path):
+    tmp_file = os.path.join(tmp_path, "foobar")
+    with patch("builtins.open") as open_mock, patch(
+        "salt.utils.files.is_fcntl_available", MagicMock(return_value=False)
+    ):
+        salt.utils.files.fopen(os.path.join(tmp_path, "foobar"), mode="b", 
buffering=1)
+        assert open_mock.called
+        assert open_mock.call_args[1]["buffering"] == io.DEFAULT_BUFFER_SIZE
+
+
 def _create_temp_structure(temp_directory, structure):
     for folder, files in structure.items():
         current_directory = os.path.join(temp_directory, folder)
-- 
2.37.3



++++++ ignore-non-utf8-characters-while-reading-files-with-.patch ++++++
>From b4945a0608b3d8996e8b5593dcc458c15b11d6ba Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <[email protected]>
Date: Wed, 14 Sep 2022 14:57:29 +0300
Subject: [PATCH] Ignore non utf8 characters while reading files with
 core grains module (bsc#1202165)

* Ignore UnicodeDecodeError on reading files with core grains

* Add tests for non utf8 chars in cmdline

* Blacken modified lines

* Fix the tests

* Add changelog entry

* Change ignore to surrogateescape for kernelparameters

* Turn static test files to dynamic
---
 changelog/62633.fixed                  |   1 +
 salt/grains/core.py                    |  12 ++-
 tests/pytests/unit/grains/test_core.py | 118 +++++++++++++++++++++++++
 3 files changed, 128 insertions(+), 3 deletions(-)
 create mode 100644 changelog/62633.fixed

diff --git a/changelog/62633.fixed b/changelog/62633.fixed
new file mode 100644
index 0000000000..1ab74f9122
--- /dev/null
+++ b/changelog/62633.fixed
@@ -0,0 +1 @@
+Prevent possible tracebacks in core grains module by ignoring non utf8 
characters in /proc/1/environ, /proc/1/cmdline, /proc/cmdline
diff --git a/salt/grains/core.py b/salt/grains/core.py
index 9530a43fc5..b543144da2 100644
--- a/salt/grains/core.py
+++ b/salt/grains/core.py
@@ -1093,7 +1093,9 @@ def _virtual(osdata):
         if ("virtual_subtype" not in grains) or (grains["virtual_subtype"] != 
"LXC"):
             if os.path.isfile("/proc/1/environ"):
                 try:
-                    with salt.utils.files.fopen("/proc/1/environ", "r") as fhr:
+                    with salt.utils.files.fopen(
+                        "/proc/1/environ", "r", errors="ignore"
+                    ) as fhr:
                         fhr_contents = fhr.read()
                     if "container=lxc" in fhr_contents:
                         grains["virtual"] = "container"
@@ -1911,7 +1913,9 @@ def os_data():
             grains["init"] = "systemd"
         except OSError:
             try:
-                with salt.utils.files.fopen("/proc/1/cmdline") as fhr:
+                with salt.utils.files.fopen(
+                    "/proc/1/cmdline", "r", errors="ignore"
+                ) as fhr:
                     init_cmdline = fhr.read().replace("\x00", " ").split()
             except OSError:
                 pass
@@ -3160,7 +3164,9 @@ def kernelparams():
         return {}
     else:
         try:
-            with salt.utils.files.fopen("/proc/cmdline", "r") as fhr:
+            with salt.utils.files.fopen(
+                "/proc/cmdline", "r", errors="surrogateescape"
+            ) as fhr:
                 cmdline = fhr.read()
                 grains = {"kernelparams": []}
                 for data in [
diff --git a/tests/pytests/unit/grains/test_core.py 
b/tests/pytests/unit/grains/test_core.py
index 84dd97d62f..e640a07f76 100644
--- a/tests/pytests/unit/grains/test_core.py
+++ b/tests/pytests/unit/grains/test_core.py
@@ -11,6 +11,7 @@ import os
 import pathlib
 import platform
 import socket
+import tempfile
 import textwrap
 from collections import namedtuple
 
@@ -2635,6 +2636,38 @@ def test_kernelparams_return_linux(cmdline, expectation):
         assert core.kernelparams() == expectation
 
 
[email protected]_unless_on_linux
+def test_kernelparams_return_linux_non_utf8():
+    _salt_utils_files_fopen = salt.utils.files.fopen
+
+    expected = {
+        "kernelparams": [
+            ("TEST_KEY1", "VAL1"),
+            ("TEST_KEY2", "VAL2"),
+            ("BOOTABLE_FLAG", "\udc80"),
+            ("TEST_KEY_NOVAL", None),
+            ("TEST_KEY3", "3"),
+        ]
+    }
+
+    with tempfile.TemporaryDirectory() as tempdir:
+
+        def _open_mock(file_name, *args, **kwargs):
+            return _salt_utils_files_fopen(
+                os.path.join(tempdir, "cmdline"), *args, **kwargs
+            )
+
+        with salt.utils.files.fopen(
+            os.path.join(tempdir, "cmdline"),
+            "wb",
+        ) as cmdline_fh, patch("salt.utils.files.fopen", _open_mock):
+            cmdline_fh.write(
+                b'TEST_KEY1=VAL1 TEST_KEY2=VAL2 BOOTABLE_FLAG="\x80" 
TEST_KEY_NOVAL TEST_KEY3=3\n'
+            )
+            cmdline_fh.close()
+            assert core.kernelparams() == expected
+
+
 def test_linux_gpus():
     """
     Test GPU detection on Linux systems
@@ -2837,3 +2870,88 @@ def test_virtual_set_virtual_ec2():
 
         assert virtual_grains["virtual"] == "kvm"
         assert "virtual_subtype" not in virtual_grains
+
+
[email protected]_on_windows
+def test_linux_proc_files_with_non_utf8_chars():
+    _salt_utils_files_fopen = salt.utils.files.fopen
+
+    empty_mock = MagicMock(return_value={})
+
+    with tempfile.TemporaryDirectory() as tempdir:
+
+        def _mock_open(filename, *args, **kwargs):
+            return _salt_utils_files_fopen(
+                os.path.join(tempdir, "cmdline-1"), *args, **kwargs
+            )
+
+        with salt.utils.files.fopen(
+            os.path.join(tempdir, "cmdline-1"),
+            "wb",
+        ) as cmdline_fh, patch("os.path.isfile", return_value=False), patch(
+            "salt.utils.files.fopen", _mock_open
+        ), patch.dict(
+            core.__salt__,
+            {
+                "cmd.retcode": salt.modules.cmdmod.retcode,
+                "cmd.run": MagicMock(return_value=""),
+            },
+        ), patch.object(
+            core, "_linux_bin_exists", return_value=False
+        ), patch.object(
+            core, "_parse_lsb_release", return_value=empty_mock
+        ), patch.object(
+            core, "_parse_os_release", return_value=empty_mock
+        ), patch.object(
+            core, "_hw_data", return_value=empty_mock
+        ), patch.object(
+            core, "_virtual", return_value=empty_mock
+        ), patch.object(
+            core, "_bsd_cpudata", return_value=empty_mock
+        ), patch.object(
+            os, "stat", side_effect=OSError()
+        ):
+            cmdline_fh.write(
+                
b"/usr/lib/systemd/systemd\x00--switched-root\x00--system\x00--deserialize\x0028\x80\x00"
+            )
+            cmdline_fh.close()
+            os_grains = core.os_data()
+            assert os_grains != {}
+
+
[email protected]_on_windows
+def test_virtual_linux_proc_files_with_non_utf8_chars():
+    _salt_utils_files_fopen = salt.utils.files.fopen
+
+    def _is_file_mock(filename):
+        if filename == "/proc/1/environ":
+            return True
+        return False
+
+    with tempfile.TemporaryDirectory() as tempdir:
+
+        def _mock_open(filename, *args, **kwargs):
+            return _salt_utils_files_fopen(
+                os.path.join(tempdir, "environ"), *args, **kwargs
+            )
+
+        with salt.utils.files.fopen(
+            os.path.join(tempdir, "environ"),
+            "wb",
+        ) as environ_fh, patch("os.path.isfile", _is_file_mock), patch(
+            "salt.utils.files.fopen", _mock_open
+        ), patch.object(
+            salt.utils.path, "which", MagicMock(return_value=None)
+        ), patch.dict(
+            core.__salt__,
+            {
+                "cmd.run_all": MagicMock(
+                    return_value={"retcode": 1, "stderr": "", "stdout": ""}
+                ),
+                "cmd.run": MagicMock(return_value=""),
+            },
+        ):
+            environ_fh.write(b"KEY1=VAL1 KEY2=VAL2\x80 KEY2=VAL2")
+            environ_fh.close()
+            virt_grains = core._virtual({"kernel": "Linux"})
+            assert virt_grains == {"virtual": "physical"}
-- 
2.37.3


++++++ make-pass-renderer-configurable-other-fixes-532.patch ++++++
>From 7b4f5007b7e6a35386d197afe53d02c8d7b41d53 Mon Sep 17 00:00:00 2001
From: Daniel Mach <[email protected]>
Date: Thu, 6 Oct 2022 11:58:23 +0200
Subject: [PATCH] Make pass renderer configurable & other fixes (#532)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* pass: Use a secure way of handling pass arguments

The original code would fail on pass paths with spaces,
because they would be split into multiple arguments.

* pass: Strip only trailing newline characters from the secret

* pass: Do not modify $HOME env globally

Just set $HOME for calling the pass binary
to avoid affecting anything outside the pass renderer.

* pass: Use pass executable path from _get_pass_exec()

* Make the pass renderer more configurable

1. Allow us to make the pass renderer fail during pillar rendering
   when a secret corresponding with a pass path cannot be fetched.
   For this we add a master config variable pass_strict_fetch.

2. Allow to have prefix for variables that should be processed
   with the pass renderer.
   For this we add a master config variable pass_variable_prefix.

3. Allow us to configure pass' GNUPGHOME and PASSWORD_STORE_DIR
   environmental variables.
   For this we add master config variables pass_gnupghome and pass_dir.

* Add tests for the pass renderer

* pass: Handle FileNotFoundError when pass binary is not available

Co-authored-by: Marcus R??ckert <[email protected]>
---
 changelog/62120.added                     |   4 +
 changelog/62120.fixed                     |   4 +
 salt/config/__init__.py                   |  12 ++
 salt/renderers/pass.py                    | 104 ++++++++++++--
 tests/pytests/unit/renderers/test_pass.py | 164 ++++++++++++++++++++++
 5 files changed, 274 insertions(+), 14 deletions(-)
 create mode 100644 changelog/62120.added
 create mode 100644 changelog/62120.fixed
 create mode 100644 tests/pytests/unit/renderers/test_pass.py

diff --git a/changelog/62120.added b/changelog/62120.added
new file mode 100644
index 0000000000..4303d124f0
--- /dev/null
+++ b/changelog/62120.added
@@ -0,0 +1,4 @@
+Config option pass_variable_prefix allows to distinguish variables that 
contain paths to pass secrets.
+Config option pass_strict_fetch allows to error out when a secret cannot be 
fetched from pass.
+Config option pass_dir allows setting the PASSWORD_STORE_DIR env for pass.
+Config option pass_gnupghome allows setting the $GNUPGHOME env for pass.
diff --git a/changelog/62120.fixed b/changelog/62120.fixed
new file mode 100644
index 0000000000..22a9711383
--- /dev/null
+++ b/changelog/62120.fixed
@@ -0,0 +1,4 @@
+Pass executable path from _get_path_exec() is used when calling the program.
+The $HOME env is no longer modified globally.
+Only trailing newlines are stripped from the fetched secret.
+Pass process arguments are handled in a secure way.
diff --git a/salt/config/__init__.py b/salt/config/__init__.py
index 2c42290598..9e72a5b4b7 100644
--- a/salt/config/__init__.py
+++ b/salt/config/__init__.py
@@ -960,6 +960,14 @@ VALID_OPTS = immutabletypes.freeze(
         # Use Adler32 hashing algorithm for server_id (default False until 
Sodium, "adler32" after)
         # Possible values are: False, adler32, crc32
         "server_id_use_crc": (bool, str),
+        # pass renderer: Fetch secrets only for the template variables 
matching the prefix
+        "pass_variable_prefix": str,
+        # pass renderer: Whether to error out when unable to fetch a secret
+        "pass_strict_fetch": bool,
+        # pass renderer: Set GNUPGHOME env for Pass
+        "pass_gnupghome": str,
+        # pass renderer: Set PASSWORD_STORE_DIR env for Pass
+        "pass_dir": str,
     }
 )
 
@@ -1601,6 +1609,10 @@ DEFAULT_MASTER_OPTS = immutabletypes.freeze(
         "fips_mode": False,
         "detect_remote_minions": False,
         "remote_minions_port": 22,
+        "pass_variable_prefix": "",
+        "pass_strict_fetch": False,
+        "pass_gnupghome": "",
+        "pass_dir": "",
     }
 )
 
diff --git a/salt/renderers/pass.py b/salt/renderers/pass.py
index 71b1021b96..ba0f152c23 100644
--- a/salt/renderers/pass.py
+++ b/salt/renderers/pass.py
@@ -45,6 +45,34 @@ Install pass binary
 
         pass:
           pkg.installed
+
+Salt master configuration options
+
+.. code-block:: yaml
+
+        # If the prefix is *not* set (default behavior), all template 
variables are
+        # considered for fetching secrets from Pass. Those that cannot be 
resolved
+        # to a secret are passed through.
+        #
+        # If the prefix is set, only the template variables with matching 
prefix are
+        # considered for fetching the secrets, other variables are passed 
through.
+        #
+        # For ease of use it is recommended to set the following options as 
well:
+        #       renderer: 'jinja|yaml|pass'
+        #       pass_strict_fetch: true
+        #
+        pass_variable_prefix: 'pass:'
+
+        # If set to 'true', error out when unable to fetch a secret for a 
template variable.
+        pass_strict_fetch: true
+
+        # Set GNUPGHOME env for Pass.
+        # Defaults to: ~/.gnupg
+        pass_gnupghome: <path>
+
+        # Set PASSWORD_STORE_DIR env for Pass.
+        # Defaults to: ~/.password-store
+        pass_dir: <path>
 """
 
 
@@ -54,7 +82,7 @@ from os.path import expanduser
 from subprocess import PIPE, Popen
 
 import salt.utils.path
-from salt.exceptions import SaltRenderError
+from salt.exceptions import SaltConfigurationError, SaltRenderError
 
 log = logging.getLogger(__name__)
 
@@ -75,18 +103,71 @@ def _fetch_secret(pass_path):
     Fetch secret from pass based on pass_path. If there is
     any error, return back the original pass_path value
     """
-    cmd = "pass show {}".format(pass_path.strip())
-    log.debug("Fetching secret: %s", cmd)
+    pass_exec = _get_pass_exec()
+
+    # Make a backup in case we want to return the original value without 
stripped whitespaces
+    original_pass_path = pass_path
+
+    # Remove the optional prefix from pass path
+    pass_prefix = __opts__["pass_variable_prefix"]
+    if pass_prefix:
+        # If we do not see our prefix we do not want to process this variable
+        # and we return the unmodified pass path
+        if not pass_path.startswith(pass_prefix):
+            return pass_path
+
+        # strip the prefix from the start of the string
+        pass_path = pass_path[len(pass_prefix) :]
+
+    # The pass_strict_fetch option must be used with pass_variable_prefix
+    pass_strict_fetch = __opts__["pass_strict_fetch"]
+    if pass_strict_fetch and not pass_prefix:
+        msg = "The 'pass_strict_fetch' option requires 'pass_variable_prefix' 
option enabled"
+        raise SaltConfigurationError(msg)
+
+    # Remove whitespaces from the pass_path
+    pass_path = pass_path.strip()
 
-    proc = Popen(cmd.split(" "), stdout=PIPE, stderr=PIPE)
-    pass_data, pass_error = proc.communicate()
+    cmd = [pass_exec, "show", pass_path]
+    log.debug("Fetching secret: %s", " ".join(cmd))
+
+    # Make sure environment variable HOME is set, since Pass looks for the
+    # password-store under ~/.password-store.
+    env = os.environ.copy()
+    env["HOME"] = expanduser("~")
+
+    pass_dir = __opts__["pass_dir"]
+    if pass_dir:
+        env["PASSWORD_STORE_DIR"] = pass_dir
+
+    pass_gnupghome = __opts__["pass_gnupghome"]
+    if pass_gnupghome:
+        env["GNUPGHOME"] = pass_gnupghome
+
+    try:
+        proc = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env)
+        pass_data, pass_error = proc.communicate()
+        pass_returncode = proc.returncode
+    except OSError as e:
+        pass_data, pass_error = "", str(e)
+        pass_returncode = 1
 
     # The version of pass used during development sent output to
     # stdout instead of stderr even though its returncode was non zero.
-    if proc.returncode or not pass_data:
-        log.warning("Could not fetch secret: %s %s", pass_data, pass_error)
-        pass_data = pass_path
-    return pass_data.strip()
+    if pass_returncode or not pass_data:
+        try:
+            pass_error = pass_error.decode("utf-8")
+        except (AttributeError, ValueError):
+            pass
+        msg = "Could not fetch secret '{}' from the password store: {}".format(
+            pass_path, pass_error
+        )
+        if pass_strict_fetch:
+            raise SaltRenderError(msg)
+        else:
+            log.warning(msg)
+            return original_pass_path
+    return pass_data.rstrip("\r\n")
 
 
 def _decrypt_object(obj):
@@ -108,9 +189,4 @@ def render(pass_info, saltenv="base", sls="", argline="", 
**kwargs):
     """
     Fetch secret from pass based on pass_path
     """
-    _get_pass_exec()
-
-    # Make sure environment variable HOME is set, since Pass looks for the
-    # password-store under ~/.password-store.
-    os.environ["HOME"] = expanduser("~")
     return _decrypt_object(pass_info)
diff --git a/tests/pytests/unit/renderers/test_pass.py 
b/tests/pytests/unit/renderers/test_pass.py
new file mode 100644
index 0000000000..74e822c7ec
--- /dev/null
+++ b/tests/pytests/unit/renderers/test_pass.py
@@ -0,0 +1,164 @@
+import importlib
+
+import pytest
+
+import salt.config
+import salt.exceptions
+from tests.support.mock import MagicMock, patch
+
+# "pass" is a reserved keyword, we need to import it differently
+pass_ = importlib.import_module("salt.renderers.pass")
+
+
[email protected]
+def configure_loader_modules():
+    return {
+        pass_: {
+            "__opts__": salt.config.DEFAULT_MASTER_OPTS.copy(),
+            "_get_pass_exec": MagicMock(return_value="/usr/bin/pass"),
+        }
+    }
+
+
+# The default behavior is that if fetching a secret from pass fails,
+# the value is passed through. Even the trailing newlines are preserved.
+def test_passthrough():
+    pass_path = "secret\n"
+    expected = pass_path
+    result = pass_.render(pass_path)
+
+    assert result == expected
+
+
+# Fetch a secret in the strict mode.
+def test_strict_fetch():
+    config = {
+        "pass_variable_prefix": "pass:",
+        "pass_strict_fetch": True,
+    }
+
+    popen_mock = MagicMock(spec=pass_.Popen)
+    popen_mock.return_value.communicate.return_value = ("password123456\n", "")
+    popen_mock.return_value.returncode = 0
+
+    mocks = {
+        "Popen": popen_mock,
+    }
+
+    pass_path = "pass:secret"
+    expected = "password123456"
+    with patch.dict(pass_.__opts__, config), patch.dict(pass_.__dict__, mocks):
+        result = pass_.render(pass_path)
+
+    assert result == expected
+
+
+# Fail to fetch a secret in the strict mode.
+def test_strict_fetch_fail():
+    config = {
+        "pass_variable_prefix": "pass:",
+        "pass_strict_fetch": True,
+    }
+
+    popen_mock = MagicMock(spec=pass_.Popen)
+    popen_mock.return_value.communicate.return_value = ("", "Secret not found")
+    popen_mock.return_value.returncode = 1
+
+    mocks = {
+        "Popen": popen_mock,
+    }
+
+    pass_path = "pass:secret"
+    with patch.dict(pass_.__opts__, config), patch.dict(pass_.__dict__, mocks):
+        with pytest.raises(salt.exceptions.SaltRenderError):
+            pass_.render(pass_path)
+
+
+# Passthrough a value that doesn't have a pass prefix.
+def test_strict_fetch_passthrough():
+    config = {
+        "pass_variable_prefix": "pass:",
+        "pass_strict_fetch": True,
+    }
+
+    pass_path = "variable-without-pass-prefix\n"
+    expected = pass_path
+    with patch.dict(pass_.__opts__, config):
+        result = pass_.render(pass_path)
+
+    assert result == expected
+
+
+# Fetch a secret in the strict mode. The pass path contains spaces.
+def test_strict_fetch_pass_path_with_spaces():
+    config = {
+        "pass_variable_prefix": "pass:",
+        "pass_strict_fetch": True,
+    }
+
+    popen_mock = MagicMock(spec=pass_.Popen)
+    popen_mock.return_value.communicate.return_value = ("password123456\n", "")
+    popen_mock.return_value.returncode = 0
+
+    mocks = {
+        "Popen": popen_mock,
+    }
+
+    pass_path = "pass:se cr et"
+    with patch.dict(pass_.__opts__, config), patch.dict(pass_.__dict__, mocks):
+        pass_.render(pass_path)
+
+    call_args, call_kwargs = popen_mock.call_args_list[0]
+    assert call_args[0] == ["/usr/bin/pass", "show", "se cr et"]
+
+
+# Fetch a secret in the strict mode. The secret contains leading and trailing 
whitespaces.
+def test_strict_fetch_secret_with_whitespaces():
+    config = {
+        "pass_variable_prefix": "pass:",
+        "pass_strict_fetch": True,
+    }
+
+    popen_mock = MagicMock(spec=pass_.Popen)
+    popen_mock.return_value.communicate.return_value = (" \tpassword123456\t 
\r\n", "")
+    popen_mock.return_value.returncode = 0
+
+    mocks = {
+        "Popen": popen_mock,
+    }
+
+    pass_path = "pass:secret"
+    expected = " \tpassword123456\t "  # only the trailing newlines get striped
+    with patch.dict(pass_.__opts__, config), patch.dict(pass_.__dict__, mocks):
+        result = pass_.render(pass_path)
+
+    assert result == expected
+
+
+# Test setting env variables based on config values:
+# - pass_gnupghome -> GNUPGHOME
+# - pass_dir -> PASSWORD_STORE_DIR
+def test_env():
+    config = {
+        "pass_variable_prefix": "pass:",
+        "pass_strict_fetch": True,
+        "pass_gnupghome": "/path/to/gnupghome",
+        "pass_dir": "/path/to/secretstore",
+    }
+
+    popen_mock = MagicMock(spec=pass_.Popen)
+    popen_mock.return_value.communicate.return_value = ("password123456\n", "")
+    popen_mock.return_value.returncode = 0
+
+    mocks = {
+        "Popen": popen_mock,
+    }
+
+    pass_path = "pass:secret"
+    expected = " \tpassword123456\t "  # only the trailing newlines get striped
+    with patch.dict(pass_.__opts__, config), patch.dict(pass_.__dict__, mocks):
+        result = pass_.render(pass_path)
+
+    call_args, call_kwargs = popen_mock.call_args_list[0]
+    assert call_kwargs["env"]["GNUPGHOME"] == config["pass_gnupghome"]
+    assert call_kwargs["env"]["PASSWORD_STORE_DIR"] == config["pass_dir"]
-- 
2.37.3



++++++ retry-if-rpm-lock-is-temporarily-unavailable-547.patch ++++++
>From cedde1082b3a11b941327ba8e213f44637fb8a6b Mon Sep 17 00:00:00 2001
From: Witek Bedyk <[email protected]>
Date: Mon, 29 Aug 2022 14:16:00 +0200
Subject: [PATCH] Retry if RPM lock is temporarily unavailable (#547)

* Retry if RPM lock is temporarily unavailable

Backported from saltstack/salt#62204

Signed-off-by: Witek Bedyk <[email protected]>

* Sync formating fixes from upstream

Signed-off-by: Witek Bedyk <[email protected]>
---
 changelog/62204.fixed                |   1 +
 salt/modules/zypperpkg.py            | 117 +++++++++++++++++----------
 tests/unit/modules/test_zypperpkg.py |  45 ++++++++++-
 3 files changed, 115 insertions(+), 48 deletions(-)
 create mode 100644 changelog/62204.fixed

diff --git a/changelog/62204.fixed b/changelog/62204.fixed
new file mode 100644
index 0000000000..59f1914593
--- /dev/null
+++ b/changelog/62204.fixed
@@ -0,0 +1 @@
+Fixed Zypper module failing on RPM lock file being temporarily unavailable.
diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py
index b622105e15..7a249486fb 100644
--- a/salt/modules/zypperpkg.py
+++ b/salt/modules/zypperpkg.py
@@ -14,6 +14,7 @@ Package support for openSUSE via the zypper package manager
 
 import configparser
 import datetime
+import errno
 import fnmatch
 import logging
 import os
@@ -39,6 +40,9 @@ from salt.exceptions import CommandExecutionError, 
MinionError, SaltInvocationEr
 # pylint: disable=import-error,redefined-builtin,no-name-in-module
 from salt.utils.versions import LooseVersion
 
+if salt.utils.files.is_fcntl_available():
+    import fcntl
+
 log = logging.getLogger(__name__)
 
 HAS_ZYPP = False
@@ -106,6 +110,7 @@ class _Zypper:
     XML_DIRECTIVES = ["-x", "--xmlout"]
     # ZYPPER_LOCK is not affected by --root
     ZYPPER_LOCK = "/var/run/zypp.pid"
+    RPM_LOCK = "/var/lib/rpm/.rpm.lock"
     TAG_RELEASED = "zypper/released"
     TAG_BLOCKED = "zypper/blocked"
 
@@ -276,7 +281,7 @@ class _Zypper:
             and self.exit_code not in self.WARNING_EXIT_CODES
         )
 
-    def _is_lock(self):
+    def _is_zypper_lock(self):
         """
         Is this is a lock error code?
 
@@ -284,6 +289,23 @@ class _Zypper:
         """
         return self.exit_code == self.LOCK_EXIT_CODE
 
+    def _is_rpm_lock(self):
+        """
+        Is this an RPM lock error?
+        """
+        if salt.utils.files.is_fcntl_available():
+            if self.exit_code > 0 and os.path.exists(self.RPM_LOCK):
+                with salt.utils.files.fopen(self.RPM_LOCK, mode="w+") as rfh:
+                    try:
+                        fcntl.lockf(rfh, fcntl.LOCK_EX | fcntl.LOCK_NB)
+                    except OSError as err:
+                        if err.errno == errno.EAGAIN:
+                            return True
+                    else:
+                        fcntl.lockf(rfh, fcntl.LOCK_UN)
+
+        return False
+
     def _is_xml_mode(self):
         """
         Is Zypper's output is in XML format?
@@ -306,7 +328,7 @@ class _Zypper:
             raise CommandExecutionError("No output result from Zypper?")
 
         self.exit_code = self.__call_result["retcode"]
-        if self._is_lock():
+        if self._is_zypper_lock() or self._is_rpm_lock():
             return False
 
         if self._is_error():
@@ -387,48 +409,11 @@ class _Zypper:
             if self._check_result():
                 break
 
-            if os.path.exists(self.ZYPPER_LOCK):
-                try:
-                    with salt.utils.files.fopen(self.ZYPPER_LOCK) as rfh:
-                        data = __salt__["ps.proc_info"](
-                            int(rfh.readline()),
-                            attrs=["pid", "name", "cmdline", "create_time"],
-                        )
-                        data["cmdline"] = " ".join(data["cmdline"])
-                        data["info"] = "Blocking process created at 
{}.".format(
-                            datetime.datetime.utcfromtimestamp(
-                                data["create_time"]
-                            ).isoformat()
-                        )
-                        data["success"] = True
-                except Exception as err:  # pylint: disable=broad-except
-                    data = {
-                        "info": (
-                            "Unable to retrieve information about blocking 
process: {}".format(
-                                err.message
-                            )
-                        ),
-                        "success": False,
-                    }
-            else:
-                data = {
-                    "info": "Zypper is locked, but no Zypper lock has been 
found.",
-                    "success": False,
-                }
-
-            if not data["success"]:
-                log.debug("Unable to collect data about blocking process.")
-            else:
-                log.debug("Collected data about blocking process.")
-
-            __salt__["event.fire_master"](data, self.TAG_BLOCKED)
-            log.debug(
-                "Fired a Zypper blocked event to the master with the data: 
%s", data
-            )
-            log.debug("Waiting 5 seconds for Zypper gets released...")
-            time.sleep(5)
-            if not was_blocked:
-                was_blocked = True
+            if self._is_zypper_lock():
+                self._handle_zypper_lock_file()
+            if self._is_rpm_lock():
+                self._handle_rpm_lock_file()
+            was_blocked = True
 
         if was_blocked:
             __salt__["event.fire_master"](
@@ -451,6 +436,50 @@ class _Zypper:
             or self.__call_result["stdout"]
         )
 
+    def _handle_zypper_lock_file(self):
+        if os.path.exists(self.ZYPPER_LOCK):
+            try:
+                with salt.utils.files.fopen(self.ZYPPER_LOCK) as rfh:
+                    data = __salt__["ps.proc_info"](
+                        int(rfh.readline()),
+                        attrs=["pid", "name", "cmdline", "create_time"],
+                    )
+                    data["cmdline"] = " ".join(data["cmdline"])
+                    data["info"] = "Blocking process created at {}.".format(
+                        datetime.datetime.utcfromtimestamp(
+                            data["create_time"]
+                        ).isoformat()
+                    )
+                    data["success"] = True
+            except Exception as err:  # pylint: disable=broad-except
+                data = {
+                    "info": (
+                        "Unable to retrieve information about "
+                        "blocking process: {}".format(err)
+                    ),
+                    "success": False,
+                }
+        else:
+            data = {
+                "info": "Zypper is locked, but no Zypper lock has been found.",
+                "success": False,
+            }
+        if not data["success"]:
+            log.debug("Unable to collect data about blocking process.")
+        else:
+            log.debug("Collected data about blocking process.")
+        __salt__["event.fire_master"](data, self.TAG_BLOCKED)
+        log.debug("Fired a Zypper blocked event to the master with the data: 
%s", data)
+        log.debug("Waiting 5 seconds for Zypper gets released...")
+        time.sleep(5)
+
+    def _handle_rpm_lock_file(self):
+        data = {"info": "RPM is temporarily locked.", "success": True}
+        __salt__["event.fire_master"](data, self.TAG_BLOCKED)
+        log.debug("Fired an RPM blocked event to the master with the data: 
%s", data)
+        log.debug("Waiting 5 seconds for RPM to get released...")
+        time.sleep(5)
+
 
 __zypper__ = _Zypper()
 
diff --git a/tests/unit/modules/test_zypperpkg.py 
b/tests/unit/modules/test_zypperpkg.py
index 3f1560a385..37d555844c 100644
--- a/tests/unit/modules/test_zypperpkg.py
+++ b/tests/unit/modules/test_zypperpkg.py
@@ -4,6 +4,7 @@
 
 
 import configparser
+import errno
 import io
 import os
 from xml.dom import minidom
@@ -97,7 +98,7 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin):
         }
         with patch.dict(
             zypper.__salt__, {"cmd.run_all": MagicMock(return_value=ref_out)}
-        ):
+        ), patch.object(zypper.__zypper__, "_is_rpm_lock", return_value=False):
             upgrades = zypper.list_upgrades(refresh=False)
             self.assertEqual(len(upgrades), 3)
             for pkg, version in {
@@ -198,7 +199,9 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin):
             ' type="error">Booya!</message></stream>'
         )
         sniffer = RunSniffer(stdout=stdout_xml_snippet, retcode=1)
-        with patch.dict("salt.modules.zypperpkg.__salt__", {"cmd.run_all": 
sniffer}):
+        with patch.dict(
+            "salt.modules.zypperpkg.__salt__", {"cmd.run_all": sniffer}
+        ), patch.object(zypper.__zypper__, "_is_rpm_lock", return_value=False):
             with self.assertRaisesRegex(
                 CommandExecutionError, "^Zypper command failure: Booya!$"
             ):
@@ -232,7 +235,7 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin):
         with patch.dict(
             "salt.modules.zypperpkg.__salt__",
             {"cmd.run_all": MagicMock(return_value=ref_out)},
-        ):
+        ), patch.object(zypper.__zypper__, "_is_rpm_lock", return_value=False):
             with self.assertRaisesRegex(
                 CommandExecutionError,
                 "^Zypper command failure: Some handled zypper internal 
error{}Another"
@@ -245,7 +248,7 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin):
         with patch.dict(
             "salt.modules.zypperpkg.__salt__",
             {"cmd.run_all": MagicMock(return_value=ref_out)},
-        ):
+        ), patch.object(zypper.__zypper__, "_is_rpm_lock", return_value=False):
             with self.assertRaisesRegex(
                 CommandExecutionError, "^Zypper command failure: Check 
Zypper's logs.$"
             ):
@@ -2064,3 +2067,37 @@ pattern() = package-c"""
                 python_shell=False,
                 env={"ZYPP_READONLY_HACK": "1"},
             )
+
+    def test_is_rpm_lock_no_error(self):
+        with patch.object(os.path, "exists", return_value=True):
+            self.assertFalse(zypper.__zypper__._is_rpm_lock())
+
+    def test_rpm_lock_does_not_exist(self):
+        if salt.utils.files.is_fcntl_available():
+            zypper.__zypper__.exit_code = 1
+            with patch.object(
+                os.path, "exists", return_value=False
+            ) as mock_path_exists:
+                self.assertFalse(zypper.__zypper__._is_rpm_lock())
+                mock_path_exists.assert_called_with(zypper.__zypper__.RPM_LOCK)
+            zypper.__zypper__._reset()
+
+    def test_rpm_lock_acquirable(self):
+        if salt.utils.files.is_fcntl_available():
+            zypper.__zypper__.exit_code = 1
+            with patch.object(os.path, "exists", return_value=True), patch(
+                "fcntl.lockf", side_effect=OSError(errno.EAGAIN, "")
+            ) as lockf_mock, patch("salt.utils.files.fopen", mock_open()):
+                self.assertTrue(zypper.__zypper__._is_rpm_lock())
+                lockf_mock.assert_called()
+            zypper.__zypper__._reset()
+
+    def test_rpm_lock_not_acquirable(self):
+        if salt.utils.files.is_fcntl_available():
+            zypper.__zypper__.exit_code = 1
+            with patch.object(os.path, "exists", return_value=True), patch(
+                "fcntl.lockf"
+            ) as lockf_mock, patch("salt.utils.files.fopen", mock_open()):
+                self.assertFalse(zypper.__zypper__._is_rpm_lock())
+                self.assertEqual(lockf_mock.call_count, 2)
+            zypper.__zypper__._reset()
-- 
2.37.2

Reply via email to