test_minion.py 12 KB

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