test_call.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  4. tests.integration.shell.call
  5. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  6. '''
  7. # Import python libs
  8. from __future__ import absolute_import
  9. import logging
  10. import os
  11. import re
  12. import shutil
  13. import sys
  14. # Import Salt Testing libs
  15. from tests.support.runtests import RUNTIME_VARS
  16. from tests.support.case import ShellCase
  17. from tests.support.unit import skipIf
  18. from tests.support.mixins import ShellCaseCommonTestsMixin
  19. from tests.support.helpers import flaky, with_tempfile
  20. from tests.integration.utils import testprogram
  21. # Import salt libs
  22. import salt.utils.files
  23. import salt.utils.json
  24. import salt.utils.platform
  25. import salt.utils.yaml
  26. from salt.ext import six
  27. import pytest
  28. log = logging.getLogger(__name__)
  29. @pytest.mark.windows_whitelisted
  30. class CallTest(ShellCase, testprogram.TestProgramCase, ShellCaseCommonTestsMixin):
  31. _call_binary_ = 'salt-call'
  32. def test_default_output(self):
  33. out = self.run_call('-l quiet test.fib 3')
  34. expect = ['local:',
  35. ' - 2']
  36. self.assertEqual(expect, out[:-1])
  37. def test_text_output(self):
  38. out = self.run_call('-l quiet --out txt test.fib 3')
  39. expect = [
  40. 'local: (2'
  41. ]
  42. self.assertEqual(''.join(expect), ''.join(out).rsplit(",", 1)[0])
  43. def test_json_out_indent(self):
  44. out = self.run_call('test.ping -l quiet --out=json --out-indent=-1')
  45. self.assertIn('"local": true', ''.join(out))
  46. out = self.run_call('test.ping -l quiet --out=json --out-indent=0')
  47. self.assertIn('"local": true', ''.join(out))
  48. out = self.run_call('test.ping -l quiet --out=json --out-indent=1')
  49. self.assertIn('"local": true', ''.join(out))
  50. def test_local_sls_call(self):
  51. fileroot = os.path.join(RUNTIME_VARS.FILES, 'file', 'base')
  52. out = self.run_call('--file-root {0} state.sls saltcalllocal'.format(fileroot), local=True)
  53. self.assertIn('Name: test.echo', ''.join(out))
  54. self.assertIn('Result: True', ''.join(out))
  55. self.assertIn('hello', ''.join(out))
  56. self.assertIn('Succeeded: 1', ''.join(out))
  57. @with_tempfile()
  58. def test_local_salt_call(self, name):
  59. '''
  60. This tests to make sure that salt-call does not execute the
  61. function twice, see https://github.com/saltstack/salt/pull/49552
  62. '''
  63. def _run_call(cmd):
  64. cmd = '--out=json ' + cmd
  65. return salt.utils.json.loads(''.join(self.run_call(cmd, local=True)))['local']
  66. ret = _run_call('state.single file.append name={0} text="foo"'.format(name))
  67. ret = ret[next(iter(ret))]
  68. # Make sure we made changes
  69. assert ret['changes']
  70. # 2nd sanity check: make sure that "foo" only exists once in the file
  71. with salt.utils.files.fopen(name) as fp_:
  72. contents = fp_.read()
  73. assert contents.count('foo') == 1, contents
  74. @skipIf(salt.utils.platform.is_windows() or salt.utils.platform.is_darwin(), 'This test requires a supported master')
  75. def test_user_delete_kw_output(self):
  76. ret = self.run_call('-l quiet -d user.delete')
  77. assert 'salt \'*\' user.delete name remove=True force=True' in ''.join(ret)
  78. def test_salt_documentation_too_many_arguments(self):
  79. '''
  80. Test to see if passing additional arguments shows an error
  81. '''
  82. data = self.run_call('-d virtualenv.create /tmp/ve', catch_stderr=True)
  83. self.assertIn('You can only get documentation for one method at one time', '\n'.join(data[1]))
  84. def test_issue_6973_state_highstate_exit_code(self):
  85. '''
  86. If there is no tops/master_tops or state file matches
  87. for this minion, salt-call should exit non-zero if invoked with
  88. option --retcode-passthrough
  89. '''
  90. src = os.path.join(RUNTIME_VARS.BASE_FILES, 'top.sls')
  91. dst = os.path.join(RUNTIME_VARS.BASE_FILES, 'top.sls.bak')
  92. shutil.move(src, dst)
  93. expected_comment = 'No states found for this minion'
  94. try:
  95. stdout, retcode = self.run_call(
  96. '-l quiet --retcode-passthrough state.highstate',
  97. with_retcode=True
  98. )
  99. finally:
  100. shutil.move(dst, src)
  101. self.assertIn(expected_comment, ''.join(stdout))
  102. self.assertNotEqual(0, retcode)
  103. @skipIf(sys.platform.startswith('win'), 'This test does not apply on Win')
  104. @skipIf(True, 'to be re-enabled when #23623 is merged')
  105. def test_return(self):
  106. self.run_call('cmd.run "echo returnTOmaster"')
  107. jobs = [a for a in self.run_run('jobs.list_jobs')]
  108. self.assertTrue(True in ['returnTOmaster' in j for j in jobs])
  109. # lookback jid
  110. first_match = [(i, j)
  111. for i, j in enumerate(jobs)
  112. if 'returnTOmaster' in j][0]
  113. jid, idx = None, first_match[0]
  114. while idx > 0:
  115. jid = re.match("([0-9]+):", jobs[idx])
  116. if jid:
  117. jid = jid.group(1)
  118. break
  119. idx -= 1
  120. assert idx > 0
  121. assert jid
  122. master_out = [
  123. a for a in self.run_run('jobs.lookup_jid {0}'.format(jid))
  124. ]
  125. self.assertTrue(True in ['returnTOmaster' in a for a in master_out])
  126. @skipIf(salt.utils.platform.is_windows(), 'Skip on Windows')
  127. def test_syslog_file_not_found(self):
  128. '''
  129. test when log_file is set to a syslog file that does not exist
  130. '''
  131. old_cwd = os.getcwd()
  132. config_dir = os.path.join(RUNTIME_VARS.TMP, 'log_file_incorrect')
  133. if not os.path.isdir(config_dir):
  134. os.makedirs(config_dir)
  135. os.chdir(config_dir)
  136. with salt.utils.files.fopen(self.get_config_file_path('minion'), 'r') as fh_:
  137. minion_config = salt.utils.yaml.load(fh_.read())
  138. minion_config['log_file'] = 'file:///dev/doesnotexist'
  139. with salt.utils.files.fopen(os.path.join(config_dir, 'minion'), 'w') as fh_:
  140. fh_.write(
  141. salt.utils.yaml.dump(minion_config, default_flow_style=False)
  142. )
  143. ret = self.run_script(
  144. 'salt-call',
  145. '--config-dir {0} cmd.run "echo foo"'.format(
  146. config_dir
  147. ),
  148. timeout=120,
  149. catch_stderr=True,
  150. with_retcode=True
  151. )
  152. try:
  153. if sys.version_info >= (3, 5, 4):
  154. self.assertIn('local:', ret[0])
  155. self.assertIn('[WARNING ] The log_file does not exist. Logging not setup correctly or syslog service not started.', ret[1])
  156. self.assertEqual(ret[2], 0)
  157. else:
  158. self.assertIn(
  159. 'Failed to setup the Syslog logging handler', '\n'.join(ret[1])
  160. )
  161. self.assertEqual(ret[2], 2)
  162. finally:
  163. self.chdir(old_cwd)
  164. if os.path.isdir(config_dir):
  165. shutil.rmtree(config_dir)
  166. @skipIf(True, 'This test is unreliable. Need to investigate why more deeply.')
  167. @flaky
  168. def test_issue_15074_output_file_append(self):
  169. output_file_append = os.path.join(RUNTIME_VARS.TMP, 'issue-15074')
  170. try:
  171. # Let's create an initial output file with some data
  172. _ = self.run_script(
  173. 'salt-call',
  174. '-c {0} --output-file={1} test.versions'.format(
  175. self.config_dir,
  176. output_file_append
  177. ),
  178. catch_stderr=True,
  179. with_retcode=True
  180. )
  181. with salt.utils.files.fopen(output_file_append) as ofa:
  182. output = ofa.read()
  183. self.run_script(
  184. 'salt-call',
  185. '-c {0} --output-file={1} --output-file-append test.versions'.format(
  186. self.config_dir,
  187. output_file_append
  188. ),
  189. catch_stderr=True,
  190. with_retcode=True
  191. )
  192. with salt.utils.files.fopen(output_file_append) as ofa:
  193. self.assertEqual(ofa.read(), output + output)
  194. finally:
  195. if os.path.exists(output_file_append):
  196. os.unlink(output_file_append)
  197. @skipIf(True, 'This test is unreliable. Need to investigate why more deeply.')
  198. @flaky
  199. def test_issue_14979_output_file_permissions(self):
  200. output_file = os.path.join(RUNTIME_VARS.TMP, 'issue-14979')
  201. with salt.utils.files.set_umask(0o077):
  202. try:
  203. # Let's create an initial output file with some data
  204. self.run_script(
  205. 'salt-call',
  206. '-c {0} --output-file={1} -l trace -g'.format(
  207. self.config_dir,
  208. output_file
  209. ),
  210. catch_stderr=True,
  211. with_retcode=True
  212. )
  213. try:
  214. stat1 = os.stat(output_file)
  215. except OSError:
  216. self.fail('Failed to generate output file, see log for details')
  217. # Let's change umask
  218. os.umask(0o777) # pylint: disable=blacklisted-function
  219. self.run_script(
  220. 'salt-call',
  221. '-c {0} --output-file={1} --output-file-append -g'.format(
  222. self.config_dir,
  223. output_file
  224. ),
  225. catch_stderr=True,
  226. with_retcode=True
  227. )
  228. try:
  229. stat2 = os.stat(output_file)
  230. except OSError:
  231. self.fail('Failed to generate output file, see log for details')
  232. self.assertEqual(stat1.st_mode, stat2.st_mode)
  233. # Data was appeneded to file
  234. self.assertTrue(stat1.st_size < stat2.st_size)
  235. # Let's remove the output file
  236. os.unlink(output_file)
  237. # Not appending data
  238. self.run_script(
  239. 'salt-call',
  240. '-c {0} --output-file={1} -g'.format(
  241. self.config_dir,
  242. output_file
  243. ),
  244. catch_stderr=True,
  245. with_retcode=True
  246. )
  247. try:
  248. stat3 = os.stat(output_file)
  249. except OSError:
  250. self.fail('Failed to generate output file, see log for details')
  251. # Mode must have changed since we're creating a new log file
  252. self.assertNotEqual(stat1.st_mode, stat3.st_mode)
  253. finally:
  254. if os.path.exists(output_file):
  255. os.unlink(output_file)
  256. @skipIf(sys.platform.startswith('win'), 'This test does not apply on Win')
  257. def test_42116_cli_pillar_override(self):
  258. ret = self.run_call(
  259. 'state.apply issue-42116-cli-pillar-override '
  260. 'pillar=\'{"myhost": "localhost"}\''
  261. )
  262. for line in ret:
  263. line = line.lstrip()
  264. if line == 'Comment: Command "ping -c 2 localhost" run':
  265. # Successful test
  266. break
  267. else:
  268. log.debug('salt-call output:\n\n%s', '\n'.join(ret))
  269. self.fail('CLI pillar override not found in pillar data')
  270. def test_pillar_items_masterless(self):
  271. '''
  272. Test to ensure we get expected output
  273. from pillar.items with salt-call
  274. '''
  275. get_items = self.run_call('pillar.items', local=True)
  276. exp_out = [' - Lancelot', ' - Galahad', ' - Bedevere',
  277. ' monty:', ' python']
  278. for out in exp_out:
  279. self.assertIn(out, get_items)
  280. def tearDown(self):
  281. '''
  282. Teardown method to remove installed packages
  283. '''
  284. user = ''
  285. user_info = self.run_call(' grains.get username', local=True)
  286. if user_info and isinstance(user_info, (list, tuple)) and isinstance(user_info[-1], six.string_types):
  287. user = user_info[-1].strip()
  288. super(CallTest, self).tearDown()
  289. # pylint: disable=invalid-name
  290. def test_exit_status_unknown_argument(self):
  291. '''
  292. Ensure correct exit status when an unknown argument is passed to salt-call.
  293. '''
  294. call = testprogram.TestProgramSaltCall(
  295. name='unknown_argument',
  296. parent_dir=self._test_dir,
  297. )
  298. # Call setup here to ensure config and script exist
  299. call.setup()
  300. stdout, stderr, status = call.run(
  301. args=['--unknown-argument'],
  302. catch_stderr=True,
  303. with_retcode=True,
  304. )
  305. self.assert_exit_status(
  306. status, 'EX_USAGE',
  307. message='unknown argument',
  308. stdout=stdout, stderr=stderr
  309. )
  310. def test_masterless_highstate(self):
  311. '''
  312. test state.highstate in masterless mode
  313. '''
  314. ret = self.run_call('state.highstate', local=True)
  315. destpath = os.path.join(RUNTIME_VARS.TMP, 'testfile')
  316. exp_out = [' Function: file.managed', ' Result: True',
  317. ' ID: {0}'.format(destpath)]
  318. for out in exp_out:
  319. self.assertIn(out, ret)
  320. self.assertTrue(os.path.exists(destpath))
  321. def test_exit_status_correct_usage(self):
  322. '''
  323. Ensure correct exit status when salt-call starts correctly.
  324. '''
  325. call = testprogram.TestProgramSaltCall(
  326. name='correct_usage',
  327. parent_dir=self._test_dir,
  328. )
  329. # Call setup here to ensure config and script exist
  330. call.setup()
  331. stdout, stderr, status = call.run(
  332. args=['--local', 'test.true'],
  333. catch_stderr=True,
  334. with_retcode=True,
  335. )
  336. self.assert_exit_status(
  337. status, 'EX_OK',
  338. message='correct usage',
  339. stdout=stdout, stderr=stderr
  340. )