1
0

test_mac_utils.py 16 KB

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