test_salt_call.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. """
  2. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  3. tests.integration.shell.call
  4. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  5. """
  6. import copy
  7. import logging
  8. import os
  9. import pprint
  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 tests.support.helpers import PRE_PYTEST_SKIP, PRE_PYTEST_SKIP_REASON, slowTest
  19. from tests.support.runtests import RUNTIME_VARS
  20. log = logging.getLogger(__name__)
  21. pytestmark = pytest.mark.windows_whitelisted
  22. @slowTest
  23. def test_fib(salt_call_cli):
  24. ret = salt_call_cli.run("test.fib", "3")
  25. assert ret.exitcode == 0
  26. assert ret.json[0] == 2
  27. @slowTest
  28. def test_fib_txt_output(salt_call_cli):
  29. ret = salt_call_cli.run("--output=txt", "test.fib", "3")
  30. assert ret.exitcode == 0
  31. assert ret.json is None
  32. assert (
  33. re.match(r"local: \(2, [0-9]{1}\.(([0-9]+)(e-([0-9]+))?)\)\s", ret.stdout)
  34. is not None
  35. )
  36. @slowTest
  37. @pytest.mark.parametrize("indent", [-1, 0, 1])
  38. def test_json_out_indent(salt_call_cli, indent):
  39. ret = salt_call_cli.run("--out=json", "--out-indent={}".format(indent), "test.ping")
  40. assert ret.exitcode == 0
  41. assert ret.json is True
  42. if indent == -1:
  43. expected_output = '{"local": true}\n'
  44. elif indent == 0:
  45. expected_output = '{\n"local": true\n}\n'
  46. else:
  47. expected_output = '{\n "local": true\n}\n'
  48. stdout = ret.stdout
  49. assert ret.stdout == expected_output
  50. @slowTest
  51. def test_local_sls_call(salt_call_cli):
  52. fileroot = os.path.join(RUNTIME_VARS.FILES, "file", "base")
  53. ret = salt_call_cli.run(
  54. "--local", "--file-root", fileroot, "state.sls", "saltcalllocal"
  55. )
  56. assert ret.exitcode == 0
  57. state_run_dict = next(iter(ret.json.values()))
  58. assert state_run_dict["name"] == "test.echo"
  59. assert state_run_dict["result"] is True
  60. assert state_run_dict["changes"]["ret"] == "hello"
  61. @slowTest
  62. def test_local_salt_call(salt_call_cli):
  63. """
  64. This tests to make sure that salt-call does not execute the
  65. function twice, see https://github.com/saltstack/salt/pull/49552
  66. """
  67. with pytest.helpers.temp_file() as filename:
  68. ret = salt_call_cli.run(
  69. "--local", "state.single", "file.append", name=filename, text="foo"
  70. )
  71. assert ret.exitcode == 0
  72. state_run_dict = next(iter(ret.json.values()))
  73. assert state_run_dict["changes"]
  74. # 2nd sanity check: make sure that "foo" only exists once in the file
  75. with salt.utils.files.fopen(filename) as fp_:
  76. contents = fp_.read()
  77. assert contents.count("foo") == 1, contents
  78. @slowTest
  79. @pytest.mark.skip_on_windows(reason=PRE_PYTEST_SKIP_REASON)
  80. def test_user_delete_kw_output(salt_call_cli):
  81. ret = salt_call_cli.run("-d", "user.delete", _timeout=120)
  82. assert ret.exitcode == 0
  83. expected_output = "salt '*' user.delete name"
  84. if not salt.utils.platform.is_windows():
  85. expected_output += " remove=True force=True"
  86. assert expected_output in ret.stdout
  87. @slowTest
  88. def test_salt_documentation_too_many_arguments(salt_call_cli):
  89. """
  90. Test to see if passing additional arguments shows an error
  91. """
  92. ret = salt_call_cli.run("-d", "virtualenv.create", "/tmp/ve")
  93. assert ret.exitcode != 0
  94. assert "You can only get documentation for one method at one time" in ret.stderr
  95. @slowTest
  96. def test_issue_6973_state_highstate_exit_code(salt_call_cli):
  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. ret = salt_call_cli.run("--retcode-passthrough", "state.highstate")
  108. finally:
  109. shutil.move(dst, src)
  110. assert ret.exitcode != 0
  111. assert expected_comment in ret.stdout
  112. @slowTest
  113. @PRE_PYTEST_SKIP
  114. def test_issue_15074_output_file_append(salt_call_cli):
  115. with pytest.helpers.temp_file(name="issue-15074") as output_file_append:
  116. ret = salt_call_cli.run("--output-file", output_file_append, "test.versions")
  117. assert ret.exitcode == 0
  118. with salt.utils.files.fopen(output_file_append) as ofa:
  119. first_run_output = ofa.read()
  120. assert first_run_output
  121. ret = salt_call_cli.run(
  122. "--output-file",
  123. output_file_append,
  124. "--output-file-append",
  125. "test.versions",
  126. )
  127. assert ret.exitcode == 0
  128. with salt.utils.files.fopen(output_file_append) as ofa:
  129. second_run_output = ofa.read()
  130. assert second_run_output
  131. assert second_run_output == first_run_output + first_run_output
  132. @slowTest
  133. @PRE_PYTEST_SKIP
  134. def test_issue_14979_output_file_permissions(salt_call_cli):
  135. with pytest.helpers.temp_file(name="issue-14979") as output_file:
  136. with salt.utils.files.set_umask(0o077):
  137. # Let's create an initial output file with some data
  138. ret = salt_call_cli.run("--output-file", output_file, "--grains")
  139. assert ret.exitcode == 0
  140. try:
  141. stat1 = os.stat(output_file)
  142. except OSError:
  143. pytest.fail("Failed to generate output file {}".format(output_file))
  144. # Let's change umask
  145. os.umask(0o777) # pylint: disable=blacklisted-function
  146. ret = salt_call_cli.run(
  147. "--output-file", output_file, "--output-file-append", "--grains"
  148. )
  149. assert ret.exitcode == 0
  150. stat2 = os.stat(output_file)
  151. assert stat1.st_mode == stat2.st_mode
  152. # Data was appeneded to file
  153. assert stat1.st_size < stat2.st_size
  154. # Let's remove the output file
  155. os.unlink(output_file)
  156. # Not appending data
  157. ret = salt_call_cli.run("--output-file", output_file, "--grains")
  158. assert ret.exitcode == 0
  159. try:
  160. stat3 = os.stat(output_file)
  161. except OSError:
  162. pytest.fail("Failed to generate output file {}".format(output_file))
  163. # Mode must have changed since we're creating a new log file
  164. assert stat1.st_mode != stat3.st_mode
  165. @slowTest
  166. @pytest.mark.skip_on_windows(reason="This test does not apply on Win")
  167. def test_42116_cli_pillar_override(salt_call_cli):
  168. ret = salt_call_cli.run(
  169. "state.apply",
  170. "issue-42116-cli-pillar-override",
  171. pillar={"myhost": "localhost"},
  172. )
  173. state_run_dict = next(iter(ret.json.values()))
  174. assert state_run_dict["changes"]
  175. assert (
  176. state_run_dict["comment"] == 'Command "ping -c 2 localhost" run'
  177. ), "CLI pillar override not found in pillar data. State Run Dictionary:\n{}".format(
  178. pprint.pformat(state_run_dict)
  179. )
  180. @slowTest
  181. def test_pillar_items_masterless(salt_minion, salt_call_cli):
  182. """
  183. Test to ensure we get expected output
  184. from pillar.items with salt-call
  185. """
  186. TOP = """
  187. base:
  188. '{}':
  189. - generic
  190. """.format(
  191. salt_minion.id
  192. )
  193. with pytest.helpers.temp_pillar_file("top.sls", TOP):
  194. ret = salt_call_cli.run("--local", "pillar.items")
  195. assert ret.exitcode == 0
  196. assert "knights" in ret.json
  197. assert sorted(ret.json["knights"]) == sorted(
  198. ["Lancelot", "Galahad", "Bedevere", "Robin"]
  199. )
  200. assert "monty" in ret.json
  201. assert ret.json["monty"] == "python"
  202. @slowTest
  203. def test_masterless_highstate(salt_call_cli):
  204. """
  205. test state.highstate in masterless mode
  206. """
  207. destpath = os.path.join(RUNTIME_VARS.TMP, "testfile")
  208. ret = salt_call_cli.run("--local", "state.highstate")
  209. assert ret.exitcode == 0
  210. state_run_dict = next(iter(ret.json.values()))
  211. assert state_run_dict["result"] is True
  212. assert state_run_dict["__id__"] == destpath
  213. @slowTest
  214. @pytest.mark.skip_on_windows
  215. def test_syslog_file_not_found(salt_minion, salt_call_cli):
  216. """
  217. test when log_file is set to a syslog file that does not exist
  218. """
  219. old_cwd = os.getcwd()
  220. with pytest.helpers.temp_directory("log_file_incorrect") as config_dir:
  221. try:
  222. os.chdir(config_dir)
  223. minion_config = copy.deepcopy(salt_minion.config)
  224. minion_config["log_file"] = "file:///dev/doesnotexist"
  225. with salt.utils.files.fopen(os.path.join(config_dir, "minion"), "w") as fh_:
  226. fh_.write(salt.utils.yaml.dump(minion_config, default_flow_style=False))
  227. ret = salt_call_cli.run(
  228. "--config-dir", config_dir, "--log-level=debug", "cmd.run", "echo foo",
  229. )
  230. if sys.version_info >= (3, 5, 4):
  231. assert ret.exitcode == 0
  232. assert (
  233. "[WARNING ] The log_file does not exist. Logging not setup correctly or syslog service not started."
  234. in ret.stderr
  235. )
  236. assert ret.json == "foo", ret
  237. else:
  238. assert ret.exitcode == 2
  239. assert "Failed to setup the Syslog logging handler" in ret.stderr
  240. finally:
  241. os.chdir(old_cwd)
  242. @slowTest
  243. @PRE_PYTEST_SKIP
  244. @pytest.mark.skip_on_windows
  245. def test_return(salt_call_cli, salt_run_cli):
  246. command = "echo returnTOmaster"
  247. ret = salt_call_cli.run("cmd.run", command)
  248. assert ret.exitcode == 0
  249. assert ret.json == "returnTOmaster"
  250. ret = salt_run_cli.run("jobs.list_jobs")
  251. assert ret.exitcode == 0
  252. jid = target = None
  253. for jid, details in ret.json.items():
  254. if command in details["Arguments"]:
  255. target = details["Target"]
  256. break
  257. ret = salt_run_cli.run("jobs.lookup_jid", jid, _timeout=60)
  258. assert ret.exitcode == 0
  259. assert target in ret.json
  260. assert ret.json[target] == "returnTOmaster"
  261. @slowTest
  262. def test_exit_status_unknown_argument(salt_call_cli):
  263. """
  264. Ensure correct exit status when an unknown argument is passed to salt CLI.
  265. """
  266. ret = salt_call_cli.run("--unknown-argument")
  267. assert ret.exitcode == salt.defaults.exitcodes.EX_USAGE, ret
  268. assert "Usage" in ret.stderr
  269. assert "no such option: --unknown-argument" in ret.stderr
  270. @slowTest
  271. def test_exit_status_correct_usage(salt_call_cli):
  272. """
  273. Ensure correct exit status when salt CLI starts correctly.
  274. """
  275. ret = salt_call_cli.run("test.true")
  276. assert ret.exitcode == salt.defaults.exitcodes.EX_OK, ret
  277. @slowTest
  278. def test_context_retcode_salt_call(salt_call_cli):
  279. """
  280. Test that a nonzero retcode set in the context dunder will cause the
  281. salt CLI to set a nonzero retcode.
  282. """
  283. # Test salt-call, making sure to also confirm the behavior of
  284. # retcode_passthrough.
  285. ret = salt_call_cli.run("test.retcode", "0")
  286. assert ret.exitcode == 0, ret
  287. ret = salt_call_cli.run("test.retcode", "42")
  288. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  289. ret = salt_call_cli.run("--retcode-passthrough", "test.retcode", "42")
  290. assert ret.exitcode == 42, ret
  291. # Test a state run that exits with one or more failures
  292. ret = salt_call_cli.run("state.single", "test.fail_without_changes", "foo")
  293. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  294. ret = salt_call_cli.run(
  295. "--retcode-passthrough", "state.single", "test.fail_without_changes", "foo"
  296. )
  297. assert ret.exitcode == salt.defaults.exitcodes.EX_STATE_FAILURE, ret
  298. # Test a state compiler error
  299. ret = salt_call_cli.run("state.apply", "thisslsfiledoesnotexist")
  300. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  301. ret = salt_call_cli.run(
  302. "--retcode-passthrough", "state.apply", "thisslsfiledoesnotexist"
  303. )
  304. assert ret.exitcode == salt.defaults.exitcodes.EX_STATE_COMPILER_ERROR, ret
  305. @slowTest
  306. def test_salt_call_error(salt_call_cli):
  307. """
  308. Test that we return the expected retcode when a minion function raises
  309. an exception.
  310. """
  311. ret = salt_call_cli.run("test.raise_exception", "TypeError")
  312. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  313. ret = salt_call_cli.run(
  314. "test.raise_exception", "salt.exceptions.CommandNotFoundError"
  315. )
  316. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  317. ret = salt_call_cli.run(
  318. "test.raise_exception", "salt.exceptions.CommandExecutionError"
  319. )
  320. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  321. ret = salt_call_cli.run(
  322. "test.raise_exception", "salt.exceptions.SaltInvocationError"
  323. )
  324. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  325. ret = salt_call_cli.run(
  326. "test.raise_exception",
  327. "OSError",
  328. "2",
  329. "No such file or directory",
  330. "/tmp/foo.txt",
  331. )
  332. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  333. ret = salt_call_cli.run("test.echo", "{foo: bar, result: False}")
  334. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret