test_minion.py 11 KB

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