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