test_mac_utils.py 17 KB

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