1
0

test_reactor.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import, print_function, unicode_literals
  3. import codecs
  4. import glob
  5. import logging
  6. import os
  7. import textwrap
  8. import salt.loader
  9. import salt.utils.data
  10. import salt.utils.files
  11. import salt.utils.reactor as reactor
  12. import salt.utils.yaml
  13. from tests.support.mixins import AdaptedConfigurationTestCaseMixin
  14. from tests.support.mock import MagicMock, Mock, mock_open, patch
  15. from tests.support.unit import TestCase
  16. REACTOR_CONFIG = """\
  17. reactor:
  18. - old_runner:
  19. - /srv/reactor/old_runner.sls
  20. - old_wheel:
  21. - /srv/reactor/old_wheel.sls
  22. - old_local:
  23. - /srv/reactor/old_local.sls
  24. - old_cmd:
  25. - /srv/reactor/old_cmd.sls
  26. - old_caller:
  27. - /srv/reactor/old_caller.sls
  28. - new_runner:
  29. - /srv/reactor/new_runner.sls
  30. - new_wheel:
  31. - /srv/reactor/new_wheel.sls
  32. - new_local:
  33. - /srv/reactor/new_local.sls
  34. - new_cmd:
  35. - /srv/reactor/new_cmd.sls
  36. - new_caller:
  37. - /srv/reactor/new_caller.sls
  38. """
  39. REACTOR_DATA = {
  40. "runner": {"data": {"message": "This is an error"}},
  41. "wheel": {"data": {"id": "foo"}},
  42. "local": {"data": {"pkg": "zsh", "repo": "updates"}},
  43. "cmd": {"data": {"pkg": "zsh", "repo": "updates"}},
  44. "caller": {"data": {"path": "/tmp/foo"}},
  45. }
  46. SLS = {
  47. "/srv/reactor/old_runner.sls": textwrap.dedent(
  48. """\
  49. raise_error:
  50. runner.error.error:
  51. - name: Exception
  52. - message: {{ data['data']['message'] }}
  53. """
  54. ),
  55. "/srv/reactor/old_wheel.sls": textwrap.dedent(
  56. """\
  57. remove_key:
  58. wheel.key.delete:
  59. - match: {{ data['data']['id'] }}
  60. """
  61. ),
  62. "/srv/reactor/old_local.sls": textwrap.dedent(
  63. """\
  64. install_zsh:
  65. local.state.single:
  66. - tgt: test
  67. - arg:
  68. - pkg.installed
  69. - {{ data['data']['pkg'] }}
  70. - kwarg:
  71. fromrepo: {{ data['data']['repo'] }}
  72. """
  73. ),
  74. "/srv/reactor/old_cmd.sls": textwrap.dedent(
  75. """\
  76. install_zsh:
  77. cmd.state.single:
  78. - tgt: test
  79. - arg:
  80. - pkg.installed
  81. - {{ data['data']['pkg'] }}
  82. - kwarg:
  83. fromrepo: {{ data['data']['repo'] }}
  84. """
  85. ),
  86. "/srv/reactor/old_caller.sls": textwrap.dedent(
  87. """\
  88. touch_file:
  89. caller.file.touch:
  90. - args:
  91. - {{ data['data']['path'] }}
  92. """
  93. ),
  94. "/srv/reactor/new_runner.sls": textwrap.dedent(
  95. """\
  96. raise_error:
  97. runner.error.error:
  98. - args:
  99. - name: Exception
  100. - message: {{ data['data']['message'] }}
  101. """
  102. ),
  103. "/srv/reactor/new_wheel.sls": textwrap.dedent(
  104. """\
  105. remove_key:
  106. wheel.key.delete:
  107. - args:
  108. - match: {{ data['data']['id'] }}
  109. """
  110. ),
  111. "/srv/reactor/new_local.sls": textwrap.dedent(
  112. """\
  113. install_zsh:
  114. local.state.single:
  115. - tgt: test
  116. - args:
  117. - fun: pkg.installed
  118. - name: {{ data['data']['pkg'] }}
  119. - fromrepo: {{ data['data']['repo'] }}
  120. """
  121. ),
  122. "/srv/reactor/new_cmd.sls": textwrap.dedent(
  123. """\
  124. install_zsh:
  125. cmd.state.single:
  126. - tgt: test
  127. - args:
  128. - fun: pkg.installed
  129. - name: {{ data['data']['pkg'] }}
  130. - fromrepo: {{ data['data']['repo'] }}
  131. """
  132. ),
  133. "/srv/reactor/new_caller.sls": textwrap.dedent(
  134. """\
  135. touch_file:
  136. caller.file.touch:
  137. - args:
  138. - name: {{ data['data']['path'] }}
  139. """
  140. ),
  141. }
  142. LOW_CHUNKS = {
  143. # Note that the "name" value in the chunk has been overwritten by the
  144. # "name" argument in the SLS. This is one reason why the new schema was
  145. # needed.
  146. "old_runner": [
  147. {
  148. "state": "runner",
  149. "__id__": "raise_error",
  150. "__sls__": "/srv/reactor/old_runner.sls",
  151. "order": 1,
  152. "fun": "error.error",
  153. "name": "Exception",
  154. "message": "This is an error",
  155. }
  156. ],
  157. "old_wheel": [
  158. {
  159. "state": "wheel",
  160. "__id__": "remove_key",
  161. "name": "remove_key",
  162. "__sls__": "/srv/reactor/old_wheel.sls",
  163. "order": 1,
  164. "fun": "key.delete",
  165. "match": "foo",
  166. }
  167. ],
  168. "old_local": [
  169. {
  170. "state": "local",
  171. "__id__": "install_zsh",
  172. "name": "install_zsh",
  173. "__sls__": "/srv/reactor/old_local.sls",
  174. "order": 1,
  175. "tgt": "test",
  176. "fun": "state.single",
  177. "arg": ["pkg.installed", "zsh"],
  178. "kwarg": {"fromrepo": "updates"},
  179. }
  180. ],
  181. "old_cmd": [
  182. {
  183. "state": "local", # 'cmd' should be aliased to 'local'
  184. "__id__": "install_zsh",
  185. "name": "install_zsh",
  186. "__sls__": "/srv/reactor/old_cmd.sls",
  187. "order": 1,
  188. "tgt": "test",
  189. "fun": "state.single",
  190. "arg": ["pkg.installed", "zsh"],
  191. "kwarg": {"fromrepo": "updates"},
  192. }
  193. ],
  194. "old_caller": [
  195. {
  196. "state": "caller",
  197. "__id__": "touch_file",
  198. "name": "touch_file",
  199. "__sls__": "/srv/reactor/old_caller.sls",
  200. "order": 1,
  201. "fun": "file.touch",
  202. "args": ["/tmp/foo"],
  203. }
  204. ],
  205. "new_runner": [
  206. {
  207. "state": "runner",
  208. "__id__": "raise_error",
  209. "name": "raise_error",
  210. "__sls__": "/srv/reactor/new_runner.sls",
  211. "order": 1,
  212. "fun": "error.error",
  213. "args": [{"name": "Exception"}, {"message": "This is an error"}],
  214. }
  215. ],
  216. "new_wheel": [
  217. {
  218. "state": "wheel",
  219. "__id__": "remove_key",
  220. "name": "remove_key",
  221. "__sls__": "/srv/reactor/new_wheel.sls",
  222. "order": 1,
  223. "fun": "key.delete",
  224. "args": [{"match": "foo"}],
  225. }
  226. ],
  227. "new_local": [
  228. {
  229. "state": "local",
  230. "__id__": "install_zsh",
  231. "name": "install_zsh",
  232. "__sls__": "/srv/reactor/new_local.sls",
  233. "order": 1,
  234. "tgt": "test",
  235. "fun": "state.single",
  236. "args": [
  237. {"fun": "pkg.installed"},
  238. {"name": "zsh"},
  239. {"fromrepo": "updates"},
  240. ],
  241. }
  242. ],
  243. "new_cmd": [
  244. {
  245. "state": "local",
  246. "__id__": "install_zsh",
  247. "name": "install_zsh",
  248. "__sls__": "/srv/reactor/new_cmd.sls",
  249. "order": 1,
  250. "tgt": "test",
  251. "fun": "state.single",
  252. "args": [
  253. {"fun": "pkg.installed"},
  254. {"name": "zsh"},
  255. {"fromrepo": "updates"},
  256. ],
  257. }
  258. ],
  259. "new_caller": [
  260. {
  261. "state": "caller",
  262. "__id__": "touch_file",
  263. "name": "touch_file",
  264. "__sls__": "/srv/reactor/new_caller.sls",
  265. "order": 1,
  266. "fun": "file.touch",
  267. "args": [{"name": "/tmp/foo"}],
  268. }
  269. ],
  270. }
  271. WRAPPER_CALLS = {
  272. "old_runner": (
  273. "error.error",
  274. {
  275. "__state__": "runner",
  276. "__id__": "raise_error",
  277. "__sls__": "/srv/reactor/old_runner.sls",
  278. "__user__": "Reactor",
  279. "order": 1,
  280. "arg": [],
  281. "kwarg": {"name": "Exception", "message": "This is an error"},
  282. "name": "Exception",
  283. "message": "This is an error",
  284. },
  285. ),
  286. "old_wheel": (
  287. "key.delete",
  288. {
  289. "__state__": "wheel",
  290. "__id__": "remove_key",
  291. "name": "remove_key",
  292. "__sls__": "/srv/reactor/old_wheel.sls",
  293. "order": 1,
  294. "__user__": "Reactor",
  295. "arg": ["foo"],
  296. "kwarg": {},
  297. "match": "foo",
  298. },
  299. ),
  300. "old_local": {
  301. "args": ("test", "state.single"),
  302. "kwargs": {
  303. "state": "local",
  304. "__id__": "install_zsh",
  305. "name": "install_zsh",
  306. "__sls__": "/srv/reactor/old_local.sls",
  307. "order": 1,
  308. "arg": ["pkg.installed", "zsh"],
  309. "kwarg": {"fromrepo": "updates"},
  310. },
  311. },
  312. "old_cmd": {
  313. "args": ("test", "state.single"),
  314. "kwargs": {
  315. "state": "local",
  316. "__id__": "install_zsh",
  317. "name": "install_zsh",
  318. "__sls__": "/srv/reactor/old_cmd.sls",
  319. "order": 1,
  320. "arg": ["pkg.installed", "zsh"],
  321. "kwarg": {"fromrepo": "updates"},
  322. },
  323. },
  324. "old_caller": {"args": ("file.touch", "/tmp/foo"), "kwargs": {}},
  325. "new_runner": (
  326. "error.error",
  327. {
  328. "__state__": "runner",
  329. "__id__": "raise_error",
  330. "name": "raise_error",
  331. "__sls__": "/srv/reactor/new_runner.sls",
  332. "__user__": "Reactor",
  333. "order": 1,
  334. "arg": (),
  335. "kwarg": {"name": "Exception", "message": "This is an error"},
  336. },
  337. ),
  338. "new_wheel": (
  339. "key.delete",
  340. {
  341. "__state__": "wheel",
  342. "__id__": "remove_key",
  343. "name": "remove_key",
  344. "__sls__": "/srv/reactor/new_wheel.sls",
  345. "order": 1,
  346. "__user__": "Reactor",
  347. "arg": (),
  348. "kwarg": {"match": "foo"},
  349. },
  350. ),
  351. "new_local": {
  352. "args": ("test", "state.single"),
  353. "kwargs": {
  354. "state": "local",
  355. "__id__": "install_zsh",
  356. "name": "install_zsh",
  357. "__sls__": "/srv/reactor/new_local.sls",
  358. "order": 1,
  359. "arg": (),
  360. "kwarg": {"fun": "pkg.installed", "name": "zsh", "fromrepo": "updates"},
  361. },
  362. },
  363. "new_cmd": {
  364. "args": ("test", "state.single"),
  365. "kwargs": {
  366. "state": "local",
  367. "__id__": "install_zsh",
  368. "name": "install_zsh",
  369. "__sls__": "/srv/reactor/new_cmd.sls",
  370. "order": 1,
  371. "arg": (),
  372. "kwarg": {"fun": "pkg.installed", "name": "zsh", "fromrepo": "updates"},
  373. },
  374. },
  375. "new_caller": {"args": ("file.touch",), "kwargs": {"name": "/tmp/foo"}},
  376. }
  377. log = logging.getLogger(__name__)
  378. class TestReactor(TestCase, AdaptedConfigurationTestCaseMixin):
  379. """
  380. Tests for constructing the low chunks to be executed via the Reactor
  381. """
  382. @classmethod
  383. def setUpClass(cls):
  384. """
  385. Load the reactor config for mocking
  386. """
  387. cls.opts = cls.get_temp_config("master")
  388. reactor_config = salt.utils.yaml.safe_load(REACTOR_CONFIG)
  389. cls.opts.update(reactor_config)
  390. cls.reactor = reactor.Reactor(cls.opts)
  391. cls.reaction_map = salt.utils.data.repack_dictlist(reactor_config["reactor"])
  392. renderers = salt.loader.render(cls.opts, {})
  393. cls.render_pipe = [(renderers[x], "") for x in ("jinja", "yaml")]
  394. @classmethod
  395. def tearDownClass(cls):
  396. del cls.opts
  397. del cls.reactor
  398. del cls.render_pipe
  399. def test_list_reactors(self):
  400. """
  401. Ensure that list_reactors() returns the correct list of reactor SLS
  402. files for each tag.
  403. """
  404. for schema in ("old", "new"):
  405. for rtype in REACTOR_DATA:
  406. tag = "_".join((schema, rtype))
  407. self.assertEqual(
  408. self.reactor.list_reactors(tag), self.reaction_map[tag]
  409. )
  410. def test_reactions(self):
  411. """
  412. Ensure that the correct reactions are built from the configured SLS
  413. files and tag data.
  414. """
  415. for schema in ("old", "new"):
  416. for rtype in REACTOR_DATA:
  417. tag = "_".join((schema, rtype))
  418. log.debug("test_reactions: processing %s", tag)
  419. reactors = self.reactor.list_reactors(tag)
  420. log.debug("test_reactions: %s reactors: %s", tag, reactors)
  421. # No globbing in our example SLS, and the files don't actually
  422. # exist, so mock glob.glob to just return back the path passed
  423. # to it.
  424. with patch.object(glob, "glob", MagicMock(side_effect=lambda x: [x])):
  425. # The below four mocks are all so that
  426. # salt.template.compile_template() will read the templates
  427. # we've mocked up in the SLS global variable above.
  428. with patch.object(os.path, "isfile", MagicMock(return_value=True)):
  429. with patch.object(
  430. salt.utils.files, "is_empty", MagicMock(return_value=False)
  431. ):
  432. with patch.object(
  433. codecs, "open", mock_open(read_data=SLS[reactors[0]])
  434. ):
  435. with patch.object(
  436. salt.template,
  437. "template_shebang",
  438. MagicMock(return_value=self.render_pipe),
  439. ):
  440. reactions = self.reactor.reactions(
  441. tag, REACTOR_DATA[rtype], reactors,
  442. )
  443. log.debug(
  444. "test_reactions: %s reactions: %s",
  445. tag,
  446. reactions,
  447. )
  448. self.assertEqual(reactions, LOW_CHUNKS[tag])
  449. class TestReactWrap(TestCase, AdaptedConfigurationTestCaseMixin):
  450. """
  451. Tests that we are formulating the wrapper calls properly
  452. """
  453. @classmethod
  454. def setUpClass(cls):
  455. cls.wrap = reactor.ReactWrap(cls.get_temp_config("master"))
  456. @classmethod
  457. def tearDownClass(cls):
  458. del cls.wrap
  459. def test_runner(self):
  460. """
  461. Test runner reactions using both the old and new config schema
  462. """
  463. for schema in ("old", "new"):
  464. tag = "_".join((schema, "runner"))
  465. chunk = LOW_CHUNKS[tag][0]
  466. thread_pool = Mock()
  467. thread_pool.fire_async = Mock()
  468. with patch.object(self.wrap, "pool", thread_pool):
  469. self.wrap.run(chunk)
  470. thread_pool.fire_async.assert_called_with(
  471. self.wrap.client_cache["runner"].low, args=WRAPPER_CALLS[tag]
  472. )
  473. def test_wheel(self):
  474. """
  475. Test wheel reactions using both the old and new config schema
  476. """
  477. for schema in ("old", "new"):
  478. tag = "_".join((schema, "wheel"))
  479. chunk = LOW_CHUNKS[tag][0]
  480. thread_pool = Mock()
  481. thread_pool.fire_async = Mock()
  482. with patch.object(self.wrap, "pool", thread_pool):
  483. self.wrap.run(chunk)
  484. thread_pool.fire_async.assert_called_with(
  485. self.wrap.client_cache["wheel"].low, args=WRAPPER_CALLS[tag]
  486. )
  487. def test_local(self):
  488. """
  489. Test local reactions using both the old and new config schema
  490. """
  491. for schema in ("old", "new"):
  492. tag = "_".join((schema, "local"))
  493. chunk = LOW_CHUNKS[tag][0]
  494. client_cache = {"local": Mock()}
  495. client_cache["local"].cmd_async = Mock()
  496. with patch.object(self.wrap, "client_cache", client_cache):
  497. self.wrap.run(chunk)
  498. client_cache["local"].cmd_async.assert_called_with(
  499. *WRAPPER_CALLS[tag]["args"], **WRAPPER_CALLS[tag]["kwargs"]
  500. )
  501. def test_cmd(self):
  502. """
  503. Test cmd reactions (alias for 'local') using both the old and new
  504. config schema
  505. """
  506. for schema in ("old", "new"):
  507. tag = "_".join((schema, "cmd"))
  508. chunk = LOW_CHUNKS[tag][0]
  509. client_cache = {"local": Mock()}
  510. client_cache["local"].cmd_async = Mock()
  511. with patch.object(self.wrap, "client_cache", client_cache):
  512. self.wrap.run(chunk)
  513. client_cache["local"].cmd_async.assert_called_with(
  514. *WRAPPER_CALLS[tag]["args"], **WRAPPER_CALLS[tag]["kwargs"]
  515. )
  516. def test_caller(self):
  517. """
  518. Test caller reactions using both the old and new config schema
  519. """
  520. for schema in ("old", "new"):
  521. tag = "_".join((schema, "caller"))
  522. chunk = LOW_CHUNKS[tag][0]
  523. client_cache = {"caller": Mock()}
  524. client_cache["caller"].cmd = Mock()
  525. with patch.object(self.wrap, "client_cache", client_cache):
  526. self.wrap.run(chunk)
  527. client_cache["caller"].cmd.assert_called_with(
  528. *WRAPPER_CALLS[tag]["args"], **WRAPPER_CALLS[tag]["kwargs"]
  529. )