1
0

helpers.py 48 KB

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