1
0

case.py 31 KB

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