1
0

test_state.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :codeauthor: Nicole Thomas <nicole@saltstack.com>
  4. '''
  5. # Import Python libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. import os
  8. import shutil
  9. import tempfile
  10. # Import Salt Testing libs
  11. from tests.support.unit import TestCase, skipIf
  12. from tests.support.mock import (
  13. MagicMock,
  14. patch)
  15. from tests.support.mixins import AdaptedConfigurationTestCaseMixin
  16. from tests.support.runtime import RUNTIME_VARS
  17. # Import Salt libs
  18. import salt.exceptions
  19. import salt.state
  20. from salt.utils.odict import OrderedDict
  21. from salt.utils.decorators import state as statedecorators
  22. try:
  23. import pytest
  24. except ImportError as err:
  25. pytest = None
  26. class StateCompilerTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  27. '''
  28. TestCase for the state compiler.
  29. '''
  30. def test_format_log_non_ascii_character(self):
  31. '''
  32. Tests running a non-ascii character through the state.format_log
  33. function. See Issue #33605.
  34. '''
  35. # There is no return to test against as the format_log
  36. # function doesn't return anything. However, we do want
  37. # to make sure that the function doesn't stacktrace when
  38. # called.
  39. ret = {'changes': {'Français': {'old': 'something old',
  40. 'new': 'something new'}},
  41. 'result': True}
  42. salt.state.format_log(ret)
  43. def test_render_error_on_invalid_requisite(self):
  44. '''
  45. Test that the state compiler correctly deliver a rendering
  46. exception when a requisite cannot be resolved
  47. '''
  48. with patch('salt.state.State._gather_pillar') as state_patch:
  49. high_data = {
  50. 'git': OrderedDict([
  51. ('pkg', [
  52. OrderedDict([
  53. ('require', [
  54. OrderedDict([
  55. ('file', OrderedDict(
  56. [('test1', 'test')]))])])]),
  57. 'installed', {'order': 10000}]), ('__sls__', 'issue_35226'), ('__env__', 'base')])}
  58. minion_opts = self.get_temp_config('minion')
  59. minion_opts['pillar'] = {'git': OrderedDict([('test1', 'test')])}
  60. state_obj = salt.state.State(minion_opts)
  61. with self.assertRaises(salt.exceptions.SaltRenderError):
  62. state_obj.call_high(high_data)
  63. class HighStateTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  64. def setUp(self):
  65. root_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  66. self.state_tree_dir = os.path.join(root_dir, 'state_tree')
  67. cache_dir = os.path.join(root_dir, 'cachedir')
  68. for dpath in (root_dir, self.state_tree_dir, cache_dir):
  69. if not os.path.isdir(dpath):
  70. os.makedirs(dpath)
  71. overrides = {}
  72. overrides['root_dir'] = root_dir
  73. overrides['state_events'] = False
  74. overrides['id'] = 'match'
  75. overrides['file_client'] = 'local'
  76. overrides['file_roots'] = dict(base=[self.state_tree_dir])
  77. overrides['cachedir'] = cache_dir
  78. overrides['test'] = False
  79. self.config = self.get_temp_config('minion', **overrides)
  80. self.addCleanup(delattr, self, 'config')
  81. self.highstate = salt.state.HighState(self.config)
  82. self.addCleanup(delattr, self, 'highstate')
  83. self.highstate.push_active()
  84. def tearDown(self):
  85. self.highstate.pop_active()
  86. def test_top_matches_with_list(self):
  87. top = {'env': {'match': ['state1', 'state2'], 'nomatch': ['state3']}}
  88. matches = self.highstate.top_matches(top)
  89. self.assertEqual(matches, {'env': ['state1', 'state2']})
  90. def test_top_matches_with_string(self):
  91. top = {'env': {'match': 'state1', 'nomatch': 'state2'}}
  92. matches = self.highstate.top_matches(top)
  93. self.assertEqual(matches, {'env': ['state1']})
  94. def test_matches_whitelist(self):
  95. matches = {'env': ['state1', 'state2', 'state3']}
  96. matches = self.highstate.matches_whitelist(matches, ['state2'])
  97. self.assertEqual(matches, {'env': ['state2']})
  98. def test_matches_whitelist_with_string(self):
  99. matches = {'env': ['state1', 'state2', 'state3']}
  100. matches = self.highstate.matches_whitelist(matches,
  101. 'state2,state3')
  102. self.assertEqual(matches, {'env': ['state2', 'state3']})
  103. def test_show_state_usage(self):
  104. # monkey patch sub methods
  105. self.highstate.avail = {
  106. 'base': ['state.a', 'state.b', 'state.c']
  107. }
  108. def verify_tops(*args, **kwargs):
  109. return []
  110. def get_top(*args, **kwargs):
  111. return None
  112. def top_matches(*args, **kwargs):
  113. return {'base': ['state.a', 'state.b']}
  114. self.highstate.verify_tops = verify_tops
  115. self.highstate.get_top = get_top
  116. self.highstate.top_matches = top_matches
  117. # get compile_state_usage() result
  118. state_usage_dict = self.highstate.compile_state_usage()
  119. self.assertEqual(state_usage_dict['base']['count_unused'], 1)
  120. self.assertEqual(state_usage_dict['base']['count_used'], 2)
  121. self.assertEqual(state_usage_dict['base']['count_all'], 3)
  122. self.assertEqual(state_usage_dict['base']['used'], ['state.a', 'state.b'])
  123. self.assertEqual(state_usage_dict['base']['unused'], ['state.c'])
  124. def test_find_sls_ids_with_exclude(self):
  125. '''
  126. See https://github.com/saltstack/salt/issues/47182
  127. '''
  128. sls_dir = 'issue-47182'
  129. shutil.copytree(
  130. os.path.join(RUNTIME_VARS.BASE_FILES, sls_dir),
  131. os.path.join(self.state_tree_dir, sls_dir)
  132. )
  133. shutil.move(
  134. os.path.join(self.state_tree_dir, sls_dir, 'top.sls'),
  135. self.state_tree_dir
  136. )
  137. # Manually compile the high data. We don't have to worry about all of
  138. # the normal error checking we do here since we know that all the SLS
  139. # files exist and there is no whitelist/blacklist being used.
  140. top = self.highstate.get_top() # pylint: disable=assignment-from-none
  141. matches = self.highstate.top_matches(top)
  142. high, _ = self.highstate.render_highstate(matches)
  143. ret = salt.state.find_sls_ids('issue-47182.stateA.newer', high)
  144. self.assertEqual(ret, [('somestuff', 'cmd')])
  145. @skipIf(pytest is None, 'PyTest is missing')
  146. class StateReturnsTestCase(TestCase):
  147. '''
  148. TestCase for code handling state returns.
  149. '''
  150. def test_state_output_check_changes_is_dict(self):
  151. '''
  152. Test that changes key contains a dictionary.
  153. :return:
  154. '''
  155. data = {'changes': []}
  156. out = statedecorators.OutputUnifier('content_check')(lambda: data)()
  157. assert "'Changes' should be a dictionary" in out['comment']
  158. assert not out['result']
  159. def test_state_output_check_return_is_dict(self):
  160. '''
  161. Test for the entire return is a dictionary
  162. :return:
  163. '''
  164. data = ['whatever']
  165. out = statedecorators.OutputUnifier('content_check')(lambda: data)()
  166. assert 'Malformed state return. Data must be a dictionary type' in out['comment']
  167. assert not out['result']
  168. def test_state_output_check_return_has_nrc(self):
  169. '''
  170. Test for name/result/comment keys are inside the return.
  171. :return:
  172. '''
  173. data = {'arbitrary': 'data', 'changes': {}}
  174. out = statedecorators.OutputUnifier('content_check')(lambda: data)()
  175. assert ' The following keys were not present in the state return: name, result, comment' in out['comment']
  176. assert not out['result']
  177. def test_state_output_unifier_comment_is_not_list(self):
  178. '''
  179. Test for output is unified so the comment is converted to a multi-line string
  180. :return:
  181. '''
  182. data = {'comment': ['data', 'in', 'the', 'list'], 'changes': {}, 'name': None, 'result': 'fantastic!'}
  183. expected = {'comment': 'data\nin\nthe\nlist', 'changes': {}, 'name': None, 'result': True}
  184. assert statedecorators.OutputUnifier('unify')(lambda: data)() == expected
  185. data = {'comment': ['data', 'in', 'the', 'list'], 'changes': {}, 'name': None, 'result': None}
  186. expected = 'data\nin\nthe\nlist'
  187. assert statedecorators.OutputUnifier('unify')(lambda: data)()['comment'] == expected
  188. def test_state_output_unifier_result_converted_to_true(self):
  189. '''
  190. Test for output is unified so the result is converted to True
  191. :return:
  192. '''
  193. data = {'comment': ['data', 'in', 'the', 'list'], 'changes': {}, 'name': None, 'result': 'Fantastic'}
  194. assert statedecorators.OutputUnifier('unify')(lambda: data)()['result'] is True
  195. def test_state_output_unifier_result_converted_to_false(self):
  196. '''
  197. Test for output is unified so the result is converted to False
  198. :return:
  199. '''
  200. data = {'comment': ['data', 'in', 'the', 'list'], 'changes': {}, 'name': None, 'result': ''}
  201. assert statedecorators.OutputUnifier('unify')(lambda: data)()['result'] is False
  202. class StateFormatSlotsTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  203. '''
  204. TestCase for code handling slots
  205. '''
  206. def setUp(self):
  207. with patch('salt.state.State._gather_pillar'):
  208. minion_opts = self.get_temp_config('minion')
  209. self.state_obj = salt.state.State(minion_opts)
  210. def test_format_slots_no_slots(self):
  211. '''
  212. Test the format slots keeps data without slots untouched.
  213. '''
  214. cdata = {
  215. 'args': [
  216. 'arg',
  217. ],
  218. 'kwargs': {
  219. 'key': 'val',
  220. }
  221. }
  222. self.state_obj.format_slots(cdata)
  223. self.assertEqual(cdata, {'args': ['arg'], 'kwargs': {'key': 'val'}})
  224. def test_format_slots_arg(self):
  225. '''
  226. Test the format slots is calling a slot specified in args with corresponding arguments.
  227. '''
  228. cdata = {
  229. 'args': [
  230. '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)',
  231. ],
  232. 'kwargs': {
  233. 'key': 'val',
  234. }
  235. }
  236. mock = MagicMock(return_value='fun_return')
  237. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  238. self.state_obj.format_slots(cdata)
  239. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  240. self.assertEqual(cdata, {'args': ['fun_return'], 'kwargs': {'key': 'val'}})
  241. def test_format_slots_dict_arg(self):
  242. '''
  243. Test the format slots is calling a slot specified in dict arg.
  244. '''
  245. cdata = {
  246. 'args': [
  247. {'subarg': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)'},
  248. ],
  249. 'kwargs': {
  250. 'key': 'val',
  251. }
  252. }
  253. mock = MagicMock(return_value='fun_return')
  254. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  255. self.state_obj.format_slots(cdata)
  256. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  257. self.assertEqual(cdata, {'args': [{'subarg': 'fun_return'}], 'kwargs': {'key': 'val'}})
  258. def test_format_slots_listdict_arg(self):
  259. '''
  260. Test the format slots is calling a slot specified in list containing a dict.
  261. '''
  262. cdata = {
  263. 'args': [[
  264. {'subarg': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)'},
  265. ]],
  266. 'kwargs': {
  267. 'key': 'val',
  268. }
  269. }
  270. mock = MagicMock(return_value='fun_return')
  271. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  272. self.state_obj.format_slots(cdata)
  273. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  274. self.assertEqual(cdata, {'args': [[{'subarg': 'fun_return'}]], 'kwargs': {'key': 'val'}})
  275. def test_format_slots_liststr_arg(self):
  276. '''
  277. Test the format slots is calling a slot specified in list containing a dict.
  278. '''
  279. cdata = {
  280. 'args': [[
  281. '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)',
  282. ]],
  283. 'kwargs': {
  284. 'key': 'val',
  285. }
  286. }
  287. mock = MagicMock(return_value='fun_return')
  288. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  289. self.state_obj.format_slots(cdata)
  290. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  291. self.assertEqual(cdata, {'args': [['fun_return']], 'kwargs': {'key': 'val'}})
  292. def test_format_slots_kwarg(self):
  293. '''
  294. Test the format slots is calling a slot specified in kwargs with corresponding arguments.
  295. '''
  296. cdata = {
  297. 'args': [
  298. 'arg',
  299. ],
  300. 'kwargs': {
  301. 'key': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val)',
  302. }
  303. }
  304. mock = MagicMock(return_value='fun_return')
  305. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  306. self.state_obj.format_slots(cdata)
  307. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  308. self.assertEqual(cdata, {'args': ['arg'], 'kwargs': {'key': 'fun_return'}})
  309. def test_format_slots_multi(self):
  310. '''
  311. Test the format slots is calling all slots with corresponding arguments when multiple slots
  312. specified.
  313. '''
  314. cdata = {
  315. 'args': [
  316. '__slot__:salt:test_mod.fun_a(a_arg, a_key=a_kwarg)',
  317. '__slot__:salt:test_mod.fun_b(b_arg, b_key=b_kwarg)',
  318. ],
  319. 'kwargs': {
  320. 'kw_key_1': '__slot__:salt:test_mod.fun_c(c_arg, c_key=c_kwarg)',
  321. 'kw_key_2': '__slot__:salt:test_mod.fun_d(d_arg, d_key=d_kwarg)',
  322. }
  323. }
  324. mock_a = MagicMock(return_value='fun_a_return')
  325. mock_b = MagicMock(return_value='fun_b_return')
  326. mock_c = MagicMock(return_value='fun_c_return')
  327. mock_d = MagicMock(return_value='fun_d_return')
  328. with patch.dict(self.state_obj.functions, {'test_mod.fun_a': mock_a,
  329. 'test_mod.fun_b': mock_b,
  330. 'test_mod.fun_c': mock_c,
  331. 'test_mod.fun_d': mock_d}):
  332. self.state_obj.format_slots(cdata)
  333. mock_a.assert_called_once_with('a_arg', a_key='a_kwarg')
  334. mock_b.assert_called_once_with('b_arg', b_key='b_kwarg')
  335. mock_c.assert_called_once_with('c_arg', c_key='c_kwarg')
  336. mock_d.assert_called_once_with('d_arg', d_key='d_kwarg')
  337. self.assertEqual(cdata, {'args': ['fun_a_return',
  338. 'fun_b_return'],
  339. 'kwargs': {'kw_key_1': 'fun_c_return',
  340. 'kw_key_2': 'fun_d_return'}})
  341. def test_format_slots_malformed(self):
  342. '''
  343. Test the format slots keeps malformed slots untouched.
  344. '''
  345. sls_data = {
  346. 'args': [
  347. '__slot__:NOT_SUPPORTED:not.called()',
  348. '__slot__:salt:not.called(',
  349. '__slot__:salt:',
  350. '__slot__:salt',
  351. '__slot__:',
  352. '__slot__',
  353. ],
  354. 'kwargs': {
  355. 'key3': '__slot__:NOT_SUPPORTED:not.called()',
  356. 'key4': '__slot__:salt:not.called(',
  357. 'key5': '__slot__:salt:',
  358. 'key6': '__slot__:salt',
  359. 'key7': '__slot__:',
  360. 'key8': '__slot__',
  361. }
  362. }
  363. cdata = sls_data.copy()
  364. mock = MagicMock(return_value='return')
  365. with patch.dict(self.state_obj.functions, {'not.called': mock}):
  366. self.state_obj.format_slots(cdata)
  367. mock.assert_not_called()
  368. self.assertEqual(cdata, sls_data)
  369. def test_slot_traverse_dict(self):
  370. '''
  371. Test the slot parsing of dict response.
  372. '''
  373. cdata = {
  374. 'args': [
  375. 'arg',
  376. ],
  377. 'kwargs': {
  378. 'key': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val).key1',
  379. }
  380. }
  381. return_data = {'key1': 'value1'}
  382. mock = MagicMock(return_value=return_data)
  383. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  384. self.state_obj.format_slots(cdata)
  385. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  386. self.assertEqual(cdata, {'args': ['arg'], 'kwargs': {'key': 'value1'}})
  387. def test_slot_append(self):
  388. '''
  389. Test the slot parsing of dict response.
  390. '''
  391. cdata = {
  392. 'args': [
  393. 'arg',
  394. ],
  395. 'kwargs': {
  396. 'key': '__slot__:salt:mod.fun(fun_arg, fun_key=fun_val).key1 ~ thing~',
  397. }
  398. }
  399. return_data = {'key1': 'value1'}
  400. mock = MagicMock(return_value=return_data)
  401. with patch.dict(self.state_obj.functions, {'mod.fun': mock}):
  402. self.state_obj.format_slots(cdata)
  403. mock.assert_called_once_with('fun_arg', fun_key='fun_val')
  404. self.assertEqual(cdata, {'args': ['arg'], 'kwargs': {'key': 'value1thing~'}})