test_mac_utils.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. # -*- coding: utf-8 -*-
  2. """
  3. mac_utils tests
  4. """
  5. # Import python libs
  6. from __future__ import absolute_import, unicode_literals
  7. import os
  8. import plistlib
  9. import xml.parsers.expat
  10. # Import Salt libs
  11. import salt.utils.mac_utils as mac_utils
  12. import salt.utils.platform
  13. from salt.exceptions import CommandExecutionError, SaltInvocationError
  14. from salt.ext import six
  15. # Import 3rd-party libs
  16. from salt.ext.six.moves import range
  17. from tests.support.mixins import LoaderModuleMockMixin
  18. from tests.support.mock import MagicMock, call, mock_open, patch
  19. # Import Salt Testing Libs
  20. from tests.support.unit import TestCase, skipIf
  21. @skipIf(not salt.utils.platform.is_darwin(), "These tests run only on mac")
  22. class MacUtilsTestCase(TestCase, LoaderModuleMockMixin):
  23. """
  24. test mac_utils salt utility
  25. """
  26. def setup_loader_modules(self):
  27. return {mac_utils: {}}
  28. def test_execute_return_success_not_supported(self):
  29. """
  30. test execute_return_success function
  31. command not supported
  32. """
  33. mock_cmd = MagicMock(
  34. return_value={"retcode": 0, "stdout": "not supported", "stderr": "error"}
  35. )
  36. with patch.object(mac_utils, "_run_all", mock_cmd):
  37. self.assertRaises(
  38. CommandExecutionError, mac_utils.execute_return_success, "dir c:\\"
  39. )
  40. def test_execute_return_success_command_failed(self):
  41. """
  42. test execute_return_success function
  43. command failed
  44. """
  45. mock_cmd = MagicMock(
  46. return_value={"retcode": 1, "stdout": "spongebob", "stderr": "error"}
  47. )
  48. with patch.object(mac_utils, "_run_all", mock_cmd):
  49. self.assertRaises(
  50. CommandExecutionError, mac_utils.execute_return_success, "dir c:\\"
  51. )
  52. def test_execute_return_success_command_succeeded(self):
  53. """
  54. test execute_return_success function
  55. command succeeded
  56. """
  57. mock_cmd = MagicMock(return_value={"retcode": 0, "stdout": "spongebob"})
  58. with patch.object(mac_utils, "_run_all", mock_cmd):
  59. ret = mac_utils.execute_return_success("dir c:\\")
  60. self.assertEqual(ret, True)
  61. def test_execute_return_result_command_failed(self):
  62. """
  63. test execute_return_result function
  64. command failed
  65. """
  66. mock_cmd = MagicMock(
  67. return_value={"retcode": 1, "stdout": "spongebob", "stderr": "squarepants"}
  68. )
  69. with patch.object(mac_utils, "_run_all", mock_cmd):
  70. self.assertRaises(
  71. CommandExecutionError, mac_utils.execute_return_result, "dir c:\\"
  72. )
  73. def test_execute_return_result_command_succeeded(self):
  74. """
  75. test execute_return_result function
  76. command succeeded
  77. """
  78. mock_cmd = MagicMock(return_value={"retcode": 0, "stdout": "spongebob"})
  79. with patch.object(mac_utils, "_run_all", mock_cmd):
  80. ret = mac_utils.execute_return_result("dir c:\\")
  81. self.assertEqual(ret, "spongebob")
  82. def test_parse_return_space(self):
  83. """
  84. test parse_return function
  85. space after colon
  86. """
  87. self.assertEqual(
  88. mac_utils.parse_return("spongebob: squarepants"), "squarepants"
  89. )
  90. def test_parse_return_new_line(self):
  91. """
  92. test parse_return function
  93. new line after colon
  94. """
  95. self.assertEqual(
  96. mac_utils.parse_return("spongebob:\nsquarepants"), "squarepants"
  97. )
  98. def test_parse_return_no_delimiter(self):
  99. """
  100. test parse_return function
  101. no delimiter
  102. """
  103. self.assertEqual(mac_utils.parse_return("squarepants"), "squarepants")
  104. def test_validate_enabled_on(self):
  105. """
  106. test validate_enabled function
  107. test on
  108. """
  109. self.assertEqual(mac_utils.validate_enabled("On"), "on")
  110. def test_validate_enabled_off(self):
  111. """
  112. test validate_enabled function
  113. test off
  114. """
  115. self.assertEqual(mac_utils.validate_enabled("Off"), "off")
  116. def test_validate_enabled_bad_string(self):
  117. """
  118. test validate_enabled function
  119. test bad string
  120. """
  121. self.assertRaises(SaltInvocationError, mac_utils.validate_enabled, "bad string")
  122. def test_validate_enabled_non_zero(self):
  123. """
  124. test validate_enabled function
  125. test non zero
  126. """
  127. for x in range(1, 179, 3):
  128. self.assertEqual(mac_utils.validate_enabled(x), "on")
  129. def test_validate_enabled_0(self):
  130. """
  131. test validate_enabled function
  132. test 0
  133. """
  134. self.assertEqual(mac_utils.validate_enabled(0), "off")
  135. def test_validate_enabled_true(self):
  136. """
  137. test validate_enabled function
  138. test True
  139. """
  140. self.assertEqual(mac_utils.validate_enabled(True), "on")
  141. def test_validate_enabled_false(self):
  142. """
  143. test validate_enabled function
  144. test False
  145. """
  146. self.assertEqual(mac_utils.validate_enabled(False), "off")
  147. def test_launchctl(self):
  148. """
  149. test launchctl function
  150. """
  151. mock_cmd = MagicMock(
  152. return_value={"retcode": 0, "stdout": "success", "stderr": "none"}
  153. )
  154. with patch("salt.utils.mac_utils.__salt__", {"cmd.run_all": mock_cmd}):
  155. ret = mac_utils.launchctl("enable", "org.salt.minion")
  156. self.assertEqual(ret, True)
  157. def test_launchctl_return_stdout(self):
  158. """
  159. test launchctl function and return stdout
  160. """
  161. mock_cmd = MagicMock(
  162. return_value={"retcode": 0, "stdout": "success", "stderr": "none"}
  163. )
  164. with patch("salt.utils.mac_utils.__salt__", {"cmd.run_all": mock_cmd}):
  165. ret = mac_utils.launchctl("enable", "org.salt.minion", return_stdout=True)
  166. self.assertEqual(ret, "success")
  167. def test_launchctl_error(self):
  168. """
  169. test launchctl function returning an error
  170. """
  171. mock_cmd = MagicMock(
  172. return_value={"retcode": 1, "stdout": "failure", "stderr": "test failure"}
  173. )
  174. error = (
  175. "Failed to enable service:\n"
  176. "stdout: failure\n"
  177. "stderr: test failure\n"
  178. "retcode: 1"
  179. )
  180. with patch("salt.utils.mac_utils.__salt__", {"cmd.run_all": mock_cmd}):
  181. try:
  182. mac_utils.launchctl("enable", "org.salt.minion")
  183. except CommandExecutionError as exc:
  184. self.assertEqual(exc.message, error)
  185. @patch("salt.utils.path.os_walk")
  186. @patch("os.path.exists")
  187. def test_available_services_result(self, mock_exists, mock_os_walk):
  188. """
  189. test available_services results are properly formed dicts.
  190. """
  191. results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]}
  192. mock_os_walk.side_effect = _get_walk_side_effects(results)
  193. mock_exists.return_value = True
  194. plists = [{"Label": "com.apple.lla1"}]
  195. ret = _run_available_services(plists)
  196. file_path = os.sep + os.path.join(
  197. "Library", "LaunchAgents", "com.apple.lla1.plist"
  198. )
  199. if salt.utils.platform.is_windows():
  200. file_path = "c:" + file_path
  201. expected = {
  202. "com.apple.lla1": {
  203. "file_name": "com.apple.lla1.plist",
  204. "file_path": file_path,
  205. "plist": plists[0],
  206. }
  207. }
  208. self.assertEqual(ret, expected)
  209. @patch("salt.utils.path.os_walk")
  210. @patch("os.path.exists")
  211. @patch("os.listdir")
  212. @patch("os.path.isdir")
  213. def test_available_services_dirs(
  214. self, mock_isdir, mock_listdir, mock_exists, mock_os_walk
  215. ):
  216. """
  217. test available_services checks all of the expected dirs.
  218. """
  219. results = {
  220. "/Library/LaunchAgents": ["com.apple.lla1.plist"],
  221. "/Library/LaunchDaemons": ["com.apple.lld1.plist"],
  222. "/System/Library/LaunchAgents": ["com.apple.slla1.plist"],
  223. "/System/Library/LaunchDaemons": ["com.apple.slld1.plist"],
  224. "/Users/saltymcsaltface/Library/LaunchAgents": ["com.apple.uslla1.plist"],
  225. }
  226. mock_os_walk.side_effect = _get_walk_side_effects(results)
  227. mock_listdir.return_value = ["saltymcsaltface"]
  228. mock_isdir.return_value = True
  229. mock_exists.return_value = True
  230. plists = [
  231. {"Label": "com.apple.lla1"},
  232. {"Label": "com.apple.lld1"},
  233. {"Label": "com.apple.slla1"},
  234. {"Label": "com.apple.slld1"},
  235. {"Label": "com.apple.uslla1"},
  236. ]
  237. ret = _run_available_services(plists)
  238. self.assertEqual(len(ret), 5)
  239. @patch("salt.utils.path.os_walk")
  240. @patch("os.path.exists")
  241. @patch("plistlib.readPlist" if six.PY2 else "plistlib.load")
  242. def test_available_services_broken_symlink(
  243. self, mock_read_plist, mock_exists, mock_os_walk
  244. ):
  245. """
  246. test available_services when it encounters a broken symlink.
  247. """
  248. results = {
  249. "/Library/LaunchAgents": ["com.apple.lla1.plist", "com.apple.lla2.plist"]
  250. }
  251. mock_os_walk.side_effect = _get_walk_side_effects(results)
  252. mock_exists.side_effect = [True, False]
  253. plists = [{"Label": "com.apple.lla1"}]
  254. ret = _run_available_services(plists)
  255. file_path = os.sep + os.path.join(
  256. "Library", "LaunchAgents", "com.apple.lla1.plist"
  257. )
  258. if salt.utils.platform.is_windows():
  259. file_path = "c:" + file_path
  260. expected = {
  261. "com.apple.lla1": {
  262. "file_name": "com.apple.lla1.plist",
  263. "file_path": file_path,
  264. "plist": plists[0],
  265. }
  266. }
  267. self.assertEqual(ret, expected)
  268. @patch("salt.utils.path.os_walk")
  269. @patch("os.path.exists")
  270. @patch("plistlib.readPlist")
  271. @patch("salt.utils.mac_utils.__salt__")
  272. @patch("plistlib.readPlistFromString", create=True)
  273. def test_available_services_binary_plist(
  274. self,
  275. mock_read_plist_from_string,
  276. mock_run,
  277. mock_read_plist,
  278. mock_exists,
  279. mock_os_walk,
  280. ):
  281. """
  282. test available_services handles binary plist files.
  283. """
  284. results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]}
  285. mock_os_walk.side_effect = _get_walk_side_effects(results)
  286. mock_exists.return_value = True
  287. plists = [{"Label": "com.apple.lla1"}]
  288. file_path = os.sep + os.path.join(
  289. "Library", "LaunchAgents", "com.apple.lla1.plist"
  290. )
  291. if salt.utils.platform.is_windows():
  292. file_path = "c:" + file_path
  293. if six.PY2:
  294. attrs = {"cmd.run": MagicMock()}
  295. def getitem(name):
  296. return attrs[name]
  297. mock_run.__getitem__.side_effect = getitem
  298. mock_run.configure_mock(**attrs)
  299. cmd = '/usr/bin/plutil -convert xml1 -o - -- "{}"'.format(file_path)
  300. calls = [call.cmd.run(cmd)]
  301. mock_read_plist.side_effect = xml.parsers.expat.ExpatError
  302. mock_read_plist_from_string.side_effect = plists
  303. ret = mac_utils._available_services()
  304. else:
  305. # Py3 plistlib knows how to handle binary plists without
  306. # any extra work, so this test doesn't really do anything
  307. # new.
  308. ret = _run_available_services(plists)
  309. expected = {
  310. "com.apple.lla1": {
  311. "file_name": "com.apple.lla1.plist",
  312. "file_path": file_path,
  313. "plist": plists[0],
  314. }
  315. }
  316. self.assertEqual(ret, expected)
  317. if six.PY2:
  318. mock_run.assert_has_calls(calls, any_order=True)
  319. @patch("salt.utils.path.os_walk")
  320. @patch("os.path.exists")
  321. def test_available_services_invalid_file(self, mock_exists, mock_os_walk):
  322. """
  323. test available_services excludes invalid files.
  324. The py3 plistlib raises an InvalidFileException when a plist
  325. file cannot be parsed. This test only asserts things for py3.
  326. """
  327. if six.PY3:
  328. results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]}
  329. mock_os_walk.side_effect = _get_walk_side_effects(results)
  330. mock_exists.return_value = True
  331. plists = [{"Label": "com.apple.lla1"}]
  332. mock_load = MagicMock()
  333. mock_load.side_effect = plistlib.InvalidFileException
  334. with patch("salt.utils.files.fopen", mock_open()):
  335. with patch("plistlib.load", mock_load):
  336. ret = mac_utils._available_services()
  337. self.assertEqual(len(ret), 0)
  338. @patch("salt.utils.mac_utils.__salt__")
  339. @patch("plistlib.readPlist")
  340. @patch("salt.utils.path.os_walk")
  341. @patch("os.path.exists")
  342. def test_available_services_expat_error(
  343. self, mock_exists, mock_os_walk, mock_read_plist, mock_run
  344. ):
  345. """
  346. test available_services excludes files with expat errors.
  347. Poorly formed XML will raise an ExpatError on py2. It will
  348. also be raised by some almost-correct XML on py3.
  349. """
  350. results = {"/Library/LaunchAgents": ["com.apple.lla1.plist"]}
  351. mock_os_walk.side_effect = _get_walk_side_effects(results)
  352. mock_exists.return_value = True
  353. file_path = os.sep + os.path.join(
  354. "Library", "LaunchAgents", "com.apple.lla1.plist"
  355. )
  356. if salt.utils.platform.is_windows():
  357. file_path = "c:" + file_path
  358. if six.PY3:
  359. mock_load = MagicMock()
  360. mock_load.side_effect = xml.parsers.expat.ExpatError
  361. with patch("salt.utils.files.fopen", mock_open()):
  362. with patch("plistlib.load", mock_load):
  363. ret = mac_utils._available_services()
  364. else:
  365. attrs = {"cmd.run": MagicMock()}
  366. def getitem(name):
  367. return attrs[name]
  368. mock_run.__getitem__.side_effect = getitem
  369. mock_run.configure_mock(**attrs)
  370. cmd = '/usr/bin/plutil -convert xml1 -o - -- "{}"'.format(file_path)
  371. calls = [call.cmd.run(cmd)]
  372. mock_raise_expat_error = MagicMock(side_effect=xml.parsers.expat.ExpatError)
  373. with patch("plistlib.readPlist", mock_raise_expat_error):
  374. with patch("plistlib.readPlistFromString", mock_raise_expat_error):
  375. ret = mac_utils._available_services()
  376. mock_run.assert_has_calls(calls, any_order=True)
  377. self.assertEqual(len(ret), 0)
  378. def _get_walk_side_effects(results):
  379. """
  380. Data generation helper function for service tests.
  381. """
  382. def walk_side_effect(*args, **kwargs):
  383. return [(args[0], [], results.get(args[0], []))]
  384. return walk_side_effect
  385. def _run_available_services(plists):
  386. if six.PY2:
  387. mock_read_plist = MagicMock()
  388. mock_read_plist.side_effect = plists
  389. with patch("plistlib.readPlist", mock_read_plist):
  390. ret = mac_utils._available_services()
  391. else:
  392. mock_load = MagicMock()
  393. mock_load.side_effect = plists
  394. with patch("salt.utils.files.fopen", mock_open()):
  395. with patch("plistlib.load", mock_load):
  396. ret = mac_utils._available_services()
  397. return ret