test_mac_utils.py 16 KB

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