test_salt_call.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  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=str(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. contents = filename.read_text()
  76. assert contents.count("foo") == 1, contents
  77. @slowTest
  78. @pytest.mark.skip_on_windows(reason=PRE_PYTEST_SKIP_REASON)
  79. def test_user_delete_kw_output(salt_call_cli):
  80. ret = salt_call_cli.run("-d", "user.delete", _timeout=120)
  81. assert ret.exitcode == 0
  82. expected_output = "salt '*' user.delete name"
  83. if not salt.utils.platform.is_windows():
  84. expected_output += " remove=True force=True"
  85. assert expected_output in ret.stdout
  86. @slowTest
  87. def test_salt_documentation_too_many_arguments(salt_call_cli):
  88. """
  89. Test to see if passing additional arguments shows an error
  90. """
  91. ret = salt_call_cli.run("-d", "virtualenv.create", "/tmp/ve")
  92. assert ret.exitcode != 0
  93. assert "You can only get documentation for one method at one time" in ret.stderr
  94. @slowTest
  95. def test_issue_6973_state_highstate_exit_code(salt_call_cli):
  96. """
  97. If there is no tops/master_tops or state file matches
  98. for this minion, salt-call should exit non-zero if invoked with
  99. option --retcode-passthrough
  100. """
  101. src = os.path.join(RUNTIME_VARS.BASE_FILES, "top.sls")
  102. dst = os.path.join(RUNTIME_VARS.BASE_FILES, "top.sls.bak")
  103. shutil.move(src, dst)
  104. expected_comment = "No states found for this minion"
  105. try:
  106. ret = salt_call_cli.run("--retcode-passthrough", "state.highstate")
  107. finally:
  108. shutil.move(dst, src)
  109. assert ret.exitcode != 0
  110. assert expected_comment in ret.stdout
  111. @slowTest
  112. @PRE_PYTEST_SKIP
  113. def test_issue_15074_output_file_append(salt_call_cli):
  114. with pytest.helpers.temp_file(name="issue-15074") as output_file_append:
  115. ret = salt_call_cli.run(
  116. "--output-file", str(output_file_append), "test.versions"
  117. )
  118. assert ret.exitcode == 0
  119. first_run_output = output_file_append.read_text()
  120. assert first_run_output
  121. ret = salt_call_cli.run(
  122. "--output-file",
  123. str(output_file_append),
  124. "--output-file-append",
  125. "test.versions",
  126. )
  127. assert ret.exitcode == 0
  128. second_run_output = output_file_append.read_text()
  129. assert second_run_output
  130. assert second_run_output == first_run_output + first_run_output
  131. @slowTest
  132. @PRE_PYTEST_SKIP
  133. def test_issue_14979_output_file_permissions(salt_call_cli):
  134. with pytest.helpers.temp_file(name="issue-14979") as output_file:
  135. with salt.utils.files.set_umask(0o077):
  136. # Let's create an initial output file with some data
  137. ret = salt_call_cli.run("--output-file", str(output_file), "--grains")
  138. assert ret.exitcode == 0
  139. try:
  140. stat1 = output_file.stat()
  141. except OSError:
  142. pytest.fail("Failed to generate output file {}".format(output_file))
  143. # Let's change umask
  144. os.umask(0o777) # pylint: disable=blacklisted-function
  145. ret = salt_call_cli.run(
  146. "--output-file", str(output_file), "--output-file-append", "--grains"
  147. )
  148. assert ret.exitcode == 0
  149. stat2 = output_file.stat()
  150. assert stat1.st_mode == stat2.st_mode
  151. # Data was appeneded to file
  152. assert stat1.st_size < stat2.st_size
  153. # Let's remove the output file
  154. output_file.unlink()
  155. # Not appending data
  156. ret = salt_call_cli.run("--output-file", str(output_file), "--grains")
  157. assert ret.exitcode == 0
  158. try:
  159. stat3 = output_file.stat()
  160. except OSError:
  161. pytest.fail("Failed to generate output file {}".format(output_file))
  162. # Mode must have changed since we're creating a new log file
  163. assert stat1.st_mode != stat3.st_mode
  164. @slowTest
  165. @pytest.mark.skip_on_windows(reason="This test does not apply on Win")
  166. def test_42116_cli_pillar_override(salt_call_cli):
  167. ret = salt_call_cli.run(
  168. "state.apply",
  169. "issue-42116-cli-pillar-override",
  170. pillar={"myhost": "localhost"},
  171. )
  172. state_run_dict = next(iter(ret.json.values()))
  173. assert state_run_dict["changes"]
  174. assert (
  175. state_run_dict["comment"] == 'Command "ping -c 2 localhost" run'
  176. ), "CLI pillar override not found in pillar data. State Run Dictionary:\n{}".format(
  177. pprint.pformat(state_run_dict)
  178. )
  179. @slowTest
  180. def test_pillar_items_masterless(
  181. salt_minion, salt_call_cli, base_env_pillar_tree_root_dir
  182. ):
  183. """
  184. Test to ensure we get expected output
  185. from pillar.items with salt-call
  186. """
  187. top_file = """
  188. base:
  189. '{}':
  190. - basic
  191. """.format(
  192. salt_minion.id
  193. )
  194. basic_pillar_file = """
  195. monty: python
  196. knights:
  197. - Lancelot
  198. - Galahad
  199. - Bedevere
  200. - Robin
  201. """
  202. top_tempfile = pytest.helpers.temp_file(
  203. "top.sls", top_file, base_env_pillar_tree_root_dir
  204. )
  205. basic_tempfile = pytest.helpers.temp_file(
  206. "basic.sls", basic_pillar_file, base_env_pillar_tree_root_dir
  207. )
  208. with top_tempfile, basic_tempfile:
  209. ret = salt_call_cli.run("--local", "pillar.items")
  210. assert ret.exitcode == 0
  211. assert "knights" in ret.json
  212. assert sorted(ret.json["knights"]) == sorted(
  213. ["Lancelot", "Galahad", "Bedevere", "Robin"]
  214. )
  215. assert "monty" in ret.json
  216. assert ret.json["monty"] == "python"
  217. @slowTest
  218. def test_masterless_highstate(salt_call_cli):
  219. """
  220. test state.highstate in masterless mode
  221. """
  222. destpath = os.path.join(RUNTIME_VARS.TMP, "testfile")
  223. ret = salt_call_cli.run("--local", "state.highstate")
  224. assert ret.exitcode == 0
  225. state_run_dict = next(iter(ret.json.values()))
  226. assert state_run_dict["result"] is True
  227. assert state_run_dict["__id__"] == destpath
  228. @slowTest
  229. @pytest.mark.skip_on_windows
  230. def test_syslog_file_not_found(salt_minion, salt_call_cli):
  231. """
  232. test when log_file is set to a syslog file that does not exist
  233. """
  234. old_cwd = os.getcwd()
  235. with pytest.helpers.temp_directory("log_file_incorrect") as config_dir:
  236. try:
  237. os.chdir(config_dir)
  238. minion_config = copy.deepcopy(salt_minion.config)
  239. minion_config["log_file"] = "file:///dev/doesnotexist"
  240. with salt.utils.files.fopen(os.path.join(config_dir, "minion"), "w") as fh_:
  241. fh_.write(salt.utils.yaml.dump(minion_config, default_flow_style=False))
  242. ret = salt_call_cli.run(
  243. "--config-dir", config_dir, "--log-level=debug", "cmd.run", "echo foo",
  244. )
  245. if sys.version_info >= (3, 5, 4):
  246. assert ret.exitcode == 0
  247. assert (
  248. "[WARNING ] The log_file does not exist. Logging not setup correctly or syslog service not started."
  249. in ret.stderr
  250. )
  251. assert ret.json == "foo", ret
  252. else:
  253. assert ret.exitcode == 2
  254. assert "Failed to setup the Syslog logging handler" in ret.stderr
  255. finally:
  256. os.chdir(old_cwd)
  257. @slowTest
  258. @PRE_PYTEST_SKIP
  259. @pytest.mark.skip_on_windows
  260. def test_return(salt_call_cli, salt_run_cli):
  261. command = "echo returnTOmaster"
  262. ret = salt_call_cli.run("cmd.run", command)
  263. assert ret.exitcode == 0
  264. assert ret.json == "returnTOmaster"
  265. ret = salt_run_cli.run("jobs.list_jobs")
  266. assert ret.exitcode == 0
  267. jid = target = None
  268. for jid, details in ret.json.items():
  269. if command in details["Arguments"]:
  270. target = details["Target"]
  271. break
  272. ret = salt_run_cli.run("jobs.lookup_jid", jid, _timeout=60)
  273. assert ret.exitcode == 0
  274. assert target in ret.json
  275. assert ret.json[target] == "returnTOmaster"
  276. @slowTest
  277. def test_exit_status_unknown_argument(salt_call_cli):
  278. """
  279. Ensure correct exit status when an unknown argument is passed to salt CLI.
  280. """
  281. ret = salt_call_cli.run("--unknown-argument")
  282. assert ret.exitcode == salt.defaults.exitcodes.EX_USAGE, ret
  283. assert "Usage" in ret.stderr
  284. assert "no such option: --unknown-argument" in ret.stderr
  285. @slowTest
  286. def test_exit_status_correct_usage(salt_call_cli):
  287. """
  288. Ensure correct exit status when salt CLI starts correctly.
  289. """
  290. ret = salt_call_cli.run("test.true")
  291. assert ret.exitcode == salt.defaults.exitcodes.EX_OK, ret
  292. @slowTest
  293. def test_context_retcode_salt_call(salt_call_cli):
  294. """
  295. Test that a nonzero retcode set in the context dunder will cause the
  296. salt CLI to set a nonzero retcode.
  297. """
  298. # Test salt-call, making sure to also confirm the behavior of
  299. # retcode_passthrough.
  300. ret = salt_call_cli.run("test.retcode", "0")
  301. assert ret.exitcode == 0, ret
  302. ret = salt_call_cli.run("test.retcode", "42")
  303. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  304. ret = salt_call_cli.run("--retcode-passthrough", "test.retcode", "42")
  305. assert ret.exitcode == 42, ret
  306. # Test a state run that exits with one or more failures
  307. ret = salt_call_cli.run("state.single", "test.fail_without_changes", "foo")
  308. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  309. ret = salt_call_cli.run(
  310. "--retcode-passthrough", "state.single", "test.fail_without_changes", "foo"
  311. )
  312. assert ret.exitcode == salt.defaults.exitcodes.EX_STATE_FAILURE, ret
  313. # Test a state compiler error
  314. ret = salt_call_cli.run("state.apply", "thisslsfiledoesnotexist")
  315. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  316. ret = salt_call_cli.run(
  317. "--retcode-passthrough", "state.apply", "thisslsfiledoesnotexist"
  318. )
  319. assert ret.exitcode == salt.defaults.exitcodes.EX_STATE_COMPILER_ERROR, ret
  320. @slowTest
  321. def test_salt_call_error(salt_call_cli):
  322. """
  323. Test that we return the expected retcode when a minion function raises
  324. an exception.
  325. """
  326. ret = salt_call_cli.run("test.raise_exception", "TypeError")
  327. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  328. ret = salt_call_cli.run(
  329. "test.raise_exception", "salt.exceptions.CommandNotFoundError"
  330. )
  331. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  332. ret = salt_call_cli.run(
  333. "test.raise_exception", "salt.exceptions.CommandExecutionError"
  334. )
  335. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  336. ret = salt_call_cli.run(
  337. "test.raise_exception", "salt.exceptions.SaltInvocationError"
  338. )
  339. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  340. ret = salt_call_cli.run(
  341. "test.raise_exception",
  342. "OSError",
  343. "2",
  344. "No such file or directory",
  345. "/tmp/foo.txt",
  346. )
  347. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret
  348. ret = salt_call_cli.run("test.echo", "{foo: bar, result: False}")
  349. assert ret.exitcode == salt.defaults.exitcodes.EX_GENERIC, ret