test_minion.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :codeauthor: Mike Place <mp@saltstack.com>
  4. '''
  5. # Import python libs
  6. from __future__ import absolute_import
  7. import copy
  8. import os
  9. import pytest
  10. # Import Salt Testing libs
  11. from tests.support.unit import TestCase
  12. from tests.support.mock import patch, MagicMock
  13. from tests.support.mixins import AdaptedConfigurationTestCaseMixin
  14. # Import salt libs
  15. import salt.minion
  16. import salt.utils.event as event
  17. from salt.exceptions import SaltSystemExit, SaltMasterUnresolvableError
  18. import salt.syspaths
  19. import tornado
  20. import tornado.testing
  21. from salt.ext.six.moves import range
  22. class MinionTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  23. def setUp(self):
  24. self.opts = {}
  25. self.addCleanup(delattr, self, 'opts')
  26. def test_invalid_master_address(self):
  27. with patch.dict(self.opts, {'ipv6': False, 'master': float('127.0'), 'master_port': '4555', 'retry_dns': False}):
  28. self.assertRaises(SaltSystemExit, salt.minion.resolve_dns, self.opts)
  29. def test_source_int_name_local(self):
  30. '''
  31. test when file_client local and
  32. source_interface_name is set
  33. '''
  34. interfaces = {'bond0.1234': {'hwaddr': '01:01:01:d0:d0:d0',
  35. 'up': True, 'inet':
  36. [{'broadcast': '111.1.111.255',
  37. 'netmask': '111.1.0.0',
  38. 'label': 'bond0',
  39. 'address': '111.1.0.1'}]}}
  40. with patch.dict(self.opts, {'ipv6': False, 'master': '127.0.0.1',
  41. 'master_port': '4555', 'file_client': 'local',
  42. 'source_interface_name': 'bond0.1234',
  43. 'source_ret_port': 49017,
  44. 'source_publish_port': 49018}), \
  45. patch('salt.utils.network.interfaces',
  46. MagicMock(return_value=interfaces)):
  47. assert salt.minion.resolve_dns(self.opts) == {'master_ip': '127.0.0.1',
  48. 'source_ip': '111.1.0.1',
  49. 'source_ret_port': 49017,
  50. 'source_publish_port': 49018,
  51. 'master_uri': 'tcp://127.0.0.1:4555'}
  52. def test_source_int_name_remote(self):
  53. '''
  54. test when file_client remote and
  55. source_interface_name is set and
  56. interface is down
  57. '''
  58. interfaces = {'bond0.1234': {'hwaddr': '01:01:01:d0:d0:d0',
  59. 'up': False, 'inet':
  60. [{'broadcast': '111.1.111.255',
  61. 'netmask': '111.1.0.0',
  62. 'label': 'bond0',
  63. 'address': '111.1.0.1'}]}}
  64. with patch.dict(self.opts, {'ipv6': False, 'master': '127.0.0.1',
  65. 'master_port': '4555', 'file_client': 'remote',
  66. 'source_interface_name': 'bond0.1234',
  67. 'source_ret_port': 49017,
  68. 'source_publish_port': 49018}), \
  69. patch('salt.utils.network.interfaces',
  70. MagicMock(return_value=interfaces)):
  71. assert salt.minion.resolve_dns(self.opts) == {'master_ip': '127.0.0.1',
  72. 'source_ret_port': 49017,
  73. 'source_publish_port': 49018,
  74. 'master_uri': 'tcp://127.0.0.1:4555'}
  75. def test_source_address(self):
  76. '''
  77. test when source_address is set
  78. '''
  79. interfaces = {'bond0.1234': {'hwaddr': '01:01:01:d0:d0:d0',
  80. 'up': False, 'inet':
  81. [{'broadcast': '111.1.111.255',
  82. 'netmask': '111.1.0.0',
  83. 'label': 'bond0',
  84. 'address': '111.1.0.1'}]}}
  85. with patch.dict(self.opts, {'ipv6': False, 'master': '127.0.0.1',
  86. 'master_port': '4555', 'file_client': 'local',
  87. 'source_interface_name': '',
  88. 'source_address': '111.1.0.1',
  89. 'source_ret_port': 49017,
  90. 'source_publish_port': 49018}), \
  91. patch('salt.utils.network.interfaces',
  92. MagicMock(return_value=interfaces)):
  93. assert salt.minion.resolve_dns(self.opts) == {'source_publish_port': 49018,
  94. 'source_ret_port': 49017,
  95. 'master_uri': 'tcp://127.0.0.1:4555',
  96. 'source_ip': '111.1.0.1',
  97. 'master_ip': '127.0.0.1'}
  98. # Tests for _handle_decoded_payload in the salt.minion.Minion() class: 3
  99. def test_handle_decoded_payload_jid_match_in_jid_queue(self):
  100. '''
  101. Tests that the _handle_decoded_payload function returns when a jid is given that is already present
  102. in the jid_queue.
  103. Note: This test doesn't contain all of the patch decorators above the function like the other tests
  104. for _handle_decoded_payload below. This is essential to this test as the call to the function must
  105. return None BEFORE any of the processes are spun up because we should be avoiding firing duplicate
  106. jobs.
  107. '''
  108. mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
  109. mock_data = {'fun': 'foo.bar',
  110. 'jid': 123}
  111. mock_jid_queue = [123]
  112. minion = salt.minion.Minion(mock_opts, jid_queue=copy.copy(mock_jid_queue), io_loop=tornado.ioloop.IOLoop())
  113. try:
  114. ret = minion._handle_decoded_payload(mock_data).result()
  115. self.assertEqual(minion.jid_queue, mock_jid_queue)
  116. self.assertIsNone(ret)
  117. finally:
  118. minion.destroy()
  119. def test_handle_decoded_payload_jid_queue_addition(self):
  120. '''
  121. Tests that the _handle_decoded_payload function adds a jid to the minion's jid_queue when the new
  122. jid isn't already present in the jid_queue.
  123. '''
  124. with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
  125. patch('salt.utils.process.SignalHandlingProcess.start', MagicMock(return_value=True)), \
  126. patch('salt.utils.process.SignalHandlingProcess.join', MagicMock(return_value=True)):
  127. mock_jid = 11111
  128. mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
  129. mock_data = {'fun': 'foo.bar',
  130. 'jid': mock_jid}
  131. mock_jid_queue = [123, 456]
  132. minion = salt.minion.Minion(mock_opts, jid_queue=copy.copy(mock_jid_queue), io_loop=tornado.ioloop.IOLoop())
  133. try:
  134. # Assert that the minion's jid_queue attribute matches the mock_jid_queue as a baseline
  135. # This can help debug any test failures if the _handle_decoded_payload call fails.
  136. self.assertEqual(minion.jid_queue, mock_jid_queue)
  137. # Call the _handle_decoded_payload function and update the mock_jid_queue to include the new
  138. # mock_jid. The mock_jid should have been added to the jid_queue since the mock_jid wasn't
  139. # previously included. The minion's jid_queue attribute and the mock_jid_queue should be equal.
  140. minion._handle_decoded_payload(mock_data).result()
  141. mock_jid_queue.append(mock_jid)
  142. self.assertEqual(minion.jid_queue, mock_jid_queue)
  143. finally:
  144. minion.destroy()
  145. def test_handle_decoded_payload_jid_queue_reduced_minion_jid_queue_hwm(self):
  146. '''
  147. Tests that the _handle_decoded_payload function removes a jid from the minion's jid_queue when the
  148. minion's jid_queue high water mark (minion_jid_queue_hwm) is hit.
  149. '''
  150. with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
  151. patch('salt.utils.process.SignalHandlingProcess.start', MagicMock(return_value=True)), \
  152. patch('salt.utils.process.SignalHandlingProcess.join', MagicMock(return_value=True)):
  153. mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
  154. mock_opts['minion_jid_queue_hwm'] = 2
  155. mock_data = {'fun': 'foo.bar',
  156. 'jid': 789}
  157. mock_jid_queue = [123, 456]
  158. minion = salt.minion.Minion(mock_opts, jid_queue=copy.copy(mock_jid_queue), io_loop=tornado.ioloop.IOLoop())
  159. try:
  160. # Assert that the minion's jid_queue attribute matches the mock_jid_queue as a baseline
  161. # This can help debug any test failures if the _handle_decoded_payload call fails.
  162. self.assertEqual(minion.jid_queue, mock_jid_queue)
  163. # Call the _handle_decoded_payload function and check that the queue is smaller by one item
  164. # and contains the new jid
  165. minion._handle_decoded_payload(mock_data).result()
  166. self.assertEqual(len(minion.jid_queue), 2)
  167. self.assertEqual(minion.jid_queue, [456, 789])
  168. finally:
  169. minion.destroy()
  170. def test_process_count_max(self):
  171. '''
  172. Tests that the _handle_decoded_payload function does not spawn more than the configured amount of processes,
  173. as per process_count_max.
  174. '''
  175. with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
  176. patch('salt.utils.process.SignalHandlingProcess.start', MagicMock(return_value=True)), \
  177. patch('salt.utils.process.SignalHandlingProcess.join', MagicMock(return_value=True)), \
  178. patch('salt.utils.minion.running', MagicMock(return_value=[])), \
  179. patch('tornado.gen.sleep', MagicMock(return_value=tornado.concurrent.Future())):
  180. process_count_max = 10
  181. mock_opts = salt.config.DEFAULT_MINION_OPTS.copy()
  182. mock_opts['__role'] = 'minion'
  183. mock_opts['minion_jid_queue_hwm'] = 100
  184. mock_opts["process_count_max"] = process_count_max
  185. io_loop = tornado.ioloop.IOLoop()
  186. minion = salt.minion.Minion(mock_opts, jid_queue=[], io_loop=io_loop)
  187. try:
  188. # mock gen.sleep to throw a special Exception when called, so that we detect it
  189. class SleepCalledException(Exception):
  190. """Thrown when sleep is called"""
  191. pass
  192. tornado.gen.sleep.return_value.set_exception(SleepCalledException())
  193. # up until process_count_max: gen.sleep does not get called, processes are started normally
  194. for i in range(process_count_max):
  195. mock_data = {'fun': 'foo.bar',
  196. 'jid': i}
  197. io_loop.run_sync(lambda data=mock_data: minion._handle_decoded_payload(data))
  198. self.assertEqual(salt.utils.process.SignalHandlingProcess.start.call_count, i + 1)
  199. self.assertEqual(len(minion.jid_queue), i + 1)
  200. salt.utils.minion.running.return_value += [i]
  201. # above process_count_max: gen.sleep does get called, JIDs are created but no new processes are started
  202. mock_data = {'fun': 'foo.bar',
  203. 'jid': process_count_max + 1}
  204. self.assertRaises(SleepCalledException,
  205. lambda: io_loop.run_sync(lambda: minion._handle_decoded_payload(mock_data)))
  206. self.assertEqual(salt.utils.process.SignalHandlingProcess.start.call_count,
  207. process_count_max)
  208. self.assertEqual(len(minion.jid_queue), process_count_max + 1)
  209. finally:
  210. minion.destroy()
  211. def test_beacons_before_connect(self):
  212. '''
  213. Tests that the 'beacons_before_connect' option causes the beacons to be initialized before connect.
  214. '''
  215. with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
  216. patch('salt.minion.Minion.sync_connect_master', MagicMock(side_effect=RuntimeError('stop execution'))), \
  217. patch('salt.utils.process.SignalHandlingProcess.start', MagicMock(return_value=True)), \
  218. patch('salt.utils.process.SignalHandlingProcess.join', MagicMock(return_value=True)):
  219. mock_opts = self.get_config('minion', from_scratch=True)
  220. mock_opts['beacons_before_connect'] = True
  221. io_loop = tornado.ioloop.IOLoop()
  222. io_loop.make_current()
  223. minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
  224. try:
  225. try:
  226. minion.tune_in(start=True)
  227. except RuntimeError:
  228. pass
  229. # Make sure beacons are initialized but the sheduler is not
  230. self.assertTrue('beacons' in minion.periodic_callbacks)
  231. self.assertTrue('schedule' not in minion.periodic_callbacks)
  232. finally:
  233. minion.destroy()
  234. def test_scheduler_before_connect(self):
  235. '''
  236. Tests that the 'scheduler_before_connect' option causes the scheduler to be initialized before connect.
  237. '''
  238. with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
  239. patch('salt.minion.Minion.sync_connect_master', MagicMock(side_effect=RuntimeError('stop execution'))), \
  240. patch('salt.utils.process.SignalHandlingProcess.start', MagicMock(return_value=True)), \
  241. patch('salt.utils.process.SignalHandlingProcess.join', MagicMock(return_value=True)):
  242. mock_opts = self.get_config('minion', from_scratch=True)
  243. mock_opts['scheduler_before_connect'] = True
  244. io_loop = tornado.ioloop.IOLoop()
  245. io_loop.make_current()
  246. minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
  247. try:
  248. try:
  249. minion.tune_in(start=True)
  250. except RuntimeError:
  251. pass
  252. # Make sure the scheduler is initialized but the beacons are not
  253. self.assertTrue('schedule' in minion.periodic_callbacks)
  254. self.assertTrue('beacons' not in minion.periodic_callbacks)
  255. finally:
  256. minion.destroy()
  257. def test_when_ping_interval_is_set_the_callback_should_be_added_to_periodic_callbacks(self):
  258. with patch('salt.minion.Minion.ctx', MagicMock(return_value={})), \
  259. patch('salt.minion.Minion.sync_connect_master', MagicMock(side_effect=RuntimeError('stop execution'))), \
  260. patch('salt.utils.process.SignalHandlingProcess.start', MagicMock(return_value=True)), \
  261. patch('salt.utils.process.SignalHandlingProcess.join', MagicMock(return_value=True)):
  262. mock_opts = self.get_config('minion', from_scratch=True)
  263. mock_opts['ping_interval'] = 10
  264. io_loop = tornado.ioloop.IOLoop()
  265. io_loop.make_current()
  266. minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
  267. try:
  268. try:
  269. minion.connected = MagicMock(side_effect=(False, True))
  270. minion._fire_master_minion_start = MagicMock()
  271. minion.tune_in(start=False)
  272. except RuntimeError:
  273. pass
  274. # Make sure the scheduler is initialized but the beacons are not
  275. self.assertTrue('ping' in minion.periodic_callbacks)
  276. finally:
  277. minion.destroy()
  278. def test_minion_retry_dns_count(self):
  279. '''
  280. Tests that the resolve_dns will retry dns look ups for a maximum of
  281. 3 times before raising a SaltMasterUnresolvableError exception.
  282. '''
  283. with patch.dict(self.opts, {'ipv6': False, 'master': 'dummy',
  284. 'master_port': '4555',
  285. 'retry_dns': 1, 'retry_dns_count': 3}):
  286. self.assertRaises(SaltMasterUnresolvableError,
  287. salt.minion.resolve_dns, self.opts)
  288. def test_gen_modules_executors(self):
  289. '''
  290. Ensure gen_modules is called with the correct arguments #54429
  291. '''
  292. mock_opts = self.get_config('minion', from_scratch=True)
  293. io_loop = tornado.ioloop.IOLoop()
  294. io_loop.make_current()
  295. minion = salt.minion.Minion(mock_opts, io_loop=io_loop)
  296. class MockPillarCompiler(object):
  297. def compile_pillar(self):
  298. return {}
  299. try:
  300. with patch('salt.pillar.get_pillar', return_value=MockPillarCompiler()):
  301. with patch('salt.loader.executors') as execmock:
  302. minion.gen_modules()
  303. assert execmock.called_with(minion.opts, minion.functions)
  304. finally:
  305. minion.destroy()
  306. class MinionAsyncTestCase(TestCase, AdaptedConfigurationTestCaseMixin, tornado.testing.AsyncTestCase):
  307. def setUp(self):
  308. super(MinionAsyncTestCase, self).setUp()
  309. self.opts = {}
  310. self.addCleanup(delattr, self, 'opts')
  311. @pytest.mark.skip_if_not_root
  312. def test_sock_path_len(self):
  313. '''
  314. This tests whether or not a larger hash causes the sock path to exceed
  315. the system's max sock path length. See the below link for more
  316. information.
  317. https://github.com/saltstack/salt/issues/12172#issuecomment-43903643
  318. '''
  319. opts = {
  320. 'id': 'salt-testing',
  321. 'hash_type': 'sha512',
  322. 'sock_dir': os.path.join(salt.syspaths.SOCK_DIR, 'minion'),
  323. 'extension_modules': ''
  324. }
  325. with patch.dict(self.opts, opts):
  326. try:
  327. event_publisher = event.AsyncEventPublisher(self.opts)
  328. result = True
  329. except ValueError:
  330. # There are rare cases where we operate a closed socket, especially in containers.
  331. # In this case, don't fail the test because we'll catch it down the road.
  332. result = True
  333. except SaltSystemExit:
  334. result = False
  335. self.assertTrue(result)