case.py 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  4. ====================================
  5. Custom Salt TestCase Implementations
  6. ====================================
  7. Custom reusable :class:`TestCase<python2:unittest.TestCase>`
  8. implementations.
  9. '''
  10. # pylint: disable=repr-flag-used-in-string
  11. # Import python libs
  12. from __future__ import absolute_import, unicode_literals
  13. import os
  14. import re
  15. import sys
  16. import time
  17. import stat
  18. import errno
  19. import signal
  20. import textwrap
  21. import logging
  22. import tempfile
  23. import subprocess
  24. from datetime import datetime, timedelta
  25. # Import salt testing libs
  26. from tests.support.unit import TestCase
  27. from tests.support.helpers import (
  28. RedirectStdStreams, requires_sshd_server, win32_kill_process_tree
  29. )
  30. from tests.support.runtests import RUNTIME_VARS
  31. from tests.support.mixins import (
  32. AdaptedConfigurationTestCaseMixin,
  33. SaltClientTestCaseMixin,
  34. SaltMultimasterClientTestCaseMixin,
  35. )
  36. from tests.support.paths import ScriptPathMixin, INTEGRATION_TEST_DIR, CODE_DIR, PYEXEC, SCRIPT_DIR
  37. # Import 3rd-party libs
  38. from salt.ext import six
  39. from salt.ext.six.moves import cStringIO, range # pylint: disable=import-error
  40. STATE_FUNCTION_RUNNING_RE = re.compile(
  41. r'''The function (?:"|')(?P<state_func>.*)(?:"|') is running as PID '''
  42. r'(?P<pid>[\d]+) and was started at (?P<date>.*) with jid (?P<jid>[\d]+)'
  43. )
  44. SCRIPT_TEMPLATES = {
  45. 'salt': [
  46. 'from salt.scripts import salt_main\n',
  47. 'if __name__ == \'__main__\':'
  48. ' salt_main()'
  49. ],
  50. 'salt-api': [
  51. 'import salt.cli\n',
  52. 'def main():',
  53. ' sapi = salt.cli.SaltAPI()',
  54. ' sapi.run()\n',
  55. 'if __name__ == \'__main__\':',
  56. ' main()'
  57. ],
  58. 'common': [
  59. 'from salt.scripts import salt_{0}\n',
  60. 'if __name__ == \'__main__\':',
  61. ' salt_{0}()'
  62. ]
  63. }
  64. log = logging.getLogger(__name__)
  65. class ShellTestCase(TestCase, AdaptedConfigurationTestCaseMixin):
  66. '''
  67. Execute a test for a shell command
  68. '''
  69. def get_script_path(self, script_name):
  70. '''
  71. Return the path to a testing runtime script
  72. '''
  73. if not os.path.isdir(RUNTIME_VARS.TMP_SCRIPT_DIR):
  74. os.makedirs(RUNTIME_VARS.TMP_SCRIPT_DIR)
  75. script_path = os.path.join(RUNTIME_VARS.TMP_SCRIPT_DIR, script_name)
  76. if not os.path.isfile(script_path):
  77. log.debug('Generating {0}'.format(script_path))
  78. # Late import
  79. import salt.utils.files
  80. with salt.utils.files.fopen(script_path, 'w') as sfh:
  81. script_template = SCRIPT_TEMPLATES.get(script_name, None)
  82. if script_template is None:
  83. script_template = SCRIPT_TEMPLATES.get('common', None)
  84. if script_template is None:
  85. raise RuntimeError(
  86. '{0} does not know how to handle the {1} script'.format(
  87. self.__class__.__name__,
  88. script_name
  89. )
  90. )
  91. contents = (
  92. '#!{0}\n'.format(sys.executable) +
  93. '\n'.join(script_template).format(script_name.replace('salt-', ''))
  94. )
  95. sfh.write(contents)
  96. log.debug(
  97. 'Wrote the following contents to temp script %s:\n%s',
  98. script_path, contents
  99. )
  100. st = os.stat(script_path)
  101. os.chmod(script_path, st.st_mode | stat.S_IEXEC)
  102. return script_path
  103. def run_salt(self, arg_str, with_retcode=False, catch_stderr=False, timeout=15):
  104. r'''
  105. Run the ``salt`` CLI tool with the provided arguments
  106. .. code-block:: python
  107. class MatchTest(ShellTestCase):
  108. def test_list(self):
  109. """
  110. test salt -L matcher
  111. """
  112. data = self.run_salt('-L minion test.ping')
  113. data = '\n'.join(data)
  114. self.assertIn('minion', data)
  115. '''
  116. arg_str = '-c {0} -t {1} {2}'.format(self.config_dir, timeout, arg_str)
  117. return self.run_script('salt', arg_str, with_retcode=with_retcode, catch_stderr=catch_stderr, timeout=timeout)
  118. def run_ssh(self, arg_str, with_retcode=False, timeout=25,
  119. catch_stderr=False, wipe=False, raw=False):
  120. '''
  121. Execute salt-ssh
  122. '''
  123. arg_str = '{0} {1} -c {2} -i --priv {3} --roster-file {4} localhost {5} --out=json'.format(
  124. ' -W' if wipe else '',
  125. ' -r' if raw else '',
  126. self.config_dir,
  127. os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'key_test'),
  128. os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'roster'),
  129. arg_str
  130. )
  131. return self.run_script('salt-ssh', arg_str, with_retcode=with_retcode, catch_stderr=catch_stderr, raw=True, timeout=timeout)
  132. def run_run(self,
  133. arg_str,
  134. with_retcode=False,
  135. catch_stderr=False,
  136. asynchronous=False,
  137. timeout=60,
  138. config_dir=None,
  139. **kwargs):
  140. '''
  141. Execute salt-run
  142. '''
  143. asynchronous = kwargs.get('async', asynchronous)
  144. arg_str = '-c {0}{async_flag} -t {timeout} {1}'.format(
  145. config_dir or self.config_dir,
  146. arg_str,
  147. timeout=timeout,
  148. async_flag=' --async' if asynchronous else '')
  149. return self.run_script('salt-run',
  150. arg_str,
  151. with_retcode=with_retcode,
  152. catch_stderr=catch_stderr,
  153. timeout=timeout)
  154. def run_run_plus(self, fun, *arg, **kwargs):
  155. '''
  156. Execute the runner function and return the return data and output in a dict
  157. '''
  158. ret = {'fun': fun}
  159. # Late import
  160. import salt.config
  161. import salt.output
  162. import salt.runner
  163. from salt.ext.six.moves import cStringIO
  164. opts = salt.config.master_config(
  165. self.get_config_file_path('master')
  166. )
  167. opts_arg = list(arg)
  168. if kwargs:
  169. opts_arg.append({'__kwarg__': True})
  170. opts_arg[-1].update(kwargs)
  171. opts.update({'doc': False, 'fun': fun, 'arg': opts_arg})
  172. with RedirectStdStreams():
  173. runner = salt.runner.Runner(opts)
  174. ret['return'] = runner.run()
  175. try:
  176. ret['jid'] = runner.jid
  177. except AttributeError:
  178. ret['jid'] = None
  179. # Compile output
  180. # TODO: Support outputters other than nested
  181. opts['color'] = False
  182. opts['output_file'] = cStringIO()
  183. try:
  184. salt.output.display_output(ret['return'], opts=opts)
  185. ret['out'] = opts['output_file'].getvalue()
  186. finally:
  187. opts['output_file'].close()
  188. return ret
  189. def run_key(self, arg_str, catch_stderr=False, with_retcode=False):
  190. '''
  191. Execute salt-key
  192. '''
  193. arg_str = '-c {0} {1}'.format(self.config_dir, arg_str)
  194. return self.run_script(
  195. 'salt-key',
  196. arg_str,
  197. catch_stderr=catch_stderr,
  198. with_retcode=with_retcode
  199. )
  200. def run_cp(self, arg_str, with_retcode=False, catch_stderr=False):
  201. '''
  202. Execute salt-cp
  203. '''
  204. arg_str = '--config-dir {0} {1}'.format(self.config_dir, arg_str)
  205. return self.run_script('salt-cp', arg_str, with_retcode=with_retcode, catch_stderr=catch_stderr)
  206. def run_call(self, arg_str, with_retcode=False, catch_stderr=False, local=False, timeout=15):
  207. arg_str = '{0} --config-dir {1} {2}'.format('--local' if local else '',
  208. self.config_dir, arg_str)
  209. return self.run_script('salt-call',
  210. arg_str,
  211. with_retcode=with_retcode,
  212. catch_stderr=catch_stderr,
  213. timeout=timeout)
  214. def run_cloud(self, arg_str, catch_stderr=False, timeout=None):
  215. '''
  216. Execute salt-cloud
  217. '''
  218. arg_str = '-c {0} {1}'.format(self.config_dir, arg_str)
  219. return self.run_script('salt-cloud', arg_str, catch_stderr, timeout)
  220. def run_script(self,
  221. script,
  222. arg_str,
  223. catch_stderr=False,
  224. with_retcode=False,
  225. catch_timeout=False,
  226. # FIXME A timeout of zero or disabling timeouts may not return results!
  227. timeout=15,
  228. raw=False,
  229. popen_kwargs=None,
  230. log_output=None):
  231. '''
  232. Execute a script with the given argument string
  233. The ``log_output`` argument is ternary, it can be True, False, or None.
  234. If the value is boolean, then it forces the results to either be logged
  235. or not logged. If it is None, then the return code of the subprocess
  236. determines whether or not to log results.
  237. '''
  238. import salt.utils.platform
  239. script_path = self.get_script_path(script)
  240. if not os.path.isfile(script_path):
  241. return False
  242. popen_kwargs = popen_kwargs or {}
  243. if salt.utils.platform.is_windows():
  244. cmd = 'python '
  245. if 'cwd' not in popen_kwargs:
  246. popen_kwargs['cwd'] = os.getcwd()
  247. if 'env' not in popen_kwargs:
  248. popen_kwargs['env'] = os.environ.copy()
  249. if sys.version_info[0] < 3:
  250. popen_kwargs['env'][b'PYTHONPATH'] = CODE_DIR.encode()
  251. else:
  252. popen_kwargs['env']['PYTHONPATH'] = CODE_DIR
  253. else:
  254. cmd = 'PYTHONPATH='
  255. python_path = os.environ.get('PYTHONPATH', None)
  256. if python_path is not None:
  257. cmd += '{0}:'.format(python_path)
  258. if sys.version_info[0] < 3:
  259. cmd += '{0} '.format(':'.join(sys.path[1:]))
  260. else:
  261. cmd += '{0} '.format(':'.join(sys.path[0:]))
  262. cmd += 'python{0}.{1} '.format(*sys.version_info)
  263. cmd += '{0} '.format(script_path)
  264. cmd += '{0} '.format(arg_str)
  265. tmp_file = tempfile.SpooledTemporaryFile()
  266. popen_kwargs = dict({
  267. 'shell': True,
  268. 'stdout': tmp_file,
  269. 'universal_newlines': True,
  270. }, **popen_kwargs)
  271. if catch_stderr is True:
  272. popen_kwargs['stderr'] = subprocess.PIPE
  273. if not sys.platform.lower().startswith('win'):
  274. popen_kwargs['close_fds'] = True
  275. def detach_from_parent_group():
  276. # detach from parent group (no more inherited signals!)
  277. os.setpgrp()
  278. popen_kwargs['preexec_fn'] = detach_from_parent_group
  279. def format_return(retcode, stdout, stderr=None, timed_out=False):
  280. '''
  281. DRY helper to log script result if it failed, and then return the
  282. desired output based on whether or not stderr was desired, and
  283. wither or not a retcode was desired.
  284. '''
  285. log_func = log.debug
  286. if timed_out:
  287. log.error(
  288. 'run_script timed out after %d seconds (process killed)',
  289. timeout
  290. )
  291. log_func = log.error
  292. if log_output is True \
  293. or timed_out \
  294. or (log_output is None and retcode != 0):
  295. log_func(
  296. 'run_script results for: %s %s\n'
  297. 'return code: %s\n'
  298. 'stdout:\n'
  299. '%s\n\n'
  300. 'stderr:\n'
  301. '%s',
  302. script, arg_str, retcode, stdout, stderr
  303. )
  304. stdout = stdout or ''
  305. stderr = stderr or ''
  306. if not raw:
  307. stdout = stdout.splitlines()
  308. stderr = stderr.splitlines()
  309. ret = [stdout]
  310. if catch_stderr:
  311. ret.append(stderr)
  312. if with_retcode:
  313. ret.append(retcode)
  314. if catch_timeout:
  315. ret.append(timed_out)
  316. return ret[0] if len(ret) == 1 else tuple(ret)
  317. process = subprocess.Popen(cmd, **popen_kwargs)
  318. if timeout is not None:
  319. stop_at = datetime.now() + timedelta(seconds=timeout)
  320. term_sent = False
  321. while True:
  322. process.poll()
  323. time.sleep(0.1)
  324. if datetime.now() <= stop_at:
  325. # We haven't reached the timeout yet
  326. if process.returncode is not None:
  327. break
  328. else:
  329. # We've reached the timeout
  330. if term_sent is False:
  331. # Kill the process group since sending the term signal
  332. # would only terminate the shell, not the command
  333. # executed in the shell
  334. if salt.utils.platform.is_windows():
  335. _, alive = win32_kill_process_tree(process.pid)
  336. if alive:
  337. log.error("Child processes still alive: %s", alive)
  338. else:
  339. os.killpg(os.getpgid(process.pid), signal.SIGINT)
  340. term_sent = True
  341. continue
  342. try:
  343. # As a last resort, kill the process group
  344. if salt.utils.platform.is_windows():
  345. _, alive = win32_kill_process_tree(process.pid)
  346. if alive:
  347. log.error("Child processes still alive: %s", alive)
  348. else:
  349. os.killpg(os.getpgid(process.pid), signal.SIGINT)
  350. except OSError as exc:
  351. if exc.errno != errno.ESRCH:
  352. # If errno is not "no such process", raise
  353. raise
  354. return format_return(
  355. process.returncode,
  356. *process.communicate(),
  357. timed_out=True
  358. )
  359. tmp_file.seek(0)
  360. if sys.version_info >= (3,):
  361. try:
  362. out = tmp_file.read().decode(__salt_system_encoding__)
  363. except (NameError, UnicodeDecodeError):
  364. # Let's cross our fingers and hope for the best
  365. out = tmp_file.read().decode('utf-8')
  366. else:
  367. out = tmp_file.read()
  368. if catch_stderr:
  369. if sys.version_info < (2, 7):
  370. # On python 2.6, the subprocess'es communicate() method uses
  371. # select which, is limited by the OS to 1024 file descriptors
  372. # We need more available descriptors to run the tests which
  373. # need the stderr output.
  374. # So instead of .communicate() we wait for the process to
  375. # finish, but, as the python docs state "This will deadlock
  376. # when using stdout=PIPE and/or stderr=PIPE and the child
  377. # process generates enough output to a pipe such that it
  378. # blocks waiting for the OS pipe buffer to accept more data.
  379. # Use communicate() to avoid that." <- a catch, catch situation
  380. #
  381. # Use this work around were it's needed only, python 2.6
  382. process.wait()
  383. err = process.stderr.read()
  384. else:
  385. _, err = process.communicate()
  386. # Force closing stderr/stdout to release file descriptors
  387. if process.stdout is not None:
  388. process.stdout.close()
  389. if process.stderr is not None:
  390. process.stderr.close()
  391. # pylint: disable=maybe-no-member
  392. try:
  393. return format_return(process.returncode, out, err or '')
  394. finally:
  395. try:
  396. if os.path.exists(tmp_file.name):
  397. if isinstance(tmp_file.name, six.string_types):
  398. # tmp_file.name is an int when using SpooledTemporaryFiles
  399. # int types cannot be used with os.remove() in Python 3
  400. os.remove(tmp_file.name)
  401. else:
  402. # Clean up file handles
  403. tmp_file.close()
  404. process.terminate()
  405. except OSError as err:
  406. # process already terminated
  407. pass
  408. # pylint: enable=maybe-no-member
  409. # TODO Remove this?
  410. process.communicate()
  411. if process.stdout is not None:
  412. process.stdout.close()
  413. try:
  414. return format_return(process.returncode, out)
  415. finally:
  416. try:
  417. if os.path.exists(tmp_file.name):
  418. if isinstance(tmp_file.name, six.string_types):
  419. # tmp_file.name is an int when using SpooledTemporaryFiles
  420. # int types cannot be used with os.remove() in Python 3
  421. os.remove(tmp_file.name)
  422. else:
  423. # Clean up file handles
  424. tmp_file.close()
  425. process.terminate()
  426. except OSError as err:
  427. # process already terminated
  428. pass
  429. class MultiMasterTestShellCase(ShellTestCase):
  430. '''
  431. Execute a test for a shell command when running multi-master tests
  432. '''
  433. @property
  434. def config_dir(self):
  435. return RUNTIME_VARS.TMP_MM_CONF_DIR
  436. class ShellCase(ShellTestCase, AdaptedConfigurationTestCaseMixin, ScriptPathMixin):
  437. '''
  438. Execute a test for a shell command
  439. '''
  440. _code_dir_ = CODE_DIR
  441. _script_dir_ = SCRIPT_DIR
  442. _python_executable_ = PYEXEC
  443. RUN_TIMEOUT = 500
  444. def chdir(self, dirname):
  445. try:
  446. os.chdir(dirname)
  447. except OSError:
  448. os.chdir(INTEGRATION_TEST_DIR)
  449. def run_salt(self, arg_str, with_retcode=False, catch_stderr=False, # pylint: disable=W0221
  450. timeout=RUN_TIMEOUT, popen_kwargs=None):
  451. '''
  452. Execute salt
  453. '''
  454. arg_str = '-c {0} -t {1} {2}'.format(self.config_dir, timeout, arg_str)
  455. ret = self.run_script('salt',
  456. arg_str,
  457. with_retcode=with_retcode,
  458. catch_stderr=catch_stderr,
  459. timeout=timeout,
  460. popen_kwargs=popen_kwargs)
  461. log.debug('Result of run_salt for command \'%s\': %s', arg_str, ret)
  462. return ret
  463. def run_spm(self, arg_str, with_retcode=False, catch_stderr=False, timeout=RUN_TIMEOUT): # pylint: disable=W0221
  464. '''
  465. Execute spm
  466. '''
  467. ret = self.run_script('spm',
  468. arg_str,
  469. with_retcode=with_retcode,
  470. catch_stderr=catch_stderr,
  471. timeout=timeout)
  472. log.debug('Result of run_spm for command \'%s\': %s', arg_str, ret)
  473. return ret
  474. def run_ssh(self, arg_str, with_retcode=False, catch_stderr=False, # pylint: disable=W0221
  475. timeout=RUN_TIMEOUT, wipe=True, raw=False):
  476. '''
  477. Execute salt-ssh
  478. '''
  479. arg_str = '{0} -ldebug{1} -c {2} -i --priv {3} --roster-file {4} --out=json localhost {5}'.format(
  480. ' -W' if wipe else '',
  481. ' -r' if raw else '',
  482. self.config_dir,
  483. os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'key_test'),
  484. os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'roster'),
  485. arg_str)
  486. ret = self.run_script('salt-ssh',
  487. arg_str,
  488. with_retcode=with_retcode,
  489. catch_stderr=catch_stderr,
  490. timeout=timeout,
  491. raw=True)
  492. log.debug('Result of run_ssh for command \'%s\': %s', arg_str, ret)
  493. return ret
  494. def run_run(self, arg_str, with_retcode=False, catch_stderr=False,
  495. asynchronous=False, timeout=RUN_TIMEOUT, config_dir=None, **kwargs):
  496. '''
  497. Execute salt-run
  498. '''
  499. asynchronous = kwargs.get('async', asynchronous)
  500. arg_str = '-c {0}{async_flag} -t {timeout} {1}'.format(config_dir or self.config_dir,
  501. arg_str,
  502. timeout=timeout,
  503. async_flag=' --async' if asynchronous else '')
  504. ret = self.run_script('salt-run',
  505. arg_str,
  506. with_retcode=with_retcode,
  507. catch_stderr=catch_stderr,
  508. timeout=timeout + 10)
  509. log.debug('Result of run_run for command \'%s\': %s', arg_str, ret)
  510. return ret
  511. def run_run_plus(self, fun, *arg, **kwargs):
  512. '''
  513. Execute the runner function and return the return data and output in a dict
  514. '''
  515. # Late import
  516. import salt.runner
  517. import salt.output
  518. ret = {'fun': fun}
  519. from_scratch = bool(kwargs.pop('__reload_config', False))
  520. # Have to create an empty dict and then update it, as the result from
  521. # self.get_config() is an ImmutableDict which cannot be updated.
  522. opts = {}
  523. opts.update(self.get_config('client_config', from_scratch=from_scratch))
  524. opts_arg = list(arg)
  525. if kwargs:
  526. opts_arg.append({'__kwarg__': True})
  527. opts_arg[-1].update(kwargs)
  528. opts.update({'doc': False, 'fun': fun, 'arg': opts_arg})
  529. with RedirectStdStreams():
  530. runner = salt.runner.Runner(opts)
  531. ret['return'] = runner.run()
  532. try:
  533. ret['jid'] = runner.jid
  534. except AttributeError:
  535. ret['jid'] = None
  536. # Compile output
  537. # TODO: Support outputters other than nested
  538. opts['color'] = False
  539. opts['output_file'] = cStringIO()
  540. try:
  541. salt.output.display_output(ret['return'], opts=opts)
  542. ret['out'] = opts['output_file'].getvalue().splitlines()
  543. finally:
  544. opts['output_file'].close()
  545. log.debug('Result of run_run_plus for fun \'%s\' with arg \'%s\': %s',
  546. fun, opts_arg, ret)
  547. return ret
  548. def run_key(self, arg_str, catch_stderr=False, with_retcode=False, # pylint: disable=W0221
  549. timeout=RUN_TIMEOUT):
  550. '''
  551. Execute salt-key
  552. '''
  553. arg_str = '-c {0} {1}'.format(self.config_dir, arg_str)
  554. ret = self.run_script('salt-key',
  555. arg_str,
  556. catch_stderr=catch_stderr,
  557. with_retcode=with_retcode,
  558. timeout=timeout)
  559. log.debug('Result of run_key for command \'%s\': %s', arg_str, ret)
  560. return ret
  561. def run_cp(self, arg_str, with_retcode=False, catch_stderr=False, # pylint: disable=W0221
  562. timeout=RUN_TIMEOUT):
  563. '''
  564. Execute salt-cp
  565. '''
  566. # Note: not logging result of run_cp because it will log a bunch of
  567. # bytes which will not be very helpful.
  568. arg_str = '--config-dir {0} {1}'.format(self.config_dir, arg_str)
  569. return self.run_script('salt-cp',
  570. arg_str,
  571. with_retcode=with_retcode,
  572. catch_stderr=catch_stderr,
  573. timeout=timeout)
  574. def run_call(self, arg_str, with_retcode=False, catch_stderr=False, # pylint: disable=W0221
  575. local=False, timeout=RUN_TIMEOUT):
  576. '''
  577. Execute salt-call.
  578. '''
  579. arg_str = '{0} --config-dir {1} {2}'.format('--local' if local else '',
  580. self.config_dir, arg_str)
  581. ret = self.run_script('salt-call',
  582. arg_str,
  583. with_retcode=with_retcode,
  584. catch_stderr=catch_stderr,
  585. timeout=timeout)
  586. log.debug('Result of run_call for command \'%s\': %s', arg_str, ret)
  587. return ret
  588. def run_cloud(self, arg_str, catch_stderr=False, timeout=RUN_TIMEOUT):
  589. '''
  590. Execute salt-cloud
  591. '''
  592. arg_str = '-c {0} {1}'.format(self.config_dir, arg_str)
  593. ret = self.run_script('salt-cloud',
  594. arg_str,
  595. catch_stderr,
  596. timeout=timeout)
  597. log.debug('Result of run_cloud for command \'%s\': %s', arg_str, ret)
  598. return ret
  599. class SPMTestUserInterface(object):
  600. '''
  601. Test user interface to SPMClient
  602. '''
  603. def __init__(self):
  604. self._status = []
  605. self._confirm = []
  606. self._error = []
  607. def status(self, msg):
  608. self._status.append(msg)
  609. def confirm(self, action):
  610. self._confirm.append(action)
  611. def error(self, msg):
  612. self._error.append(msg)
  613. class SPMCase(TestCase, AdaptedConfigurationTestCaseMixin):
  614. '''
  615. Class for handling spm commands
  616. '''
  617. def _spm_build_files(self, config):
  618. self.formula_dir = os.path.join(' '.join(config['file_roots']['base']), 'formulas')
  619. self.formula_sls_dir = os.path.join(self.formula_dir, 'apache')
  620. self.formula_sls = os.path.join(self.formula_sls_dir, 'apache.sls')
  621. self.formula_file = os.path.join(self.formula_dir, 'FORMULA')
  622. dirs = [self.formula_dir, self.formula_sls_dir]
  623. for f_dir in dirs:
  624. os.makedirs(f_dir)
  625. # Late import
  626. import salt.utils.files
  627. with salt.utils.files.fopen(self.formula_sls, 'w') as fp:
  628. fp.write(textwrap.dedent('''\
  629. install-apache:
  630. pkg.installed:
  631. - name: apache2
  632. '''))
  633. with salt.utils.files.fopen(self.formula_file, 'w') as fp:
  634. fp.write(textwrap.dedent('''\
  635. name: apache
  636. os: RedHat, Debian, Ubuntu, Suse, FreeBSD
  637. os_family: RedHat, Debian, Suse, FreeBSD
  638. version: 201506
  639. release: 2
  640. summary: Formula for installing Apache
  641. description: Formula for installing Apache
  642. '''))
  643. def _spm_config(self, assume_yes=True):
  644. self._tmp_spm = tempfile.mkdtemp()
  645. config = self.get_temp_config('minion', **{
  646. 'spm_logfile': os.path.join(self._tmp_spm, 'log'),
  647. 'spm_repos_config': os.path.join(self._tmp_spm, 'etc', 'spm.repos'),
  648. 'spm_cache_dir': os.path.join(self._tmp_spm, 'cache'),
  649. 'spm_build_dir': os.path.join(self._tmp_spm, 'build'),
  650. 'spm_build_exclude': ['apache/.git'],
  651. 'spm_db_provider': 'sqlite3',
  652. 'spm_files_provider': 'local',
  653. 'spm_db': os.path.join(self._tmp_spm, 'packages.db'),
  654. 'extension_modules': os.path.join(self._tmp_spm, 'modules'),
  655. 'file_roots': {'base': [self._tmp_spm, ]},
  656. 'formula_path': os.path.join(self._tmp_spm, 'salt'),
  657. 'pillar_path': os.path.join(self._tmp_spm, 'pillar'),
  658. 'reactor_path': os.path.join(self._tmp_spm, 'reactor'),
  659. 'assume_yes': True if assume_yes else False,
  660. 'force': False,
  661. 'verbose': False,
  662. 'cache': 'localfs',
  663. 'cachedir': os.path.join(self._tmp_spm, 'cache'),
  664. 'spm_repo_dups': 'ignore',
  665. 'spm_share_dir': os.path.join(self._tmp_spm, 'share'),
  666. })
  667. import salt.utils.files
  668. import salt.utils.yaml
  669. if not os.path.isdir(config['formula_path']):
  670. os.makedirs(config['formula_path'])
  671. with salt.utils.files.fopen(os.path.join(self._tmp_spm, 'spm'), 'w') as fp:
  672. salt.utils.yaml.safe_dump(config, fp)
  673. return config
  674. def _spm_create_update_repo(self, config):
  675. build_spm = self.run_spm('build', self.config, self.formula_dir)
  676. c_repo = self.run_spm('create_repo', self.config,
  677. self.config['spm_build_dir'])
  678. repo_conf_dir = self.config['spm_repos_config'] + '.d'
  679. os.makedirs(repo_conf_dir)
  680. # Late import
  681. import salt.utils.files
  682. with salt.utils.files.fopen(os.path.join(repo_conf_dir, 'spm.repo'), 'w') as fp:
  683. fp.write(textwrap.dedent('''\
  684. local_repo:
  685. url: file://{0}
  686. '''.format(self.config['spm_build_dir'])))
  687. u_repo = self.run_spm('update_repo', self.config)
  688. def _spm_client(self, config):
  689. import salt.spm
  690. self.ui = SPMTestUserInterface()
  691. client = salt.spm.SPMClient(self.ui, config)
  692. return client
  693. def run_spm(self, cmd, config, arg=None):
  694. client = self._spm_client(config)
  695. spm_cmd = client.run([cmd, arg])
  696. client._close()
  697. return self.ui._status
  698. class ModuleCase(TestCase, SaltClientTestCaseMixin):
  699. '''
  700. Execute a module function
  701. '''
  702. RUN_FUNCTION_TIMEOUT = 300
  703. def wait_for_all_jobs(self, minions=('minion', 'sub_minion',), sleep=.3):
  704. '''
  705. Wait for all jobs currently running on the list of minions to finish
  706. '''
  707. for minion in minions:
  708. while True:
  709. ret = self.run_function('saltutil.running', minion_tgt=minion, timeout=300)
  710. if ret:
  711. log.debug('Waiting for minion\'s jobs: %s', minion)
  712. time.sleep(sleep)
  713. else:
  714. break
  715. def minion_run(self, _function, *args, **kw):
  716. '''
  717. Run a single salt function on the 'minion' target and condition
  718. the return down to match the behavior of the raw function call
  719. '''
  720. return self.run_function(_function, args, **kw)
  721. def run_function(self, function, arg=(), minion_tgt='minion', timeout=None, master_tgt=None, **kwargs):
  722. '''
  723. Run a single salt function and condition the return down to match the
  724. behavior of the raw function call
  725. '''
  726. if timeout is None:
  727. timeout = self.RUN_FUNCTION_TIMEOUT
  728. known_to_return_none = (
  729. 'data.get',
  730. 'file.chown',
  731. 'file.chgrp',
  732. 'pkg.refresh_db',
  733. 'ssh.recv_known_host_entries',
  734. 'time.sleep'
  735. )
  736. if minion_tgt == 'sub_minion':
  737. known_to_return_none += ('mine.update',)
  738. if 'f_arg' in kwargs:
  739. kwargs['arg'] = kwargs.pop('f_arg')
  740. if 'f_timeout' in kwargs:
  741. kwargs['timeout'] = kwargs.pop('f_timeout')
  742. client = self.client if master_tgt is None else self.clients[master_tgt]
  743. orig = client.cmd(minion_tgt,
  744. function,
  745. arg,
  746. timeout=timeout,
  747. kwarg=kwargs)
  748. if minion_tgt not in orig:
  749. self.skipTest(
  750. 'WARNING(SHOULD NOT HAPPEN #1935): Failed to get a reply '
  751. 'from the minion \'{0}\'. Command output: {1}'.format(
  752. minion_tgt, orig
  753. )
  754. )
  755. elif orig[minion_tgt] is None and function not in known_to_return_none:
  756. self.skipTest(
  757. 'WARNING(SHOULD NOT HAPPEN #1935): Failed to get \'{0}\' from '
  758. 'the minion \'{1}\'. Command output: {2}'.format(
  759. function, minion_tgt, orig
  760. )
  761. )
  762. # Try to match stalled state functions
  763. orig[minion_tgt] = self._check_state_return(orig[minion_tgt])
  764. return orig[minion_tgt]
  765. def run_function_all_masters(self, function, arg=(), minion_tgt='minion', timeout=300, **kwargs):
  766. '''
  767. Run a single salt function from all the masters in multimaster environment
  768. and condition the return down to match the behavior of the raw function call
  769. '''
  770. ret = []
  771. for master in range(len(self.clients)):
  772. ret.append(self.run_function(function, arg, minion_tgt, timeout, master_tgt=master, **kwargs))
  773. return ret
  774. def run_state(self, function, **kwargs):
  775. '''
  776. Run the state.single command and return the state return structure
  777. '''
  778. ret = self.run_function('state.single', [function], **kwargs)
  779. return self._check_state_return(ret)
  780. def _check_state_return(self, ret):
  781. if isinstance(ret, dict):
  782. # This is the supposed return format for state calls
  783. return ret
  784. if isinstance(ret, list):
  785. jids = []
  786. # These are usually errors
  787. for item in ret[:]:
  788. if not isinstance(item, six.string_types):
  789. # We don't know how to handle this
  790. continue
  791. match = STATE_FUNCTION_RUNNING_RE.match(item)
  792. if not match:
  793. # We don't know how to handle this
  794. continue
  795. jid = match.group('jid')
  796. if jid in jids:
  797. continue
  798. jids.append(jid)
  799. job_data = self.run_function('saltutil.find_job', [jid])
  800. job_kill = self.run_function('saltutil.kill_job', [jid])
  801. msg = (
  802. 'A running state.single was found causing a state lock. '
  803. 'Job details: \'{0}\' Killing Job Returned: \'{1}\''.format(
  804. job_data, job_kill
  805. )
  806. )
  807. ret.append('[TEST SUITE ENFORCED]{0}'
  808. '[/TEST SUITE ENFORCED]'.format(msg))
  809. return ret
  810. class MultimasterModuleCase(ModuleCase, SaltMultimasterClientTestCaseMixin):
  811. '''
  812. Execute a module function
  813. '''
  814. def run_function(self, function, arg=(), minion_tgt='minion', timeout=300, master_tgt=0, **kwargs):
  815. '''
  816. Run a single salt function and condition the return down to match the
  817. behavior of the raw function call
  818. '''
  819. known_to_return_none = (
  820. 'data.get',
  821. 'file.chown',
  822. 'file.chgrp',
  823. 'pkg.refresh_db',
  824. 'ssh.recv_known_host_entries',
  825. 'time.sleep'
  826. )
  827. if minion_tgt == 'sub_minion':
  828. known_to_return_none += ('mine.update',)
  829. if 'f_arg' in kwargs:
  830. kwargs['arg'] = kwargs.pop('f_arg')
  831. if 'f_timeout' in kwargs:
  832. kwargs['timeout'] = kwargs.pop('f_timeout')
  833. orig = self.clients[master_tgt].cmd(minion_tgt,
  834. function,
  835. arg,
  836. timeout=timeout,
  837. kwarg=kwargs)
  838. if minion_tgt not in orig:
  839. self.skipTest(
  840. 'WARNING(SHOULD NOT HAPPEN #1935): Failed to get a reply '
  841. 'from the minion \'{0}\'. Command output: {1}'.format(
  842. minion_tgt, orig
  843. )
  844. )
  845. elif orig[minion_tgt] is None and function not in known_to_return_none:
  846. self.skipTest(
  847. 'WARNING(SHOULD NOT HAPPEN #1935): Failed to get \'{0}\' from '
  848. 'the minion \'{1}\'. Command output: {2}'.format(
  849. function, minion_tgt, orig
  850. )
  851. )
  852. # Try to match stalled state functions
  853. orig[minion_tgt] = self._check_state_return(orig[minion_tgt])
  854. return orig[minion_tgt]
  855. def run_function_all_masters(self, function, arg=(), minion_tgt='minion', timeout=300, **kwargs):
  856. '''
  857. Run a single salt function from all the masters in multimaster environment
  858. and condition the return down to match the behavior of the raw function call
  859. '''
  860. ret = []
  861. for master in range(len(self.clients)):
  862. ret.append(self.run_function(function, arg, minion_tgt, timeout, master_tgt=master, **kwargs))
  863. return ret
  864. class SyndicCase(TestCase, SaltClientTestCaseMixin):
  865. '''
  866. Execute a syndic based execution test
  867. '''
  868. _salt_client_config_file_name_ = 'syndic_master'
  869. def run_function(self, function, arg=(), timeout=90):
  870. '''
  871. Run a single salt function and condition the return down to match the
  872. behavior of the raw function call
  873. '''
  874. orig = self.client.cmd('minion', function, arg, timeout=timeout)
  875. if 'minion' not in orig:
  876. self.skipTest(
  877. 'WARNING(SHOULD NOT HAPPEN #1935): Failed to get a reply '
  878. 'from the minion. Command output: {0}'.format(orig)
  879. )
  880. return orig['minion']
  881. @requires_sshd_server
  882. class SSHCase(ShellCase):
  883. '''
  884. Execute a command via salt-ssh
  885. '''
  886. def _arg_str(self, function, arg):
  887. return '{0} {1}'.format(function, ' '.join(arg))
  888. def run_function(self, function, arg=(), timeout=180, wipe=True, raw=False, **kwargs):
  889. '''
  890. We use a 180s timeout here, which some slower systems do end up needing
  891. '''
  892. ret = self.run_ssh(self._arg_str(function, arg), timeout=timeout,
  893. wipe=wipe, raw=raw)
  894. log.debug('SSHCase run_function executed %s with arg %s', function, arg)
  895. log.debug('SSHCase JSON return: %s', ret)
  896. # Late import
  897. import salt.utils.json
  898. try:
  899. return salt.utils.json.loads(ret)['localhost']
  900. except Exception:
  901. return ret
  902. class ClientCase(AdaptedConfigurationTestCaseMixin, TestCase):
  903. '''
  904. A base class containing relevant options for starting the various Salt
  905. Python API entrypoints
  906. '''
  907. def get_opts(self):
  908. # Late import
  909. import salt.config
  910. return salt.config.client_config(self.get_config_file_path('master'))
  911. def mkdir_p(self, path):
  912. try:
  913. os.makedirs(path)
  914. except OSError as exc: # Python >2.5
  915. if exc.errno == errno.EEXIST and os.path.isdir(path):
  916. pass
  917. else:
  918. raise
  919. # ----- Backwards Compatible Imports -------------------------------------------------------------------------------->
  920. from tests.support.mixins import ShellCaseCommonTestsMixin # pylint: disable=unused-import
  921. # <---- Backwards Compatible Imports ---------------------------------------------------------------------------------