test_mac_utils.py 16 KB

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