salt/tests/integration/shell/minion.py
plastikos 0efbbcd17f * Improve init script: specifically manage salt configurations rather than arbitrary salt processes (#32666)
* * Improve init script: specifically manage salt configurations rather than arbitrary salt processes

Unfortunately SysV init scripts tend to rummage through PIDs filtering for
appropriate processes to manage.  Unfortunately the filters are usually weak
and don't account for similar processes run by other users, PIDs of dead
processes being re-used for completely different executables, etc..  These
weaknesses can result in killing unrelated processes with potentially serious
results.

These improvements to the SysV init script is a complete rewrite with the
following improvements:

  * Specifically manage individual salt configurations rather than looking for
    salt minion-like processes.
  * Obtain salt minion information from the salt configuration - use the
    information to manage the specifically configured process.
  * Drop all of the platform-specific helper functions that allow the
    previously-mentioned weaknesses.

    + Unfortunately this means that the output information may not match the
      specific platform (this could easily be corrected).

  * Now can manage multiple salt processes started by different users

    + Unfortunately starts/stops/restarts as a group and is unable to manage
      them both as a group or as individual processes (this could easily be
      corrected)

The new initscript also allows various control variables to be overridden by
environment variables or through settings put in ``/etc/sysconf/salt`` or
``/etc/default/salt``.

:SALTMINION_DEBUG: Dump each line expansion before execution, output system
                   information on failure.  Default: unset

:SALTMINION_BINDIR: Location of ``salt-minion``, ``salt-call`` and other
                    executables.  Default: ``/usr/bin``

:SALTMINION_SYSCONFDIR: The parent directory for the ``salt`` configuration
                        directory and the ``sysconfig`` or ``default``
                        directory.

* Add lines that went missing in the rebase+squash
2016-05-09 10:57:13 -07:00

260 lines
8.8 KiB
Python

# -*- coding: utf-8 -*-
'''
:codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`
tests.integration.shell.minion
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'''
# Import python libs
from __future__ import absolute_import
import os
import sys
import getpass
import platform
import yaml
import signal
import shutil
import tempfile
import logging
# Import Salt Testing libs
from salttesting.helpers import ensure_in_syspath
ensure_in_syspath('../../')
# Import salt libs
import integration
from integration.utils import testprogram
import salt.utils
import salt.defaults.exitcodes
log = logging.getLogger(__name__)
DEBUG = True
class MinionTest(integration.ShellCase, integration.ShellCaseCommonTestsMixIn):
'''
Various integration tests for the salt-minion executable.
'''
_call_binary_ = 'salt-minion'
_test_dir = None
_test_minions = (
'minion',
'subminion',
)
def setUp(self):
# Setup for scripts
self._test_dir = tempfile.mkdtemp(prefix='salt-testdaemon-')
def tearDown(self):
# shutdown for scripts
if self._test_dir and os.path.sep == self._test_dir[0]:
shutil.rmtree(self._test_dir)
self._test_dir = None
def test_issue_7754(self):
old_cwd = os.getcwd()
config_dir = os.path.join(integration.TMP, 'issue-7754')
if not os.path.isdir(config_dir):
os.makedirs(config_dir)
os.chdir(config_dir)
config_file_name = 'minion'
pid_path = os.path.join(config_dir, '{0}.pid'.format(config_file_name))
with salt.utils.fopen(self.get_config_file_path(config_file_name), 'r') as fhr:
config = yaml.load(fhr.read())
config['log_file'] = 'file:///tmp/log/LOG_LOCAL3'
with salt.utils.fopen(os.path.join(config_dir, config_file_name), 'w') as fhw:
fhw.write(
yaml.dump(config, default_flow_style=False)
)
ret = self.run_script(
self._call_binary_,
'--disable-keepalive --config-dir {0} --pid-file {1} -l debug'.format(
config_dir,
pid_path
),
timeout=5,
catch_stderr=True,
with_retcode=True
)
# Now kill it if still running
if os.path.exists(pid_path):
with salt.utils.fopen(pid_path) as fhr:
try:
os.kill(int(fhr.read()), signal.SIGKILL)
except OSError:
pass
try:
self.assertFalse(os.path.isdir(os.path.join(config_dir, 'file:')))
self.assertIn(
'Failed to setup the Syslog logging handler', '\n'.join(ret[1])
)
self.assertEqual(ret[2], 2)
finally:
self.chdir(old_cwd)
if os.path.isdir(config_dir):
shutil.rmtree(config_dir)
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.
'''
ret = init_script.run(
[action],
catch_stderr=True,
with_retcode=True,
timeout=90,
)
# Check minion state
for minion in minions:
self.assertEqual(
minion.is_running(),
minion_running,
'Minion "{0}" must be {1} and is not.\nSTDOUT:{2}\nSTDERR:{3}'.format(
minion.name,
["stopped", "running"][minion_running],
'\nSTDOUT:'.join(ret[0]),
'\nSTDERR:'.join(ret[1]),
)
)
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))
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,
'\nSTDOUT:'.join(ret[0]),
'\nSTDERR:'.join(ret[1]),
)
)
return ret
def _initscript_setup(self, minions):
'''Re-usable setup for running salt-minion tests'''
user = getpass.getuser()
_minions = []
for mname in minions:
minion = testprogram.TestDaemonSaltMinion(
name=mname,
config={'user': user},
parent_dir=self._test_dir,
)
# 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(parent_dir=self._test_dir)
# Ensure that run-time files are generated
salt_call.setup()
sysconf_dir = os.path.dirname(_minions[0].config_dir)
cmd_env = {
'PATH': ':'.join([salt_call.script_dir, os.getenv('PATH')]),
'PYTHONPATH': ':'.join(sys.path),
'SALTMINION_DEBUG': '1' if DEBUG else '',
'SALTMINION_PYTHON': sys.executable,
'SALTMINION_SYSCONFDIR': sysconf_dir,
'SALTMINION_BINDIR': _minions[0].script_dir,
'SALTMINION_CONFIGS': '\n'.join([
'{0} {1}'.format(user, minion.config_dir) for minion in _minions
]),
}
default_dir = os.path.join(sysconf_dir, 'default')
if not os.path.exists(default_dir):
os.makedirs(default_dir)
with open(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(integration.CODE_DIR, 'pkg', 'rpm', 'salt-minion'),
env=cmd_env,
)
return _minions, salt_call, init_script
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[:1])
try:
# 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, False, 'bogusaction', 2)
ret = self._run_initscript(init_script, minions, False, 'reload', 3) # Not implemented
ret = self._run_initscript(init_script, minions, False, 'stop', 0, 'when not running')
ret = self._run_initscript(init_script, minions, False, 'status', 3, 'when not running')
ret = self._run_initscript(init_script, minions, False, 'condrestart', 7, 'when not running')
ret = self._run_initscript(init_script, minions, 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
for (minion, stdout) in zip(minions, ret[0]):
status_pid = int(stdout.rsplit(' ', 1)[-1])
self.assertEqual(
status_pid,
minion.daemon_pid,
'PID in "{0}" is {1} and does not match status PID {2}'.format(
minion.pid_path,
minion.daemon_pid,
status_pid
)
)
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()
if __name__ == '__main__':
integration.run_tests(MinionTest)