1
0

case.py 35 KB

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