1
0

case.py 32 KB

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