1
0

test_state.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: Nicole Thomas <nicole@saltstack.com>
  4. """
  5. from __future__ import absolute_import, print_function, unicode_literals
  6. import os
  7. import shutil
  8. import tempfile
  9. import salt.exceptions
  10. import salt.state
  11. import salt.utils.files
  12. import salt.utils.platform
  13. from salt.exceptions import CommandExecutionError
  14. from salt.utils.decorators import state as statedecorators
  15. from salt.utils.odict import OrderedDict
  16. from tests.support.helpers import slowTest, with_tempfile
  17. from tests.support.mixins import AdaptedConfigurationTestCaseMixin
  18. from tests.support.mock import MagicMock, patch
  19. from tests.support.runtests import RUNTIME_VARS
  20. from tests.support.unit import TestCase, skipIf
  21. try:
  22. import pytest
  23. except ImportError as err:
  24. pytest = None
  25. class StateCompilerTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  26. """
  27. TestCase for the state compiler.
  28. """
  29. def test_format_log_non_ascii_character(self):
  30. """
  31. Tests running a non-ascii character through the state.format_log
  32. function. See Issue #33605.
  33. """
  34. # There is no return to test against as the format_log
  35. # function doesn't return anything. However, we do want
  36. # to make sure that the function doesn't stacktrace when
  37. # called.
  38. ret = {
  39. "changes": {"Français": {"old": "something old", "new": "something new"}},
  40. "result": True,
  41. }
  42. salt.state.format_log(ret)
  43. @slowTest
  44. def test_render_error_on_invalid_requisite(self):
  45. """
  46. Test that the state compiler correctly deliver a rendering
  47. exception when a requisite cannot be resolved
  48. """
  49. with patch("salt.state.State._gather_pillar") as state_patch:
  50. high_data = {
  51. "git": OrderedDict(
  52. [
  53. (
  54. "pkg",
  55. [
  56. OrderedDict(
  57. [
  58. (
  59. "require",
  60. [
  61. OrderedDict(
  62. [
  63. (
  64. "file",
  65. OrderedDict(
  66. [("test1", "test")]
  67. ),
  68. )
  69. ]
  70. )
  71. ],
  72. )
  73. ]
  74. ),
  75. "installed",
  76. {"order": 10000},
  77. ],
  78. ),
  79. ("__sls__", "issue_35226"),
  80. ("__env__", "base"),
  81. ]
  82. )
  83. }
  84. minion_opts = self.get_temp_config("minion")
  85. minion_opts["pillar"] = {"git": OrderedDict([("test1", "test")])}
  86. state_obj = salt.state.State(minion_opts)
  87. with self.assertRaises(salt.exceptions.SaltRenderError):
  88. state_obj.call_high(high_data)
  89. def test_verify_onlyif_parse(self):
  90. low_data = {
  91. "onlyif": [{"fun": "test.arg", "args": ["arg1", "arg2"]}],
  92. "name": "mysql-server-5.7",
  93. "state": "debconf",
  94. "__id__": "set root password",
  95. "fun": "set",
  96. "__env__": "base",
  97. "__sls__": "debconf",
  98. "data": {
  99. "mysql-server/root_password": {"type": "password", "value": "temp123"}
  100. },
  101. "order": 10000,
  102. }
  103. expected_result = {"comment": "onlyif condition is true", "result": False}
  104. with patch("salt.state.State._gather_pillar") as state_patch:
  105. minion_opts = self.get_temp_config("minion")
  106. state_obj = salt.state.State(minion_opts)
  107. return_result = state_obj._run_check_onlyif(low_data, "")
  108. self.assertEqual(expected_result, return_result)
  109. def test_verify_onlyif_cmd_error(self):
  110. """
  111. Simulates a failure in cmd.retcode from onlyif
  112. This could occur is runas is specified with a user that does not exist
  113. """
  114. low_data = {
  115. "onlyif": "somecommand",
  116. "runas" "doesntexist" "name": "echo something",
  117. "state": "cmd",
  118. "__id__": "this is just a test",
  119. "fun": "run",
  120. "__env__": "base",
  121. "__sls__": "sometest",
  122. "order": 10000,
  123. }
  124. expected_result = {
  125. "comment": "onlyif condition is false",
  126. "result": True,
  127. "skip_watch": True,
  128. }
  129. with patch("salt.state.State._gather_pillar") as state_patch:
  130. minion_opts = self.get_temp_config("minion")
  131. state_obj = salt.state.State(minion_opts)
  132. mock = MagicMock(side_effect=CommandExecutionError("Boom!"))
  133. with patch.dict(state_obj.functions, {"cmd.retcode": mock}):
  134. # The mock handles the exception, but the runas dict is being passed as it would actually be
  135. return_result = state_obj._run_check_onlyif(
  136. low_data, {"runas": "doesntexist"}
  137. )
  138. self.assertEqual(expected_result, return_result)
  139. def test_verify_unless_cmd_error(self):
  140. """
  141. Simulates a failure in cmd.retcode from unless
  142. This could occur is runas is specified with a user that does not exist
  143. """
  144. low_data = {
  145. "unless": "somecommand",
  146. "runas" "doesntexist" "name": "echo something",
  147. "state": "cmd",
  148. "__id__": "this is just a test",
  149. "fun": "run",
  150. "__env__": "base",
  151. "__sls__": "sometest",
  152. "order": 10000,
  153. }
  154. expected_result = {
  155. "comment": "unless condition is true",
  156. "result": True,
  157. "skip_watch": True,
  158. }
  159. with patch("salt.state.State._gather_pillar") as state_patch:
  160. minion_opts = self.get_temp_config("minion")
  161. state_obj = salt.state.State(minion_opts)
  162. mock = MagicMock(side_effect=CommandExecutionError("Boom!"))
  163. with patch.dict(state_obj.functions, {"cmd.retcode": mock}):
  164. # The mock handles the exception, but the runas dict is being passed as it would actually be
  165. return_result = state_obj._run_check_unless(
  166. low_data, {"runas": "doesntexist"}
  167. )
  168. self.assertEqual(expected_result, return_result)
  169. def test_verify_unless_parse(self):
  170. low_data = {
  171. "unless": [{"fun": "test.arg", "args": ["arg1", "arg2"]}],
  172. "name": "mysql-server-5.7",
  173. "state": "debconf",
  174. "__id__": "set root password",
  175. "fun": "set",
  176. "__env__": "base",
  177. "__sls__": "debconf",
  178. "data": {
  179. "mysql-server/root_password": {"type": "password", "value": "temp123"}
  180. },
  181. "order": 10000,
  182. }
  183. expected_result = {
  184. "comment": "unless condition is true",
  185. "result": True,
  186. "skip_watch": True,
  187. }
  188. with patch("salt.state.State._gather_pillar") as state_patch:
  189. minion_opts = self.get_temp_config("minion")
  190. state_obj = salt.state.State(minion_opts)
  191. return_result = state_obj._run_check_unless(low_data, "")
  192. self.assertEqual(expected_result, return_result)
  193. def test_verify_creates(self):
  194. low_data = {
  195. "state": "cmd",
  196. "name": 'echo "something"',
  197. "__sls__": "tests.creates",
  198. "__env__": "base",
  199. "__id__": "do_a_thing",
  200. "creates": "/tmp/thing",
  201. "order": 10000,
  202. "fun": "run",
  203. }
  204. with patch("salt.state.State._gather_pillar") as state_patch:
  205. minion_opts = self.get_temp_config("minion")
  206. state_obj = salt.state.State(minion_opts)
  207. with patch("os.path.exists") as path_mock:
  208. path_mock.return_value = True
  209. expected_result = {
  210. "comment": "/tmp/thing exists",
  211. "result": True,
  212. "skip_watch": True,
  213. }
  214. return_result = state_obj._run_check_creates(low_data)
  215. self.assertEqual(expected_result, return_result)
  216. path_mock.return_value = False
  217. expected_result = {
  218. "comment": "Creates files not found",
  219. "result": False,
  220. }
  221. return_result = state_obj._run_check_creates(low_data)
  222. self.assertEqual(expected_result, return_result)
  223. def test_verify_creates_list(self):
  224. low_data = {
  225. "state": "cmd",
  226. "name": 'echo "something"',
  227. "__sls__": "tests.creates",
  228. "__env__": "base",
  229. "__id__": "do_a_thing",
  230. "creates": ["/tmp/thing", "/tmp/thing2"],
  231. "order": 10000,
  232. "fun": "run",
  233. }
  234. with patch("salt.state.State._gather_pillar") as state_patch:
  235. minion_opts = self.get_temp_config("minion")
  236. state_obj = salt.state.State(minion_opts)
  237. with patch("os.path.exists") as path_mock:
  238. path_mock.return_value = True
  239. expected_result = {
  240. "comment": "All files in creates exist",
  241. "result": True,
  242. "skip_watch": True,
  243. }
  244. return_result = state_obj._run_check_creates(low_data)
  245. self.assertEqual(expected_result, return_result)
  246. path_mock.return_value = False
  247. expected_result = {
  248. "comment": "Creates files not found",
  249. "result": False,
  250. }
  251. return_result = state_obj._run_check_creates(low_data)
  252. self.assertEqual(expected_result, return_result)
  253. def _expand_win_path(self, path):
  254. """
  255. Expand C:/users/admini~1/appdata/local/temp/salt-tests-tmpdir/...
  256. into C:/users/adminitrator/appdata/local/temp/salt-tests-tmpdir/...
  257. to prevent file.search from expanding the "~" with os.path.expanduser
  258. """
  259. if salt.utils.platform.is_windows():
  260. import win32file
  261. return win32file.GetLongPathName(path).replace("\\", "/")
  262. else:
  263. return path
  264. @with_tempfile()
  265. def test_verify_onlyif_parse_slots(self, name):
  266. with salt.utils.files.fopen(name, "w") as fp:
  267. fp.write("file-contents")
  268. low_data = {
  269. "onlyif": [
  270. {
  271. "fun": "file.search",
  272. "args": [
  273. "__slot__:salt:test.echo({})".format(
  274. self._expand_win_path(name)
  275. ),
  276. ],
  277. "pattern": "__slot__:salt:test.echo(file-contents)",
  278. }
  279. ],
  280. "name": "mysql-server-5.7",
  281. "state": "debconf",
  282. "__id__": "set root password",
  283. "fun": "set",
  284. "__env__": "base",
  285. "__sls__": "debconf",
  286. "data": {
  287. "mysql-server/root_password": {"type": "password", "value": "temp123"}
  288. },
  289. "order": 10000,
  290. }
  291. expected_result = {"comment": "onlyif condition is true", "result": False}
  292. with patch("salt.state.State._gather_pillar") as state_patch:
  293. minion_opts = self.get_temp_config("minion")
  294. state_obj = salt.state.State(minion_opts)
  295. return_result = state_obj._run_check_onlyif(low_data, "")
  296. self.assertEqual(expected_result, return_result)
  297. def test_verify_onlyif_list_cmd(self):
  298. low_data = {
  299. "state": "cmd",
  300. "name": 'echo "something"',
  301. "__sls__": "tests.cmd",
  302. "__env__": "base",
  303. "__id__": "check onlyif",
  304. "onlyif": ["/bin/true", "/bin/false"],
  305. "order": 10001,
  306. "fun": "run",
  307. }
  308. expected_result = {
  309. "comment": "onlyif condition is false",
  310. "result": True,
  311. "skip_watch": True,
  312. }
  313. with patch("salt.state.State._gather_pillar") as state_patch:
  314. minion_opts = self.get_temp_config("minion")
  315. state_obj = salt.state.State(minion_opts)
  316. return_result = state_obj._run_check_onlyif(low_data, {})
  317. self.assertEqual(expected_result, return_result)
  318. @with_tempfile()
  319. def test_verify_unless_parse_slots(self, name):
  320. with salt.utils.files.fopen(name, "w") as fp:
  321. fp.write("file-contents")
  322. low_data = {
  323. "unless": [
  324. {
  325. "fun": "file.search",
  326. "args": [
  327. "__slot__:salt:test.echo({})".format(
  328. self._expand_win_path(name)
  329. ),
  330. ],
  331. "pattern": "__slot__:salt:test.echo(file-contents)",
  332. }
  333. ],
  334. "name": "mysql-server-5.7",
  335. "state": "debconf",
  336. "__id__": "set root password",
  337. "fun": "set",
  338. "__env__": "base",
  339. "__sls__": "debconf",
  340. "data": {
  341. "mysql-server/root_password": {"type": "password", "value": "temp123"}
  342. },
  343. "order": 10000,
  344. }
  345. expected_result = {
  346. "comment": "unless condition is true",
  347. "result": True,
  348. "skip_watch": True,
  349. }
  350. with patch("salt.state.State._gather_pillar") as state_patch:
  351. minion_opts = self.get_temp_config("minion")
  352. state_obj = salt.state.State(minion_opts)
  353. return_result = state_obj._run_check_unless(low_data, "")
  354. self.assertEqual(expected_result, return_result)
  355. def test_verify_retry_parsing(self):
  356. low_data = {
  357. "state": "file",
  358. "name": "/tmp/saltstack.README.rst",
  359. "__sls__": "demo.download",
  360. "__env__": "base",
  361. "__id__": "download sample data",
  362. "retry": {"attempts": 5, "interval": 5},
  363. "unless": ["test -f /tmp/saltstack.README.rst"],
  364. "source": [
  365. "https://raw.githubusercontent.com/saltstack/salt/develop/README.rst"
  366. ],
  367. "source_hash": "f2bc8c0aa2ae4f5bb5c2051686016b48",
  368. "order": 10000,
  369. "fun": "managed",
  370. }
  371. expected_result = {
  372. "__id__": "download sample data",
  373. "__run_num__": 0,
  374. "__sls__": "demo.download",
  375. "changes": {},
  376. "comment": "['unless condition is true'] The state would be retried every 5 "
  377. "seconds (with a splay of up to 0 seconds) a maximum of 5 times or "
  378. "until a result of True is returned",
  379. "name": "/tmp/saltstack.README.rst",
  380. "result": True,
  381. "skip_watch": True,
  382. }
  383. with patch("salt.state.State._gather_pillar") as state_patch:
  384. minion_opts = self.get_temp_config("minion")
  385. minion_opts["test"] = True
  386. minion_opts["file_client"] = "local"
  387. state_obj = salt.state.State(minion_opts)
  388. mock = {
  389. "result": True,
  390. "comment": ["unless condition is true"],
  391. "skip_watch": True,
  392. }
  393. with patch.object(state_obj, "_run_check", return_value=mock):
  394. self.assertDictContainsSubset(expected_result, state_obj.call(low_data))
  395. def test_render_requisite_require_disabled(self):
  396. """
  397. Test that the state compiler correctly deliver a rendering
  398. exception when a requisite cannot be resolved
  399. """
  400. with patch("salt.state.State._gather_pillar") as state_patch:
  401. high_data = {
  402. "step_one": OrderedDict(
  403. [
  404. (
  405. "test",
  406. [
  407. OrderedDict(
  408. [("require", [OrderedDict([("test", "step_two")])])]
  409. ),
  410. "succeed_with_changes",
  411. {"order": 10000},
  412. ],
  413. ),
  414. ("__sls__", "test.disable_require"),
  415. ("__env__", "base"),
  416. ]
  417. ),
  418. "step_two": {
  419. "test": ["succeed_with_changes", {"order": 10001}],
  420. "__env__": "base",
  421. "__sls__": "test.disable_require",
  422. },
  423. }
  424. minion_opts = self.get_temp_config("minion")
  425. minion_opts["disabled_requisites"] = ["require"]
  426. state_obj = salt.state.State(minion_opts)
  427. ret = state_obj.call_high(high_data)
  428. run_num = ret["test_|-step_one_|-step_one_|-succeed_with_changes"][
  429. "__run_num__"
  430. ]
  431. self.assertEqual(run_num, 0)
  432. def test_render_requisite_require_in_disabled(self):
  433. """
  434. Test that the state compiler correctly deliver a rendering
  435. exception when a requisite cannot be resolved
  436. """
  437. with patch("salt.state.State._gather_pillar") as state_patch:
  438. high_data = {
  439. "step_one": {
  440. "test": ["succeed_with_changes", {"order": 10000}],
  441. "__env__": "base",
  442. "__sls__": "test.disable_require_in",
  443. },
  444. "step_two": OrderedDict(
  445. [
  446. (
  447. "test",
  448. [
  449. OrderedDict(
  450. [
  451. (
  452. "require_in",
  453. [OrderedDict([("test", "step_one")])],
  454. )
  455. ]
  456. ),
  457. "succeed_with_changes",
  458. {"order": 10001},
  459. ],
  460. ),
  461. ("__sls__", "test.disable_require_in"),
  462. ("__env__", "base"),
  463. ]
  464. ),
  465. }
  466. minion_opts = self.get_temp_config("minion")
  467. minion_opts["disabled_requisites"] = ["require_in"]
  468. state_obj = salt.state.State(minion_opts)
  469. ret = state_obj.call_high(high_data)
  470. run_num = ret["test_|-step_one_|-step_one_|-succeed_with_changes"][
  471. "__run_num__"
  472. ]
  473. self.assertEqual(run_num, 0)
  474. class HighStateTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  475. def setUp(self):
  476. root_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  477. self.state_tree_dir = os.path.join(root_dir, "state_tree")
  478. cache_dir = os.path.join(root_dir, "cachedir")
  479. for dpath in (root_dir, self.state_tree_dir, cache_dir):
  480. if not os.path.isdir(dpath):
  481. os.makedirs(dpath)
  482. overrides = {}
  483. overrides["root_dir"] = root_dir
  484. overrides["state_events"] = False
  485. overrides["id"] = "match"
  486. overrides["file_client"] = "local"
  487. overrides["file_roots"] = dict(base=[self.state_tree_dir])
  488. overrides["cachedir"] = cache_dir
  489. overrides["test"] = False
  490. self.config = self.get_temp_config("minion", **overrides)
  491. self.addCleanup(delattr, self, "config")
  492. self.highstate = salt.state.HighState(self.config)
  493. self.addCleanup(delattr, self, "highstate")
  494. self.highstate.push_active()
  495. def tearDown(self):
  496. self.highstate.pop_active()
  497. def test_top_matches_with_list(self):
  498. top = {"env": {"match": ["state1", "state2"], "nomatch": ["state3"]}}
  499. matches = self.highstate.top_matches(top)
  500. self.assertEqual(matches, {"env": ["state1", "state2"]})
  501. def test_top_matches_with_string(self):
  502. top = {"env": {"match": "state1", "nomatch": "state2"}}
  503. matches = self.highstate.top_matches(top)
  504. self.assertEqual(matches, {"env": ["state1"]})
  505. def test_matches_whitelist(self):
  506. matches = {"env": ["state1", "state2", "state3"]}
  507. matches = self.highstate.matches_whitelist(matches, ["state2"])
  508. self.assertEqual(matches, {"env": ["state2"]})
  509. def test_matches_whitelist_with_string(self):
  510. matches = {"env": ["state1", "state2", "state3"]}
  511. matches = self.highstate.matches_whitelist(matches, "state2,state3")
  512. self.assertEqual(matches, {"env": ["state2", "state3"]})
  513. def test_show_state_usage(self):
  514. # monkey patch sub methods
  515. self.highstate.avail = {"base": ["state.a", "state.b", "state.c"]}
  516. def verify_tops(*args, **kwargs):
  517. return []
  518. def get_top(*args, **kwargs):
  519. return None
  520. def top_matches(*args, **kwargs):
  521. return {"base": ["state.a", "state.b"]}
  522. self.highstate.verify_tops = verify_tops
  523. self.highstate.get_top = get_top
  524. self.highstate.top_matches = top_matches
  525. # get compile_state_usage() result
  526. state_usage_dict = self.highstate.compile_state_usage()
  527. self.assertEqual(state_usage_dict["base"]["count_unused"], 1)
  528. self.assertEqual(state_usage_dict["base"]["count_used"], 2)
  529. self.assertEqual(state_usage_dict["base"]["count_all"], 3)
  530. self.assertEqual(state_usage_dict["base"]["used"], ["state.a", "state.b"])
  531. self.assertEqual(state_usage_dict["base"]["unused"], ["state.c"])
  532. def test_find_sls_ids_with_exclude(self):
  533. """
  534. See https://github.com/saltstack/salt/issues/47182
  535. """
  536. sls_dir = "issue-47182"
  537. shutil.copytree(
  538. os.path.join(RUNTIME_VARS.BASE_FILES, sls_dir),
  539. os.path.join(self.state_tree_dir, sls_dir),
  540. )
  541. shutil.move(
  542. os.path.join(self.state_tree_dir, sls_dir, "top.sls"), self.state_tree_dir
  543. )
  544. # Manually compile the high data. We don't have to worry about all of
  545. # the normal error checking we do here since we know that all the SLS
  546. # files exist and there is no whitelist/blacklist being used.
  547. top = self.highstate.get_top() # pylint: disable=assignment-from-none
  548. matches = self.highstate.top_matches(top)
  549. high, _ = self.highstate.render_highstate(matches)
  550. ret = salt.state.find_sls_ids("issue-47182.stateA.newer", high)
  551. self.assertEqual(ret, [("somestuff", "cmd")])
  552. class MultiEnvHighStateTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  553. def setUp(self):
  554. root_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  555. self.base_state_tree_dir = os.path.join(root_dir, "base")
  556. self.other_state_tree_dir = os.path.join(root_dir, "other")
  557. cache_dir = os.path.join(root_dir, "cachedir")
  558. for dpath in (
  559. root_dir,
  560. self.base_state_tree_dir,
  561. self.other_state_tree_dir,
  562. cache_dir,
  563. ):
  564. if not os.path.isdir(dpath):
  565. os.makedirs(dpath)
  566. shutil.copy(
  567. os.path.join(RUNTIME_VARS.BASE_FILES, "top.sls"), self.base_state_tree_dir
  568. )
  569. shutil.copy(
  570. os.path.join(RUNTIME_VARS.BASE_FILES, "core.sls"), self.base_state_tree_dir
  571. )
  572. shutil.copy(
  573. os.path.join(RUNTIME_VARS.BASE_FILES, "test.sls"), self.other_state_tree_dir
  574. )
  575. overrides = {}
  576. overrides["root_dir"] = root_dir
  577. overrides["state_events"] = False
  578. overrides["id"] = "match"
  579. overrides["file_client"] = "local"
  580. overrides["file_roots"] = dict(
  581. base=[self.base_state_tree_dir], other=[self.other_state_tree_dir]
  582. )
  583. overrides["cachedir"] = cache_dir
  584. overrides["test"] = False
  585. self.config = self.get_temp_config("minion", **overrides)
  586. self.addCleanup(delattr, self, "config")
  587. self.highstate = salt.state.HighState(self.config)
  588. self.addCleanup(delattr, self, "highstate")
  589. self.highstate.push_active()
  590. def tearDown(self):
  591. self.highstate.pop_active()
  592. def test_lazy_avail_states_base(self):
  593. # list_states not called yet
  594. self.assertEqual(self.highstate.avail._filled, False)
  595. self.assertEqual(self.highstate.avail._avail, {"base": None})
  596. # After getting 'base' env available states
  597. self.highstate.avail["base"] # pylint: disable=pointless-statement
  598. self.assertEqual(self.highstate.avail._filled, False)
  599. self.assertEqual(self.highstate.avail._avail, {"base": ["core", "top"]})
  600. def test_lazy_avail_states_other(self):
  601. # list_states not called yet
  602. self.assertEqual(self.highstate.avail._filled, False)
  603. self.assertEqual(self.highstate.avail._avail, {"base": None})
  604. # After getting 'other' env available states
  605. self.highstate.avail["other"] # pylint: disable=pointless-statement
  606. self.assertEqual(self.highstate.avail._filled, True)
  607. self.assertEqual(self.highstate.avail._avail, {"base": None, "other": ["test"]})
  608. def test_lazy_avail_states_multi(self):
  609. # list_states not called yet
  610. self.assertEqual(self.highstate.avail._filled, False)
  611. self.assertEqual(self.highstate.avail._avail, {"base": None})
  612. # After getting 'base' env available states
  613. self.highstate.avail["base"] # pylint: disable=pointless-statement
  614. self.assertEqual(self.highstate.avail._filled, False)
  615. self.assertEqual(self.highstate.avail._avail, {"base": ["core", "top"]})
  616. # After getting 'other' env available states
  617. self.highstate.avail["other"] # pylint: disable=pointless-statement
  618. self.assertEqual(self.highstate.avail._filled, True)
  619. self.assertEqual(
  620. self.highstate.avail._avail, {"base": ["core", "top"], "other": ["test"]}
  621. )
  622. @skipIf(pytest is None, "PyTest is missing")
  623. class StateReturnsTestCase(TestCase):
  624. """
  625. TestCase for code handling state returns.
  626. """
  627. def test_state_output_check_changes_is_dict(self):
  628. """
  629. Test that changes key contains a dictionary.
  630. :return:
  631. """
  632. data = {"changes": []}
  633. out = statedecorators.OutputUnifier("content_check")(lambda: data)()
  634. assert "'Changes' should be a dictionary" in out["comment"]
  635. assert not out["result"]
  636. def test_state_output_check_return_is_dict(self):
  637. """
  638. Test for the entire return is a dictionary
  639. :return:
  640. """
  641. data = ["whatever"]
  642. out = statedecorators.OutputUnifier("content_check")(lambda: data)()
  643. assert (
  644. "Malformed state return. Data must be a dictionary type" in out["comment"]
  645. )
  646. assert not out["result"]
  647. def test_state_output_check_return_has_nrc(self):
  648. """
  649. Test for name/result/comment keys are inside the return.
  650. :return:
  651. """
  652. data = {"arbitrary": "data", "changes": {}}
  653. out = statedecorators.OutputUnifier("content_check")(lambda: data)()
  654. assert (
  655. " The following keys were not present in the state return: name, result, comment"
  656. in out["comment"]
  657. )
  658. assert not out["result"]
  659. def test_state_output_unifier_comment_is_not_list(self):
  660. """
  661. Test for output is unified so the comment is converted to a multi-line string
  662. :return:
  663. """
  664. data = {
  665. "comment": ["data", "in", "the", "list"],
  666. "changes": {},
  667. "name": None,
  668. "result": "fantastic!",
  669. }
  670. expected = {
  671. "comment": "data\nin\nthe\nlist",
  672. "changes": {},
  673. "name": None,
  674. "result": True,
  675. }
  676. assert statedecorators.OutputUnifier("unify")(lambda: data)() == expected
  677. data = {
  678. "comment": ["data", "in", "the", "list"],
  679. "changes": {},
  680. "name": None,
  681. "result": None,
  682. }
  683. expected = "data\nin\nthe\nlist"
  684. assert (
  685. statedecorators.OutputUnifier("unify")(lambda: data)()["comment"]
  686. == expected
  687. )
  688. def test_state_output_unifier_result_converted_to_true(self):
  689. """
  690. Test for output is unified so the result is converted to True
  691. :return:
  692. """
  693. data = {
  694. "comment": ["data", "in", "the", "list"],
  695. "changes": {},
  696. "name": None,
  697. "result": "Fantastic",
  698. }
  699. assert statedecorators.OutputUnifier("unify")(lambda: data)()["result"] is True
  700. def test_state_output_unifier_result_converted_to_false(self):
  701. """
  702. Test for output is unified so the result is converted to False
  703. :return:
  704. """
  705. data = {
  706. "comment": ["data", "in", "the", "list"],
  707. "changes": {},
  708. "name": None,
  709. "result": "",
  710. }
  711. assert statedecorators.OutputUnifier("unify")(lambda: data)()["result"] is False
  712. class StateFormatSlotsTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  713. """
  714. TestCase for code handling slots
  715. """
  716. def setUp(self):
  717. with patch("salt.state.State._gather_pillar"):
  718. minion_opts = self.get_temp_config("minion")
  719. self.state_obj = salt.state.State(minion_opts)
  720. def test_format_slots_no_slots(self):
  721. """
  722. Test the format slots keeps data without slots untouched.
  723. """
  724. cdata = {"args": ["arg"], "kwargs": {"key": "val"}}
  725. self.state_obj.format_slots(cdata)
  726. self.assertEqual(cdata, {"args": ["arg"], "kwargs": {"key": "val"}})
  727. @slowTest
  728. def test_format_slots_arg(self):
  729. """
  730. Test the format slots is calling a slot specified in args with corresponding arguments.
  731. """
  732. cdata = {
  733. "args": ["__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)"],
  734. "kwargs": {"key": "val"},
  735. }
  736. mock = MagicMock(return_value="fun_return")
  737. with patch.dict(self.state_obj.functions, {"mod.fun": mock}):
  738. self.state_obj.format_slots(cdata)
  739. mock.assert_called_once_with("fun_arg", fun_key="fun_val")
  740. self.assertEqual(cdata, {"args": ["fun_return"], "kwargs": {"key": "val"}})
  741. @slowTest
  742. def test_format_slots_dict_arg(self):
  743. """
  744. Test the format slots is calling a slot specified in dict arg.
  745. """
  746. cdata = {
  747. "args": [{"subarg": "__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)"}],
  748. "kwargs": {"key": "val"},
  749. }
  750. mock = MagicMock(return_value="fun_return")
  751. with patch.dict(self.state_obj.functions, {"mod.fun": mock}):
  752. self.state_obj.format_slots(cdata)
  753. mock.assert_called_once_with("fun_arg", fun_key="fun_val")
  754. self.assertEqual(
  755. cdata, {"args": [{"subarg": "fun_return"}], "kwargs": {"key": "val"}}
  756. )
  757. @slowTest
  758. def test_format_slots_listdict_arg(self):
  759. """
  760. Test the format slots is calling a slot specified in list containing a dict.
  761. """
  762. cdata = {
  763. "args": [[{"subarg": "__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)"}]],
  764. "kwargs": {"key": "val"},
  765. }
  766. mock = MagicMock(return_value="fun_return")
  767. with patch.dict(self.state_obj.functions, {"mod.fun": mock}):
  768. self.state_obj.format_slots(cdata)
  769. mock.assert_called_once_with("fun_arg", fun_key="fun_val")
  770. self.assertEqual(
  771. cdata, {"args": [[{"subarg": "fun_return"}]], "kwargs": {"key": "val"}}
  772. )
  773. @slowTest
  774. def test_format_slots_liststr_arg(self):
  775. """
  776. Test the format slots is calling a slot specified in list containing a dict.
  777. """
  778. cdata = {
  779. "args": [["__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)"]],
  780. "kwargs": {"key": "val"},
  781. }
  782. mock = MagicMock(return_value="fun_return")
  783. with patch.dict(self.state_obj.functions, {"mod.fun": mock}):
  784. self.state_obj.format_slots(cdata)
  785. mock.assert_called_once_with("fun_arg", fun_key="fun_val")
  786. self.assertEqual(cdata, {"args": [["fun_return"]], "kwargs": {"key": "val"}})
  787. @slowTest
  788. def test_format_slots_kwarg(self):
  789. """
  790. Test the format slots is calling a slot specified in kwargs with corresponding arguments.
  791. """
  792. cdata = {
  793. "args": ["arg"],
  794. "kwargs": {"key": "__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)"},
  795. }
  796. mock = MagicMock(return_value="fun_return")
  797. with patch.dict(self.state_obj.functions, {"mod.fun": mock}):
  798. self.state_obj.format_slots(cdata)
  799. mock.assert_called_once_with("fun_arg", fun_key="fun_val")
  800. self.assertEqual(cdata, {"args": ["arg"], "kwargs": {"key": "fun_return"}})
  801. @slowTest
  802. def test_format_slots_multi(self):
  803. """
  804. Test the format slots is calling all slots with corresponding arguments when multiple slots
  805. specified.
  806. """
  807. cdata = {
  808. "args": [
  809. "__slot__:salt:test_mod.fun_a(a_arg, a_key=a_kwarg)",
  810. "__slot__:salt:test_mod.fun_b(b_arg, b_key=b_kwarg)",
  811. ],
  812. "kwargs": {
  813. "kw_key_1": "__slot__:salt:test_mod.fun_c(c_arg, c_key=c_kwarg)",
  814. "kw_key_2": "__slot__:salt:test_mod.fun_d(d_arg, d_key=d_kwarg)",
  815. },
  816. }
  817. mock_a = MagicMock(return_value="fun_a_return")
  818. mock_b = MagicMock(return_value="fun_b_return")
  819. mock_c = MagicMock(return_value="fun_c_return")
  820. mock_d = MagicMock(return_value="fun_d_return")
  821. with patch.dict(
  822. self.state_obj.functions,
  823. {
  824. "test_mod.fun_a": mock_a,
  825. "test_mod.fun_b": mock_b,
  826. "test_mod.fun_c": mock_c,
  827. "test_mod.fun_d": mock_d,
  828. },
  829. ):
  830. self.state_obj.format_slots(cdata)
  831. mock_a.assert_called_once_with("a_arg", a_key="a_kwarg")
  832. mock_b.assert_called_once_with("b_arg", b_key="b_kwarg")
  833. mock_c.assert_called_once_with("c_arg", c_key="c_kwarg")
  834. mock_d.assert_called_once_with("d_arg", d_key="d_kwarg")
  835. self.assertEqual(
  836. cdata,
  837. {
  838. "args": ["fun_a_return", "fun_b_return"],
  839. "kwargs": {"kw_key_1": "fun_c_return", "kw_key_2": "fun_d_return"},
  840. },
  841. )
  842. @slowTest
  843. def test_format_slots_malformed(self):
  844. """
  845. Test the format slots keeps malformed slots untouched.
  846. """
  847. sls_data = {
  848. "args": [
  849. "__slot__:NOT_SUPPORTED:not.called()",
  850. "__slot__:salt:not.called(",
  851. "__slot__:salt:",
  852. "__slot__:salt",
  853. "__slot__:",
  854. "__slot__",
  855. ],
  856. "kwargs": {
  857. "key3": "__slot__:NOT_SUPPORTED:not.called()",
  858. "key4": "__slot__:salt:not.called(",
  859. "key5": "__slot__:salt:",
  860. "key6": "__slot__:salt",
  861. "key7": "__slot__:",
  862. "key8": "__slot__",
  863. },
  864. }
  865. cdata = sls_data.copy()
  866. mock = MagicMock(return_value="return")
  867. with patch.dict(self.state_obj.functions, {"not.called": mock}):
  868. self.state_obj.format_slots(cdata)
  869. mock.assert_not_called()
  870. self.assertEqual(cdata, sls_data)
  871. @slowTest
  872. def test_slot_traverse_dict(self):
  873. """
  874. Test the slot parsing of dict response.
  875. """
  876. cdata = {
  877. "args": ["arg"],
  878. "kwargs": {"key": "__slot__:salt:mod.fun(fun_arg, fun_key=fun_val).key1"},
  879. }
  880. return_data = {"key1": "value1"}
  881. mock = MagicMock(return_value=return_data)
  882. with patch.dict(self.state_obj.functions, {"mod.fun": mock}):
  883. self.state_obj.format_slots(cdata)
  884. mock.assert_called_once_with("fun_arg", fun_key="fun_val")
  885. self.assertEqual(cdata, {"args": ["arg"], "kwargs": {"key": "value1"}})
  886. @slowTest
  887. def test_slot_append(self):
  888. """
  889. Test the slot parsing of dict response.
  890. """
  891. cdata = {
  892. "args": ["arg"],
  893. "kwargs": {
  894. "key": "__slot__:salt:mod.fun(fun_arg, fun_key=fun_val).key1 ~ thing~",
  895. },
  896. }
  897. return_data = {"key1": "value1"}
  898. mock = MagicMock(return_value=return_data)
  899. with patch.dict(self.state_obj.functions, {"mod.fun": mock}):
  900. self.state_obj.format_slots(cdata)
  901. mock.assert_called_once_with("fun_arg", fun_key="fun_val")
  902. self.assertEqual(cdata, {"args": ["arg"], "kwargs": {"key": "value1thing~"}})
  903. # Skip on windows like integration.modules.test_state.StateModuleTest.test_parallel_state_with_long_tag
  904. @skipIf(
  905. salt.utils.platform.is_windows(),
  906. "Skipped until parallel states can be fixed on Windows",
  907. )
  908. def test_format_slots_parallel(self):
  909. """
  910. Test if slots work with "parallel: true".
  911. """
  912. high_data = {
  913. "always-changes-and-succeeds": {
  914. "test": [
  915. {"changes": True},
  916. {"comment": "__slot__:salt:test.echo(fun_return)"},
  917. {"parallel": True},
  918. "configurable_test_state",
  919. {"order": 10000},
  920. ],
  921. "__env__": "base",
  922. "__sls__": "parallel_slots",
  923. }
  924. }
  925. self.state_obj.jid = "123"
  926. res = self.state_obj.call_high(high_data)
  927. self.state_obj.jid = None
  928. [(_, data)] = res.items()
  929. self.assertEqual(data["comment"], "fun_return")