test_minion.py 11 KB

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