test_client.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: Mike Place <mp@saltstack.com>
  4. """
  5. from __future__ import absolute_import, print_function, unicode_literals
  6. import salt.utils.platform
  7. from salt import client
  8. from salt.exceptions import (
  9. EauthAuthenticationError,
  10. SaltClientError,
  11. SaltInvocationError,
  12. SaltReqTimeoutError,
  13. )
  14. from salt.ext.tornado.concurrent import Future
  15. from tests.support.helpers import slowTest
  16. from tests.support.mixins import SaltClientTestCaseMixin
  17. from tests.support.mock import MagicMock, patch
  18. from tests.support.unit import TestCase, skipIf
  19. class LocalClientTestCase(TestCase, SaltClientTestCaseMixin):
  20. def test_job_result_return_success(self):
  21. """
  22. Should return the `expected_return`, since there is a job with the right jid.
  23. """
  24. minions = ()
  25. jid = "0815"
  26. raw_return = {"id": "fake-id", "jid": jid, "data": "", "return": "fake-return"}
  27. expected_return = {"fake-id": {"ret": "fake-return"}}
  28. local_client = client.LocalClient(mopts=self.get_temp_config("master"))
  29. local_client.event.get_event = MagicMock(return_value=raw_return)
  30. local_client.returners = MagicMock()
  31. ret = local_client.get_event_iter_returns(jid, minions)
  32. val = next(ret)
  33. self.assertEqual(val, expected_return)
  34. def test_job_result_return_failure(self):
  35. """
  36. We are _not_ getting a job return, because the jid is different. Instead we should
  37. get a StopIteration exception.
  38. """
  39. minions = ()
  40. jid = "0815"
  41. raw_return = {
  42. "id": "fake-id",
  43. "jid": "0816",
  44. "data": "",
  45. "return": "fake-return",
  46. }
  47. local_client = client.LocalClient(mopts=self.get_temp_config("master"))
  48. local_client.event.get_event = MagicMock()
  49. local_client.event.get_event.side_effect = [raw_return, None]
  50. local_client.returners = MagicMock()
  51. ret = local_client.get_event_iter_returns(jid, minions)
  52. with self.assertRaises(StopIteration):
  53. next(ret)
  54. def test_create_local_client(self):
  55. local_client = client.LocalClient(mopts=self.get_temp_config("master"))
  56. self.assertIsInstance(
  57. local_client,
  58. client.LocalClient,
  59. "LocalClient did not create a LocalClient instance",
  60. )
  61. def test_check_pub_data(self):
  62. just_minions = {"minions": ["m1", "m2"]}
  63. jid_no_minions = {"jid": "1234", "minions": []}
  64. valid_pub_data = {"minions": ["m1", "m2"], "jid": "1234"}
  65. self.assertRaises(EauthAuthenticationError, self.client._check_pub_data, "")
  66. self.assertDictEqual(
  67. {},
  68. self.client._check_pub_data(just_minions),
  69. "Did not handle lack of jid correctly",
  70. )
  71. self.assertDictEqual(
  72. {},
  73. self.client._check_pub_data({"jid": "0"}),
  74. "Passing JID of zero is not handled gracefully",
  75. )
  76. with patch.dict(self.client.opts, {}):
  77. self.client._check_pub_data(jid_no_minions)
  78. self.assertDictEqual(
  79. valid_pub_data, self.client._check_pub_data(valid_pub_data)
  80. )
  81. def test_cmd_subset(self):
  82. with patch(
  83. "salt.client.LocalClient.cmd",
  84. return_value={
  85. "minion1": ["first.func", "second.func"],
  86. "minion2": ["first.func", "second.func"],
  87. },
  88. ):
  89. with patch("salt.client.LocalClient.cmd_cli") as cmd_cli_mock:
  90. self.client.cmd_subset("*", "first.func", sub=1, cli=True)
  91. try:
  92. cmd_cli_mock.assert_called_with(
  93. ["minion2"],
  94. "first.func",
  95. (),
  96. progress=False,
  97. kwarg=None,
  98. tgt_type="list",
  99. full_return=False,
  100. ret="",
  101. )
  102. except AssertionError:
  103. cmd_cli_mock.assert_called_with(
  104. ["minion1"],
  105. "first.func",
  106. (),
  107. progress=False,
  108. kwarg=None,
  109. tgt_type="list",
  110. full_return=False,
  111. ret="",
  112. )
  113. self.client.cmd_subset("*", "first.func", sub=10, cli=True)
  114. try:
  115. cmd_cli_mock.assert_called_with(
  116. ["minion2", "minion1"],
  117. "first.func",
  118. (),
  119. progress=False,
  120. kwarg=None,
  121. tgt_type="list",
  122. full_return=False,
  123. ret="",
  124. )
  125. except AssertionError:
  126. cmd_cli_mock.assert_called_with(
  127. ["minion1", "minion2"],
  128. "first.func",
  129. (),
  130. progress=False,
  131. kwarg=None,
  132. tgt_type="list",
  133. full_return=False,
  134. ret="",
  135. )
  136. ret = self.client.cmd_subset(
  137. "*", "first.func", sub=1, cli=True, full_return=True
  138. )
  139. try:
  140. cmd_cli_mock.assert_called_with(
  141. ["minion2"],
  142. "first.func",
  143. (),
  144. progress=False,
  145. kwarg=None,
  146. tgt_type="list",
  147. full_return=True,
  148. ret="",
  149. )
  150. except AssertionError:
  151. cmd_cli_mock.assert_called_with(
  152. ["minion1"],
  153. "first.func",
  154. (),
  155. progress=False,
  156. kwarg=None,
  157. tgt_type="list",
  158. full_return=True,
  159. ret="",
  160. )
  161. @skipIf(salt.utils.platform.is_windows(), "Not supported on Windows")
  162. def test_pub(self):
  163. """
  164. Tests that the client cleanly returns when the publisher is not running
  165. Note: Requires ZeroMQ's IPC transport which is not supported on windows.
  166. """
  167. if self.get_config("minion")["transport"] != "zeromq":
  168. self.skipTest("This test only works with ZeroMQ")
  169. # Make sure we cleanly return if the publisher isn't running
  170. with patch("os.path.exists", return_value=False):
  171. self.assertRaises(
  172. SaltClientError, lambda: self.client.pub("*", "test.ping")
  173. )
  174. # Check nodegroups behavior
  175. with patch("os.path.exists", return_value=True):
  176. with patch.dict(
  177. self.client.opts,
  178. {
  179. "nodegroups": {
  180. "group1": "L@foo.domain.com,bar.domain.com,baz.domain.com or bl*.domain.com"
  181. }
  182. },
  183. ):
  184. # Do we raise an exception if the nodegroup can't be matched?
  185. self.assertRaises(
  186. SaltInvocationError,
  187. self.client.pub,
  188. "non_existent_group",
  189. "test.ping",
  190. tgt_type="nodegroup",
  191. )
  192. @skipIf(not salt.utils.platform.is_windows(), "Windows only test")
  193. @slowTest
  194. def test_pub_win32(self):
  195. """
  196. Tests that the client raises a timeout error when using ZeroMQ's TCP
  197. transport and publisher is not running.
  198. Note: Requires ZeroMQ's TCP transport, this is only the default on Windows.
  199. """
  200. if self.get_config("minion")["transport"] != "zeromq":
  201. self.skipTest("This test only works with ZeroMQ")
  202. # Make sure we cleanly return if the publisher isn't running
  203. with patch("os.path.exists", return_value=False):
  204. self.assertRaises(
  205. SaltReqTimeoutError, lambda: self.client.pub("*", "test.ping")
  206. )
  207. # Check nodegroups behavior
  208. with patch("os.path.exists", return_value=True):
  209. with patch.dict(
  210. self.client.opts,
  211. {
  212. "nodegroups": {
  213. "group1": "L@foo.domain.com,bar.domain.com,baz.domain.com or bl*.domain.com"
  214. }
  215. },
  216. ):
  217. # Do we raise an exception if the nodegroup can't be matched?
  218. self.assertRaises(
  219. SaltInvocationError,
  220. self.client.pub,
  221. "non_existent_group",
  222. "test.ping",
  223. tgt_type="nodegroup",
  224. )
  225. # all of these parse_input test wrapper tests can be replaced by
  226. # parameterize if/when we switch to pytest runner
  227. # @pytest.mark.parametrize('method', [('run_job', 'cmd', ...)])
  228. def _test_parse_input(self, method, asynchronous=False):
  229. if asynchronous:
  230. target = "salt.client.LocalClient.pub_async"
  231. pub_ret = Future()
  232. pub_ret.set_result({"jid": "123456789", "minions": ["m1"]})
  233. else:
  234. target = "salt.client.LocalClient.pub"
  235. pub_ret = {"jid": "123456789", "minions": ["m1"]}
  236. with patch(target, return_value=pub_ret) as pub_mock:
  237. with patch(
  238. "salt.client.LocalClient.get_cli_event_returns",
  239. return_value=[{"m1": {"ret": ["test.arg"]}}],
  240. ):
  241. with patch(
  242. "salt.client.LocalClient.get_iter_returns",
  243. return_value=[{"m1": {"ret": True}}],
  244. ):
  245. ret = getattr(self.client, method)(
  246. "*",
  247. "test.arg",
  248. arg=[
  249. "a",
  250. 5,
  251. "yaml_arg={qux: Qux}",
  252. "another_yaml={bax: 12345}",
  253. ],
  254. jid="123456789",
  255. )
  256. # iterate generator if needed
  257. if asynchronous:
  258. pass
  259. else:
  260. ret = list(ret)
  261. # main test here is that yaml_arg is getting deserialized properly
  262. parsed_args = [
  263. "a",
  264. 5,
  265. {
  266. "yaml_arg": {"qux": "Qux"},
  267. "another_yaml": {"bax": 12345},
  268. "__kwarg__": True,
  269. },
  270. ]
  271. self.assertTrue(
  272. any(parsed_args in call[0] for call in pub_mock.call_args_list)
  273. )
  274. def test_parse_input_is_called(self):
  275. self._test_parse_input("run_job")
  276. self._test_parse_input("cmd")
  277. self._test_parse_input("cmd_subset")
  278. self._test_parse_input("cmd_batch")
  279. self._test_parse_input("cmd_cli")
  280. self._test_parse_input("cmd_full_return")
  281. self._test_parse_input("cmd_iter")
  282. self._test_parse_input("cmd_iter_no_block")
  283. self._test_parse_input("cmd_async")
  284. self._test_parse_input("run_job_async", asynchronous=True)