test_minion.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  4. tests.integration.shell.minion
  5. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  6. """
  7. from __future__ import absolute_import
  8. import getpass
  9. import logging
  10. import os
  11. import platform
  12. import sys
  13. import pytest
  14. import salt.utils.files
  15. import salt.utils.platform
  16. import salt.utils.yaml
  17. import tests.integration.utils
  18. from salt.ext import six
  19. from tests.integration.utils import testprogram
  20. from tests.support.case import ShellCase
  21. from tests.support.mixins import ShellCaseCommonTestsMixin
  22. from tests.support.runtests import RUNTIME_VARS
  23. from tests.support.unit import skipIf
  24. log = logging.getLogger(__name__)
  25. DEBUG = True
  26. @pytest.mark.windows_whitelisted
  27. class MinionTest(ShellCase, testprogram.TestProgramCase, ShellCaseCommonTestsMixin):
  28. """
  29. Various integration tests for the salt-minion executable.
  30. """
  31. _call_binary_ = "salt-minion"
  32. _test_minions = (
  33. "minion",
  34. "subminion",
  35. )
  36. def _run_initscript(
  37. self, init_script, minions, minion_running, action, exitstatus=None, message=""
  38. ):
  39. """
  40. Wrapper that runs the initscript for the configured minions and
  41. verifies the results.
  42. """
  43. user = getpass.getuser()
  44. ret = init_script.run(
  45. [action],
  46. catch_stderr=True,
  47. with_retcode=True,
  48. env={
  49. "SALTMINION_CONFIGS": "\n".join(
  50. [
  51. "{0} {1}".format(user, minion.abs_path(minion.config_dir))
  52. for minion in minions
  53. ]
  54. ),
  55. },
  56. timeout=90,
  57. )
  58. for line in ret[0]:
  59. log.debug("script: salt-minion: stdout: {0}".format(line))
  60. for line in ret[1]:
  61. log.debug("script: salt-minion: stderr: {0}".format(line))
  62. log.debug("exit status: {0}".format(ret[2]))
  63. if six.PY3:
  64. std_out = b"\nSTDOUT:".join(ret[0])
  65. std_err = b"\nSTDERR:".join(ret[1])
  66. else:
  67. std_out = "\nSTDOUT:".join(ret[0])
  68. std_err = "\nSTDERR:".join(ret[1])
  69. # Check minion state
  70. for minion in minions:
  71. self.assertEqual(
  72. minion.is_running(),
  73. minion_running,
  74. 'script action "{0}" should result in minion "{1}" {2} and is not.\nSTDOUT:{3}\nSTDERR:{4}'.format(
  75. action,
  76. minion.name,
  77. ["stopped", "running"][minion_running],
  78. std_out,
  79. std_err,
  80. ),
  81. )
  82. if exitstatus is not None:
  83. self.assertEqual(
  84. ret[2],
  85. exitstatus,
  86. 'script action "{0}" {1} exited {2}, must be {3}\nSTDOUT:{4}\nSTDERR:{5}'.format(
  87. action, message, ret[2], exitstatus, std_out, std_err,
  88. ),
  89. )
  90. return ret
  91. def _initscript_setup(self, minions):
  92. """Re-usable setup for running salt-minion tests"""
  93. _minions = []
  94. for mname in minions:
  95. pid_file = "salt-{0}.pid".format(mname)
  96. minion = testprogram.TestDaemonSaltMinion(
  97. name=mname,
  98. root_dir="init_script",
  99. config_dir=os.path.join("etc", mname),
  100. parent_dir=self._test_dir,
  101. pid_file=pid_file,
  102. configs={
  103. "minion": {
  104. "map": {
  105. "pidfile": os.path.join("var", "run", pid_file),
  106. "sock_dir": os.path.join("var", "run", "salt", mname),
  107. "log_file": os.path.join("var", "log", "salt", mname),
  108. },
  109. },
  110. },
  111. )
  112. # Call setup here to ensure config and script exist
  113. minion.setup()
  114. _minions.append(minion)
  115. # Need salt-call, salt-minion for wrapper script
  116. salt_call = testprogram.TestProgramSaltCall(
  117. root_dir="init_script", parent_dir=self._test_dir
  118. )
  119. # Ensure that run-time files are generated
  120. salt_call.setup()
  121. sysconf_dir = os.path.dirname(_minions[0].abs_path(_minions[0].config_dir))
  122. cmd_env = {
  123. "PATH": ":".join(
  124. [salt_call.abs_path(salt_call.script_dir), os.getenv("PATH")]
  125. ),
  126. "SALTMINION_DEBUG": "1" if DEBUG else "",
  127. "SALTMINION_PYTHON": sys.executable,
  128. "SALTMINION_SYSCONFDIR": sysconf_dir,
  129. "SALTMINION_BINDIR": _minions[0].abs_path(_minions[0].script_dir),
  130. }
  131. default_dir = os.path.join(sysconf_dir, "default")
  132. if not os.path.exists(default_dir):
  133. os.makedirs(default_dir)
  134. with salt.utils.files.fopen(os.path.join(default_dir, "salt"), "w") as defaults:
  135. # Test suites is quite slow - extend the timeout
  136. defaults.write("TIMEOUT=60\n" "TICK=1\n")
  137. init_script = testprogram.TestProgram(
  138. name="init:salt-minion",
  139. program=os.path.join(RUNTIME_VARS.CODE_DIR, "pkg", "rpm", "salt-minion"),
  140. env=cmd_env,
  141. )
  142. return _minions, salt_call, init_script
  143. @skipIf(True, "Disabled. Test suite hanging")
  144. def test_linux_initscript(self):
  145. """
  146. Various tests of the init script to verify that it properly controls a salt minion.
  147. """
  148. pform = platform.uname()[0].lower()
  149. if pform not in ("linux",):
  150. self.skipTest(
  151. "salt-minion init script is unavailable on {1}".format(platform)
  152. )
  153. minions, _, init_script = self._initscript_setup(self._test_minions)
  154. try:
  155. # These tests are grouped together, rather than split into individual test functions,
  156. # because subsequent tests leverage the state from the previous test which minimizes
  157. # setup for each test.
  158. # I take visual readability with aligned columns over strict PEP8
  159. # (bad-whitespace) Exactly one space required after comma
  160. # pylint: disable=C0326
  161. ret = self._run_initscript(
  162. init_script, minions[:1], False, "bogusaction", 2
  163. )
  164. ret = self._run_initscript(
  165. init_script, minions[:1], False, "reload", 3
  166. ) # Not implemented
  167. ret = self._run_initscript(
  168. init_script, minions[:1], False, "stop", 0, "when not running"
  169. )
  170. ret = self._run_initscript(
  171. init_script, minions[:1], False, "status", 3, "when not running"
  172. )
  173. ret = self._run_initscript(
  174. init_script, minions[:1], False, "condrestart", 7, "when not running"
  175. )
  176. ret = self._run_initscript(
  177. init_script, minions[:1], False, "try-restart", 7, "when not running"
  178. )
  179. ret = self._run_initscript(
  180. init_script, minions, True, "start", 0, "when not running"
  181. )
  182. ret = self._run_initscript(
  183. init_script, minions, True, "status", 0, "when running"
  184. )
  185. # Verify that PIDs match
  186. mpids = {}
  187. for line in ret[0]:
  188. segs = line.decode(__salt_system_encoding__).split()
  189. minfo = segs[0].split(":")
  190. mpids[minfo[-1]] = int(segs[-1]) if segs[-1].isdigit() else None
  191. for minion in minions:
  192. self.assertEqual(
  193. minion.daemon_pid,
  194. mpids[minion.name],
  195. 'PID in "{0}" is {1} and does not match status PID {2}'.format(
  196. minion.abs_path(minion.pid_path),
  197. minion.daemon_pid,
  198. mpids[minion.name],
  199. ),
  200. )
  201. ret = self._run_initscript(
  202. init_script, minions, True, "start", 0, "when running"
  203. )
  204. ret = self._run_initscript(
  205. init_script, minions, True, "condrestart", 0, "when running"
  206. )
  207. ret = self._run_initscript(
  208. init_script, minions, True, "try-restart", 0, "when running"
  209. )
  210. ret = self._run_initscript(
  211. init_script, minions, False, "stop", 0, "when running"
  212. )
  213. finally:
  214. # Ensure that minions are shutdown
  215. for minion in minions:
  216. minion.shutdown()
  217. @skipIf(salt.utils.platform.is_windows(), "Skip on Windows OS")
  218. @skipIf(True, "SLOWTEST skip")
  219. def test_exit_status_unknown_user(self):
  220. """
  221. Ensure correct exit status when the minion is configured to run as an unknown user.
  222. Skipped on windows because daemonization not supported
  223. """
  224. minion = testprogram.TestDaemonSaltMinion(
  225. name="unknown_user",
  226. configs={"minion": {"map": {"user": "some_unknown_user_xyz"}}},
  227. parent_dir=self._test_dir,
  228. )
  229. # Call setup here to ensure config and script exist
  230. minion.setup()
  231. stdout, stderr, status = minion.run(
  232. args=["-d"], catch_stderr=True, with_retcode=True,
  233. )
  234. try:
  235. self.assert_exit_status(
  236. status,
  237. "EX_NOUSER",
  238. message="unknown user not on system",
  239. stdout=stdout,
  240. stderr=tests.integration.utils.decode_byte_list(stderr),
  241. )
  242. finally:
  243. # Although the start-up should fail, call shutdown() to set the
  244. # internal _shutdown flag and avoid the registered atexit calls to
  245. # cause timeout exceptions and respective traceback
  246. minion.shutdown()
  247. # @skipIf(salt.utils.platform.is_windows(), 'Skip on Windows OS')
  248. @skipIf(True, "SLOWTEST skip")
  249. def test_exit_status_unknown_argument(self):
  250. """
  251. Ensure correct exit status when an unknown argument is passed to salt-minion.
  252. """
  253. minion = testprogram.TestDaemonSaltMinion(
  254. name="unknown_argument", parent_dir=self._test_dir,
  255. )
  256. # Call setup here to ensure config and script exist
  257. minion.setup()
  258. stdout, stderr, status = minion.run(
  259. args=["-d", "--unknown-argument"], catch_stderr=True, with_retcode=True,
  260. )
  261. try:
  262. self.assert_exit_status(
  263. status,
  264. "EX_USAGE",
  265. message="unknown argument",
  266. stdout=stdout,
  267. stderr=tests.integration.utils.decode_byte_list(stderr),
  268. )
  269. finally:
  270. # Although the start-up should fail, call shutdown() to set the
  271. # internal _shutdown flag and avoid the registered atexit calls to
  272. # cause timeout exceptions and respective traceback
  273. minion.shutdown()
  274. @skipIf(salt.utils.platform.is_windows(), "Skip on Windows OS")
  275. @skipIf(True, "SLOWTEST skip")
  276. def test_exit_status_correct_usage(self):
  277. """
  278. Ensure correct exit status when salt-minion starts correctly.
  279. Skipped on windows because daemonization not supported
  280. """
  281. minion = testprogram.TestDaemonSaltMinion(
  282. name="correct_usage", parent_dir=self._test_dir,
  283. )
  284. # Call setup here to ensure config and script exist
  285. minion.setup()
  286. stdout, stderr, status = minion.run(
  287. args=["-d"], catch_stderr=True, with_retcode=True,
  288. )
  289. self.assert_exit_status(
  290. status, "EX_OK", message="correct usage", stdout=stdout, stderr=stderr
  291. )
  292. minion.shutdown(wait_for_orphans=3)