1
0

test_pydsl.py 17 KB

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