case.py 37 KB

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