case.py 29 KB

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