test_mac_utils.py 16 KB

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