# -*- coding: utf-8 -*- """ :codeauthor: Erik Johnson """ 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----- lQOYBFiKrcYBCADAj92+fz20uKxxH0ffMwcryGG9IogkiUi2QrNYilB4hwrY5Qt7 Sbywlk/mSDMcABxMxS0vegqc5pgglvAnsi9w7j//9nfjiirsyiTYOOD1akTFQr7b qT6zuGFA4oYmYHvfBOena485qvlyitYLKYT9h27TDiiH6Jgt4xSRbjeyhTf3/fKD JzHA9ii5oeVi1pH/8/4USgXanBdKwO0JKQtci+PF0qe/nkzRswqTIkdgx1oyNUqL tYJ0XPOy+UyOC4J4QDIt9PQbAmiur8By4g2lLYWlGOCjs7Fcj3n5meWKzf1pmXoY lAnSab8kUZSSkoWQoTO7RbjFypULKCZui45/ABEBAAEAB/wM1wsAMtfYfx/wgxd1 yJ9HyhrKU80kMotIq/Xth3uKLecJQ2yakfYlCEDXqCTQTymT7OnwaoDeqXmnYqks 3HLRYvGdjb+8ym/GTkxapqBJfQaM6MB1QTnPHhJOE0zCrlhULK2NulxYihAMFTnk kKYviaJYLG+DcH0FQkkS0XihTKcqnsoJiS6iNd5SME3pa0qijR0D5f78fkvNzzEE 9vgAX1TgQ5PDJGN6nYlW2bWxTcg+FR2cUAQPTiP9wXCH6VyJoQay7KHVr3r/7SsU 89otfcx5HVDYPrez6xnP6wN0P/mKxCDbkERLDjZjWOmNXg2zn+/t3u02e+ybfAIp kTTxBADY/FmPgLpJ2bpcPH141twpHwhKIbENlTB9745Qknr6aLA0QVCkz49/3joO Sj+SZ7Jhl6cfbynrfHwX3b1bOFTzBUH2Tsi0HX40PezEFH0apf55FLZuMOBt/lc1 ET6evpIHF0dcM+BvZa7E7MyTyEq8S7Cc9RoJyfeGbS7MG5FfuwQA4y9QOb/OQglq ZffkVItwY52RKWb/b2WQmt+IcVax/j7DmBva765SIfPDvOCMrYhJBI/uYHQ0Zia7 SnC9+ez55wdYqgHkYojc21CIOnUvsPSj+rOpryoXzmcTuvKeVIyIA0h/mQyWjimR ENrikC4+O8GBMY6V4uvS4EFhLfHE9g0D/20lNOKkpAKPenr8iAPWcl0/pijJCGxF agnT7O2GQ9Lr5hSjW86agkevbGktu2ja5t/fHq0wpLQ4DVLMrR0/poaprTr307kW AlQV3z/C2cMHNysz4ulOgQrudQbhUEz2A8nQxRtIfWunkEugKLr1QiCkE1LJW8Np ZLxE6Qp0/KzdQva0HVNhbHQgR1BHIDxlcmlrQHNhbHRzdGFjay5jb20+iQFUBBMB CAA+FiEE+AxQ1ELHGEyFTZPYw5x3k9EbHGsFAliKrcYCGwMFCQPCZwAFCwkIBwIG FQgJCgsCBBYCAwECHgECF4AACgkQw5x3k9EbHGubUAf+PLdp1oTLVokockZgLyIQ wxOd3ofNOgNk4QoAkSMNSbtnYoQFKumRw/yGyPSIoHMsOC/ga98r8TAJEKfx3DLA rsD34oMAaYUT+XUd0KoSmlHqBrtDD1+eBASKYsCosHpCiKuQFfLKSxvpEr2YyL8L X3Q2TY5zFlGA9Eeq5g+rlb++yRZrruFN28EWtY/pyXFZgIB30ReDwPkM9hrioPZM 0Qf3+dWZSK1rWViclB51oNy4un9stTiFZptAqz4NTNssU5A4AcNQPwBwnKIYoE58 Y/Zyv8HzILGykT+qFebqRlRBI/13eHdzgJOL1iPRfjTk5Cvr+vcyIxAklXOP81ja B50DmARYiq3GAQgArnzu4SPCCQGNcCNxN4QlMP5TNvRsm5KrPbcO9j8HPfB+DRXs 6B3mnuR6OJg7YuC0C2A/m2dSHJKkF0f2AwFRpxLjJ2iAFbrZAW/N0vZDx8zO+YAU HyLu0V04wdCE5DTLkgfWNR+0uMa8qZ4Kn56Gv7O+OFE7zgTHeZ7psWlxdafeW7u6 zlC/3DWksNtuNb0vQDNMM4vgXbnORIfXdyh41zvEEnr/rKw8DuJAmo20mcv6Qi51 PqqyM62ddQOEVfiMs9l4vmwZAjGFNFNInyPXnogL6UPCDmizb6hh8aX/MwG/XFIG KMJWbAVGpyBuqljKIt3qLu/s8ouPqkEN+f+nGwARAQABAAf+NA36d/kieGxZpTQ1 oQHP1Jty+OiXhBwP8SPtF0J7ZxuZh07cs+zDsfBok/y6bsepfuFSaIq84OBQis+B kajxkp3cXZPb7l+lQLv5k++7Dd7Ien+ewSE7TQN6HLwYATrM5n5nBcc1M5C6lQGc mr0A5yz42TVG2bHsTpi9kBtsaVRSPUHSh8A8T6eOyCrT+/CAJVEEf7JyNyaqH1dy LuxI1VF3ySDEtFzuwN8EZQP9Yz/4AVyEQEA7WkNEwSQsBi2bWgWEdG+qjqnL+YKa vwe7/aJYPeL1zICnP/Osd/UcpDxR78MbozstbRljML0fTLj7UJ+XDazwv+Kl0193 2ZK2QQQAwgXvS19MYNkHO7kbNVLt1VE2ll901iC9GFHBpFUam6gmoHXpCarB+ShH 8x25aoUu4MxHmFxXd+Zq3d6q2yb57doWoPgvqcefpGmigaITnb1jhV2rt65V8deA SQazZNqBEBbZNIhfn6ObxHXXvaYaqq/UOEQ7uKyR9WMJT/rmqMEEAOY5h1R1t7AB JZ5VnhyAhdsNWw1gTcXB3o8gKz4vjdnPm0F4aVIPfB3BukETDc3sc2tKmCfUF7I7 oOrh7iRez5F0RIC3KDzXF8qUuWBfPViww45JgftdKsecCIlEEYCoc+3goX0su2bP V1MDuHijMGTJCBABDgizNb0oynW5xcrbA/0QnKfpTwi7G3oRcJWv2YebVDRcU+SP dOYhq6SnmWPizEIljRG/X7FHJB+W7tzryO3sCDTAYwxFrfMwvJ2PwnAYI4349zYd lC28HowUkBYNhwBXc48xCfyhPZtD0aLx/OX1oLZ/vi8gd8TusgGupV/JjkFVO+Nd +shN/UEAldwqkkY2iQE8BBgBCAAmFiEE+AxQ1ELHGEyFTZPYw5x3k9EbHGsFAliK rcYCGwwFCQPCZwAACgkQw5x3k9EbHGu4wwf/dRFat91BRX1TJfwJl5otoAXpItYM 6kdWWf1Eb1BicAvXhI078MSH4WXdKkJjJr1fFP8Ynil513H4Mzb0rotMAhb0jLSA lSRkMbhMvPxoS2kaYzioaBpp8yXpGiNo7dF+PJXSm/Uwp3AkcFjoVbBOqDWGgxMi DvDAstzLZ9dIcmr+OmcRQykKOKXlhEl3HnR5CyuPrA8hdVup4oeVwdkJhfJFKLLb 3fR26wxJOmIOAt24eAUy721WfQ9txNAmhdy8mY842ODZESw6WatrQjRfuqosDgrk jc0cCHsEqJNZ2AB+1uEl3tcH0tyAFJa33F0znSonP17SS1Ff9sgHYBVLUg== =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