salt/tests/integration/shell/test_call.py
rallytime 668da57ab9
Mark some shell and runner integration tests as flaky
In addition to marking these tests as flaky, the commit also updates
the affected tests to use pytest notation instead of unittest notation.
2018-08-05 16:19:29 -04:00

566 lines
20 KiB
Python

# -*- coding: utf-8 -*-
'''
:codeauthor: Pedro Algarvio (pedro@algarvio.me)
tests.integration.shell.call
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'''
# Import python libs
from __future__ import absolute_import
import getpass
import os
import sys
import re
import shutil
from datetime import datetime
import logging
# Import 3rd-party libs
import yaml
# Import Salt Testing libs
from tests.support.case import ShellCase
from tests.support.unit import skipIf
from tests.support.paths import FILES, TMP
from tests.support.mixins import ShellCaseCommonTestsMixin
from tests.support.helpers import destructiveTest, flaky
from tests.integration.utils import testprogram
# Import salt libs
import salt.utils
import salt.utils.files
import salt.ext.six as six
log = logging.getLogger(__name__)
_PKG_TARGETS = {
'Arch': ['python2-django', 'libpng'],
'Debian': ['python-plist', 'apg'],
'RedHat': ['xz-devel', 'zsh-html'],
'FreeBSD': ['aalib', 'pth'],
'SUSE': ['aalib', 'python-pssh']
}
_PKGS_INSTALLED = set()
class CallTest(ShellCase, testprogram.TestProgramCase, ShellCaseCommonTestsMixin):
_call_binary_ = 'salt-call'
def test_default_output(self):
out = self.run_call('-l quiet test.fib 3')
expect = ['local:',
' - 2']
self.assertEqual(expect, out[:-1])
def test_text_output(self):
out = self.run_call('-l quiet --out txt test.fib 3')
expect = [
'local: (2'
]
self.assertEqual(''.join(expect), ''.join(out).rsplit(",", 1)[0])
def test_json_out_indent(self):
out = self.run_call('test.ping -l quiet --out=json --out-indent=-1')
self.assertIn('"local": true', ''.join(out))
out = self.run_call('test.ping -l quiet --out=json --out-indent=0')
self.assertIn('"local": true', ''.join(out))
out = self.run_call('test.ping -l quiet --out=json --out-indent=1')
self.assertIn('"local": true', ''.join(out))
def test_local_sls_call(self):
fileroot = os.path.join(FILES, 'file', 'base')
out = self.run_call('--file-root {0} --local state.sls saltcalllocal'.format(fileroot))
self.assertIn('Name: test.echo', ''.join(out))
self.assertIn('Result: True', ''.join(out))
self.assertIn('hello', ''.join(out))
self.assertIn('Succeeded: 1', ''.join(out))
@destructiveTest
@skipIf(True, 'Skipping due to off the wall failures and hangs on most os\'s. Will re-enable when fixed.')
@skipIf(sys.platform.startswith('win'), 'This test does not apply on Win')
@skipIf(getpass.getuser() == 'root', 'Requires root to test pkg.install')
def test_local_pkg_install(self):
'''
Test to ensure correct output when installing package
'''
get_os_family = self.run_call('--local grains.get os_family')
pkg_targets = _PKG_TARGETS.get(get_os_family[1].strip(), [])
check_pkg = self.run_call('--local pkg.list_pkgs')
for pkg in pkg_targets:
if pkg not in str(check_pkg):
out = self.run_call('--local pkg.install {0}'.format(pkg))
self.assertIn('local: ----------', ''.join(out))
self.assertIn('{0}: ----------'.format(pkg), ''.join(out))
self.assertIn('new:', ''.join(out))
self.assertIn('old:', ''.join(out))
_PKGS_INSTALLED.add(pkg)
else:
log.debug('The pkg: {0} is already installed on the machine'.format(pkg))
@skipIf(sys.platform.startswith('win'), 'This test does not apply on Win')
@flaky
def test_user_delete_kw_output(self):
ret = self.run_call('-l quiet -d user.delete')
assert 'salt \'*\' user.delete name remove=True force=True' in ''.join(ret)
def test_salt_documentation_too_many_arguments(self):
'''
Test to see if passing additional arguments shows an error
'''
data = self.run_call('-d virtualenv.create /tmp/ve', catch_stderr=True)
self.assertIn('You can only get documentation for one method at one time', '\n'.join(data[1]))
def test_issue_6973_state_highstate_exit_code(self):
'''
If there is no tops/master_tops or state file matches
for this minion, salt-call should exit non-zero if invoked with
option --retcode-passthrough
'''
src = os.path.join(FILES, 'file/base/top.sls')
dst = os.path.join(FILES, 'file/base/top.sls.bak')
shutil.move(src, dst)
expected_comment = 'No states found for this minion'
try:
stdout, retcode = self.run_call(
'-l quiet --retcode-passthrough state.highstate',
with_retcode=True
)
finally:
shutil.move(dst, src)
self.assertIn(expected_comment, ''.join(stdout))
self.assertNotEqual(0, retcode)
@skipIf(sys.platform.startswith('win'), 'This test does not apply on Win')
@skipIf(True, 'to be re-enabled when #23623 is merged')
def test_return(self):
self.run_call('cmd.run "echo returnTOmaster"')
jobs = [a for a in self.run_run('jobs.list_jobs')]
self.assertTrue(True in ['returnTOmaster' in j for j in jobs])
# lookback jid
first_match = [(i, j)
for i, j in enumerate(jobs)
if 'returnTOmaster' in j][0]
jid, idx = None, first_match[0]
while idx > 0:
jid = re.match("([0-9]+):", jobs[idx])
if jid:
jid = jid.group(1)
break
idx -= 1
assert idx > 0
assert jid
master_out = [
a for a in self.run_run('jobs.lookup_jid {0}'.format(jid))
]
self.assertTrue(True in ['returnTOmaster' in a for a in master_out])
@skipIf(sys.platform.startswith('win'), 'This test does not apply on Win')
def test_issue_2731_masterless(self):
root_dir = os.path.join(TMP, 'issue-2731')
config_dir = os.path.join(root_dir, 'conf')
minion_config_file = os.path.join(config_dir, 'minion')
logfile = os.path.join(root_dir, 'minion_test_issue_2731')
if not os.path.isdir(config_dir):
os.makedirs(config_dir)
with salt.utils.fopen(self.get_config_file_path('master')) as fhr:
master_config = yaml.load(fhr.read())
master_root_dir = master_config['root_dir']
this_minion_key = os.path.join(
master_root_dir, 'pki', 'master', 'minions', 'minion_test_issue_2731'
)
minion_config = {
'id': 'minion_test_issue_2731',
'master': 'localhost',
'master_port': 64506,
'root_dir': master_root_dir,
'pki_dir': 'pki',
'cachedir': 'cachedir',
'sock_dir': 'minion_sock',
'open_mode': True,
'log_file': logfile,
'log_level': 'quiet',
'log_level_logfile': 'info',
'transport': self.master_opts['transport'],
}
try:
# Remove existing logfile
if os.path.isfile(logfile):
os.unlink(logfile)
start = datetime.now()
# Let's first test with a master running
with salt.utils.fopen(minion_config_file, 'w') as fh_:
fh_.write(
yaml.dump(minion_config, default_flow_style=False)
)
ret = self.run_script(
'salt-call',
'--config-dir {0} cmd.run "echo foo"'.format(
config_dir
)
)
try:
self.assertIn('local:', ret)
except AssertionError:
if os.path.isfile(minion_config_file):
os.unlink(minion_config_file)
# Let's remove our key from the master
if os.path.isfile(this_minion_key):
os.unlink(this_minion_key)
raise
# Calculate the required timeout, since next will fail.
# I needed this because after many attempts, I was unable to catch:
# WARNING: Master hostname: salt not found. Retrying in 30 seconds
ellapsed = datetime.now() - start
timeout = ellapsed.seconds + 3
# Now let's remove the master configuration
minion_config.pop('master')
minion_config.pop('master_port')
with salt.utils.fopen(minion_config_file, 'w') as fh_:
fh_.write(
yaml.dump(minion_config, default_flow_style=False)
)
out = self.run_script(
'salt-call',
'--config-dir {0} cmd.run "echo foo"'.format(
config_dir
),
timeout=timeout,
)
try:
self.assertIn(
'Process took more than {0} seconds to complete. '
'Process Killed!'.format(timeout),
out
)
except AssertionError:
if os.path.isfile(minion_config_file):
os.unlink(minion_config_file)
# Let's remove our key from the master
if os.path.isfile(this_minion_key):
os.unlink(this_minion_key)
raise
# Should work with --local
ret = self.run_script(
'salt-call',
'--config-dir {0} --local cmd.run "echo foo"'.format(
config_dir
),
timeout=60
)
try:
self.assertIn('local:', ret)
except AssertionError:
if os.path.isfile(minion_config_file):
os.unlink(minion_config_file)
# Let's remove our key from the master
if os.path.isfile(this_minion_key):
os.unlink(this_minion_key)
raise
# Should work with local file client
minion_config['file_client'] = 'local'
with salt.utils.fopen(minion_config_file, 'w') as fh_:
fh_.write(
yaml.dump(minion_config, default_flow_style=False)
)
ret = self.run_script(
'salt-call',
'--config-dir {0} cmd.run "echo foo"'.format(
config_dir
),
timeout=60
)
self.assertIn('local:', ret)
finally:
if os.path.isfile(minion_config_file):
os.unlink(minion_config_file)
# Let's remove our key from the master
if os.path.isfile(this_minion_key):
os.unlink(this_minion_key)
def test_issue_7754(self):
old_cwd = os.getcwd()
config_dir = os.path.join(TMP, 'issue-7754')
if not os.path.isdir(config_dir):
os.makedirs(config_dir)
os.chdir(config_dir)
with salt.utils.fopen(self.get_config_file_path('minion'), 'r') as fh_:
minion_config = yaml.load(fh_.read())
minion_config['log_file'] = 'file:///dev/log/LOG_LOCAL3'
with salt.utils.fopen(os.path.join(config_dir, 'minion'), 'w') as fh_:
fh_.write(
yaml.dump(minion_config, default_flow_style=False)
)
ret = self.run_script(
'salt-call',
'--config-dir {0} cmd.run "echo foo"'.format(
config_dir
),
timeout=60,
catch_stderr=True,
with_retcode=True
)
try:
self.assertIn('local:', ret[0])
self.assertFalse(os.path.isdir(os.path.join(config_dir, 'file:')))
except AssertionError:
# We now fail when we're unable to properly set the syslog logger
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 test_syslog_file_not_found(self):
'''
test when log_file is set to a syslog file that does not exist
'''
old_cwd = os.getcwd()
config_dir = os.path.join(TMP, 'log_file_incorrect')
if not os.path.isdir(config_dir):
os.makedirs(config_dir)
os.chdir(config_dir)
with salt.utils.fopen(self.get_config_file_path('minion'), 'r') as fh_:
minion_config = yaml.load(fh_.read())
minion_config['log_file'] = 'file:///dev/doesnotexist'
with salt.utils.fopen(os.path.join(config_dir, 'minion'), 'w') as fh_:
fh_.write(
yaml.dump(minion_config, default_flow_style=False)
)
ret = self.run_script(
'salt-call',
'--config-dir {0} cmd.run "echo foo"'.format(
config_dir
),
timeout=60,
catch_stderr=True,
with_retcode=True
)
try:
if sys.version_info >= (3, 5, 4):
self.assertIn('local:', ret[0])
self.assertIn('[WARNING ] The log_file does not exist. Logging not setup correctly or syslog service not started.', ret[1])
self.assertEqual(ret[2], 0)
else:
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 test_issue_15074_output_file_append(self):
output_file_append = os.path.join(TMP, 'issue-15074')
try:
# Let's create an initial output file with some data
_ = self.run_script(
'salt-call',
'-c {0} --output-file={1} test.versions'.format(
self.get_config_dir(),
output_file_append
),
catch_stderr=True,
with_retcode=True
)
with salt.utils.fopen(output_file_append) as ofa:
output = ofa.read()
self.run_script(
'salt-call',
'-c {0} --output-file={1} --output-file-append test.versions'.format(
self.get_config_dir(),
output_file_append
),
catch_stderr=True,
with_retcode=True
)
with salt.utils.fopen(output_file_append) as ofa:
self.assertEqual(ofa.read(), output + output)
finally:
if os.path.exists(output_file_append):
os.unlink(output_file_append)
def test_issue_14979_output_file_permissions(self):
output_file = os.path.join(TMP, 'issue-14979')
with salt.utils.files.set_umask(0o077):
try:
# Let's create an initial output file with some data
self.run_script(
'salt-call',
'-c {0} --output-file={1} -g'.format(
self.get_config_dir(),
output_file
),
catch_stderr=True,
with_retcode=True
)
stat1 = os.stat(output_file)
# Let's change umask
os.umask(0o777) # pylint: disable=blacklisted-function
self.run_script(
'salt-call',
'-c {0} --output-file={1} --output-file-append -g'.format(
self.get_config_dir(),
output_file
),
catch_stderr=True,
with_retcode=True
)
stat2 = os.stat(output_file)
self.assertEqual(stat1.st_mode, stat2.st_mode)
# Data was appeneded to file
self.assertTrue(stat1.st_size < stat2.st_size)
# Let's remove the output file
os.unlink(output_file)
# Not appending data
self.run_script(
'salt-call',
'-c {0} --output-file={1} -g'.format(
self.get_config_dir(),
output_file
),
catch_stderr=True,
with_retcode=True
)
stat3 = os.stat(output_file)
# Mode must have changed since we're creating a new log file
self.assertNotEqual(stat1.st_mode, stat3.st_mode)
finally:
if os.path.exists(output_file):
os.unlink(output_file)
@skipIf(sys.platform.startswith('win'), 'This test does not apply on Win')
def test_42116_cli_pillar_override(self):
ret = self.run_call(
'state.apply issue-42116-cli-pillar-override '
'pillar=\'{"myhost": "localhost"}\''
)
for line in ret:
line = line.lstrip()
if line == 'Comment: Command "ping -c 2 localhost" run':
# Successful test
break
else:
log.debug('salt-call output:\n\n%s', '\n'.join(ret))
self.fail('CLI pillar override not found in pillar data')
def test_pillar_items_masterless(self):
'''
Test to ensure we get expected output
from pillar.items with salt-call
'''
get_items = self.run_call('pillar.items', local=True)
exp_out = [' - Lancelot', ' - Galahad', ' - Bedevere',
' monty:', ' python']
for out in exp_out:
self.assertIn(out, get_items)
def tearDown(self):
'''
Teardown method to remove installed packages
'''
user = ''
user_info = self.run_call('--local grains.get username')
if user_info and isinstance(user_info, (list, tuple)) and isinstance(user_info[-1], six.string_types):
user = user_info[-1].strip()
if user == 'root':
for pkg in _PKGS_INSTALLED:
_ = self.run_call('--local pkg.remove {0}'.format(pkg))
super(CallTest, self).tearDown()
# pylint: disable=invalid-name
def test_exit_status_unknown_argument(self):
'''
Ensure correct exit status when an unknown argument is passed to salt-call.
'''
call = testprogram.TestProgramSaltCall(
name='unknown_argument',
parent_dir=self._test_dir,
)
# Call setup here to ensure config and script exist
call.setup()
stdout, stderr, status = call.run(
args=['--unknown-argument'],
catch_stderr=True,
with_retcode=True,
)
self.assert_exit_status(
status, 'EX_USAGE',
message='unknown argument',
stdout=stdout, stderr=stderr
)
def test_masterless_highstate(self):
'''
test state.highstate in masterless mode
'''
ret = self.run_call('state.highstate', local=True)
destpath = os.path.join(TMP, 'testfile')
exp_out = [' Function: file.managed', ' Result: True',
' ID: {0}'.format(destpath)]
for out in exp_out:
self.assertIn(out, ret)
self.assertTrue(os.path.exists(destpath))
def test_exit_status_correct_usage(self):
'''
Ensure correct exit status when salt-call starts correctly.
'''
call = testprogram.TestProgramSaltCall(
name='correct_usage',
parent_dir=self._test_dir,
)
# Call setup here to ensure config and script exist
call.setup()
stdout, stderr, status = call.run(
args=['--local', 'test.true'],
catch_stderr=True,
with_retcode=True,
)
self.assert_exit_status(
status, 'EX_OK',
message='correct usage',
stdout=stdout, stderr=stderr
)