test_mac_utils.py 18 KB

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