1""" 2 :codeauthor: Pedro Algarvio (pedro@algarvio.me) 3 4 5 ==================================== 6 Custom Salt TestCase Implementations 7 ==================================== 8 9 Custom reusable :class:`TestCase<python2:unittest.TestCase>` 10 implementations. 11""" 12 13import errno 14import io 15import json 16import logging 17import os 18import re 19import subprocess 20import sys 21import tempfile 22import textwrap 23import time 24from datetime import datetime, timedelta 25 26import pytest 27import salt.utils.files 28from saltfactories.utils.processes import terminate_process 29from tests.support.cli_scripts import ScriptPathMixin 30from tests.support.helpers import RedirectStdStreams 31from tests.support.mixins import ( # pylint: disable=unused-import 32 AdaptedConfigurationTestCaseMixin, 33 SaltClientTestCaseMixin, 34) 35from tests.support.runtests import RUNTIME_VARS 36from tests.support.unit import TestCase 37 38STATE_FUNCTION_RUNNING_RE = re.compile( 39 r"""The function (?:"|')(?P<state_func>.*)(?:"|') is running as PID """ 40 r"(?P<pid>[\d]+) and was started at (?P<date>.*) with jid (?P<jid>[\d]+)" 41) 42 43log = logging.getLogger(__name__) 44 45 46class ShellCase(TestCase, AdaptedConfigurationTestCaseMixin, ScriptPathMixin): 47 """ 48 Execute a test for a shell command 49 """ 50 51 RUN_TIMEOUT = 30 52 53 def run_salt( 54 self, 55 arg_str, 56 with_retcode=False, 57 catch_stderr=False, 58 timeout=None, 59 popen_kwargs=None, 60 config_dir=None, 61 ): 62 r''' 63 Run the ``salt`` CLI tool with the provided arguments 64 65 .. code-block:: python 66 67 class MatchTest(ShellCase): 68 def test_list(self): 69 """ 70 test salt -L matcher 71 """ 72 data = self.run_salt('-L minion test.ping') 73 data = '\n'.join(data) 74 self.assertIn('minion', data) 75 ''' 76 if timeout is None: 77 timeout = self.RUN_TIMEOUT 78 79 arg_str = "-t {} {}".format(timeout, arg_str) 80 return self.run_script( 81 "salt", 82 arg_str, 83 with_retcode=with_retcode, 84 catch_stderr=catch_stderr, 85 timeout=timeout, 86 config_dir=config_dir, 87 ) 88 89 def run_ssh( 90 self, 91 arg_str, 92 with_retcode=False, 93 catch_stderr=False, 94 timeout=None, 95 wipe=False, 96 raw=False, 97 roster_file=None, 98 ssh_opts="", 99 log_level="error", 100 config_dir=None, 101 **kwargs 102 ): 103 """ 104 Execute salt-ssh 105 """ 106 if timeout is None: 107 timeout = self.RUN_TIMEOUT 108 if not roster_file: 109 roster_file = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, "roster") 110 arg_str = ( 111 "{wipe} {raw} -l {log_level} --ignore-host-keys --priv {client_key}" 112 " --roster-file {roster_file} {ssh_opts} localhost {arg_str} --out=json" 113 ).format( 114 wipe=" -W" if wipe else "", 115 raw=" -r" if raw else "", 116 log_level=log_level, 117 client_key=os.path.join(RUNTIME_VARS.TMP_SSH_CONF_DIR, "client_key"), 118 roster_file=roster_file, 119 ssh_opts=ssh_opts, 120 arg_str=arg_str, 121 ) 122 ret = self.run_script( 123 "salt-ssh", 124 arg_str, 125 with_retcode=with_retcode, 126 catch_stderr=catch_stderr, 127 raw=True, 128 timeout=timeout, 129 config_dir=config_dir, 130 **kwargs 131 ) 132 log.debug("Result of run_ssh for command '%s %s': %s", arg_str, kwargs, ret) 133 return ret 134 135 def run_run( 136 self, 137 arg_str, 138 with_retcode=False, 139 catch_stderr=False, 140 asynchronous=False, 141 timeout=None, 142 config_dir=None, 143 **kwargs 144 ): 145 """ 146 Execute salt-run 147 """ 148 if timeout is None: 149 timeout = self.RUN_TIMEOUT 150 asynchronous = kwargs.get("async", asynchronous) 151 arg_str = "{async_flag} -t {timeout} {}".format( 152 arg_str, 153 timeout=timeout, 154 async_flag=" --async" if asynchronous else "", 155 ) 156 ret = self.run_script( 157 "salt-run", 158 arg_str, 159 with_retcode=with_retcode, 160 catch_stderr=catch_stderr, 161 timeout=timeout, 162 config_dir=config_dir, 163 ) 164 log.debug("Result of run_run for command '%s': %s", arg_str, ret) 165 return ret 166 167 def run_run_plus(self, fun, *arg, **kwargs): 168 """ 169 Execute the runner function and return the return data and output in a dict 170 """ 171 output = kwargs.pop("_output", None) 172 opts_overrides = kwargs.pop("opts_overrides", None) 173 ret = {"fun": fun} 174 175 # Late import 176 import salt.config 177 import salt.output 178 import salt.runner 179 180 opts = salt.config.client_config(self.get_config_file_path("master")) 181 if opts_overrides: 182 opts.update(opts_overrides) 183 184 opts_arg = list(arg) 185 if kwargs: 186 opts_arg.append({"__kwarg__": True}) 187 opts_arg[-1].update(kwargs) 188 189 opts.update({"doc": False, "fun": fun, "arg": opts_arg}) 190 with RedirectStdStreams(): 191 runner = salt.runner.Runner(opts) 192 ret["return"] = runner.run() 193 try: 194 ret["jid"] = runner.jid 195 except AttributeError: 196 ret["jid"] = None 197 198 # Compile output 199 # TODO: Support outputters other than nested 200 opts["color"] = False 201 opts["output_file"] = io.StringIO() 202 try: 203 salt.output.display_output(ret["return"], opts=opts, out=output) 204 out = opts["output_file"].getvalue() 205 if output is None: 206 out = out.splitlines() 207 elif output == "json": 208 out = json.loads(out) 209 ret["out"] = out 210 finally: 211 opts["output_file"].close() 212 log.debug( 213 "Result of run_run_plus for fun '%s' with arg '%s': %s", fun, opts_arg, ret 214 ) 215 return ret 216 217 def run_key(self, arg_str, catch_stderr=False, with_retcode=False, config_dir=None): 218 """ 219 Execute salt-key 220 """ 221 return self.run_script( 222 "salt-key", 223 arg_str, 224 catch_stderr=catch_stderr, 225 with_retcode=with_retcode, 226 config_dir=config_dir, 227 ) 228 229 def run_cp( 230 self, 231 arg_str, 232 with_retcode=False, 233 catch_stderr=False, 234 timeout=None, 235 config_dir=None, 236 ): 237 """ 238 Execute salt-cp 239 """ 240 if timeout is None: 241 timeout = self.RUN_TIMEOUT 242 # Note: not logging result of run_cp because it will log a bunch of 243 # bytes which will not be very helpful. 244 return self.run_script( 245 "salt-cp", 246 arg_str, 247 with_retcode=with_retcode, 248 catch_stderr=catch_stderr, 249 timeout=timeout, 250 config_dir=config_dir, 251 ) 252 253 def run_call( 254 self, 255 arg_str, 256 with_retcode=False, 257 catch_stderr=False, 258 local=False, 259 timeout=None, 260 config_dir=None, 261 ): 262 if timeout is None: 263 timeout = self.RUN_TIMEOUT 264 if not config_dir: 265 config_dir = RUNTIME_VARS.TMP_MINION_CONF_DIR 266 arg_str = "{} {}".format("--local" if local else "", arg_str) 267 ret = self.run_script( 268 "salt-call", 269 arg_str, 270 with_retcode=with_retcode, 271 catch_stderr=catch_stderr, 272 timeout=timeout, 273 config_dir=config_dir, 274 ) 275 log.debug("Result of run_call for command '%s': %s", arg_str, ret) 276 return ret 277 278 def run_function( 279 self, 280 function, 281 arg=(), 282 with_retcode=False, 283 catch_stderr=False, 284 local=False, 285 timeout=RUN_TIMEOUT, 286 **kwargs 287 ): 288 """ 289 Execute function with salt-call. 290 291 This function is added for compatibility with ModuleCase. This makes it possible to use 292 decorators like @with_system_user. 293 """ 294 arg_str = "{} {} {}".format( 295 function, 296 " ".join(str(arg_) for arg_ in arg), 297 " ".join("{}={}".format(*item) for item in kwargs.items()), 298 ) 299 return self.run_call(arg_str, with_retcode, catch_stderr, local, timeout) 300 301 def run_cloud(self, arg_str, catch_stderr=False, timeout=None, config_dir=None): 302 """ 303 Execute salt-cloud 304 """ 305 if timeout is None: 306 timeout = self.RUN_TIMEOUT 307 308 ret = self.run_script( 309 "salt-cloud", arg_str, catch_stderr, timeout=timeout, config_dir=config_dir 310 ) 311 log.debug("Result of run_cloud for command '%s': %s", arg_str, ret) 312 return ret 313 314 def run_spm( 315 self, 316 arg_str, 317 with_retcode=False, 318 catch_stderr=False, 319 timeout=None, 320 config_dir=None, 321 ): 322 """ 323 Execute spm 324 """ 325 if timeout is None: 326 timeout = self.RUN_TIMEOUT 327 ret = self.run_script( 328 "spm", 329 arg_str, 330 with_retcode=with_retcode, 331 catch_stderr=catch_stderr, 332 timeout=timeout, 333 config_dir=config_dir, 334 ) 335 log.debug("Result of run_spm for command '%s': %s", arg_str, ret) 336 return ret 337 338 def run_script( 339 self, 340 script, 341 arg_str, 342 catch_stderr=False, 343 with_retcode=False, 344 catch_timeout=False, 345 # FIXME A timeout of zero or disabling timeouts may not return results! 346 timeout=15, 347 raw=False, 348 popen_kwargs=None, 349 log_output=None, 350 config_dir=None, 351 **kwargs 352 ): 353 """ 354 Execute a script with the given argument string 355 356 The ``log_output`` argument is ternary, it can be True, False, or None. 357 If the value is boolean, then it forces the results to either be logged 358 or not logged. If it is None, then the return code of the subprocess 359 determines whether or not to log results. 360 """ 361 362 import salt.utils.platform 363 364 script_path = self.get_script_path(script) 365 if not os.path.isfile(script_path): 366 return False 367 popen_kwargs = popen_kwargs or {} 368 369 python_path_env_var = os.environ.get("PYTHONPATH") or None 370 if python_path_env_var is None: 371 python_path_entries = [RUNTIME_VARS.CODE_DIR] 372 else: 373 python_path_entries = python_path_env_var.split(os.pathsep) 374 if RUNTIME_VARS.CODE_DIR in python_path_entries: 375 python_path_entries.remove(RUNTIME_VARS.CODE_DIR) 376 python_path_entries.insert(0, RUNTIME_VARS.CODE_DIR) 377 python_path_entries.extend(sys.path[0:]) 378 379 if "env" not in popen_kwargs: 380 popen_kwargs["env"] = os.environ.copy() 381 382 popen_kwargs["env"]["PYTHONPATH"] = os.pathsep.join(python_path_entries) 383 384 if "cwd" not in popen_kwargs: 385 popen_kwargs["cwd"] = RUNTIME_VARS.TMP 386 387 if salt.utils.platform.is_windows(): 388 cmd = "python " 389 else: 390 cmd = "python{}.{} ".format(*sys.version_info) 391 392 cmd += "{} --config-dir={} {} ".format( 393 script_path, config_dir or RUNTIME_VARS.TMP_CONF_DIR, arg_str 394 ) 395 if kwargs: 396 # late import 397 import salt.utils.json 398 399 for key, value in kwargs.items(): 400 cmd += "'{}={} '".format(key, salt.utils.json.dumps(value)) 401 402 tmp_file = tempfile.SpooledTemporaryFile() 403 404 popen_kwargs = dict( 405 {"shell": True, "stdout": tmp_file, "universal_newlines": True}, 406 **popen_kwargs 407 ) 408 409 if catch_stderr is True: 410 popen_kwargs["stderr"] = subprocess.PIPE 411 412 if salt.utils.platform.is_windows(): 413 # Windows does not support closing FDs 414 close_fds = False 415 elif salt.utils.platform.is_freebsd() and sys.version_info < (3, 9): 416 # Closing FDs in FreeBSD before Py3.9 can be slow 417 # https://bugs.python.org/issue38061 418 close_fds = False 419 else: 420 close_fds = True 421 422 popen_kwargs["close_fds"] = close_fds 423 424 if not salt.utils.platform.is_windows(): 425 426 def detach_from_parent_group(): 427 # detach from parent group (no more inherited signals!) 428 os.setpgrp() 429 430 popen_kwargs["preexec_fn"] = detach_from_parent_group 431 432 def format_return(retcode, stdout, stderr=None, timed_out=False): 433 """ 434 DRY helper to log script result if it failed, and then return the 435 desired output based on whether or not stderr was desired, and 436 wither or not a retcode was desired. 437 """ 438 log_func = log.debug 439 if timed_out: 440 log.error( 441 "run_script timed out after %d seconds (process killed)", timeout 442 ) 443 log_func = log.error 444 445 if log_output is True or timed_out or (log_output is None and retcode != 0): 446 log_func( 447 "run_script results for: %s %s\n" 448 "return code: %s\n" 449 "stdout:\n" 450 "%s\n\n" 451 "stderr:\n" 452 "%s", 453 script, 454 arg_str, 455 retcode, 456 stdout, 457 stderr, 458 ) 459 460 stdout = stdout or "" 461 stderr = stderr or "" 462 463 if not raw: 464 stdout = stdout.splitlines() 465 stderr = stderr.splitlines() 466 467 ret = [stdout] 468 if catch_stderr: 469 ret.append(stderr) 470 if with_retcode: 471 ret.append(retcode) 472 if catch_timeout: 473 ret.append(timed_out) 474 475 return ret[0] if len(ret) == 1 else tuple(ret) 476 477 log.debug("Running Popen(%r, %r)", cmd, popen_kwargs) 478 process = subprocess.Popen(cmd, **popen_kwargs) 479 480 if timeout is not None: 481 stop_at = datetime.now() + timedelta(seconds=timeout) 482 while True: 483 process.poll() 484 time.sleep(0.1) 485 if datetime.now() <= stop_at: 486 # We haven't reached the timeout yet 487 if process.returncode is not None: 488 break 489 else: 490 terminate_process(process.pid, kill_children=True) 491 return format_return( 492 process.returncode, *process.communicate(), timed_out=True 493 ) 494 495 tmp_file.seek(0) 496 497 try: 498 out = tmp_file.read().decode(__salt_system_encoding__) 499 except (NameError, UnicodeDecodeError): 500 # Let's cross our fingers and hope for the best 501 out = tmp_file.read().decode("utf-8") 502 503 if catch_stderr: 504 _, err = process.communicate() 505 # Force closing stderr/stdout to release file descriptors 506 if process.stdout is not None: 507 process.stdout.close() 508 if process.stderr is not None: 509 process.stderr.close() 510 511 # pylint: disable=maybe-no-member 512 try: 513 return format_return(process.returncode, out, err or "") 514 finally: 515 try: 516 if os.path.exists(tmp_file.name): 517 if isinstance(tmp_file.name, str): 518 # tmp_file.name is an int when using SpooledTemporaryFiles 519 # int types cannot be used with os.remove() in Python 3 520 os.remove(tmp_file.name) 521 else: 522 # Clean up file handles 523 tmp_file.close() 524 process.terminate() 525 except OSError as err: 526 # process already terminated 527 pass 528 # pylint: enable=maybe-no-member 529 530 # TODO Remove this? 531 process.communicate() 532 if process.stdout is not None: 533 process.stdout.close() 534 535 try: 536 return format_return(process.returncode, out) 537 finally: 538 try: 539 if os.path.exists(tmp_file.name): 540 if isinstance(tmp_file.name, str): 541 # tmp_file.name is an int when using SpooledTemporaryFiles 542 # int types cannot be used with os.remove() in Python 3 543 os.remove(tmp_file.name) 544 else: 545 # Clean up file handles 546 tmp_file.close() 547 process.terminate() 548 except OSError as err: 549 # process already terminated 550 pass 551 552 553class SPMTestUserInterface: 554 """ 555 Test user interface to SPMClient 556 """ 557 558 def __init__(self): 559 self._status = [] 560 self._confirm = [] 561 self._error = [] 562 563 def status(self, msg): 564 self._status.append(msg) 565 566 def confirm(self, action): 567 self._confirm.append(action) 568 569 def error(self, msg): 570 self._error.append(msg) 571 572 573class SPMCase(TestCase, AdaptedConfigurationTestCaseMixin): 574 """ 575 Class for handling spm commands 576 """ 577 578 def _spm_build_files(self, config): 579 self.formula_dir = os.path.join( 580 " ".join(config["file_roots"]["base"]), "formulas" 581 ) 582 self.formula_sls_dir = os.path.join(self.formula_dir, "apache") 583 self.formula_sls = os.path.join(self.formula_sls_dir, "apache.sls") 584 self.formula_file = os.path.join(self.formula_dir, "FORMULA") 585 586 dirs = [self.formula_dir, self.formula_sls_dir] 587 for f_dir in dirs: 588 os.makedirs(f_dir) 589 590 with salt.utils.files.fopen(self.formula_sls, "w") as fp: 591 fp.write( 592 textwrap.dedent( 593 """\ 594 install-apache: 595 pkg.installed: 596 - name: apache2 597 """ 598 ) 599 ) 600 601 with salt.utils.files.fopen(self.formula_file, "w") as fp: 602 fp.write( 603 textwrap.dedent( 604 """\ 605 name: apache 606 os: RedHat, Debian, Ubuntu, Suse, FreeBSD 607 os_family: RedHat, Debian, Suse, FreeBSD 608 version: 201506 609 release: 2 610 summary: Formula for installing Apache 611 description: Formula for installing Apache 612 """ 613 ) 614 ) 615 616 def _spm_config(self, assume_yes=True): 617 self._tmp_spm = tempfile.mkdtemp() 618 config = self.get_temp_config( 619 "minion", 620 **{ 621 "spm_logfile": os.path.join(self._tmp_spm, "log"), 622 "spm_repos_config": os.path.join(self._tmp_spm, "etc", "spm.repos"), 623 "spm_cache_dir": os.path.join(self._tmp_spm, "cache"), 624 "spm_build_dir": os.path.join(self._tmp_spm, "build"), 625 "spm_build_exclude": ["apache/.git"], 626 "spm_db_provider": "sqlite3", 627 "spm_files_provider": "local", 628 "spm_db": os.path.join(self._tmp_spm, "packages.db"), 629 "extension_modules": os.path.join(self._tmp_spm, "modules"), 630 "file_roots": {"base": [self._tmp_spm]}, 631 "formula_path": os.path.join(self._tmp_spm, "salt"), 632 "pillar_path": os.path.join(self._tmp_spm, "pillar"), 633 "reactor_path": os.path.join(self._tmp_spm, "reactor"), 634 "assume_yes": True if assume_yes else False, 635 "force": False, 636 "verbose": False, 637 "cache": "localfs", 638 "cachedir": os.path.join(self._tmp_spm, "cache"), 639 "spm_repo_dups": "ignore", 640 "spm_share_dir": os.path.join(self._tmp_spm, "share"), 641 } 642 ) 643 644 import salt.utils.yaml 645 646 if not os.path.isdir(config["formula_path"]): 647 os.makedirs(config["formula_path"]) 648 649 with salt.utils.files.fopen(os.path.join(self._tmp_spm, "spm"), "w") as fp: 650 salt.utils.yaml.safe_dump(config, fp) 651 652 return config 653 654 def _spm_create_update_repo(self, config): 655 656 build_spm = self.run_spm("build", self.config, self.formula_dir) 657 658 c_repo = self.run_spm("create_repo", self.config, self.config["spm_build_dir"]) 659 660 repo_conf_dir = self.config["spm_repos_config"] + ".d" 661 os.makedirs(repo_conf_dir) 662 663 with salt.utils.files.fopen(os.path.join(repo_conf_dir, "spm.repo"), "w") as fp: 664 fp.write( 665 textwrap.dedent( 666 """\ 667 local_repo: 668 url: file://{} 669 """.format( 670 self.config["spm_build_dir"] 671 ) 672 ) 673 ) 674 675 u_repo = self.run_spm("update_repo", self.config) 676 677 def _spm_client(self, config): 678 import salt.spm 679 680 self.ui = SPMTestUserInterface() 681 client = salt.spm.SPMClient(self.ui, config) 682 return client 683 684 def run_spm(self, cmd, config, arg=None): 685 client = self._spm_client(config) 686 client.run([cmd, arg]) 687 client._close() 688 return self.ui._status 689 690 691class ModuleCase(TestCase, SaltClientTestCaseMixin): 692 """ 693 Execute a module function 694 """ 695 696 def wait_for_all_jobs(self, minions=("minion", "sub_minion"), sleep=0.3): 697 """ 698 Wait for all jobs currently running on the list of minions to finish 699 """ 700 for minion in minions: 701 while True: 702 ret = self.run_function( 703 "saltutil.running", minion_tgt=minion, timeout=300 704 ) 705 if ret: 706 log.debug("Waiting for minion's jobs: %s", minion) 707 time.sleep(sleep) 708 else: 709 break 710 711 def minion_run(self, _function, *args, **kw): 712 """ 713 Run a single salt function on the 'minion' target and condition 714 the return down to match the behavior of the raw function call 715 """ 716 return self.run_function(_function, args, **kw) 717 718 def run_function( 719 self, 720 function, 721 arg=(), 722 minion_tgt="minion", 723 timeout=300, 724 master_tgt=None, 725 **kwargs 726 ): 727 """ 728 Run a single salt function and condition the return down to match the 729 behavior of the raw function call 730 """ 731 known_to_return_none = ( 732 "data.get", 733 "file.chown", 734 "file.chgrp", 735 "pkg.refresh_db", 736 "ssh.recv_known_host_entries", 737 "time.sleep", 738 "grains.delkey", 739 "grains.delval", 740 ) 741 if "f_arg" in kwargs: 742 kwargs["arg"] = kwargs.pop("f_arg") 743 if "f_timeout" in kwargs: 744 kwargs["timeout"] = kwargs.pop("f_timeout") 745 client = self.client if master_tgt is None else self.clients[master_tgt] 746 log.debug( 747 "Running client.cmd(minion_tgt=%r, function=%r, arg=%r, timeout=%r," 748 " kwarg=%r)", 749 minion_tgt, 750 function, 751 arg, 752 timeout, 753 kwargs, 754 ) 755 orig = client.cmd(minion_tgt, function, arg, timeout=timeout, kwarg=kwargs) 756 757 if minion_tgt not in orig: 758 self.fail( 759 "WARNING(SHOULD NOT HAPPEN #1935): Failed to get a reply " 760 "from the minion '{}'. Command output: {}".format(minion_tgt, orig) 761 ) 762 elif orig[minion_tgt] is None and function not in known_to_return_none: 763 self.fail( 764 "WARNING(SHOULD NOT HAPPEN #1935): Failed to get '{}' from " 765 "the minion '{}'. Command output: {}".format(function, minion_tgt, orig) 766 ) 767 768 # Try to match stalled state functions 769 orig[minion_tgt] = self._check_state_return(orig[minion_tgt]) 770 771 return orig[minion_tgt] 772 773 def run_state(self, function, **kwargs): 774 """ 775 Run the state.single command and return the state return structure 776 """ 777 ret = self.run_function("state.single", [function], **kwargs) 778 return self._check_state_return(ret) 779 780 def _check_state_return(self, ret): 781 if isinstance(ret, dict): 782 # This is the supposed return format for state calls 783 return ret 784 785 if isinstance(ret, list): 786 jids = [] 787 # These are usually errors 788 for item in ret[:]: 789 if not isinstance(item, str): 790 # We don't know how to handle this 791 continue 792 match = STATE_FUNCTION_RUNNING_RE.match(item) 793 if not match: 794 # We don't know how to handle this 795 continue 796 jid = match.group("jid") 797 if jid in jids: 798 continue 799 800 jids.append(jid) 801 802 job_data = self.run_function("saltutil.find_job", [jid]) 803 job_kill = self.run_function("saltutil.kill_job", [jid]) 804 msg = ( 805 "A running state.single was found causing a state lock. " 806 "Job details: '{}' Killing Job Returned: '{}'".format( 807 job_data, job_kill 808 ) 809 ) 810 ret.append("[TEST SUITE ENFORCED]{}[/TEST SUITE ENFORCED]".format(msg)) 811 return ret 812 813 814class SyndicCase(TestCase, SaltClientTestCaseMixin): 815 """ 816 Execute a syndic based execution test 817 """ 818 819 _salt_client_config_file_name_ = "syndic_master" 820 821 def run_function(self, function, arg=(), timeout=90): 822 """ 823 Run a single salt function and condition the return down to match the 824 behavior of the raw function call 825 """ 826 orig = self.client.cmd("minion", function, arg, timeout=timeout) 827 if "minion" not in orig: 828 self.fail( 829 "WARNING(SHOULD NOT HAPPEN #1935): Failed to get a reply " 830 "from the minion. Command output: {}".format(orig) 831 ) 832 return orig["minion"] 833 834 835@pytest.mark.requires_sshd_server 836class SSHCase(ShellCase): 837 """ 838 Execute a command via salt-ssh 839 """ 840 841 def _arg_str(self, function, arg): 842 return "{} {}".format(function, " ".join(arg)) 843 844 # pylint: disable=arguments-differ 845 def run_function( 846 self, function, arg=(), timeout=180, wipe=True, raw=False, **kwargs 847 ): 848 """ 849 We use a 180s timeout here, which some slower systems do end up needing 850 """ 851 ret = self.run_ssh( 852 self._arg_str(function, arg), timeout=timeout, wipe=wipe, raw=raw, **kwargs 853 ) 854 log.debug( 855 "SSHCase run_function executed %s with arg %s and kwargs %s", 856 function, 857 arg, 858 kwargs, 859 ) 860 log.debug("SSHCase JSON return: %s", ret) 861 862 # Late import 863 import salt.utils.json 864 865 try: 866 return salt.utils.json.loads(ret)["localhost"] 867 except Exception: # pylint: disable=broad-except 868 return ret 869 870 # pylint: enable=arguments-differ 871 def custom_roster(self, new_roster, data): 872 """ 873 helper method to create a custom roster to use for a ssh test 874 """ 875 roster = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, "roster") 876 877 with salt.utils.files.fopen(roster, "r") as fp_: 878 conf = salt.utils.yaml.safe_load(fp_) 879 880 conf["localhost"].update(data) 881 882 with salt.utils.files.fopen(new_roster, "w") as fp_: 883 salt.utils.yaml.safe_dump(conf, fp_) 884 885 886class ClientCase(AdaptedConfigurationTestCaseMixin, TestCase): 887 """ 888 A base class containing relevant options for starting the various Salt 889 Python API entrypoints 890 """ 891 892 def get_opts(self): 893 # Late import 894 import salt.config 895 896 return salt.config.client_config(self.get_config_file_path("master")) 897 898 def mkdir_p(self, path): 899 try: 900 os.makedirs(path) 901 except OSError as exc: # Python >2.5 902 if exc.errno == errno.EEXIST and os.path.isdir(path): 903 pass 904 else: 905 raise 906