test_reactor.py 17 KB

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