Migrate integration.minion.test_pillar to PyTest

This commit is contained in:
Pedro Algarvio 2020-12-03 18:48:02 +00:00
parent 0c349e0f83
commit 19c72464df
9 changed files with 707 additions and 732 deletions

View file

@ -180,6 +180,9 @@ salt/minion.py:
- integration.minion.test_pillar
- integration.minion.test_timeout
- integration.shell.test_matcher
- pytests.functional.pillar.test_top
- pytests.functional.pillar.test_gpg
- pytests.integration.modules.test_pillar
salt/modules/*_sysctl.py:
- unit.states.test_sysctl
@ -199,8 +202,10 @@ salt/output/*:
- integration.output.test_output
salt/pillar/__init__.py:
- integration.minion.test_pillar
- integration.pillar.test_pillar_include
- pytests.functional.pillar.test_top
- pytests.functional.pillar.test_gpg
- pytests.integration.modules.test_pillar
salt/(cli/run\.py|runner\.py):
- pytests.integration.cli.test_salt_run

View file

@ -1,729 +0,0 @@
# -*- coding: utf-8 -*-
"""
:codeauthor: Erik Johnson <erik@saltstack.com>
"""
from __future__ import absolute_import
import copy
import errno
import logging
import os
import shutil
import subprocess
import textwrap
import pytest
import salt.pillar as pillar
import salt.utils.files
import salt.utils.path
import salt.utils.stringutils
import salt.utils.yaml
from tests.support.case import ModuleCase
from tests.support.helpers import dedent, requires_system_grains, slowTest
from tests.support.runtests import RUNTIME_VARS
from tests.support.unit import skipIf
log = logging.getLogger(__name__)
TEST_KEY = """\
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=06Tz
-----END PGP PRIVATE KEY BLOCK-----
"""
GPG_PILLAR_YAML = """\
secrets:
vault:
foo: |
-----BEGIN PGP MESSAGE-----
hQEMAw2B674HRhwSAQgAhTrN8NizwUv/VunVrqa4/X8t6EUulrnhKcSeb8sZS4th
W1Qz3K2NjL4lkUHCQHKZVx/VoZY7zsddBIFvvoGGfj8+2wjkEDwFmFjGE4DEsS74
ZLRFIFJC1iB/O0AiQ+oU745skQkU6OEKxqavmKMrKo3rvJ8ZCXDC470+i2/Hqrp7
+KWGmaDOO422JaSKRm5D9bQZr9oX7KqnrPG9I1+UbJyQSJdsdtquPWmeIpamEVHb
VMDNQRjSezZ1yKC4kCWm3YQbBF76qTHzG1VlLF5qOzuGI9VkyvlMaLfMibriqY73
zBbPzf6Bkp2+Y9qyzuveYMmwS4sEOuZL/PetqisWe9JGAWD/O+slQ2KRu9hNww06
KMDPJRdyj5bRuBVE4hHkkP23KrYr7SuhW2vpe7O/MvWEJ9uDNegpMLhTWruGngJh
iFndxegN9w==
=bAuo
-----END PGP MESSAGE-----
bar: this was unencrypted already
baz: |
-----BEGIN PGP MESSAGE-----
hQEMAw2B674HRhwSAQf+Ne+IfsP2IcPDrUWct8sTJrga47jQvlPCmO+7zJjOVcqz
gLjUKvMajrbI/jorBWxyAbF+5E7WdG9WHHVnuoywsyTB9rbmzuPqYCJCe+ZVyqWf
9qgJ+oUjcvYIFmH3h7H68ldqbxaAUkAOQbTRHdr253wwaTIC91ZeX0SCj64HfTg7
Izwk383CRWonEktXJpientApQFSUWNeLUWagEr/YPNFA3vzpPF5/Ia9X8/z/6oO2
q+D5W5mVsns3i2HHbg2A8Y+pm4TWnH6mTSh/gdxPqssi9qIrzGQ6H1tEoFFOEq1V
kJBe0izlfudqMq62XswzuRB4CYT5Iqw1c97T+1RqENJCASG0Wz8AGhinTdlU5iQl
JkLKqBxcBz4L70LYWyHhYwYROJWjHgKAywX5T67ftq0wi8APuZl9olnOkwSK+wrY
1OZi
=7epf
-----END PGP MESSAGE-----
qux:
- foo
- bar
- |
-----BEGIN PGP MESSAGE-----
hQEMAw2B674HRhwSAQgAg1YCmokrweoOI1c9HO0BLamWBaFPTMblOaTo0WJLZoTS
ksbQ3OJAMkrkn3BnnM/djJc5C7vNs86ZfSJ+pvE8Sp1Rhtuxh25EKMqGOn/SBedI
gR6N5vGUNiIpG5Tf3DuYAMNFDUqw8uY0MyDJI+ZW3o3xrMUABzTH0ew+Piz85FDA
YrVgwZfqyL+9OQuu6T66jOIdwQNRX2NPFZqvon8liZUPus5VzD8E5cAL9OPxQ3sF
f7/zE91YIXUTimrv3L7eCgU1dSxKhhfvA2bEUi+AskMWFXFuETYVrIhFJAKnkFmE
uZx+O9R9hADW3hM5hWHKH9/CRtb0/cC84I9oCWIQPdI+AaPtICxtsD2N8Q98hhhd
4M7I0sLZhV+4ZJqzpUsOnSpaGyfh1Zy/1d3ijJi99/l+uVHuvmMllsNmgR+ZTj0=
=LrCQ
-----END PGP MESSAGE-----
"""
GPG_PILLAR_ENCRYPTED = {
"secrets": {
"vault": {
"foo": "-----BEGIN PGP MESSAGE-----\n"
"\n"
"hQEMAw2B674HRhwSAQgAhTrN8NizwUv/VunVrqa4/X8t6EUulrnhKcSeb8sZS4th\n"
"W1Qz3K2NjL4lkUHCQHKZVx/VoZY7zsddBIFvvoGGfj8+2wjkEDwFmFjGE4DEsS74\n"
"ZLRFIFJC1iB/O0AiQ+oU745skQkU6OEKxqavmKMrKo3rvJ8ZCXDC470+i2/Hqrp7\n"
"+KWGmaDOO422JaSKRm5D9bQZr9oX7KqnrPG9I1+UbJyQSJdsdtquPWmeIpamEVHb\n"
"VMDNQRjSezZ1yKC4kCWm3YQbBF76qTHzG1VlLF5qOzuGI9VkyvlMaLfMibriqY73\n"
"zBbPzf6Bkp2+Y9qyzuveYMmwS4sEOuZL/PetqisWe9JGAWD/O+slQ2KRu9hNww06\n"
"KMDPJRdyj5bRuBVE4hHkkP23KrYr7SuhW2vpe7O/MvWEJ9uDNegpMLhTWruGngJh\n"
"iFndxegN9w==\n"
"=bAuo\n"
"-----END PGP MESSAGE-----\n",
"bar": "this was unencrypted already",
"baz": "-----BEGIN PGP MESSAGE-----\n"
"\n"
"hQEMAw2B674HRhwSAQf+Ne+IfsP2IcPDrUWct8sTJrga47jQvlPCmO+7zJjOVcqz\n"
"gLjUKvMajrbI/jorBWxyAbF+5E7WdG9WHHVnuoywsyTB9rbmzuPqYCJCe+ZVyqWf\n"
"9qgJ+oUjcvYIFmH3h7H68ldqbxaAUkAOQbTRHdr253wwaTIC91ZeX0SCj64HfTg7\n"
"Izwk383CRWonEktXJpientApQFSUWNeLUWagEr/YPNFA3vzpPF5/Ia9X8/z/6oO2\n"
"q+D5W5mVsns3i2HHbg2A8Y+pm4TWnH6mTSh/gdxPqssi9qIrzGQ6H1tEoFFOEq1V\n"
"kJBe0izlfudqMq62XswzuRB4CYT5Iqw1c97T+1RqENJCASG0Wz8AGhinTdlU5iQl\n"
"JkLKqBxcBz4L70LYWyHhYwYROJWjHgKAywX5T67ftq0wi8APuZl9olnOkwSK+wrY\n"
"1OZi\n"
"=7epf\n"
"-----END PGP MESSAGE-----\n",
"qux": [
"foo",
"bar",
"-----BEGIN PGP MESSAGE-----\n"
"\n"
"hQEMAw2B674HRhwSAQgAg1YCmokrweoOI1c9HO0BLamWBaFPTMblOaTo0WJLZoTS\n"
"ksbQ3OJAMkrkn3BnnM/djJc5C7vNs86ZfSJ+pvE8Sp1Rhtuxh25EKMqGOn/SBedI\n"
"gR6N5vGUNiIpG5Tf3DuYAMNFDUqw8uY0MyDJI+ZW3o3xrMUABzTH0ew+Piz85FDA\n"
"YrVgwZfqyL+9OQuu6T66jOIdwQNRX2NPFZqvon8liZUPus5VzD8E5cAL9OPxQ3sF\n"
"f7/zE91YIXUTimrv3L7eCgU1dSxKhhfvA2bEUi+AskMWFXFuETYVrIhFJAKnkFmE\n"
"uZx+O9R9hADW3hM5hWHKH9/CRtb0/cC84I9oCWIQPdI+AaPtICxtsD2N8Q98hhhd\n"
"4M7I0sLZhV+4ZJqzpUsOnSpaGyfh1Zy/1d3ijJi99/l+uVHuvmMllsNmgR+ZTj0=\n"
"=LrCQ\n"
"-----END PGP MESSAGE-----\n",
],
},
},
}
GPG_PILLAR_DECRYPTED = {
"secrets": {
"vault": {
"foo": "supersecret",
"bar": "this was unencrypted already",
"baz": "rosebud",
"qux": ["foo", "bar", "baz"],
},
},
}
class _CommonBase(ModuleCase):
@classmethod
def setUpClass(cls):
cls.pillar_base = os.path.join(
RUNTIME_VARS.TMP, "test-decrypt-pillar", "pillar"
)
cls.top_sls = os.path.join(cls.pillar_base, "top.sls")
cls.gpg_sls = os.path.join(cls.pillar_base, "gpg.sls")
cls.default_opts = {
"cachedir": os.path.join(RUNTIME_VARS.TMP, "rootdir", "cache"),
"optimization_order": [0, 1, 2],
"extension_modules": os.path.join(
RUNTIME_VARS.TMP, "test-decrypt-pillar", "extmods"
),
"pillar_roots": {"base": [cls.pillar_base]},
"ext_pillar_first": False,
"ext_pillar": [],
"decrypt_pillar_default": "gpg",
"decrypt_pillar_delimiter": ":",
"decrypt_pillar_renderers": ["gpg"],
}
cls.additional_opts = (
"conf_file",
"file_roots",
"state_top",
"renderer",
"renderer_whitelist",
"renderer_blacklist",
)
cls.gpg_homedir = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, "gpgkeys")
def _build_opts(self, opts):
ret = copy.deepcopy(self.default_opts)
for item in self.additional_opts:
ret[item] = self.master_opts[item]
ret.update(opts)
return ret
@pytest.mark.windows_whitelisted
class BasePillarTest(_CommonBase):
"""
Tests for pillar decryption
"""
@classmethod
def setUpClass(cls):
super(BasePillarTest, cls).setUpClass()
os.makedirs(cls.pillar_base)
with salt.utils.files.fopen(cls.top_sls, "w") as fp_:
fp_.write(
textwrap.dedent(
"""\
base:
'N@mins not L@minion':
- ng1
'N@missing_minion':
- ng2
"""
)
)
with salt.utils.files.fopen(
os.path.join(cls.pillar_base, "ng1.sls"), "w"
) as fp_:
fp_.write("pillar_from_nodegroup: True")
with salt.utils.files.fopen(
os.path.join(cls.pillar_base, "ng2.sls"), "w"
) as fp_:
fp_.write("pillar_from_nodegroup_with_ghost: True")
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.pillar_base)
def test_pillar_top_compound_match(self, grains=None):
"""
Test that a compound match topfile that refers to a nodegroup via N@ works
as expected.
"""
if not grains:
grains = {}
grains["os"] = "Fedora"
nodegroup_opts = salt.utils.yaml.safe_load(
textwrap.dedent(
"""\
nodegroups:
min: minion
sub_min: sub_minion
mins: N@min or N@sub_min
missing_minion: L@minion,ghostminion
"""
)
)
opts = self._build_opts(nodegroup_opts)
pillar_obj = pillar.Pillar(opts, grains, "minion", "base")
ret = pillar_obj.compile_pillar()
self.assertEqual(ret.get("pillar_from_nodegroup_with_ghost"), True)
self.assertEqual(ret.get("pillar_from_nodegroup"), None)
sub_pillar_obj = pillar.Pillar(opts, grains, "sub_minion", "base")
sub_ret = sub_pillar_obj.compile_pillar()
self.assertEqual(sub_ret.get("pillar_from_nodegroup_with_ghost"), None)
self.assertEqual(sub_ret.get("pillar_from_nodegroup"), True)
@skipIf(not salt.utils.path.which("gpg"), "GPG is not installed")
@pytest.mark.windows_whitelisted
class DecryptGPGPillarTest(_CommonBase):
"""
Tests for pillar decryption
"""
maxDiff = None
@classmethod
def setUpClass(cls):
super(DecryptGPGPillarTest, cls).setUpClass()
try:
os.makedirs(cls.gpg_homedir, mode=0o700)
except Exception: # pylint: disable=broad-except
cls.created_gpg_homedir = False
raise
else:
cls.created_gpg_homedir = True
cmd_prefix = ["gpg", "--homedir", cls.gpg_homedir]
cmd = cmd_prefix + ["--list-keys"]
log.debug("Instantiating gpg keyring using: %s", cmd)
output = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False
).communicate()[0]
log.debug("Result:\n%s", output)
cmd = cmd_prefix + ["--import", "--allow-secret-key-import"]
log.debug("Importing keypair using: %s", cmd)
output = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=False,
).communicate(input=salt.utils.stringutils.to_bytes(TEST_KEY))[0]
log.debug("Result:\n%s", output)
os.makedirs(cls.pillar_base)
with salt.utils.files.fopen(cls.top_sls, "w") as fp_:
fp_.write(
textwrap.dedent(
"""\
base:
'*':
- gpg
"""
)
)
with salt.utils.files.fopen(cls.gpg_sls, "w") as fp_:
fp_.write(GPG_PILLAR_YAML)
@classmethod
def tearDownClass(cls):
cmd = ["gpg-connect-agent", "--homedir", cls.gpg_homedir]
try:
log.debug("Killing gpg-agent using: %s", cmd)
output = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=False,
).communicate(input=b"KILLAGENT")[0]
log.debug("Result:\n%s", output)
except OSError:
log.debug("No need to kill: old gnupg doesn't start the agent.")
if cls.created_gpg_homedir:
try:
shutil.rmtree(cls.gpg_homedir)
except OSError as exc:
# GPG socket can disappear before rmtree gets to this point
if exc.errno != errno.ENOENT:
raise
shutil.rmtree(cls.pillar_base)
@requires_system_grains
def test_decrypt_pillar_default_renderer(self, grains=None):
"""
Test recursive decryption of secrets:vault as well as the fallback to
default decryption renderer.
"""
decrypt_pillar_opts = salt.utils.yaml.safe_load(
textwrap.dedent(
"""\
decrypt_pillar:
- 'secrets:vault'
"""
)
)
opts = self._build_opts(decrypt_pillar_opts)
pillar_obj = pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
self.assertEqual(ret, GPG_PILLAR_DECRYPTED)
@requires_system_grains
@slowTest
def test_decrypt_pillar_alternate_delimiter(self, grains=None):
"""
Test recursive decryption of secrets:vault using a pipe instead of a
colon as the nesting delimiter.
"""
decrypt_pillar_opts = salt.utils.yaml.safe_load(
textwrap.dedent(
"""\
decrypt_pillar_delimiter: '|'
decrypt_pillar:
- 'secrets|vault'
"""
)
)
opts = self._build_opts(decrypt_pillar_opts)
pillar_obj = pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
self.assertEqual(ret, GPG_PILLAR_DECRYPTED)
@requires_system_grains
def test_decrypt_pillar_deeper_nesting(self, grains=None):
"""
Test recursive decryption, only with a more deeply-nested target. This
should leave the other keys in secrets:vault encrypted.
"""
decrypt_pillar_opts = salt.utils.yaml.safe_load(
textwrap.dedent(
"""\
decrypt_pillar:
- 'secrets:vault:qux'
"""
)
)
opts = self._build_opts(decrypt_pillar_opts)
pillar_obj = pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
expected = copy.deepcopy(GPG_PILLAR_ENCRYPTED)
expected["secrets"]["vault"]["qux"][-1] = GPG_PILLAR_DECRYPTED["secrets"][
"vault"
]["qux"][-1]
self.assertEqual(ret, expected)
@requires_system_grains
def test_decrypt_pillar_explicit_renderer(self, grains=None):
"""
Test recursive decryption of secrets:vault, with the renderer
explicitly defined, overriding the default. Setting the default to a
nonexistent renderer so we can be sure that the override happened.
"""
decrypt_pillar_opts = salt.utils.yaml.safe_load(
textwrap.dedent(
"""\
decrypt_pillar_default: asdf
decrypt_pillar_renderers:
- asdf
- gpg
decrypt_pillar:
- 'secrets:vault': gpg
"""
)
)
opts = self._build_opts(decrypt_pillar_opts)
pillar_obj = pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
self.assertEqual(ret, GPG_PILLAR_DECRYPTED)
@requires_system_grains
def test_decrypt_pillar_missing_renderer(self, grains=None):
"""
Test decryption using a missing renderer. It should fail, leaving the
encrypted keys intact, and add an error to the pillar dictionary.
"""
decrypt_pillar_opts = salt.utils.yaml.safe_load(
textwrap.dedent(
"""\
decrypt_pillar_default: asdf
decrypt_pillar_renderers:
- asdf
decrypt_pillar:
- 'secrets:vault'
"""
)
)
opts = self._build_opts(decrypt_pillar_opts)
pillar_obj = pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
expected = copy.deepcopy(GPG_PILLAR_ENCRYPTED)
expected["_errors"] = [
"Failed to decrypt pillar key 'secrets:vault': Decryption "
"renderer 'asdf' is not available"
]
self.assertEqual(ret["_errors"], expected["_errors"])
self.assertEqual(
ret["secrets"]["vault"]["foo"], expected["secrets"]["vault"]["foo"]
)
self.assertEqual(
ret["secrets"]["vault"]["bar"], expected["secrets"]["vault"]["bar"]
)
self.assertEqual(
ret["secrets"]["vault"]["baz"], expected["secrets"]["vault"]["baz"]
)
self.assertEqual(
ret["secrets"]["vault"]["qux"], expected["secrets"]["vault"]["qux"]
)
@requires_system_grains
def test_decrypt_pillar_invalid_renderer(self, grains=None):
"""
Test decryption using a renderer which is not permitted. It should
fail, leaving the encrypted keys intact, and add an error to the pillar
dictionary.
"""
decrypt_pillar_opts = salt.utils.yaml.safe_load(
textwrap.dedent(
"""\
decrypt_pillar_default: foo
decrypt_pillar_renderers:
- foo
- bar
decrypt_pillar:
- 'secrets:vault': gpg
"""
)
)
opts = self._build_opts(decrypt_pillar_opts)
pillar_obj = pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
expected = copy.deepcopy(GPG_PILLAR_ENCRYPTED)
expected["_errors"] = [
"Failed to decrypt pillar key 'secrets:vault': 'gpg' is "
"not a valid decryption renderer. Valid choices are: foo, bar"
]
self.assertEqual(ret["_errors"], expected["_errors"])
self.assertEqual(
ret["secrets"]["vault"]["foo"], expected["secrets"]["vault"]["foo"]
)
self.assertEqual(
ret["secrets"]["vault"]["bar"], expected["secrets"]["vault"]["bar"]
)
self.assertEqual(
ret["secrets"]["vault"]["baz"], expected["secrets"]["vault"]["baz"]
)
self.assertEqual(
ret["secrets"]["vault"]["qux"], expected["secrets"]["vault"]["qux"]
)
@pytest.mark.windows_whitelisted
class RefreshPillarTest(ModuleCase):
"""
These tests validate the behavior defined in the documentation:
https://docs.saltstack.com/en/latest/topics/pillar/#in-memory-pillar-data-vs-on-demand-pillar-data
These tests also serve as a regression test for:
https://github.com/saltstack/salt/issues/54941
"""
def cleanup_pillars(self, top_path, pillar_path):
os.remove(top_path)
os.remove(pillar_path)
self.run_function("saltutil.refresh_pillar", arg=(True,))
def create_pillar(self, key):
"""
Utility method to create a pillar for the minion and a value of true,
this method also removes and cleans up the pillar at the end of the
test.
"""
top_path = os.path.join(RUNTIME_VARS.TMP_PILLAR_TREE, "top.sls")
pillar_path = os.path.join(RUNTIME_VARS.TMP_PILLAR_TREE, "test_pillar.sls")
with salt.utils.files.fopen(top_path, "w") as fd:
fd.write(
dedent(
"""
base:
'minion':
- test_pillar
"""
)
)
with salt.utils.files.fopen(pillar_path, "w") as fd:
fd.write(
dedent(
"""
{}: true
""".format(
key
)
)
)
self.addCleanup(self.cleanup_pillars, top_path, pillar_path)
@slowTest
def test_pillar_refresh_pillar_raw(self):
"""
Validate the minion's pillar.raw call behavior for new pillars
"""
key = "issue-54941-raw"
# We do not expect to see the pillar beacuse it does not exist yet
val = self.run_function("pillar.raw", arg=(key,))
assert val == {}
self.create_pillar(key)
# The pillar exists now but raw reads it from in-memory pillars
val = self.run_function("pillar.raw", arg=(key,))
assert val == {}
# Calling refresh_pillar to update in-memory pillars
ret = self.run_function("saltutil.refresh_pillar", arg=(True,))
# The pillar can now be read from in-memory pillars
val = self.run_function("pillar.raw", arg=(key,))
assert val is True, repr(val)
@slowTest
def test_pillar_refresh_pillar_get(self):
"""
Validate the minion's pillar.get call behavior for new pillars
"""
key = "issue-54941-get"
# We do not expect to see the pillar beacuse it does not exist yet
val = self.run_function("pillar.get", arg=(key,))
assert val == ""
top_path = os.path.join(RUNTIME_VARS.TMP_PILLAR_TREE, "top.sls")
pillar_path = os.path.join(RUNTIME_VARS.TMP_PILLAR_TREE, "test_pillar.sls")
self.create_pillar(key)
# The pillar exists now but get reads it from in-memory pillars, no
# refresh happens
val = self.run_function("pillar.get", arg=(key,))
assert val == ""
# Calling refresh_pillar to update in-memory pillars
ret = self.run_function("saltutil.refresh_pillar", arg=(True,))
assert ret is True
# The pillar can now be read from in-memory pillars
val = self.run_function("pillar.get", arg=(key,))
assert val is True, repr(val)
@slowTest
def test_pillar_refresh_pillar_item(self):
"""
Validate the minion's pillar.item call behavior for new pillars
"""
key = "issue-54941-item"
# We do not expect to see the pillar beacuse it does not exist yet
val = self.run_function("pillar.item", arg=(key,))
assert key in val
assert val[key] == ""
self.create_pillar(key)
# The pillar exists now but get reads it from in-memory pillars, no
# refresh happens
val = self.run_function("pillar.item", arg=(key,))
assert key in val
assert val[key] == ""
# Calling refresh_pillar to update in-memory pillars
ret = self.run_function("saltutil.refresh_pillar", arg=(True,))
assert ret is True
# The pillar can now be read from in-memory pillars
val = self.run_function("pillar.item", arg=(key,))
assert key in val
assert val[key] is True
@slowTest
def test_pillar_refresh_pillar_items(self):
"""
Validate the minion's pillar.item call behavior for new pillars
"""
key = "issue-54941-items"
# We do not expect to see the pillar beacuse it does not exist yet
val = self.run_function("pillar.items")
assert key not in val
self.create_pillar(key)
# A pillar.items call sees the pillar right away because a
# refresh_pillar event is fired.
val = self.run_function("pillar.items")
assert key in val
assert val[key] is True
@slowTest
def test_pillar_refresh_pillar_ping(self):
"""
Validate the minion's test.ping does not update pillars
See: https://github.com/saltstack/salt/issues/54941
"""
key = "issue-54941-ping"
# We do not expect to see the pillar beacuse it does not exist yet
val = self.run_function("pillar.item", arg=(key,))
assert key in val
assert val[key] == ""
self.create_pillar(key)
val = self.run_function("test.ping")
assert val is True
# The pillar exists now but get reads it from in-memory pillars, no
# refresh happens
val = self.run_function("pillar.item", arg=(key,))
assert key in val
assert val[key] == ""
# Calling refresh_pillar to update in-memory pillars
ret = self.run_function("saltutil.refresh_pillar", arg=(True,))
assert ret is True
# The pillar can now be read from in-memory pillars
val = self.run_function("pillar.item", arg=(key,))
assert key in val
assert val[key] is True

View file

@ -0,0 +1,55 @@
import shutil
import pytest
@pytest.fixture(scope="package")
def pillar_state_tree(tmp_path_factory):
_pillar_state_tree = tmp_path_factory.mktemp("pillar")
try:
yield _pillar_state_tree
finally:
shutil.rmtree(str(_pillar_state_tree), ignore_errors=True)
@pytest.fixture(scope="package")
def extension_modules(tmp_path_factory):
_extension_modules = tmp_path_factory.mktemp("pillar")
try:
yield _extension_modules
finally:
shutil.rmtree(str(_extension_modules), ignore_errors=True)
@pytest.fixture(scope="package")
def salt_master(salt_factories, pillar_state_tree, extension_modules):
config_defaults = {
"pillar_roots": {"base": [str(pillar_state_tree)]},
"open_mode": True,
"extension_modules": str(extension_modules),
"ext_pillar_first": False,
"ext_pillar": [],
"decrypt_pillar_default": "gpg",
"decrypt_pillar_delimiter": ":",
"decrypt_pillar_renderers": ["gpg"],
}
factory = salt_factories.get_salt_master_daemon(
"pillar-functional-master", config_defaults=config_defaults
)
return factory
@pytest.fixture(scope="package")
def salt_minion_1(salt_master):
factory = salt_master.get_salt_minion_daemon(
"pillar-functional-minion-1", config_defaults={"open_mode": True}
)
return factory
@pytest.fixture(scope="package")
def salt_minion_2(salt_master):
factory = salt_master.get_salt_minion_daemon(
"pillar-functional-minion-2", config_defaults={"open_mode": True}
)
return factory

View file

@ -0,0 +1,389 @@
import copy
import logging
import pathlib
import shutil
import subprocess
import pytest
import salt.pillar
import salt.utils.stringutils
from saltfactories.utils.processes import ProcessResult
from tests.support.helpers import slowTest
pytestmark = [
pytest.mark.windows_whitelisted,
pytest.mark.skip_if_binaries_missing("gpg"),
]
log = logging.getLogger(__name__)
TEST_KEY = """\
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=06Tz
-----END PGP PRIVATE KEY BLOCK-----
"""
GPG_PILLAR_YAML = """\
secrets:
vault:
foo: |
-----BEGIN PGP MESSAGE-----
hQEMAw2B674HRhwSAQgAhTrN8NizwUv/VunVrqa4/X8t6EUulrnhKcSeb8sZS4th
W1Qz3K2NjL4lkUHCQHKZVx/VoZY7zsddBIFvvoGGfj8+2wjkEDwFmFjGE4DEsS74
ZLRFIFJC1iB/O0AiQ+oU745skQkU6OEKxqavmKMrKo3rvJ8ZCXDC470+i2/Hqrp7
+KWGmaDOO422JaSKRm5D9bQZr9oX7KqnrPG9I1+UbJyQSJdsdtquPWmeIpamEVHb
VMDNQRjSezZ1yKC4kCWm3YQbBF76qTHzG1VlLF5qOzuGI9VkyvlMaLfMibriqY73
zBbPzf6Bkp2+Y9qyzuveYMmwS4sEOuZL/PetqisWe9JGAWD/O+slQ2KRu9hNww06
KMDPJRdyj5bRuBVE4hHkkP23KrYr7SuhW2vpe7O/MvWEJ9uDNegpMLhTWruGngJh
iFndxegN9w==
=bAuo
-----END PGP MESSAGE-----
bar: this was unencrypted already
baz: |
-----BEGIN PGP MESSAGE-----
hQEMAw2B674HRhwSAQf+Ne+IfsP2IcPDrUWct8sTJrga47jQvlPCmO+7zJjOVcqz
gLjUKvMajrbI/jorBWxyAbF+5E7WdG9WHHVnuoywsyTB9rbmzuPqYCJCe+ZVyqWf
9qgJ+oUjcvYIFmH3h7H68ldqbxaAUkAOQbTRHdr253wwaTIC91ZeX0SCj64HfTg7
Izwk383CRWonEktXJpientApQFSUWNeLUWagEr/YPNFA3vzpPF5/Ia9X8/z/6oO2
q+D5W5mVsns3i2HHbg2A8Y+pm4TWnH6mTSh/gdxPqssi9qIrzGQ6H1tEoFFOEq1V
kJBe0izlfudqMq62XswzuRB4CYT5Iqw1c97T+1RqENJCASG0Wz8AGhinTdlU5iQl
JkLKqBxcBz4L70LYWyHhYwYROJWjHgKAywX5T67ftq0wi8APuZl9olnOkwSK+wrY
1OZi
=7epf
-----END PGP MESSAGE-----
qux:
- foo
- bar
- |
-----BEGIN PGP MESSAGE-----
hQEMAw2B674HRhwSAQgAg1YCmokrweoOI1c9HO0BLamWBaFPTMblOaTo0WJLZoTS
ksbQ3OJAMkrkn3BnnM/djJc5C7vNs86ZfSJ+pvE8Sp1Rhtuxh25EKMqGOn/SBedI
gR6N5vGUNiIpG5Tf3DuYAMNFDUqw8uY0MyDJI+ZW3o3xrMUABzTH0ew+Piz85FDA
YrVgwZfqyL+9OQuu6T66jOIdwQNRX2NPFZqvon8liZUPus5VzD8E5cAL9OPxQ3sF
f7/zE91YIXUTimrv3L7eCgU1dSxKhhfvA2bEUi+AskMWFXFuETYVrIhFJAKnkFmE
uZx+O9R9hADW3hM5hWHKH9/CRtb0/cC84I9oCWIQPdI+AaPtICxtsD2N8Q98hhhd
4M7I0sLZhV+4ZJqzpUsOnSpaGyfh1Zy/1d3ijJi99/l+uVHuvmMllsNmgR+ZTj0=
=LrCQ
-----END PGP MESSAGE-----
"""
GPG_PILLAR_ENCRYPTED = {
"secrets": {
"vault": {
"foo": "-----BEGIN PGP MESSAGE-----\n"
"\n"
"hQEMAw2B674HRhwSAQgAhTrN8NizwUv/VunVrqa4/X8t6EUulrnhKcSeb8sZS4th\n"
"W1Qz3K2NjL4lkUHCQHKZVx/VoZY7zsddBIFvvoGGfj8+2wjkEDwFmFjGE4DEsS74\n"
"ZLRFIFJC1iB/O0AiQ+oU745skQkU6OEKxqavmKMrKo3rvJ8ZCXDC470+i2/Hqrp7\n"
"+KWGmaDOO422JaSKRm5D9bQZr9oX7KqnrPG9I1+UbJyQSJdsdtquPWmeIpamEVHb\n"
"VMDNQRjSezZ1yKC4kCWm3YQbBF76qTHzG1VlLF5qOzuGI9VkyvlMaLfMibriqY73\n"
"zBbPzf6Bkp2+Y9qyzuveYMmwS4sEOuZL/PetqisWe9JGAWD/O+slQ2KRu9hNww06\n"
"KMDPJRdyj5bRuBVE4hHkkP23KrYr7SuhW2vpe7O/MvWEJ9uDNegpMLhTWruGngJh\n"
"iFndxegN9w==\n"
"=bAuo\n"
"-----END PGP MESSAGE-----\n",
"bar": "this was unencrypted already",
"baz": "-----BEGIN PGP MESSAGE-----\n"
"\n"
"hQEMAw2B674HRhwSAQf+Ne+IfsP2IcPDrUWct8sTJrga47jQvlPCmO+7zJjOVcqz\n"
"gLjUKvMajrbI/jorBWxyAbF+5E7WdG9WHHVnuoywsyTB9rbmzuPqYCJCe+ZVyqWf\n"
"9qgJ+oUjcvYIFmH3h7H68ldqbxaAUkAOQbTRHdr253wwaTIC91ZeX0SCj64HfTg7\n"
"Izwk383CRWonEktXJpientApQFSUWNeLUWagEr/YPNFA3vzpPF5/Ia9X8/z/6oO2\n"
"q+D5W5mVsns3i2HHbg2A8Y+pm4TWnH6mTSh/gdxPqssi9qIrzGQ6H1tEoFFOEq1V\n"
"kJBe0izlfudqMq62XswzuRB4CYT5Iqw1c97T+1RqENJCASG0Wz8AGhinTdlU5iQl\n"
"JkLKqBxcBz4L70LYWyHhYwYROJWjHgKAywX5T67ftq0wi8APuZl9olnOkwSK+wrY\n"
"1OZi\n"
"=7epf\n"
"-----END PGP MESSAGE-----\n",
"qux": [
"foo",
"bar",
"-----BEGIN PGP MESSAGE-----\n"
"\n"
"hQEMAw2B674HRhwSAQgAg1YCmokrweoOI1c9HO0BLamWBaFPTMblOaTo0WJLZoTS\n"
"ksbQ3OJAMkrkn3BnnM/djJc5C7vNs86ZfSJ+pvE8Sp1Rhtuxh25EKMqGOn/SBedI\n"
"gR6N5vGUNiIpG5Tf3DuYAMNFDUqw8uY0MyDJI+ZW3o3xrMUABzTH0ew+Piz85FDA\n"
"YrVgwZfqyL+9OQuu6T66jOIdwQNRX2NPFZqvon8liZUPus5VzD8E5cAL9OPxQ3sF\n"
"f7/zE91YIXUTimrv3L7eCgU1dSxKhhfvA2bEUi+AskMWFXFuETYVrIhFJAKnkFmE\n"
"uZx+O9R9hADW3hM5hWHKH9/CRtb0/cC84I9oCWIQPdI+AaPtICxtsD2N8Q98hhhd\n"
"4M7I0sLZhV+4ZJqzpUsOnSpaGyfh1Zy/1d3ijJi99/l+uVHuvmMllsNmgR+ZTj0=\n"
"=LrCQ\n"
"-----END PGP MESSAGE-----\n",
],
},
},
}
GPG_PILLAR_DECRYPTED = {
"secrets": {
"vault": {
"foo": "supersecret",
"bar": "this was unencrypted already",
"baz": "rosebud",
"qux": ["foo", "bar", "baz"],
},
},
}
@pytest.fixture(scope="package", autouse=True)
def gpg_homedir(salt_master, pillar_state_tree):
_gpg_homedir = pathlib.Path(salt_master.config_dir) / "gpgkeys"
_gpg_homedir.mkdir(0o700)
agent_started = False
try:
cmd_prefix = ["gpg", "--homedir", str(_gpg_homedir)]
cmd = cmd_prefix + ["--list-keys"]
proc = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=True,
universal_newlines=True,
)
ret = ProcessResult(
exitcode=proc.returncode,
stdout=proc.stdout,
stderr=proc.stderr,
cmdline=proc.args,
)
log.debug("Instantiating gpg keyring...\n%s", ret)
cmd = cmd_prefix + ["--import", "--allow-secret-key-import"]
proc = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=True,
universal_newlines=True,
input=TEST_KEY,
)
ret = ProcessResult(
exitcode=proc.returncode,
stdout=proc.stdout,
stderr=proc.stderr,
cmdline=proc.args,
)
log.debug("Importing keypair...:\n%s", ret)
agent_started = True
top_file_contents = """
base:
'*':
- gpg
"""
with pytest.helpers.temp_file(
"top.sls", top_file_contents, pillar_state_tree
), pytest.helpers.temp_file("gpg.sls", GPG_PILLAR_YAML, pillar_state_tree):
yield _gpg_homedir
finally:
if agent_started:
try:
cmd = ["gpg-connect-agent", "--homedir", str(_gpg_homedir)]
proc = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=True,
universal_newlines=True,
input="KILLAGENT",
)
ret = ProcessResult(
exitcode=proc.returncode,
stdout=proc.stdout,
stderr=proc.stderr,
cmdline=proc.args,
)
log.debug("Killed gpg-agent...\n%s", ret)
except (OSError, subprocess.CalledProcessError):
log.debug("No need to kill: old gnupg doesn't start the agent.")
shutil.rmtree(str(_gpg_homedir), ignore_errors=True)
def test_decrypt_pillar_default_renderer(salt_master, grains):
"""
Test recursive decryption of secrets:vault as well as the fallback to
default decryption renderer.
"""
opts = salt_master.config.copy()
opts["decrypt_pillar"] = ["secrets:vault"]
pillar_obj = salt.pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
assert ret == GPG_PILLAR_DECRYPTED
@slowTest
def test_decrypt_pillar_alternate_delimiter(salt_master, grains):
"""
Test recursive decryption of secrets:vault using a pipe instead of a
colon as the nesting delimiter.
decrypt_pillar_delimiter: '|'
decrypt_pillar:
- 'secrets|vault'
"""
opts = salt_master.config.copy()
opts["decrypt_pillar"] = ["secrets|vault"]
opts["decrypt_pillar_delimiter"] = "|"
pillar_obj = salt.pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
assert ret == GPG_PILLAR_DECRYPTED
def test_decrypt_pillar_deeper_nesting(salt_master, grains):
"""
Test recursive decryption, only with a more deeply-nested target. This
should leave the other keys in secrets:vault encrypted.
decrypt_pillar:
- 'secrets:vault:qux'
"""
opts = salt_master.config.copy()
opts["decrypt_pillar"] = ["secrets:vault:qux"]
pillar_obj = salt.pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
expected = copy.deepcopy(GPG_PILLAR_ENCRYPTED)
expected["secrets"]["vault"]["qux"][-1] = GPG_PILLAR_DECRYPTED["secrets"]["vault"][
"qux"
][-1]
assert ret == expected
def test_decrypt_pillar_explicit_renderer(salt_master, grains):
"""
Test recursive decryption of secrets:vault, with the renderer
explicitly defined, overriding the default. Setting the default to a
nonexistent renderer so we can be sure that the override happened.
decrypt_pillar_default: asdf
decrypt_pillar_renderers:
- asdf
- gpg
decrypt_pillar:
- 'secrets:vault': gpg
"""
opts = salt_master.config.copy()
opts["decrypt_pillar"] = [{"secrets:vault": "gpg"}]
opts["decrypt_pillar_default"] = "asdf"
opts["decrypt_pillar_renderers"] = ["asdf", "gpg"]
pillar_obj = salt.pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
assert ret == GPG_PILLAR_DECRYPTED
def test_decrypt_pillar_missing_renderer(salt_master, grains):
"""
Test decryption using a missing renderer. It should fail, leaving the
encrypted keys intact, and add an error to the pillar dictionary.
decrypt_pillar_default: asdf
decrypt_pillar_renderers:
- asdf
decrypt_pillar:
- 'secrets:vault'
"""
opts = salt_master.config.copy()
opts["decrypt_pillar"] = ["secrets:vault"]
opts["decrypt_pillar_default"] = "asdf"
opts["decrypt_pillar_renderers"] = ["asdf"]
pillar_obj = salt.pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
expected = copy.deepcopy(GPG_PILLAR_ENCRYPTED)
expected["_errors"] = [
"Failed to decrypt pillar key 'secrets:vault': Decryption renderer 'asdf' is not available"
]
assert ret["_errors"] == expected["_errors"]
assert ret["secrets"]["vault"]["foo"] == expected["secrets"]["vault"]["foo"]
assert ret["secrets"]["vault"]["bar"] == expected["secrets"]["vault"]["bar"]
assert ret["secrets"]["vault"]["baz"] == expected["secrets"]["vault"]["baz"]
assert ret["secrets"]["vault"]["qux"] == expected["secrets"]["vault"]["qux"]
def test_decrypt_pillar_invalid_renderer(salt_master, grains):
"""
Test decryption using a renderer which is not permitted. It should
fail, leaving the encrypted keys intact, and add an error to the pillar
dictionary.
decrypt_pillar_default: foo
decrypt_pillar_renderers:
- foo
- bar
decrypt_pillar:
- 'secrets:vault': gpg
"""
opts = salt_master.config.copy()
opts["decrypt_pillar"] = [{"secrets:vault": "gpg"}]
opts["decrypt_pillar_default"] = "foo"
opts["decrypt_pillar_renderers"] = ["foo", "bar"]
pillar_obj = salt.pillar.Pillar(opts, grains, "test", "base")
ret = pillar_obj.compile_pillar()
expected = copy.deepcopy(GPG_PILLAR_ENCRYPTED)
expected["_errors"] = [
"Failed to decrypt pillar key 'secrets:vault': 'gpg' is not a valid decryption renderer. "
"Valid choices are: foo, bar"
]
assert ret["_errors"] == expected["_errors"]
assert ret["secrets"]["vault"]["foo"] == expected["secrets"]["vault"]["foo"]
assert ret["secrets"]["vault"]["bar"] == expected["secrets"]["vault"]["bar"]
assert ret["secrets"]["vault"]["baz"] == expected["secrets"]["vault"]["baz"]
assert ret["secrets"]["vault"]["qux"] == expected["secrets"]["vault"]["qux"]

View file

@ -0,0 +1,45 @@
import pytest
import salt.pillar
pytestmark = [
pytest.mark.windows_whitelisted,
]
def test_pillar_top_compound_match(salt_master, pillar_state_tree, grains):
"""
Test that a compound match topfile that refers to a nodegroup via N@ works
as expected.
"""
top_file_contents = """
base:
'N@mins not L@minion':
- ng1
'N@missing_minion':
- ng2
"""
ng1_pillar_contents = "pillar_from_nodegroup: True"
ng2_pillar_contents = "pillar_from_nodegroup_with_ghost: True"
with pytest.helpers.temp_file(
"top.sls", top_file_contents, pillar_state_tree
), pytest.helpers.temp_file(
"ng1.sls", ng1_pillar_contents, pillar_state_tree
), pytest.helpers.temp_file(
"ng2.sls", ng2_pillar_contents, pillar_state_tree
):
opts = salt_master.config.copy()
opts["nodegroups"] = {
"min": "minion",
"sub_min": "sub_minion",
"mins": "N@min or N@sub_min",
"missing_minion": "L@minion,ghostminion",
}
pillar_obj = salt.pillar.Pillar(opts, grains, "minion", "base")
ret = pillar_obj.compile_pillar()
assert ret.get("pillar_from_nodegroup_with_ghost") is True
assert ret.get("pillar_from_nodegroup") is None
sub_pillar_obj = salt.pillar.Pillar(opts, grains, "sub_minion", "base")
sub_ret = sub_pillar_obj.compile_pillar()
assert sub_ret.get("pillar_from_nodegroup_with_ghost") is None
assert sub_ret.get("pillar_from_nodegroup") is True

View file

@ -1,5 +1,7 @@
import pathlib
import textwrap
import attr
import pytest
from tests.support.helpers import slowTest
@ -154,3 +156,213 @@ def test_pillar_command_line(salt_call_cli, pillar_tree):
pillar_items = ret.json
assert "new" in pillar_items
assert pillar_items["new"] == "additional"
@attr.s
class PillarRefresh:
pillar_state_tree = attr.ib(repr=False)
salt_cli = attr.ib(repr=False)
top_file = attr.ib(init=False)
minion_1_id = attr.ib(repr=False)
minion_1_pillar = attr.ib(init=False)
def __attrs_post_init__(self):
self.top_file = self.pillar_state_tree / "top.sls"
top_file_contents = textwrap.dedent(
"""\
base:
{}:
- minion-1-pillar
""".format(
self.minion_1_id
)
)
self.top_file.write_text(top_file_contents)
self.minion_1_pillar = self.pillar_state_tree / "minion-1-pillar.sls"
def refresh_pillar(self, timeout=60, sleep=0.5):
ret = self.salt_cli.run(
"saltutil.refresh_pillar", wait=True, minion_tgt=self.minion_1_id
)
assert ret.exitcode == 0
assert ret.json is True
def __call__(self, pillar_key):
pillar_contents = "{}: true".format(pillar_key)
self.minion_1_pillar.write_text(pillar_contents)
return self
def __enter__(self):
return self
def __exit__(self, *args):
self.minion_1_pillar.unlink()
self.refresh_pillar()
@pytest.fixture
def key_pillar(salt_minion, salt_cli, base_env_pillar_tree_root_dir):
return PillarRefresh(
pillar_state_tree=base_env_pillar_tree_root_dir,
salt_cli=salt_cli,
minion_1_id=salt_minion.id,
)
@slowTest
def test_pillar_refresh_pillar_raw(salt_cli, salt_minion, key_pillar):
"""
Validate the minion's pillar.raw call behavior for new pillars
"""
key = "issue-54941-raw"
# We do not expect to see the pillar because it does not exist yet
ret = salt_cli.run("pillar.raw", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert val == {}
with key_pillar(key):
# The pillar exists now but raw reads it from in-memory pillars
ret = salt_cli.run("pillar.raw", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert val == {}
# Calling refresh_pillar to update in-memory pillars
key_pillar.refresh_pillar()
# The pillar can now be read from in-memory pillars
ret = salt_cli.run("pillar.raw", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert val is True, repr(val)
@slowTest
def test_pillar_refresh_pillar_get(salt_cli, salt_minion, key_pillar):
"""
Validate the minion's pillar.get call behavior for new pillars
"""
key = "issue-54941-get"
# We do not expect to see the pillar because it does not exist yet
ret = salt_cli.run("pillar.get", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert val == ""
with key_pillar(key):
# The pillar exists now but get reads it from in-memory pillars, no
# refresh happens
ret = salt_cli.run("pillar.get", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert val == ""
# Calling refresh_pillar to update in-memory pillars
key_pillar.refresh_pillar()
# The pillar can now be read from in-memory pillars
ret = salt_cli.run("pillar.get", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert val is True, repr(val)
@slowTest
def test_pillar_refresh_pillar_item(salt_cli, salt_minion, key_pillar):
"""
Validate the minion's pillar.item call behavior for new pillars
"""
key = "issue-54941-item"
# We do not expect to see the pillar because it does not exist yet
ret = salt_cli.run("pillar.item", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert key in val
assert val[key] == ""
with key_pillar(key):
# The pillar exists now but get reads it from in-memory pillars, no
# refresh happens
ret = salt_cli.run("pillar.item", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert key in val
assert val[key] == ""
# Calling refresh_pillar to update in-memory pillars
key_pillar.refresh_pillar()
# The pillar can now be read from in-memory pillars
ret = salt_cli.run("pillar.item", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert key in val
assert val[key] is True
@slowTest
def test_pillar_refresh_pillar_items(salt_cli, salt_minion, key_pillar):
"""
Validate the minion's pillar.item call behavior for new pillars
"""
key = "issue-54941-items"
# We do not expect to see the pillar because it does not exist yet
ret = salt_cli.run("pillar.items", minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert key not in val
with key_pillar(key):
# A pillar.items call sees the pillar right away because a
# refresh_pillar event is fired.
ret = salt_cli.run("pillar.items", minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert key in val
assert val[key] is True
@slowTest
def test_pillar_refresh_pillar_ping(salt_cli, salt_minion, key_pillar):
"""
Validate the minion's test.ping does not update pillars
See: https://github.com/saltstack/salt/issues/54941
"""
key = "issue-54941-ping"
# We do not expect to see the pillar because it does not exist yet
ret = salt_cli.run("pillar.item", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert key in val
assert val[key] == ""
with key_pillar(key):
ret = salt_cli.run("test.ping", minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert val is True
# The pillar exists now but get reads it from in-memory pillars, no
# refresh happens
ret = salt_cli.run("pillar.item", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert key in val
assert val[key] == ""
# Calling refresh_pillar to update in-memory pillars
key_pillar.refresh_pillar()
# The pillar can now be read from in-memory pillars
ret = salt_cli.run("pillar.item", key, minion_tgt=salt_minion.id)
assert ret.exitcode == 0
val = ret.json
assert key in val
assert val[key] is True

View file

@ -127,7 +127,6 @@ class BadTestModuleNamesTestCase(TestCase):
"integration.master.test_event_return",
"integration.minion.test_executor",
"integration.minion.test_minion_cache",
"integration.minion.test_pillar",
"integration.minion.test_timeout",
"integration.modules.test_decorators",
"integration.modules.test_pkg",

View file

@ -10,7 +10,6 @@ integration.grains.test_custom
integration.loader.test_ext_grains
integration.loader.test_ext_modules
integration.master.test_clear_funcs
integration.minion.test_pillar
integration.minion.test_timeout
integration.modules.test_aliases
integration.modules.test_archive