1
0

test_pydsl.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. # -*- coding: utf-8 -*-
  2. # Import Python libs
  3. from __future__ import absolute_import
  4. import os
  5. import sys
  6. import shutil
  7. import tempfile
  8. import textwrap
  9. import copy
  10. # Import Salt Testing libs
  11. from tests.support.runtime import RUNTIME_VARS
  12. from tests.support.helpers import with_tempdir
  13. from tests.support.unit import TestCase
  14. # Import Salt libs
  15. import salt.loader
  16. import salt.config
  17. import salt.utils.files
  18. import salt.utils.versions
  19. from salt.state import HighState
  20. from salt.utils.pydsl import PyDslError
  21. # Import 3rd-party libs
  22. from salt.ext import six
  23. from salt.ext.six.moves import StringIO
  24. REQUISITES = ['require', 'require_in', 'use', 'use_in', 'watch', 'watch_in']
  25. class CommonTestCaseBoilerplate(TestCase):
  26. def setUp(self):
  27. self.root_dir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  28. self.addCleanup(shutil.rmtree, self.root_dir, ignore_errors=True)
  29. self.state_tree_dir = os.path.join(self.root_dir, 'state_tree')
  30. self.cache_dir = os.path.join(self.root_dir, 'cachedir')
  31. if not os.path.isdir(self.root_dir):
  32. os.makedirs(self.root_dir)
  33. if not os.path.isdir(self.state_tree_dir):
  34. os.makedirs(self.state_tree_dir)
  35. if not os.path.isdir(self.cache_dir):
  36. os.makedirs(self.cache_dir)
  37. self.config = salt.config.minion_config(None)
  38. self.config['root_dir'] = self.root_dir
  39. self.config['state_events'] = False
  40. self.config['id'] = 'match'
  41. self.config['file_client'] = 'local'
  42. self.config['file_roots'] = dict(base=[self.state_tree_dir])
  43. self.config['cachedir'] = self.cache_dir
  44. self.config['test'] = False
  45. self.config['grains'] = salt.loader.grains(self.config)
  46. self.HIGHSTATE = HighState(self.config)
  47. self.HIGHSTATE.push_active()
  48. def tearDown(self):
  49. try:
  50. self.HIGHSTATE.pop_active()
  51. except IndexError:
  52. pass
  53. del self.config
  54. del self.HIGHSTATE
  55. def state_highstate(self, state, dirpath):
  56. opts = copy.copy(self.config)
  57. opts['file_roots'] = dict(base=[dirpath])
  58. HIGHSTATE = HighState(opts)
  59. HIGHSTATE.push_active()
  60. try:
  61. high, errors = HIGHSTATE.render_highstate(state)
  62. if errors:
  63. import pprint
  64. pprint.pprint('\n'.join(errors))
  65. pprint.pprint(high)
  66. out = HIGHSTATE.state.call_high(high)
  67. # pprint.pprint(out)
  68. finally:
  69. HIGHSTATE.pop_active()
  70. class PyDSLRendererTestCase(CommonTestCaseBoilerplate):
  71. '''
  72. WARNING: If tests in here are flaky, they may need
  73. to be moved to their own class. Sharing HighState, especially
  74. through setUp/tearDown can create dangerous race conditions!
  75. '''
  76. def render_sls(self, content, sls='', saltenv='base', **kws):
  77. if 'env' in kws:
  78. # "env" is not supported; Use "saltenv".
  79. kws.pop('env')
  80. return self.HIGHSTATE.state.rend['pydsl'](
  81. StringIO(content), saltenv=saltenv, sls=sls, **kws
  82. )
  83. def test_state_declarations(self):
  84. result = self.render_sls(textwrap.dedent('''
  85. state('A').cmd.run('ls -la', cwd='/var/tmp')
  86. state().file.managed('myfile.txt', source='salt://path/to/file')
  87. state('X').cmd('run', 'echo hello world', cwd='/')
  88. a_cmd = state('A').cmd
  89. a_cmd.run(shell='/bin/bash')
  90. state('A').service.running(name='apache')
  91. '''))
  92. self.assertTrue('A' in result and 'X' in result)
  93. A_cmd = result['A']['cmd']
  94. self.assertEqual(A_cmd[0], 'run')
  95. self.assertEqual(A_cmd[1]['name'], 'ls -la')
  96. self.assertEqual(A_cmd[2]['cwd'], '/var/tmp')
  97. self.assertEqual(A_cmd[3]['shell'], '/bin/bash')
  98. A_service = result['A']['service']
  99. self.assertEqual(A_service[0], 'running')
  100. self.assertEqual(A_service[1]['name'], 'apache')
  101. X_cmd = result['X']['cmd']
  102. self.assertEqual(X_cmd[0], 'run')
  103. self.assertEqual(X_cmd[1]['name'], 'echo hello world')
  104. self.assertEqual(X_cmd[2]['cwd'], '/')
  105. del result['A']
  106. del result['X']
  107. self.assertEqual(len(result), 2)
  108. # 2 rather than 1 because pydsl adds an extra no-op state
  109. # declaration.
  110. s_iter = six.itervalues(result)
  111. try:
  112. s = next(s_iter)['file']
  113. except KeyError:
  114. s = next(s_iter)['file']
  115. self.assertEqual(s[0], 'managed')
  116. self.assertEqual(s[1]['name'], 'myfile.txt')
  117. self.assertEqual(s[2]['source'], 'salt://path/to/file')
  118. def test_requisite_declarations(self):
  119. result = self.render_sls(textwrap.dedent('''
  120. state('X').cmd.run('echo hello')
  121. state('A').cmd.run('mkdir tmp', cwd='/var')
  122. state('B').cmd.run('ls -la', cwd='/var/tmp') \
  123. .require(state('X').cmd) \
  124. .require(cmd='A') \
  125. .watch(service='G')
  126. state('G').service.running(name='collectd')
  127. state('G').service.watch_in(state('A').cmd)
  128. state('H').cmd.require_in(cmd='echo hello')
  129. state('H').cmd.run('echo world')
  130. '''))
  131. self.assertTrue(len(result), 6)
  132. self.assertTrue(set("X A B G H".split()).issubset(set(result.keys())))
  133. b = result['B']['cmd']
  134. self.assertEqual(b[0], 'run')
  135. self.assertEqual(b[1]['name'], 'ls -la')
  136. self.assertEqual(b[2]['cwd'], '/var/tmp')
  137. self.assertEqual(b[3]['require'][0]['cmd'], 'X')
  138. self.assertEqual(b[4]['require'][0]['cmd'], 'A')
  139. self.assertEqual(b[5]['watch'][0]['service'], 'G')
  140. self.assertEqual(result['G']['service'][2]['watch_in'][0]['cmd'], 'A')
  141. self.assertEqual(
  142. result['H']['cmd'][1]['require_in'][0]['cmd'], 'echo hello'
  143. )
  144. def test_include_extend(self):
  145. result = self.render_sls(textwrap.dedent('''
  146. include(
  147. 'some.sls.file',
  148. 'another.sls.file',
  149. 'more.sls.file',
  150. delayed=True
  151. )
  152. A = state('A').cmd.run('echo hoho', cwd='/')
  153. state('B').cmd.run('echo hehe', cwd='/')
  154. extend(
  155. A,
  156. state('X').cmd.run(cwd='/a/b/c'),
  157. state('Y').file('managed', name='a_file.txt'),
  158. state('Z').service.watch(file='A')
  159. )
  160. '''))
  161. self.assertEqual(len(result), 4)
  162. self.assertEqual(
  163. result['include'],
  164. [{'base': sls} for sls in
  165. ('some.sls.file', 'another.sls.file', 'more.sls.file')]
  166. )
  167. extend = result['extend']
  168. self.assertEqual(extend['X']['cmd'][0], 'run')
  169. self.assertEqual(extend['X']['cmd'][1]['cwd'], '/a/b/c')
  170. self.assertEqual(extend['Y']['file'][0], 'managed')
  171. self.assertEqual(extend['Y']['file'][1]['name'], 'a_file.txt')
  172. self.assertEqual(len(extend['Z']['service']), 1)
  173. self.assertEqual(extend['Z']['service'][0]['watch'][0]['file'], 'A')
  174. self.assertEqual(result['B']['cmd'][0], 'run')
  175. self.assertTrue('A' not in result)
  176. self.assertEqual(extend['A']['cmd'][0], 'run')
  177. def test_cmd_call(self):
  178. result = self.HIGHSTATE.state.call_template_str(textwrap.dedent('''\
  179. #!pydsl
  180. state('A').cmd.run('echo this is state A', cwd='/')
  181. some_var = 12345
  182. def do_something(a, b, *args, **kws):
  183. return dict(result=True, changes={'a': a, 'b': b, 'args': args, 'kws': kws, 'some_var': some_var})
  184. state('C').cmd.call(do_something, 1, 2, 3, x=1, y=2) \
  185. .require(state('A').cmd)
  186. state('G').cmd.wait('echo this is state G', cwd='/') \
  187. .watch(state('C').cmd)
  188. '''))
  189. ret = next(result[k] for k in six.iterkeys(result) if 'do_something' in k)
  190. changes = ret['changes']
  191. self.assertEqual(
  192. changes,
  193. dict(a=1, b=2, args=(3,), kws=dict(x=1, y=2), some_var=12345)
  194. )
  195. ret = next(result[k] for k in six.iterkeys(result) if '-G_' in k)
  196. self.assertEqual(ret['changes']['stdout'], 'this is state G')
  197. def test_multiple_state_func_in_state_mod(self):
  198. with self.assertRaisesRegex(PyDslError, 'Multiple state functions'):
  199. self.render_sls(textwrap.dedent('''
  200. state('A').cmd.run('echo hoho')
  201. state('A').cmd.wait('echo hehe')
  202. '''))
  203. def test_no_state_func_in_state_mod(self):
  204. with self.assertRaisesRegex(PyDslError, 'No state function specified'):
  205. self.render_sls(textwrap.dedent('''
  206. state('B').cmd.require(cmd='hoho')
  207. '''))
  208. def test_load_highstate(self):
  209. result = self.render_sls(textwrap.dedent('''
  210. import salt.utils.yaml
  211. __pydsl__.load_highstate(salt.utils.yaml.safe_load("""
  212. A:
  213. cmd.run:
  214. - name: echo hello
  215. - cwd: /
  216. B:
  217. pkg:
  218. - installed
  219. service:
  220. - running
  221. - require:
  222. - pkg: B
  223. - watch:
  224. - cmd: A
  225. """))
  226. state('A').cmd.run(name='echo hello world')
  227. '''))
  228. self.assertEqual(len(result), 3)
  229. self.assertEqual(result['A']['cmd'][0], 'run')
  230. self.assertIn({'name': 'echo hello'}, result['A']['cmd'])
  231. self.assertIn({'cwd': '/'}, result['A']['cmd'])
  232. self.assertIn({'name': 'echo hello world'}, result['A']['cmd'])
  233. self.assertEqual(len(result['A']['cmd']), 4)
  234. self.assertEqual(len(result['B']['pkg']), 1)
  235. self.assertEqual(result['B']['pkg'][0], 'installed')
  236. self.assertEqual(result['B']['service'][0], 'running')
  237. self.assertIn({'require': [{'pkg': 'B'}]}, result['B']['service'])
  238. self.assertIn({'watch': [{'cmd': 'A'}]}, result['B']['service'])
  239. self.assertEqual(len(result['B']['service']), 3)
  240. def test_ordered_states(self):
  241. result = self.render_sls(textwrap.dedent('''
  242. __pydsl__.set(ordered=True)
  243. A = state('A')
  244. state('B').cmd.run('echo bbbb')
  245. A.cmd.run('echo aaa')
  246. state('B').cmd.run(cwd='/')
  247. state('C').cmd.run('echo ccc')
  248. state('B').file.managed(source='/a/b/c')
  249. '''))
  250. self.assertEqual(len(result['B']['cmd']), 3)
  251. self.assertEqual(result['A']['cmd'][1]['require'][0]['cmd'], 'B')
  252. self.assertEqual(result['C']['cmd'][1]['require'][0]['cmd'], 'A')
  253. self.assertEqual(result['B']['file'][1]['require'][0]['cmd'], 'C')
  254. @with_tempdir()
  255. def test_pipe_through_stateconf(self, dirpath):
  256. output = os.path.join(dirpath, 'output')
  257. write_to(os.path.join(dirpath, 'xxx.sls'), textwrap.dedent(
  258. '''#!stateconf -os yaml . jinja
  259. .X:
  260. cmd.run:
  261. - name: echo X >> {0}
  262. - cwd: /
  263. .Y:
  264. cmd.run:
  265. - name: echo Y >> {0}
  266. - cwd: /
  267. .Z:
  268. cmd.run:
  269. - name: echo Z >> {0}
  270. - cwd: /
  271. '''.format(output.replace('\\', '/'))))
  272. write_to(os.path.join(dirpath, 'yyy.sls'), textwrap.dedent('''\
  273. #!pydsl|stateconf -ps
  274. __pydsl__.set(ordered=True)
  275. state('.D').cmd.run('echo D >> {0}', cwd='/')
  276. state('.E').cmd.run('echo E >> {0}', cwd='/')
  277. state('.F').cmd.run('echo F >> {0}', cwd='/')
  278. '''.format(output.replace('\\', '/'))))
  279. write_to(os.path.join(dirpath, 'aaa.sls'), textwrap.dedent('''\
  280. #!pydsl|stateconf -ps
  281. include('xxx', 'yyy')
  282. # make all states in xxx run BEFORE states in this sls.
  283. extend(state('.start').stateconf.require(stateconf='xxx::goal'))
  284. # make all states in yyy run AFTER this sls.
  285. extend(state('.goal').stateconf.require_in(stateconf='yyy::start'))
  286. __pydsl__.set(ordered=True)
  287. state('.A').cmd.run('echo A >> {0}', cwd='/')
  288. state('.B').cmd.run('echo B >> {0}', cwd='/')
  289. state('.C').cmd.run('echo C >> {0}', cwd='/')
  290. '''.format(output.replace('\\', '/'))))
  291. self.state_highstate({'base': ['aaa']}, dirpath)
  292. with salt.utils.files.fopen(output, 'r') as f:
  293. self.assertEqual(''.join(f.read().split()), "XYZABCDEF")
  294. @with_tempdir()
  295. def test_compile_time_state_execution(self, dirpath):
  296. if not sys.stdin.isatty():
  297. self.skipTest('Not attached to a TTY')
  298. # The Windows shell will include any spaces before the redirect
  299. # in the text that is redirected.
  300. # For example: echo hello > test.txt will contain "hello "
  301. write_to(os.path.join(dirpath, 'aaa.sls'), textwrap.dedent('''\
  302. #!pydsl
  303. __pydsl__.set(ordered=True)
  304. A = state('A')
  305. A.cmd.run('echo hehe>{0}/zzz.txt', cwd='/')
  306. A.file.managed('{0}/yyy.txt', source='salt://zzz.txt')
  307. A()
  308. A()
  309. state().cmd.run('echo hoho>>{0}/yyy.txt', cwd='/')
  310. A.file.managed('{0}/xxx.txt', source='salt://zzz.txt')
  311. A()
  312. '''.format(dirpath.replace('\\', '/'))))
  313. self.state_highstate({'base': ['aaa']}, dirpath)
  314. with salt.utils.files.fopen(os.path.join(dirpath, 'yyy.txt'), 'rt') as f:
  315. self.assertEqual(f.read(), 'hehe' + os.linesep + 'hoho' + os.linesep)
  316. with salt.utils.files.fopen(os.path.join(dirpath, 'xxx.txt'), 'rt') as f:
  317. self.assertEqual(f.read(), 'hehe' + os.linesep)
  318. @with_tempdir()
  319. def test_nested_high_state_execution(self, dirpath):
  320. output = os.path.join(dirpath, 'output')
  321. write_to(os.path.join(dirpath, 'aaa.sls'), textwrap.dedent('''\
  322. #!pydsl
  323. __salt__['state.sls']('bbb')
  324. state().cmd.run('echo bbbbbb', cwd='/')
  325. '''))
  326. write_to(os.path.join(dirpath, 'bbb.sls'), textwrap.dedent(
  327. '''
  328. # {{ salt['state.sls']('ccc') }}
  329. test:
  330. cmd.run:
  331. - name: echo bbbbbbb
  332. - cwd: /
  333. '''))
  334. write_to(os.path.join(dirpath, 'ccc.sls'), textwrap.dedent(
  335. '''
  336. #!pydsl
  337. state().cmd.run('echo ccccc', cwd='/')
  338. '''))
  339. self.state_highstate({'base': ['aaa']}, dirpath)
  340. @with_tempdir()
  341. def test_repeat_includes(self, dirpath):
  342. output = os.path.join(dirpath, 'output')
  343. write_to(os.path.join(dirpath, 'b.sls'), textwrap.dedent('''\
  344. #!pydsl
  345. include('c')
  346. include('d')
  347. '''))
  348. write_to(os.path.join(dirpath, 'c.sls'), textwrap.dedent('''\
  349. #!pydsl
  350. modtest = include('e')
  351. modtest.success
  352. '''))
  353. write_to(os.path.join(dirpath, 'd.sls'), textwrap.dedent('''\
  354. #!pydsl
  355. modtest = include('e')
  356. modtest.success
  357. '''))
  358. write_to(os.path.join(dirpath, 'e.sls'), textwrap.dedent('''\
  359. #!pydsl
  360. success = True
  361. '''))
  362. self.state_highstate({'base': ['b']}, dirpath)
  363. self.state_highstate({'base': ['c', 'd']}, dirpath)
  364. def write_to(fpath, content):
  365. with salt.utils.files.fopen(fpath, 'w') as f:
  366. f.write(content)