test_call.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  4. tests.integration.shell.call
  5. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  6. """
  7. from __future__ import absolute_import
  8. import logging
  9. import os
  10. import re
  11. import shutil
  12. import sys
  13. import pytest
  14. import salt.utils.files
  15. import salt.utils.json
  16. import salt.utils.platform
  17. import salt.utils.yaml
  18. from salt.ext import six
  19. from tests.integration.utils import testprogram
  20. from tests.support.case import ShellCase
  21. from tests.support.helpers import change_cwd, flaky, slowTest, with_tempfile
  22. from tests.support.mixins import ShellCaseCommonTestsMixin
  23. from tests.support.runtests import RUNTIME_VARS
  24. from tests.support.unit import skipIf
  25. log = logging.getLogger(__name__)
  26. @pytest.mark.windows_whitelisted
  27. class CallTest(ShellCase, testprogram.TestProgramCase, ShellCaseCommonTestsMixin):
  28. _call_binary_ = "salt-call"
  29. @slowTest
  30. def test_default_output(self):
  31. out = self.run_call("-l quiet test.fib 3")
  32. expect = ["local:", " - 2"]
  33. self.assertEqual(expect, out[:-1])
  34. @slowTest
  35. def test_text_output(self):
  36. out = self.run_call("-l quiet --out txt test.fib 3")
  37. expect = ["local: (2"]
  38. self.assertEqual("".join(expect), "".join(out).rsplit(",", 1)[0])
  39. @slowTest
  40. def test_json_out_indent(self):
  41. out = self.run_call("test.ping -l quiet --out=json --out-indent=-1")
  42. self.assertIn('"local": true', "".join(out))
  43. out = self.run_call("test.ping -l quiet --out=json --out-indent=0")
  44. self.assertIn('"local": true', "".join(out))
  45. out = self.run_call("test.ping -l quiet --out=json --out-indent=1")
  46. self.assertIn('"local": true', "".join(out))
  47. @slowTest
  48. def test_local_sls_call(self):
  49. fileroot = os.path.join(RUNTIME_VARS.FILES, "file", "base")
  50. out = self.run_call(
  51. "--file-root {0} state.sls saltcalllocal".format(fileroot), local=True
  52. )
  53. self.assertIn("Name: test.echo", "".join(out))
  54. self.assertIn("Result: True", "".join(out))
  55. self.assertIn("hello", "".join(out))
  56. self.assertIn("Succeeded: 1", "".join(out))
  57. @with_tempfile()
  58. @slowTest
  59. def test_local_salt_call(self, name):
  60. """
  61. This tests to make sure that salt-call does not execute the
  62. function twice, see https://github.com/saltstack/salt/pull/49552
  63. """
  64. def _run_call(cmd):
  65. cmd = "--out=json " + cmd
  66. return salt.utils.json.loads("".join(self.run_call(cmd, local=True)))[
  67. "local"
  68. ]
  69. ret = _run_call('state.single file.append name={0} text="foo"'.format(name))
  70. ret = ret[next(iter(ret))]
  71. # Make sure we made changes
  72. assert ret["changes"]
  73. # 2nd sanity check: make sure that "foo" only exists once in the file
  74. with salt.utils.files.fopen(name) as fp_:
  75. contents = fp_.read()
  76. assert contents.count("foo") == 1, contents
  77. @skipIf(
  78. salt.utils.platform.is_windows() or salt.utils.platform.is_darwin(),
  79. "This test requires a supported master",
  80. )
  81. @slowTest
  82. def test_user_delete_kw_output(self):
  83. ret = self.run_call("-l quiet -d user.delete")
  84. assert "salt '*' user.delete name remove=True force=True" in "".join(ret)
  85. @slowTest
  86. def test_salt_documentation_too_many_arguments(self):
  87. """
  88. Test to see if passing additional arguments shows an error
  89. """
  90. data = self.run_call("-d virtualenv.create /tmp/ve", catch_stderr=True)
  91. self.assertIn(
  92. "You can only get documentation for one method at one time",
  93. "\n".join(data[1]),
  94. )
  95. @slowTest
  96. def test_issue_6973_state_highstate_exit_code(self):
  97. """
  98. If there is no tops/master_tops or state file matches
  99. for this minion, salt-call should exit non-zero if invoked with
  100. option --retcode-passthrough
  101. """
  102. src = os.path.join(RUNTIME_VARS.BASE_FILES, "top.sls")
  103. dst = os.path.join(RUNTIME_VARS.BASE_FILES, "top.sls.bak")
  104. shutil.move(src, dst)
  105. expected_comment = "No states found for this minion"
  106. try:
  107. stdout, retcode = self.run_call(
  108. "-l quiet --retcode-passthrough state.highstate", with_retcode=True
  109. )
  110. finally:
  111. shutil.move(dst, src)
  112. self.assertIn(expected_comment, "".join(stdout))
  113. self.assertNotEqual(0, retcode)
  114. @skipIf(sys.platform.startswith("win"), "This test does not apply on Win")
  115. @skipIf(True, "to be re-enabled when #23623 is merged")
  116. def test_return(self):
  117. self.run_call('cmd.run "echo returnTOmaster"')
  118. jobs = [a for a in self.run_run("jobs.list_jobs")]
  119. self.assertTrue(True in ["returnTOmaster" in j for j in jobs])
  120. # lookback jid
  121. first_match = [(i, j) for i, j in enumerate(jobs) if "returnTOmaster" in j][0]
  122. jid, idx = None, first_match[0]
  123. while idx > 0:
  124. jid = re.match("([0-9]+):", jobs[idx])
  125. if jid:
  126. jid = jid.group(1)
  127. break
  128. idx -= 1
  129. assert idx > 0
  130. assert jid
  131. master_out = [a for a in self.run_run("jobs.lookup_jid {0}".format(jid))]
  132. self.assertTrue(True in ["returnTOmaster" in a for a in master_out])
  133. @skipIf(salt.utils.platform.is_windows(), "Skip on Windows")
  134. @slowTest
  135. def test_syslog_file_not_found(self):
  136. """
  137. test when log_file is set to a syslog file that does not exist
  138. """
  139. config_dir = os.path.join(RUNTIME_VARS.TMP, "log_file_incorrect")
  140. if not os.path.isdir(config_dir):
  141. os.makedirs(config_dir)
  142. with change_cwd(config_dir):
  143. with salt.utils.files.fopen(
  144. self.get_config_file_path("minion"), "r"
  145. ) as fh_:
  146. minion_config = salt.utils.yaml.load(fh_.read())
  147. minion_config["log_file"] = "file:///dev/doesnotexist"
  148. with salt.utils.files.fopen(
  149. os.path.join(config_dir, "minion"), "w"
  150. ) as fh_:
  151. fh_.write(
  152. salt.utils.yaml.dump(minion_config, default_flow_style=False)
  153. )
  154. ret = self.run_script(
  155. "salt-call",
  156. '--config-dir {0} cmd.run "echo foo"'.format(config_dir),
  157. timeout=120,
  158. catch_stderr=True,
  159. with_retcode=True,
  160. )
  161. try:
  162. if sys.version_info >= (3, 5, 4):
  163. self.assertIn("local:", ret[0])
  164. self.assertIn(
  165. "[WARNING ] The log_file does not exist. Logging not setup correctly or syslog service not started.",
  166. ret[1],
  167. )
  168. self.assertEqual(ret[2], 0)
  169. else:
  170. self.assertIn(
  171. "Failed to setup the Syslog logging handler", "\n".join(ret[1])
  172. )
  173. self.assertEqual(ret[2], 2)
  174. finally:
  175. if os.path.isdir(config_dir):
  176. shutil.rmtree(config_dir)
  177. @skipIf(True, "This test is unreliable. Need to investigate why more deeply.")
  178. @flaky
  179. def test_issue_15074_output_file_append(self):
  180. output_file_append = os.path.join(RUNTIME_VARS.TMP, "issue-15074")
  181. try:
  182. # Let's create an initial output file with some data
  183. _ = self.run_script(
  184. "salt-call",
  185. "-c {0} --output-file={1} test.versions".format(
  186. RUNTIME_VARS.TMP_MINION_CONF_DIR, output_file_append
  187. ),
  188. catch_stderr=True,
  189. with_retcode=True,
  190. )
  191. with salt.utils.files.fopen(output_file_append) as ofa:
  192. output = ofa.read()
  193. self.run_script(
  194. "salt-call",
  195. "-c {0} --output-file={1} --output-file-append test.versions".format(
  196. self.config_dir, output_file_append
  197. ),
  198. catch_stderr=True,
  199. with_retcode=True,
  200. )
  201. with salt.utils.files.fopen(output_file_append) as ofa:
  202. self.assertEqual(ofa.read(), output + output)
  203. finally:
  204. if os.path.exists(output_file_append):
  205. os.unlink(output_file_append)
  206. @skipIf(True, "This test is unreliable. Need to investigate why more deeply.")
  207. @flaky
  208. def test_issue_14979_output_file_permissions(self):
  209. output_file = os.path.join(RUNTIME_VARS.TMP, "issue-14979")
  210. with salt.utils.files.set_umask(0o077):
  211. try:
  212. # Let's create an initial output file with some data
  213. self.run_script(
  214. "salt-call",
  215. "-c {0} --output-file={1} -l trace -g".format(
  216. RUNTIME_VARS.TMP_MINION_CONF_DIR, output_file
  217. ),
  218. catch_stderr=True,
  219. with_retcode=True,
  220. )
  221. try:
  222. stat1 = os.stat(output_file)
  223. except OSError:
  224. self.fail("Failed to generate output file, see log for details")
  225. # Let's change umask
  226. os.umask(0o777) # pylint: disable=blacklisted-function
  227. self.run_script(
  228. "salt-call",
  229. "-c {0} --output-file={1} --output-file-append -g".format(
  230. RUNTIME_VARS.TMP_MINION_CONF_DIR, output_file
  231. ),
  232. catch_stderr=True,
  233. with_retcode=True,
  234. )
  235. try:
  236. stat2 = os.stat(output_file)
  237. except OSError:
  238. self.fail("Failed to generate output file, see log for details")
  239. self.assertEqual(stat1.st_mode, stat2.st_mode)
  240. # Data was appeneded to file
  241. self.assertTrue(stat1.st_size < stat2.st_size)
  242. # Let's remove the output file
  243. os.unlink(output_file)
  244. # Not appending data
  245. self.run_script(
  246. "salt-call",
  247. "-c {0} --output-file={1} -g".format(
  248. RUNTIME_VARS.TMP_MINION_CONF_DIR, output_file
  249. ),
  250. catch_stderr=True,
  251. with_retcode=True,
  252. )
  253. try:
  254. stat3 = os.stat(output_file)
  255. except OSError:
  256. self.fail("Failed to generate output file, see log for details")
  257. # Mode must have changed since we're creating a new log file
  258. self.assertNotEqual(stat1.st_mode, stat3.st_mode)
  259. finally:
  260. if os.path.exists(output_file):
  261. os.unlink(output_file)
  262. @skipIf(sys.platform.startswith("win"), "This test does not apply on Win")
  263. @slowTest
  264. def test_42116_cli_pillar_override(self):
  265. ret = self.run_call(
  266. "state.apply issue-42116-cli-pillar-override "
  267. 'pillar=\'{"myhost": "localhost"}\''
  268. )
  269. for line in ret:
  270. line = line.lstrip()
  271. if line == 'Comment: Command "ping -c 2 localhost" run':
  272. # Successful test
  273. break
  274. else:
  275. log.debug("salt-call output:\n\n%s", "\n".join(ret))
  276. self.fail("CLI pillar override not found in pillar data")
  277. @slowTest
  278. def test_pillar_items_masterless(self):
  279. """
  280. Test to ensure we get expected output
  281. from pillar.items with salt-call
  282. """
  283. get_items = self.run_call("pillar.items", local=True)
  284. exp_out = [
  285. " - Lancelot",
  286. " - Galahad",
  287. " - Bedevere",
  288. " monty:",
  289. " python",
  290. ]
  291. for out in exp_out:
  292. self.assertIn(out, get_items)
  293. def tearDown(self):
  294. """
  295. Teardown method to remove installed packages
  296. """
  297. user = ""
  298. user_info = self.run_call(" grains.get username", local=True)
  299. if (
  300. user_info
  301. and isinstance(user_info, (list, tuple))
  302. and isinstance(user_info[-1], six.string_types)
  303. ):
  304. user = user_info[-1].strip()
  305. super(CallTest, self).tearDown()
  306. @slowTest
  307. def test_exit_status_unknown_argument(self):
  308. """
  309. Ensure correct exit status when an unknown argument is passed to salt-call.
  310. """
  311. call = testprogram.TestProgramSaltCall(
  312. name="unknown_argument", parent_dir=self._test_dir,
  313. )
  314. # Call setup here to ensure config and script exist
  315. call.setup()
  316. stdout, stderr, status = call.run(
  317. args=["--unknown-argument"], catch_stderr=True, with_retcode=True,
  318. )
  319. self.assert_exit_status(
  320. status, "EX_USAGE", message="unknown argument", stdout=stdout, stderr=stderr
  321. )
  322. @slowTest
  323. def test_masterless_highstate(self):
  324. """
  325. test state.highstate in masterless mode
  326. """
  327. ret = self.run_call("state.highstate", local=True)
  328. destpath = os.path.join(RUNTIME_VARS.TMP, "testfile")
  329. exp_out = [
  330. " Function: file.managed",
  331. " Result: True",
  332. " ID: {0}".format(destpath),
  333. ]
  334. for out in exp_out:
  335. self.assertIn(out, ret)
  336. self.assertTrue(os.path.exists(destpath))
  337. @slowTest
  338. def test_exit_status_correct_usage(self):
  339. """
  340. Ensure correct exit status when salt-call starts correctly.
  341. """
  342. call = testprogram.TestProgramSaltCall(
  343. name="correct_usage", parent_dir=self._test_dir,
  344. )
  345. # Call setup here to ensure config and script exist
  346. call.setup()
  347. stdout, stderr, status = call.run(
  348. args=["--local", "test.true"], catch_stderr=True, with_retcode=True,
  349. )
  350. self.assert_exit_status(
  351. status, "EX_OK", message="correct usage", stdout=stdout, stderr=stderr
  352. )