1
0

case.py 32 KB

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