# -*- coding: utf-8 -*- """ :codeauthor: Pedro Algarvio (pedro@algarvio.me) tests.integration.shell.minion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ from __future__ import absolute_import import getpass import logging import os import platform import sys import pytest import salt.utils.files import salt.utils.platform import salt.utils.yaml import tests.integration.utils from salt.ext import six from tests.integration.utils import testprogram from tests.support.case import ShellCase from tests.support.helpers import slowTest from tests.support.mixins import ShellCaseCommonTestsMixin from tests.support.runtests import RUNTIME_VARS from tests.support.unit import skipIf log = logging.getLogger(__name__) DEBUG = True @pytest.mark.windows_whitelisted class MinionTest(ShellCase, testprogram.TestProgramCase, ShellCaseCommonTestsMixin): """ Various integration tests for the salt-minion executable. """ _call_binary_ = "salt-minion" _test_minions = ( "minion", "subminion", ) def _run_initscript( self, init_script, minions, minion_running, action, exitstatus=None, message="" ): """ Wrapper that runs the initscript for the configured minions and verifies the results. """ user = getpass.getuser() ret = init_script.run( [action], catch_stderr=True, with_retcode=True, env={ "SALTMINION_CONFIGS": "\n".join( [ "{0} {1}".format(user, minion.abs_path(minion.config_dir)) for minion in minions ] ), }, timeout=90, ) for line in ret[0]: log.debug("script: salt-minion: stdout: {0}".format(line)) for line in ret[1]: log.debug("script: salt-minion: stderr: {0}".format(line)) log.debug("exit status: {0}".format(ret[2])) if six.PY3: std_out = b"\nSTDOUT:".join(ret[0]) std_err = b"\nSTDERR:".join(ret[1]) else: std_out = "\nSTDOUT:".join(ret[0]) std_err = "\nSTDERR:".join(ret[1]) # Check minion state for minion in minions: self.assertEqual( minion.is_running(), minion_running, 'script action "{0}" should result in minion "{1}" {2} and is not.\nSTDOUT:{3}\nSTDERR:{4}'.format( action, minion.name, ["stopped", "running"][minion_running], std_out, std_err, ), ) if exitstatus is not None: self.assertEqual( ret[2], exitstatus, 'script action "{0}" {1} exited {2}, must be {3}\nSTDOUT:{4}\nSTDERR:{5}'.format( action, message, ret[2], exitstatus, std_out, std_err, ), ) return ret def _initscript_setup(self, minions): """Re-usable setup for running salt-minion tests""" _minions = [] for mname in minions: pid_file = "salt-{0}.pid".format(mname) minion = testprogram.TestDaemonSaltMinion( name=mname, root_dir="init_script", config_dir=os.path.join("etc", mname), parent_dir=self._test_dir, pid_file=pid_file, configs={ "minion": { "map": { "pidfile": os.path.join("var", "run", pid_file), "sock_dir": os.path.join("var", "run", "salt", mname), "log_file": os.path.join("var", "log", "salt", mname), }, }, }, ) # Call setup here to ensure config and script exist minion.setup() _minions.append(minion) # Need salt-call, salt-minion for wrapper script salt_call = testprogram.TestProgramSaltCall( root_dir="init_script", parent_dir=self._test_dir ) # Ensure that run-time files are generated salt_call.setup() sysconf_dir = os.path.dirname(_minions[0].abs_path(_minions[0].config_dir)) cmd_env = { "PATH": ":".join( [salt_call.abs_path(salt_call.script_dir), os.getenv("PATH")] ), "SALTMINION_DEBUG": "1" if DEBUG else "", "SALTMINION_PYTHON": sys.executable, "SALTMINION_SYSCONFDIR": sysconf_dir, "SALTMINION_BINDIR": _minions[0].abs_path(_minions[0].script_dir), } default_dir = os.path.join(sysconf_dir, "default") if not os.path.exists(default_dir): os.makedirs(default_dir) with salt.utils.files.fopen(os.path.join(default_dir, "salt"), "w") as defaults: # Test suites is quite slow - extend the timeout defaults.write("TIMEOUT=60\n" "TICK=1\n") init_script = testprogram.TestProgram( name="init:salt-minion", program=os.path.join(RUNTIME_VARS.CODE_DIR, "pkg", "rpm", "salt-minion"), env=cmd_env, ) return _minions, salt_call, init_script @skipIf(True, "Disabled. Test suite hanging") def test_linux_initscript(self): """ Various tests of the init script to verify that it properly controls a salt minion. """ pform = platform.uname()[0].lower() if pform not in ("linux",): self.skipTest( "salt-minion init script is unavailable on {1}".format(platform) ) minions, _, init_script = self._initscript_setup(self._test_minions) try: # These tests are grouped together, rather than split into individual test functions, # because subsequent tests leverage the state from the previous test which minimizes # setup for each test. # I take visual readability with aligned columns over strict PEP8 # (bad-whitespace) Exactly one space required after comma # pylint: disable=C0326 ret = self._run_initscript( init_script, minions[:1], False, "bogusaction", 2 ) ret = self._run_initscript( init_script, minions[:1], False, "reload", 3 ) # Not implemented ret = self._run_initscript( init_script, minions[:1], False, "stop", 0, "when not running" ) ret = self._run_initscript( init_script, minions[:1], False, "status", 3, "when not running" ) ret = self._run_initscript( init_script, minions[:1], False, "condrestart", 7, "when not running" ) ret = self._run_initscript( init_script, minions[:1], False, "try-restart", 7, "when not running" ) ret = self._run_initscript( init_script, minions, True, "start", 0, "when not running" ) ret = self._run_initscript( init_script, minions, True, "status", 0, "when running" ) # Verify that PIDs match mpids = {} for line in ret[0]: segs = line.decode(__salt_system_encoding__).split() minfo = segs[0].split(":") mpids[minfo[-1]] = int(segs[-1]) if segs[-1].isdigit() else None for minion in minions: self.assertEqual( minion.daemon_pid, mpids[minion.name], 'PID in "{0}" is {1} and does not match status PID {2}'.format( minion.abs_path(minion.pid_path), minion.daemon_pid, mpids[minion.name], ), ) ret = self._run_initscript( init_script, minions, True, "start", 0, "when running" ) ret = self._run_initscript( init_script, minions, True, "condrestart", 0, "when running" ) ret = self._run_initscript( init_script, minions, True, "try-restart", 0, "when running" ) ret = self._run_initscript( init_script, minions, False, "stop", 0, "when running" ) finally: # Ensure that minions are shutdown for minion in minions: minion.shutdown() @skipIf(salt.utils.platform.is_windows(), "Skip on Windows OS") @slowTest def test_exit_status_unknown_user(self): """ Ensure correct exit status when the minion is configured to run as an unknown user. Skipped on windows because daemonization not supported """ minion = testprogram.TestDaemonSaltMinion( name="unknown_user", configs={"minion": {"map": {"user": "some_unknown_user_xyz"}}}, parent_dir=self._test_dir, ) # Call setup here to ensure config and script exist minion.setup() stdout, stderr, status = minion.run( args=["-d"], catch_stderr=True, with_retcode=True, ) try: self.assert_exit_status( status, "EX_NOUSER", message="unknown user not on system", stdout=stdout, stderr=tests.integration.utils.decode_byte_list(stderr), ) finally: # Although the start-up should fail, call shutdown() to set the # internal _shutdown flag and avoid the registered atexit calls to # cause timeout exceptions and respective traceback minion.shutdown() # @skipIf(salt.utils.platform.is_windows(), 'Skip on Windows OS') @slowTest def test_exit_status_unknown_argument(self): """ Ensure correct exit status when an unknown argument is passed to salt-minion. """ minion = testprogram.TestDaemonSaltMinion( name="unknown_argument", parent_dir=self._test_dir, ) # Call setup here to ensure config and script exist minion.setup() stdout, stderr, status = minion.run( args=["-d", "--unknown-argument"], catch_stderr=True, with_retcode=True, ) try: self.assert_exit_status( status, "EX_USAGE", message="unknown argument", stdout=stdout, stderr=tests.integration.utils.decode_byte_list(stderr), ) finally: # Although the start-up should fail, call shutdown() to set the # internal _shutdown flag and avoid the registered atexit calls to # cause timeout exceptions and respective traceback minion.shutdown() @skipIf(salt.utils.platform.is_windows(), "Skip on Windows OS") @slowTest def test_exit_status_correct_usage(self): """ Ensure correct exit status when salt-minion starts correctly. Skipped on windows because daemonization not supported """ minion = testprogram.TestDaemonSaltMinion( name="correct_usage", parent_dir=self._test_dir, ) # Call setup here to ensure config and script exist minion.setup() stdout, stderr, status = minion.run( args=["-d"], catch_stderr=True, with_retcode=True, ) self.assert_exit_status( status, "EX_OK", message="correct usage", stdout=stdout, stderr=stderr ) minion.shutdown(wait_for_orphans=3)