case.py 32 KB

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