mirror of
https://github.com/valitydev/salt.git
synced 2024-11-07 17:09:03 +00:00
461 lines
15 KiB
Python
461 lines
15 KiB
Python
'''
|
|
Set up the Salt integration test suite
|
|
'''
|
|
|
|
# Import Python libs
|
|
import optparse
|
|
import multiprocessing
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
try:
|
|
import pwd
|
|
except ImportError:
|
|
pass
|
|
|
|
# Import Salt libs
|
|
import salt
|
|
import salt.config
|
|
import salt.master
|
|
import salt.minion
|
|
import salt.runner
|
|
from salt.utils.verify import verify_env
|
|
from saltunittest import TestCase
|
|
|
|
INTEGRATION_TEST_DIR = os.path.dirname(
|
|
os.path.normpath(os.path.abspath(__file__))
|
|
)
|
|
CODE_DIR = os.path.dirname(os.path.dirname(INTEGRATION_TEST_DIR))
|
|
SCRIPT_DIR = os.path.join(CODE_DIR, 'scripts')
|
|
|
|
PYEXEC = 'python{0}.{1}'.format(sys.version_info[0], sys.version_info[1])
|
|
|
|
TMP = os.path.join(tempfile.gettempdir(), 'salt-tests-tmpdir')
|
|
FILES = os.path.join(INTEGRATION_TEST_DIR, 'files')
|
|
MOCKBIN = os.path.join(INTEGRATION_TEST_DIR, 'mockbin')
|
|
|
|
|
|
def run_tests(TestCase):
|
|
'''
|
|
Run integration tests for a chosen test case.
|
|
|
|
Function uses optparse to set up test environment
|
|
'''
|
|
from saltunittest import TestLoader, TextTestRunner
|
|
opts = parse_opts()
|
|
loader = TestLoader()
|
|
tests = loader.loadTestsFromTestCase(TestCase)
|
|
print('Setting up Salt daemons to execute tests')
|
|
with TestDaemon(clean=opts.clean):
|
|
runner = TextTestRunner(verbosity=opts.verbosity).run(tests)
|
|
sys.exit(runner.wasSuccessful())
|
|
|
|
|
|
def parse_opts():
|
|
'''
|
|
Parse command line options for running integration tests
|
|
'''
|
|
parser = optparse.OptionParser()
|
|
parser.add_option('-v',
|
|
'--verbose',
|
|
dest='verbosity',
|
|
default=1,
|
|
action='count',
|
|
help='Verbose test runner output')
|
|
parser.add_option('--clean',
|
|
dest='clean',
|
|
default=True,
|
|
action='store_true',
|
|
help=('Clean up test environment before and after '
|
|
'integration testing (default behaviour)'))
|
|
parser.add_option('--no-clean',
|
|
dest='clean',
|
|
action='store_false',
|
|
help=('Don\'t clean up test environment before and after '
|
|
'integration testing (speed up test process)'))
|
|
options, _ = parser.parse_args()
|
|
return options
|
|
|
|
|
|
class TestDaemon(object):
|
|
'''
|
|
Set up the master and minion daemons, and run related cases
|
|
'''
|
|
|
|
def __init__(self, clean):
|
|
self.clean = clean
|
|
|
|
def __enter__(self):
|
|
'''
|
|
Start a master and minion
|
|
'''
|
|
self.master_opts = salt.config.master_config(
|
|
os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'master')
|
|
)
|
|
self.minion_opts = salt.config.minion_config(
|
|
os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'minion')
|
|
)
|
|
self.sub_minion_opts = salt.config.minion_config(
|
|
os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'sub_minion')
|
|
)
|
|
self.smaster_opts = salt.config.master_config(
|
|
os.path.join(
|
|
INTEGRATION_TEST_DIR, 'files', 'conf', 'syndic_master'
|
|
)
|
|
)
|
|
self.syndic_opts = salt.config.minion_config(
|
|
os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'syndic'))
|
|
self.syndic_opts['_master_conf_file'] = os.path.join(
|
|
INTEGRATION_TEST_DIR,
|
|
'files/conf/master'
|
|
)
|
|
# Set up config options that require internal data
|
|
self.master_opts['pillar_roots'] = {
|
|
'base': [os.path.join(FILES, 'pillar', 'base')]
|
|
}
|
|
self.master_opts['file_roots'] = {
|
|
'base': [os.path.join(FILES, 'file', 'base')]
|
|
}
|
|
self.master_opts['ext_pillar'] = [
|
|
{'cmd_yaml': 'cat {0}'.format(
|
|
os.path.join(
|
|
FILES,
|
|
'ext.yaml'
|
|
)
|
|
)}
|
|
]
|
|
# clean up the old files
|
|
self._clean()
|
|
self.master_opts['hosts.file'] = os.path.join(TMP, 'hosts')
|
|
self.minion_opts['hosts.file'] = os.path.join(TMP, 'hosts')
|
|
verify_env([os.path.join(self.master_opts['pki_dir'], 'minions'),
|
|
os.path.join(self.master_opts['pki_dir'], 'minions_pre'),
|
|
os.path.join(self.master_opts['pki_dir'],
|
|
'minions_rejected'),
|
|
os.path.join(self.master_opts['cachedir'], 'jobs'),
|
|
os.path.join(self.smaster_opts['pki_dir'], 'minions'),
|
|
os.path.join(self.smaster_opts['pki_dir'], 'minions_pre'),
|
|
os.path.join(self.smaster_opts['pki_dir'],
|
|
'minions_rejected'),
|
|
os.path.join(self.smaster_opts['cachedir'], 'jobs'),
|
|
os.path.dirname(self.master_opts['log_file']),
|
|
self.minion_opts['extension_modules'],
|
|
self.sub_minion_opts['extension_modules'],
|
|
self.sub_minion_opts['pki_dir'],
|
|
self.master_opts['sock_dir'],
|
|
self.smaster_opts['sock_dir'],
|
|
self.sub_minion_opts['sock_dir'],
|
|
self.minion_opts['sock_dir'],
|
|
TMP
|
|
],
|
|
pwd.getpwuid(os.getuid()).pw_name)
|
|
|
|
# Set up PATH to mockbin
|
|
self._enter_mockbin()
|
|
|
|
master = salt.master.Master(self.master_opts)
|
|
self.master_process = multiprocessing.Process(target=master.start)
|
|
self.master_process.start()
|
|
|
|
minion = salt.minion.Minion(self.minion_opts)
|
|
self.minion_process = multiprocessing.Process(target=minion.tune_in)
|
|
self.minion_process.start()
|
|
|
|
sub_minion = salt.minion.Minion(self.sub_minion_opts)
|
|
self.sub_minion_process = multiprocessing.Process(
|
|
target=sub_minion.tune_in
|
|
)
|
|
self.sub_minion_process.start()
|
|
|
|
smaster = salt.master.Master(self.smaster_opts)
|
|
self.smaster_process = multiprocessing.Process(target=smaster.start)
|
|
self.smaster_process.start()
|
|
|
|
syndic = salt.minion.Syndic(self.syndic_opts)
|
|
self.syndic_process = multiprocessing.Process(target=syndic.tune_in)
|
|
self.syndic_process.start()
|
|
|
|
return self
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
'''
|
|
Kill the minion and master processes
|
|
'''
|
|
self.sub_minion_process.terminate()
|
|
self.minion_process.terminate()
|
|
self.master_process.terminate()
|
|
self.syndic_process.terminate()
|
|
self.smaster_process.terminate()
|
|
self._exit_mockbin()
|
|
self._clean()
|
|
|
|
def _enter_mockbin(self):
|
|
path = os.environ.get('PATH', '')
|
|
path_items = path.split(os.pathsep)
|
|
if MOCKBIN not in path_items:
|
|
path_items.insert(0, MOCKBIN)
|
|
os.environ['PATH'] = os.pathsep.join(path_items)
|
|
|
|
def _exit_mockbin(self):
|
|
path = os.environ.get('PATH', '')
|
|
path_items = path.split(os.pathsep)
|
|
try:
|
|
path_items.remove(MOCKBIN)
|
|
except ValueError:
|
|
pass
|
|
os.environ['PATH'] = os.pathsep.join(path_items)
|
|
|
|
def _clean(self):
|
|
'''
|
|
Clean out the tmp files
|
|
'''
|
|
if not self.clean:
|
|
return
|
|
if os.path.isdir(self.sub_minion_opts['root_dir']):
|
|
shutil.rmtree(self.sub_minion_opts['root_dir'])
|
|
if os.path.isdir(self.master_opts['root_dir']):
|
|
shutil.rmtree(self.master_opts['root_dir'])
|
|
if os.path.isdir(self.smaster_opts['root_dir']):
|
|
shutil.rmtree(self.smaster_opts['root_dir'])
|
|
if os.path.isdir(TMP):
|
|
shutil.rmtree(TMP)
|
|
|
|
|
|
class ModuleCase(TestCase):
|
|
'''
|
|
Execute a module function
|
|
'''
|
|
def setUp(self):
|
|
'''
|
|
Generate the tools to test a module
|
|
'''
|
|
self.client = salt.client.LocalClient(
|
|
os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'master')
|
|
)
|
|
|
|
def run_function(self, function, arg=(), minion_tgt='minion', **kwargs):
|
|
'''
|
|
Run a single salt function and condition the return down to match the
|
|
behavior of the raw function call
|
|
'''
|
|
know_to_return_none = ('file.chown', 'file.chgrp')
|
|
orig = self.client.cmd(
|
|
minion_tgt, function, arg, timeout=500, kwarg=kwargs
|
|
)
|
|
|
|
if minion_tgt not in orig:
|
|
self.skipTest(
|
|
'WARNING(SHOULD NOT HAPPEN #1935): Failed to get a reply '
|
|
'from the minion \'{0}\''.format(minion_tgt)
|
|
)
|
|
elif orig[minion_tgt] is None and function not in know_to_return_none:
|
|
self.skipTest(
|
|
'WARNING(SHOULD NOT HAPPEN #1935): Failed to get \'{0}\' from '
|
|
'the minion \'{1}\''.format(function, minion_tgt)
|
|
)
|
|
return orig[minion_tgt]
|
|
|
|
def state_result(self, ret):
|
|
'''
|
|
Return the result data from a single state return
|
|
'''
|
|
return ret[next(iter(ret))]['result']
|
|
|
|
def run_state(self, function, **kwargs):
|
|
'''
|
|
Run the state.single command and return the state return structure
|
|
'''
|
|
return self.run_function('state.single', [function], **kwargs)
|
|
|
|
@property
|
|
def minion_opts(self):
|
|
'''
|
|
Return the options used for the minion
|
|
'''
|
|
return salt.config.minion_config(
|
|
os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'minion')
|
|
)
|
|
|
|
@property
|
|
def sub_minion_opts(self):
|
|
'''
|
|
Return the options used for the minion
|
|
'''
|
|
return salt.config.minion_config(
|
|
os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'sub_minion')
|
|
)
|
|
|
|
@property
|
|
def master_opts(self):
|
|
'''
|
|
Return the options used for the minion
|
|
'''
|
|
return salt.config.master_config(
|
|
os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'master')
|
|
)
|
|
|
|
|
|
class SyndicCase(TestCase):
|
|
'''
|
|
Execute a syndic based execution test
|
|
'''
|
|
def setUp(self):
|
|
'''
|
|
Generate the tools to test a module
|
|
'''
|
|
self.client = salt.client.LocalClient(
|
|
os.path.join(
|
|
INTEGRATION_TEST_DIR,
|
|
'files', 'conf', 'syndic_master'
|
|
)
|
|
)
|
|
|
|
def run_function(self, function, arg=()):
|
|
'''
|
|
Run a single salt function and condition the return down to match the
|
|
behavior of the raw function call
|
|
'''
|
|
orig = self.client.cmd('minion', function, arg, timeout=500)
|
|
if 'minion' not in orig:
|
|
self.skipTest(
|
|
'WARNING(SHOULD NOT HAPPEN #1935): Failed to get a reply '
|
|
'from the minion. Received: \'{0}\''.format(orig)
|
|
)
|
|
return orig['minion']
|
|
|
|
|
|
class ShellCase(TestCase):
|
|
'''
|
|
Execute a test for a shell command
|
|
'''
|
|
def run_script(self, script, arg_str, catch_stderr=False):
|
|
'''
|
|
Execute a script with the given argument string
|
|
'''
|
|
path = os.path.join(SCRIPT_DIR, script)
|
|
if not os.path.isfile(path):
|
|
return False
|
|
ppath = 'PYTHONPATH={0}:{1}'.format(CODE_DIR, ':'.join(sys.path[1:]))
|
|
cmd = '{0} {1} {2} {3}'.format(ppath, PYEXEC, path, arg_str)
|
|
|
|
if catch_stderr:
|
|
process = subprocess.Popen(
|
|
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
)
|
|
if sys.version_info[0:2] < (2, 7):
|
|
# On python 2.6, the subprocess'es communicate() method uses
|
|
# select which, is limited by the OS to 1024 file descriptors
|
|
# We need more available descriptors to run the tests which
|
|
# need the stderr output.
|
|
# So instead of .communicate() we wait for the process to
|
|
# finish, but, as the python docs state "This will deadlock
|
|
# when using stdout=PIPE and/or stderr=PIPE and the child
|
|
# process generates enough output to a pipe such that it
|
|
# blocks waiting for the OS pipe buffer to accept more data.
|
|
# Use communicate() to avoid that." <- a catch, catch situation
|
|
#
|
|
# Use this work around were it's needed only, python 2.6
|
|
process.wait()
|
|
out = process.stdout.read()
|
|
err = process.stderr.read()
|
|
else:
|
|
out, err = process.communicate()
|
|
# Force closing stderr/stdout to release file descriptors
|
|
process.stdout.close()
|
|
process.stderr.close()
|
|
return out.split('\n'), err.split('\n')
|
|
|
|
process = subprocess.Popen(
|
|
cmd, shell=True, stdout=subprocess.PIPE
|
|
)
|
|
data = process.communicate()
|
|
process.stdout.close()
|
|
return data[0].split('\n')
|
|
|
|
def run_salt(self, arg_str):
|
|
'''
|
|
Execute salt
|
|
'''
|
|
mconf = os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf')
|
|
arg_str = '-c {0} {1}'.format(mconf, arg_str)
|
|
return self.run_script('salt', arg_str)
|
|
|
|
def run_run(self, arg_str):
|
|
'''
|
|
Execute salt-run
|
|
'''
|
|
mconf = os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf')
|
|
arg_str = '-c {0} {1}'.format(mconf, arg_str)
|
|
return self.run_script('salt-run', arg_str)
|
|
|
|
def run_run_plus(self, fun, options='', *arg):
|
|
'''
|
|
Execute Salt run and the salt run function and return the data from
|
|
each in a dict
|
|
'''
|
|
ret = {}
|
|
ret['out'] = self.run_run(
|
|
'{0} {1} {2}'.format(options, fun, ' '.join(arg))
|
|
)
|
|
opts = salt.config.master_config(
|
|
os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'master')
|
|
)
|
|
opts.update({'doc': False, 'fun': fun, 'arg': arg})
|
|
runner = salt.runner.Runner(opts)
|
|
ret['fun'] = runner.run()
|
|
return ret
|
|
|
|
def run_key(self, arg_str, catch_stderr=False):
|
|
'''
|
|
Execute salt-key
|
|
'''
|
|
mconf = os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf')
|
|
arg_str = '-c {0} {1}'.format(mconf, arg_str)
|
|
return self.run_script('salt-key', arg_str, catch_stderr=catch_stderr)
|
|
|
|
def run_cp(self, arg_str):
|
|
'''
|
|
Execute salt-cp
|
|
'''
|
|
mconf = os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf')
|
|
arg_str = '--config-dir {0} {1}'.format(mconf, arg_str)
|
|
return self.run_script('salt-cp', arg_str)
|
|
|
|
def run_call(self, arg_str):
|
|
mconf = os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf')
|
|
arg_str = '--config-dir {0} {1}'.format(mconf, arg_str)
|
|
return self.run_script('salt-call', arg_str)
|
|
|
|
|
|
class ShellCaseCommonTestsMixIn(object):
|
|
|
|
def test_deprecated_config(self):
|
|
"""
|
|
test for the --config deprecation warning
|
|
|
|
Once --config is fully deprecated, this test can be removed
|
|
|
|
"""
|
|
|
|
if getattr(self, '_call_binary_', None) is None:
|
|
self.skipTest("'_call_binary_' not defined.")
|
|
|
|
cfgfile = os.path.join(INTEGRATION_TEST_DIR, 'files', 'conf', 'master')
|
|
out, err = self.run_script(
|
|
self._call_binary_,
|
|
'--config {0}'.format(cfgfile),
|
|
catch_stderr=True
|
|
)
|
|
self.assertIn('Usage: {0}'.format(self._call_binary_), '\n'.join(err))
|
|
self.assertIn('deprecated', '\n'.join(err))
|
|
|
|
def test_version_includes_binary_name(self):
|
|
if getattr(self, '_call_binary_', None) is None:
|
|
self.skipTest("'_call_binary_' not defined.")
|
|
|
|
out = '\n'.join(self.run_script(self._call_binary_, "--version"))
|
|
self.assertIn(self._call_binary_, out)
|
|
self.assertIn(salt.__version__, out)
|