case.py 34 KB

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