1
0

case.py 32 KB

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