helpers.py 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :copyright: Copyright 2013-2017 by the SaltStack Team, see AUTHORS for more details.
  4. :license: Apache 2.0, see LICENSE for more details.
  5. tests.support.helpers
  6. ~~~~~~~~~~~~~~~~~~~~~
  7. Test support helpers
  8. '''
  9. # pylint: disable=repr-flag-used-in-string,wrong-import-order
  10. # Import Python libs
  11. from __future__ import absolute_import, print_function, unicode_literals
  12. import base64
  13. import errno
  14. import fnmatch
  15. import functools
  16. import inspect
  17. import logging
  18. import os
  19. import random
  20. import shutil
  21. import socket
  22. import string
  23. import subprocess
  24. import sys
  25. import tempfile
  26. import textwrap
  27. import threading
  28. import time
  29. import tornado.ioloop
  30. import tornado.web
  31. import types
  32. # Import 3rd-party libs
  33. from salt.ext import six
  34. from salt.ext.six.moves import range, builtins # pylint: disable=import-error,redefined-builtin
  35. from pytestsalt.utils import get_unused_localhost_port
  36. # Import Salt Tests Support libs
  37. from tests.support.unit import SkipTest
  38. from tests.support.mock import patch
  39. from tests.support.runtests import RUNTIME_VARS
  40. # Import Salt libs
  41. import salt.utils.files
  42. import salt.utils.platform
  43. import salt.utils.stringutils
  44. log = logging.getLogger(__name__)
  45. HAS_SYMLINKS = None
  46. def no_symlinks():
  47. '''
  48. Check if git is installed and has symlinks enabled in the configuration.
  49. '''
  50. global HAS_SYMLINKS
  51. if HAS_SYMLINKS is not None:
  52. return not HAS_SYMLINKS
  53. output = ''
  54. try:
  55. output = subprocess.Popen(
  56. ['git', 'config', '--get', 'core.symlinks'],
  57. cwd=RUNTIME_VARS.TMP,
  58. stdout=subprocess.PIPE).communicate()[0]
  59. except OSError as exc:
  60. if exc.errno != errno.ENOENT:
  61. raise
  62. except subprocess.CalledProcessError:
  63. # git returned non-zero status
  64. pass
  65. HAS_SYMLINKS = False
  66. if output.strip() == 'true':
  67. HAS_SYMLINKS = True
  68. return not HAS_SYMLINKS
  69. def flaky(caller=None, condition=True, attempts=4):
  70. '''
  71. Mark a test as flaky. The test will attempt to run five times,
  72. looking for a successful run. After an immediate second try,
  73. it will use an exponential backoff starting with one second.
  74. .. code-block:: python
  75. class MyTestCase(TestCase):
  76. @flaky
  77. def test_sometimes_works(self):
  78. pass
  79. '''
  80. if caller is None:
  81. return functools.partial(flaky, condition=condition, attempts=attempts)
  82. if isinstance(condition, bool) and condition is False:
  83. # Don't even decorate
  84. return caller
  85. elif callable(condition):
  86. if condition() is False:
  87. # Don't even decorate
  88. return caller
  89. if inspect.isclass(caller):
  90. attrs = [n for n in dir(caller) if n.startswith('test_')]
  91. for attrname in attrs:
  92. try:
  93. function = getattr(caller, attrname)
  94. if not inspect.isfunction(function) and not inspect.ismethod(function):
  95. continue
  96. setattr(caller, attrname, flaky(caller=function, condition=condition, attempts=attempts))
  97. except Exception as exc:
  98. log.exception(exc)
  99. continue
  100. return caller
  101. @functools.wraps(caller)
  102. def wrap(cls):
  103. for attempt in range(0, attempts):
  104. try:
  105. if attempt > 0:
  106. # Run through setUp again
  107. # We only run it after the first iteration(>0) because the regular
  108. # test runner will have already ran setUp the first time
  109. setup = getattr(cls, 'setUp', None)
  110. if callable(setup):
  111. setup()
  112. return caller(cls)
  113. except SkipTest as exc:
  114. cls.skipTest(exc.args[0])
  115. except Exception as exc:
  116. exc_info = sys.exc_info()
  117. if isinstance(exc, SkipTest):
  118. six.reraise(*exc_info)
  119. if not isinstance(exc, AssertionError) and log.isEnabledFor(logging.DEBUG):
  120. log.exception(exc, exc_info=exc_info)
  121. if attempt >= attempts -1:
  122. # We won't try to run tearDown once the attempts are exhausted
  123. # because the regular test runner will do that for us
  124. six.reraise(*exc_info)
  125. # Run through tearDown again
  126. teardown = getattr(cls, 'tearDown', None)
  127. if callable(teardown):
  128. teardown()
  129. backoff_time = attempt ** 2
  130. log.info(
  131. 'Found Exception. Waiting %s seconds to retry.',
  132. backoff_time
  133. )
  134. time.sleep(backoff_time)
  135. return cls
  136. return wrap
  137. def requires_sshd_server(caller):
  138. '''
  139. Mark a test as requiring the tests SSH daemon running.
  140. .. code-block:: python
  141. class MyTestCase(TestCase):
  142. @requiresSshdServer
  143. def test_create_user(self):
  144. pass
  145. '''
  146. if inspect.isclass(caller):
  147. # We're decorating a class
  148. old_setup = getattr(caller, 'setUp', None)
  149. def setUp(self, *args, **kwargs):
  150. if os.environ.get('SSH_DAEMON_RUNNING', 'False').lower() == 'false':
  151. self.skipTest('SSH tests are disabled')
  152. if old_setup is not None:
  153. old_setup(self, *args, **kwargs)
  154. caller.setUp = setUp
  155. return caller
  156. # We're simply decorating functions
  157. @functools.wraps(caller)
  158. def wrap(cls):
  159. if os.environ.get('SSH_DAEMON_RUNNING', 'False').lower() == 'false':
  160. cls.skipTest('SSH tests are disabled')
  161. return caller(cls)
  162. return wrap
  163. class RedirectStdStreams(object):
  164. '''
  165. Temporarily redirect system output to file like objects.
  166. Default is to redirect to `os.devnull`, which just mutes output, `stdout`
  167. and `stderr`.
  168. '''
  169. def __init__(self, stdout=None, stderr=None):
  170. # Late import
  171. import salt.utils.files
  172. if stdout is None:
  173. stdout = salt.utils.files.fopen(os.devnull, 'w') # pylint: disable=resource-leakage
  174. if stderr is None:
  175. stderr = salt.utils.files.fopen(os.devnull, 'w') # pylint: disable=resource-leakage
  176. self.__stdout = stdout
  177. self.__stderr = stderr
  178. self.__redirected = False
  179. self.patcher = patch.multiple(sys, stderr=self.__stderr, stdout=self.__stdout)
  180. def __enter__(self):
  181. self.redirect()
  182. return self
  183. def __exit__(self, exc_type, exc_value, traceback):
  184. self.unredirect()
  185. def redirect(self):
  186. self.old_stdout = sys.stdout
  187. self.old_stdout.flush()
  188. self.old_stderr = sys.stderr
  189. self.old_stderr.flush()
  190. self.patcher.start()
  191. self.__redirected = True
  192. def unredirect(self):
  193. if not self.__redirected:
  194. return
  195. try:
  196. self.__stdout.flush()
  197. self.__stdout.close()
  198. except ValueError:
  199. # already closed?
  200. pass
  201. try:
  202. self.__stderr.flush()
  203. self.__stderr.close()
  204. except ValueError:
  205. # already closed?
  206. pass
  207. self.patcher.stop()
  208. def flush(self):
  209. if self.__redirected:
  210. try:
  211. self.__stdout.flush()
  212. except Exception:
  213. pass
  214. try:
  215. self.__stderr.flush()
  216. except Exception:
  217. pass
  218. class TstSuiteLoggingHandler(object):
  219. '''
  220. Simple logging handler which can be used to test if certain logging
  221. messages get emitted or not:
  222. .. code-block:: python
  223. with TstSuiteLoggingHandler() as handler:
  224. # (...) Do what ever you wish here
  225. handler.messages # here are the emitted log messages
  226. '''
  227. def __init__(self, level=0, format='%(levelname)s:%(message)s'):
  228. self.level = level
  229. self.format = format
  230. self.activated = False
  231. self.prev_logging_level = None
  232. def activate(self):
  233. class Handler(logging.Handler):
  234. def __init__(self, level):
  235. logging.Handler.__init__(self, level)
  236. self.messages = []
  237. def emit(self, record):
  238. self.messages.append(self.format(record))
  239. self.handler = Handler(self.level)
  240. formatter = logging.Formatter(self.format)
  241. self.handler.setFormatter(formatter)
  242. logging.root.addHandler(self.handler)
  243. self.activated = True
  244. # Make sure we're running with the lowest logging level with our
  245. # tests logging handler
  246. current_logging_level = logging.root.getEffectiveLevel()
  247. if current_logging_level > logging.DEBUG:
  248. self.prev_logging_level = current_logging_level
  249. logging.root.setLevel(0)
  250. def deactivate(self):
  251. if not self.activated:
  252. return
  253. logging.root.removeHandler(self.handler)
  254. # Restore previous logging level if changed
  255. if self.prev_logging_level is not None:
  256. logging.root.setLevel(self.prev_logging_level)
  257. @property
  258. def messages(self):
  259. if not self.activated:
  260. return []
  261. return self.handler.messages
  262. def clear(self):
  263. self.handler.messages = []
  264. def __enter__(self):
  265. self.activate()
  266. return self
  267. def __exit__(self, type, value, traceback):
  268. self.deactivate()
  269. self.activated = False
  270. # Mimic some handler attributes and methods
  271. @property
  272. def lock(self):
  273. if self.activated:
  274. return self.handler.lock
  275. def createLock(self):
  276. if self.activated:
  277. return self.handler.createLock()
  278. def acquire(self):
  279. if self.activated:
  280. return self.handler.acquire()
  281. def release(self):
  282. if self.activated:
  283. return self.handler.release()
  284. class ForceImportErrorOn(object):
  285. '''
  286. This class is meant to be used in mock'ed test cases which require an
  287. ``ImportError`` to be raised.
  288. >>> import os.path
  289. >>> with ForceImportErrorOn('os.path'):
  290. ... import os.path
  291. ...
  292. Traceback (most recent call last):
  293. File "<stdin>", line 2, in <module>
  294. File "salttesting/helpers.py", line 263, in __import__
  295. 'Forced ImportError raised for {0!r}'.format(name)
  296. ImportError: Forced ImportError raised for 'os.path'
  297. >>>
  298. >>> with ForceImportErrorOn(('os', 'path')):
  299. ... import os.path
  300. ... sys.modules.pop('os', None)
  301. ... from os import path
  302. ...
  303. <module 'os' from '/usr/lib/python2.7/os.pyc'>
  304. Traceback (most recent call last):
  305. File "<stdin>", line 4, in <module>
  306. File "salttesting/helpers.py", line 288, in __fake_import__
  307. name, ', '.join(fromlist)
  308. ImportError: Forced ImportError raised for 'from os import path'
  309. >>>
  310. >>> with ForceImportErrorOn(('os', 'path'), 'os.path'):
  311. ... import os.path
  312. ... sys.modules.pop('os', None)
  313. ... from os import path
  314. ...
  315. Traceback (most recent call last):
  316. File "<stdin>", line 2, in <module>
  317. File "salttesting/helpers.py", line 281, in __fake_import__
  318. 'Forced ImportError raised for {0!r}'.format(name)
  319. ImportError: Forced ImportError raised for 'os.path'
  320. >>>
  321. '''
  322. def __init__(self, *module_names):
  323. self.__module_names = {}
  324. for entry in module_names:
  325. if isinstance(entry, (list, tuple)):
  326. modname = entry[0]
  327. self.__module_names[modname] = set(entry[1:])
  328. else:
  329. self.__module_names[entry] = None
  330. self.__original_import = builtins.__import__
  331. self.patcher = patch.object(builtins, '__import__', self.__fake_import__)
  332. def patch_import_function(self):
  333. self.patcher.start()
  334. def restore_import_funtion(self):
  335. self.patcher.stop()
  336. def __fake_import__(self,
  337. name,
  338. globals_={} if six.PY2 else None,
  339. locals_={} if six.PY2 else None,
  340. fromlist=[] if six.PY2 else (),
  341. level=-1 if six.PY2 else 0):
  342. if name in self.__module_names:
  343. importerror_fromlist = self.__module_names.get(name)
  344. if importerror_fromlist is None:
  345. raise ImportError(
  346. 'Forced ImportError raised for {0!r}'.format(name)
  347. )
  348. if importerror_fromlist.intersection(set(fromlist)):
  349. raise ImportError(
  350. 'Forced ImportError raised for {0!r}'.format(
  351. 'from {0} import {1}'.format(
  352. name, ', '.join(fromlist)
  353. )
  354. )
  355. )
  356. return self.__original_import(name, globals_, locals_, fromlist, level)
  357. def __enter__(self):
  358. self.patch_import_function()
  359. return self
  360. def __exit__(self, exc_type, exc_value, traceback):
  361. self.restore_import_funtion()
  362. class MockWraps(object):
  363. '''
  364. Helper class to be used with the mock library.
  365. To be used in the ``wraps`` keyword of ``Mock`` or ``MagicMock`` where you
  366. want to trigger a side effect for X times, and afterwards, call the
  367. original and un-mocked method.
  368. As an example:
  369. >>> def original():
  370. ... print 'original'
  371. ...
  372. >>> def side_effect():
  373. ... print 'side effect'
  374. ...
  375. >>> mw = MockWraps(original, 2, side_effect)
  376. >>> mw()
  377. side effect
  378. >>> mw()
  379. side effect
  380. >>> mw()
  381. original
  382. >>>
  383. '''
  384. def __init__(self, original, expected_failures, side_effect):
  385. self.__original = original
  386. self.__expected_failures = expected_failures
  387. self.__side_effect = side_effect
  388. self.__call_counter = 0
  389. def __call__(self, *args, **kwargs):
  390. try:
  391. if self.__call_counter < self.__expected_failures:
  392. if isinstance(self.__side_effect, types.FunctionType):
  393. return self.__side_effect()
  394. raise self.__side_effect
  395. return self.__original(*args, **kwargs)
  396. finally:
  397. self.__call_counter += 1
  398. def with_system_user(username, on_existing='delete', delete=True, password=None, groups=None):
  399. '''
  400. Create and optionally destroy a system user to be used within a test
  401. case. The system user is created using the ``user`` salt module.
  402. The decorated testcase function must accept 'username' as an argument.
  403. :param username: The desired username for the system user.
  404. :param on_existing: What to do when the desired username is taken. The
  405. available options are:
  406. * nothing: Do nothing, act as if the user was created.
  407. * delete: delete and re-create the existing user
  408. * skip: skip the test case
  409. '''
  410. if on_existing not in ('nothing', 'delete', 'skip'):
  411. raise RuntimeError(
  412. 'The value of \'on_existing\' can only be one of, '
  413. '\'nothing\', \'delete\' and \'skip\''
  414. )
  415. if not isinstance(delete, bool):
  416. raise RuntimeError(
  417. 'The value of \'delete\' can only be \'True\' or \'False\''
  418. )
  419. def decorator(func):
  420. @functools.wraps(func)
  421. def wrap(cls):
  422. # Let's add the user to the system.
  423. log.debug('Creating system user {0!r}'.format(username))
  424. kwargs = {'timeout': 60, 'groups': groups}
  425. if salt.utils.platform.is_windows():
  426. kwargs.update({'password': password})
  427. create_user = cls.run_function('user.add', [username], **kwargs)
  428. if not create_user:
  429. log.debug('Failed to create system user')
  430. # The user was not created
  431. if on_existing == 'skip':
  432. cls.skipTest(
  433. 'Failed to create system user {0!r}'.format(
  434. username
  435. )
  436. )
  437. if on_existing == 'delete':
  438. log.debug(
  439. 'Deleting the system user {0!r}'.format(
  440. username
  441. )
  442. )
  443. delete_user = cls.run_function(
  444. 'user.delete', [username, True, True]
  445. )
  446. if not delete_user:
  447. cls.skipTest(
  448. 'A user named {0!r} already existed on the '
  449. 'system and re-creating it was not possible'
  450. .format(username)
  451. )
  452. log.debug(
  453. 'Second time creating system user {0!r}'.format(
  454. username
  455. )
  456. )
  457. create_user = cls.run_function('user.add', [username], **kwargs)
  458. if not create_user:
  459. cls.skipTest(
  460. 'A user named {0!r} already existed, was deleted '
  461. 'as requested, but re-creating it was not possible'
  462. .format(username)
  463. )
  464. failure = None
  465. try:
  466. try:
  467. return func(cls, username)
  468. except Exception as exc: # pylint: disable=W0703
  469. log.error(
  470. 'Running {0!r} raised an exception: {1}'.format(
  471. func, exc
  472. ),
  473. exc_info=True
  474. )
  475. # Store the original exception details which will be raised
  476. # a little further down the code
  477. failure = sys.exc_info()
  478. finally:
  479. if delete:
  480. delete_user = cls.run_function(
  481. 'user.delete', [username, True, True], timeout=60
  482. )
  483. if not delete_user:
  484. if failure is None:
  485. log.warning(
  486. 'Although the actual test-case did not fail, '
  487. 'deleting the created system user {0!r} '
  488. 'afterwards did.'.format(username)
  489. )
  490. else:
  491. log.warning(
  492. 'The test-case failed and also did the removal'
  493. ' of the system user {0!r}'.format(username)
  494. )
  495. if failure is not None:
  496. # If an exception was thrown, raise it
  497. six.reraise(failure[0], failure[1], failure[2])
  498. return wrap
  499. return decorator
  500. def with_system_group(group, on_existing='delete', delete=True):
  501. '''
  502. Create and optionally destroy a system group to be used within a test
  503. case. The system user is crated using the ``group`` salt module.
  504. The decorated testcase function must accept 'group' as an argument.
  505. :param group: The desired group name for the system user.
  506. :param on_existing: What to do when the desired username is taken. The
  507. available options are:
  508. * nothing: Do nothing, act as if the group was created
  509. * delete: delete and re-create the existing user
  510. * skip: skip the test case
  511. '''
  512. if on_existing not in ('nothing', 'delete', 'skip'):
  513. raise RuntimeError(
  514. 'The value of \'on_existing\' can only be one of, '
  515. '\'nothing\', \'delete\' and \'skip\''
  516. )
  517. if not isinstance(delete, bool):
  518. raise RuntimeError(
  519. 'The value of \'delete\' can only be \'True\' or \'False\''
  520. )
  521. def decorator(func):
  522. @functools.wraps(func)
  523. def wrap(cls):
  524. # Let's add the user to the system.
  525. log.debug('Creating system group {0!r}'.format(group))
  526. create_group = cls.run_function('group.add', [group])
  527. if not create_group:
  528. log.debug('Failed to create system group')
  529. # The group was not created
  530. if on_existing == 'skip':
  531. cls.skipTest(
  532. 'Failed to create system group {0!r}'.format(group)
  533. )
  534. if on_existing == 'delete':
  535. log.debug(
  536. 'Deleting the system group {0!r}'.format(group)
  537. )
  538. delete_group = cls.run_function('group.delete', [group])
  539. if not delete_group:
  540. cls.skipTest(
  541. 'A group named {0!r} already existed on the '
  542. 'system and re-creating it was not possible'
  543. .format(group)
  544. )
  545. log.debug(
  546. 'Second time creating system group {0!r}'.format(
  547. group
  548. )
  549. )
  550. create_group = cls.run_function('group.add', [group])
  551. if not create_group:
  552. cls.skipTest(
  553. 'A group named {0!r} already existed, was deleted '
  554. 'as requested, but re-creating it was not possible'
  555. .format(group)
  556. )
  557. failure = None
  558. try:
  559. try:
  560. return func(cls, group)
  561. except Exception as exc: # pylint: disable=W0703
  562. log.error(
  563. 'Running {0!r} raised an exception: {1}'.format(
  564. func, exc
  565. ),
  566. exc_info=True
  567. )
  568. # Store the original exception details which will be raised
  569. # a little further down the code
  570. failure = sys.exc_info()
  571. finally:
  572. if delete:
  573. delete_group = cls.run_function('group.delete', [group])
  574. if not delete_group:
  575. if failure is None:
  576. log.warning(
  577. 'Although the actual test-case did not fail, '
  578. 'deleting the created system group {0!r} '
  579. 'afterwards did.'.format(group)
  580. )
  581. else:
  582. log.warning(
  583. 'The test-case failed and also did the removal'
  584. ' of the system group {0!r}'.format(group)
  585. )
  586. if failure is not None:
  587. # If an exception was thrown, raise it
  588. six.reraise(failure[0], failure[1], failure[2])
  589. return wrap
  590. return decorator
  591. def with_system_user_and_group(username, group,
  592. on_existing='delete', delete=True):
  593. '''
  594. Create and optionally destroy a system user and group to be used within a
  595. test case. The system user is crated using the ``user`` salt module, and
  596. the system group is created with the ``group`` salt module.
  597. The decorated testcase function must accept both the 'username' and 'group'
  598. arguments.
  599. :param username: The desired username for the system user.
  600. :param group: The desired name for the system group.
  601. :param on_existing: What to do when the desired username is taken. The
  602. available options are:
  603. * nothing: Do nothing, act as if the user was created.
  604. * delete: delete and re-create the existing user
  605. * skip: skip the test case
  606. '''
  607. if on_existing not in ('nothing', 'delete', 'skip'):
  608. raise RuntimeError(
  609. 'The value of \'on_existing\' can only be one of, '
  610. '\'nothing\', \'delete\' and \'skip\''
  611. )
  612. if not isinstance(delete, bool):
  613. raise RuntimeError(
  614. 'The value of \'delete\' can only be \'True\' or \'False\''
  615. )
  616. def decorator(func):
  617. @functools.wraps(func)
  618. def wrap(cls):
  619. # Let's add the user to the system.
  620. log.debug('Creating system user {0!r}'.format(username))
  621. create_user = cls.run_function('user.add', [username])
  622. log.debug('Creating system group {0!r}'.format(group))
  623. create_group = cls.run_function('group.add', [group])
  624. if not create_user:
  625. log.debug('Failed to create system user')
  626. # The user was not created
  627. if on_existing == 'skip':
  628. cls.skipTest(
  629. 'Failed to create system user {0!r}'.format(
  630. username
  631. )
  632. )
  633. if on_existing == 'delete':
  634. log.debug(
  635. 'Deleting the system user {0!r}'.format(
  636. username
  637. )
  638. )
  639. delete_user = cls.run_function(
  640. 'user.delete', [username, True, True]
  641. )
  642. if not delete_user:
  643. cls.skipTest(
  644. 'A user named {0!r} already existed on the '
  645. 'system and re-creating it was not possible'
  646. .format(username)
  647. )
  648. log.debug(
  649. 'Second time creating system user {0!r}'.format(
  650. username
  651. )
  652. )
  653. create_user = cls.run_function('user.add', [username])
  654. if not create_user:
  655. cls.skipTest(
  656. 'A user named {0!r} already existed, was deleted '
  657. 'as requested, but re-creating it was not possible'
  658. .format(username)
  659. )
  660. if not create_group:
  661. log.debug('Failed to create system group')
  662. # The group was not created
  663. if on_existing == 'skip':
  664. cls.skipTest(
  665. 'Failed to create system group {0!r}'.format(group)
  666. )
  667. if on_existing == 'delete':
  668. log.debug(
  669. 'Deleting the system group {0!r}'.format(group)
  670. )
  671. delete_group = cls.run_function('group.delete', [group])
  672. if not delete_group:
  673. cls.skipTest(
  674. 'A group named {0!r} already existed on the '
  675. 'system and re-creating it was not possible'
  676. .format(group)
  677. )
  678. log.debug(
  679. 'Second time creating system group {0!r}'.format(
  680. group
  681. )
  682. )
  683. create_group = cls.run_function('group.add', [group])
  684. if not create_group:
  685. cls.skipTest(
  686. 'A group named {0!r} already existed, was deleted '
  687. 'as requested, but re-creating it was not possible'
  688. .format(group)
  689. )
  690. failure = None
  691. try:
  692. try:
  693. return func(cls, username, group)
  694. except Exception as exc: # pylint: disable=W0703
  695. log.error(
  696. 'Running {0!r} raised an exception: {1}'.format(
  697. func, exc
  698. ),
  699. exc_info=True
  700. )
  701. # Store the original exception details which will be raised
  702. # a little further down the code
  703. failure = sys.exc_info()
  704. finally:
  705. if delete:
  706. delete_user = cls.run_function(
  707. 'user.delete', [username, True, True]
  708. )
  709. delete_group = cls.run_function('group.delete', [group])
  710. if not delete_user:
  711. if failure is None:
  712. log.warning(
  713. 'Although the actual test-case did not fail, '
  714. 'deleting the created system user {0!r} '
  715. 'afterwards did.'.format(username)
  716. )
  717. else:
  718. log.warning(
  719. 'The test-case failed and also did the removal'
  720. ' of the system user {0!r}'.format(username)
  721. )
  722. if not delete_group:
  723. if failure is None:
  724. log.warning(
  725. 'Although the actual test-case did not fail, '
  726. 'deleting the created system group {0!r} '
  727. 'afterwards did.'.format(group)
  728. )
  729. else:
  730. log.warning(
  731. 'The test-case failed and also did the removal'
  732. ' of the system group {0!r}'.format(group)
  733. )
  734. if failure is not None:
  735. # If an exception was thrown, raise it
  736. six.reraise(failure[0], failure[1], failure[2])
  737. return wrap
  738. return decorator
  739. class WithTempfile(object):
  740. def __init__(self, **kwargs):
  741. self.create = kwargs.pop('create', True)
  742. if 'dir' not in kwargs:
  743. kwargs['dir'] = RUNTIME_VARS.TMP
  744. if 'prefix' not in kwargs:
  745. kwargs['prefix'] = '__salt.test.'
  746. self.kwargs = kwargs
  747. def __call__(self, func):
  748. self.func = func
  749. return functools.wraps(func)(
  750. lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs) # pylint: disable=W0108
  751. )
  752. def wrap(self, testcase, *args, **kwargs):
  753. name = salt.utils.files.mkstemp(**self.kwargs)
  754. if not self.create:
  755. os.remove(name)
  756. try:
  757. return self.func(testcase, name, *args, **kwargs)
  758. finally:
  759. try:
  760. os.remove(name)
  761. except OSError:
  762. pass
  763. with_tempfile = WithTempfile
  764. class WithTempdir(object):
  765. def __init__(self, **kwargs):
  766. self.create = kwargs.pop('create', True)
  767. if 'dir' not in kwargs:
  768. kwargs['dir'] = RUNTIME_VARS.TMP
  769. self.kwargs = kwargs
  770. def __call__(self, func):
  771. self.func = func
  772. return functools.wraps(func)(
  773. lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs) # pylint: disable=W0108
  774. )
  775. def wrap(self, testcase, *args, **kwargs):
  776. tempdir = tempfile.mkdtemp(**self.kwargs)
  777. if not self.create:
  778. os.rmdir(tempdir)
  779. try:
  780. return self.func(testcase, tempdir, *args, **kwargs)
  781. finally:
  782. shutil.rmtree(tempdir, ignore_errors=True)
  783. with_tempdir = WithTempdir
  784. def requires_system_grains(func):
  785. '''
  786. Function decorator which loads and passes the system's grains to the test
  787. case.
  788. '''
  789. @functools.wraps(func)
  790. def decorator(*args, **kwargs):
  791. if not hasattr(requires_system_grains, '__grains__'):
  792. # Late import
  793. from tests.support.sminion import build_minion_opts
  794. opts = build_minion_opts(minion_id='runtests-internal-sminion')
  795. requires_system_grains.__grains__ = salt.loader.grains(opts)
  796. kwargs['grains'] = requires_system_grains.__grains__
  797. return func(*args, **kwargs)
  798. return decorator
  799. def repeat(caller=None, condition=True, times=5):
  800. '''
  801. Repeat a test X amount of times until the first failure.
  802. .. code-block:: python
  803. class MyTestCase(TestCase):
  804. @repeat
  805. def test_sometimes_works(self):
  806. pass
  807. '''
  808. if caller is None:
  809. return functools.partial(repeat, condition=condition, times=times)
  810. if isinstance(condition, bool) and condition is False:
  811. # Don't even decorate
  812. return caller
  813. elif callable(condition):
  814. if condition() is False:
  815. # Don't even decorate
  816. return caller
  817. if inspect.isclass(caller):
  818. attrs = [n for n in dir(caller) if n.startswith('test_')]
  819. for attrname in attrs:
  820. try:
  821. function = getattr(caller, attrname)
  822. if not inspect.isfunction(function) and not inspect.ismethod(function):
  823. continue
  824. setattr(caller, attrname, repeat(caller=function, condition=condition, times=times))
  825. except Exception as exc:
  826. log.exception(exc)
  827. continue
  828. return caller
  829. @functools.wraps(caller)
  830. def wrap(cls):
  831. result = None
  832. for attempt in range(1, times+1):
  833. log.info('%s test run %d of %s times', cls, attempt, times)
  834. caller(cls)
  835. return cls
  836. return wrap
  837. def http_basic_auth(login_cb=lambda username, password: False):
  838. '''
  839. A crude decorator to force a handler to request HTTP Basic Authentication
  840. Example usage:
  841. .. code-block:: python
  842. @http_basic_auth(lambda u, p: u == 'foo' and p == 'bar')
  843. class AuthenticatedHandler(tornado.web.RequestHandler):
  844. pass
  845. '''
  846. def wrapper(handler_class):
  847. def wrap_execute(handler_execute):
  848. def check_auth(handler, kwargs):
  849. auth = handler.request.headers.get('Authorization')
  850. if auth is None or not auth.startswith('Basic '):
  851. # No username/password entered yet, we need to return a 401
  852. # and set the WWW-Authenticate header to request login.
  853. handler.set_status(401)
  854. handler.set_header(
  855. 'WWW-Authenticate', 'Basic realm=Restricted')
  856. else:
  857. # Strip the 'Basic ' from the beginning of the auth header
  858. # leaving the base64-encoded secret
  859. username, password = \
  860. base64.b64decode(auth[6:]).split(':', 1)
  861. if login_cb(username, password):
  862. # Authentication successful
  863. return
  864. else:
  865. # Authentication failed
  866. handler.set_status(403)
  867. handler._transforms = []
  868. handler.finish()
  869. def _execute(self, transforms, *args, **kwargs):
  870. check_auth(self, kwargs)
  871. return handler_execute(self, transforms, *args, **kwargs)
  872. return _execute
  873. handler_class._execute = wrap_execute(handler_class._execute)
  874. return handler_class
  875. return wrapper
  876. def generate_random_name(prefix, size=6):
  877. '''
  878. Generates a random name by combining the provided prefix with a randomly generated
  879. ascii string.
  880. .. versionadded:: 2018.3.0
  881. prefix
  882. The string to prefix onto the randomly generated ascii string.
  883. size
  884. The number of characters to generate. Default: 6.
  885. '''
  886. return prefix + ''.join(
  887. random.choice(string.ascii_uppercase + string.digits)
  888. for x in range(size)
  889. )
  890. class Webserver(object):
  891. '''
  892. Starts a tornado webserver on 127.0.0.1 on a random available port
  893. USAGE:
  894. .. code-block:: python
  895. from tests.support.helpers import Webserver
  896. webserver = Webserver('/path/to/web/root')
  897. webserver.start()
  898. webserver.stop()
  899. '''
  900. def __init__(self,
  901. root=None,
  902. port=None,
  903. wait=5,
  904. handler=None):
  905. '''
  906. root
  907. Root directory of webserver. If not passed, it will default to the
  908. location of the base environment of the integration suite's file
  909. roots (tests/integration/files/file/base/)
  910. port
  911. Port on which to listen. If not passed, a random one will be chosen
  912. at the time the start() function is invoked.
  913. wait : 5
  914. Number of seconds to wait for the socket to be open before raising
  915. an exception
  916. handler
  917. Can be used to use a subclass of tornado.web.StaticFileHandler,
  918. such as when enforcing authentication with the http_basic_auth
  919. decorator.
  920. '''
  921. if port is not None and not isinstance(port, six.integer_types):
  922. raise ValueError('port must be an integer')
  923. if root is None:
  924. root = RUNTIME_VARS.BASE_FILES
  925. try:
  926. self.root = os.path.realpath(root)
  927. except AttributeError:
  928. raise ValueError('root must be a string')
  929. self.port = port
  930. self.wait = wait
  931. self.handler = handler \
  932. if handler is not None \
  933. else tornado.web.StaticFileHandler
  934. self.web_root = None
  935. def target(self):
  936. '''
  937. Threading target which stands up the tornado application
  938. '''
  939. self.ioloop = tornado.ioloop.IOLoop()
  940. self.ioloop.make_current()
  941. if self.handler == tornado.web.StaticFileHandler:
  942. self.application = tornado.web.Application(
  943. [(r'/(.*)', self.handler, {'path': self.root})])
  944. else:
  945. self.application = tornado.web.Application(
  946. [(r'/(.*)', self.handler)])
  947. self.application.listen(self.port)
  948. self.ioloop.start()
  949. @property
  950. def listening(self):
  951. if self.port is None:
  952. return False
  953. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  954. return sock.connect_ex(('127.0.0.1', self.port)) == 0
  955. def url(self, path):
  956. '''
  957. Convenience function which, given a file path, will return a URL that
  958. points to that path. If the path is relative, it will just be appended
  959. to self.web_root.
  960. '''
  961. if self.web_root is None:
  962. raise RuntimeError('Webserver instance has not been started')
  963. err_msg = 'invalid path, must be either a relative path or a path ' \
  964. 'within {0}'.format(self.root)
  965. try:
  966. relpath = path \
  967. if not os.path.isabs(path) \
  968. else os.path.relpath(path, self.root)
  969. if relpath.startswith('..' + os.sep):
  970. raise ValueError(err_msg)
  971. return '/'.join((self.web_root, relpath))
  972. except AttributeError:
  973. raise ValueError(err_msg)
  974. def start(self):
  975. '''
  976. Starts the webserver
  977. '''
  978. if self.port is None:
  979. self.port = get_unused_localhost_port()
  980. self.web_root = 'http://127.0.0.1:{0}'.format(self.port)
  981. self.server_thread = threading.Thread(target=self.target)
  982. self.server_thread.daemon = True
  983. self.server_thread.start()
  984. for idx in range(self.wait + 1):
  985. if self.listening:
  986. break
  987. if idx != self.wait:
  988. time.sleep(1)
  989. else:
  990. raise Exception(
  991. 'Failed to start tornado webserver on 127.0.0.1:{0} within '
  992. '{1} seconds'.format(self.port, self.wait)
  993. )
  994. def stop(self):
  995. '''
  996. Stops the webserver
  997. '''
  998. self.ioloop.add_callback(self.ioloop.stop)
  999. self.server_thread.join()
  1000. class MirrorPostHandler(tornado.web.RequestHandler):
  1001. '''
  1002. Mirror a POST body back to the client
  1003. '''
  1004. def post(self, *args): # pylint: disable=arguments-differ
  1005. '''
  1006. Handle the post
  1007. '''
  1008. body = self.request.body
  1009. log.debug('Incoming body: %s Incoming args: %s', body, args)
  1010. self.write(body)
  1011. def data_received(self): # pylint: disable=arguments-differ
  1012. '''
  1013. Streaming not used for testing
  1014. '''
  1015. raise NotImplementedError()
  1016. def dedent(text, linesep=os.linesep):
  1017. '''
  1018. A wrapper around textwrap.dedent that also sets line endings.
  1019. '''
  1020. linesep = salt.utils.stringutils.to_unicode(linesep)
  1021. unicode_text = textwrap.dedent(salt.utils.stringutils.to_unicode(text))
  1022. clean_text = linesep.join(unicode_text.splitlines())
  1023. if unicode_text.endswith(u'\n'):
  1024. clean_text += linesep
  1025. if not isinstance(text, six.text_type):
  1026. return salt.utils.stringutils.to_bytes(clean_text)
  1027. return clean_text
  1028. class PatchedEnviron(object):
  1029. def __init__(self, **kwargs):
  1030. self.cleanup_keys = kwargs.pop('__cleanup__', ())
  1031. self.kwargs = kwargs
  1032. self.original_environ = None
  1033. def __enter__(self):
  1034. self.original_environ = os.environ.copy()
  1035. for key in self.cleanup_keys:
  1036. os.environ.pop(key, None)
  1037. # Make sure there are no unicode characters in the self.kwargs if we're
  1038. # on Python 2. These are being added to `os.environ` and causing
  1039. # problems
  1040. if sys.version_info < (3,):
  1041. kwargs = self.kwargs.copy()
  1042. clean_kwargs = {}
  1043. for k in self.kwargs:
  1044. key = k
  1045. if isinstance(key, six.text_type):
  1046. key = key.encode('utf-8')
  1047. if isinstance(self.kwargs[k], six.text_type):
  1048. kwargs[k] = kwargs[k].encode('utf-8')
  1049. clean_kwargs[key] = kwargs[k]
  1050. self.kwargs = clean_kwargs
  1051. os.environ.update(**self.kwargs)
  1052. return self
  1053. def __exit__(self, *args):
  1054. os.environ.clear()
  1055. os.environ.update(self.original_environ)
  1056. patched_environ = PatchedEnviron