test_state.py 14 KB

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