helpers.py 47 KB

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