1
0

case.py 36 KB

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