1
0

helpers.py 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744
  1. # -*- coding: utf-8 -*-
  2. """
  3. :copyright: Copyright 2013-2017 by the SaltStack Team, see AUTHORS for more details.
  4. :license: Apache 2.0, see LICENSE for more details.
  5. tests.support.helpers
  6. ~~~~~~~~~~~~~~~~~~~~~
  7. Test support helpers
  8. """
  9. # pylint: disable=repr-flag-used-in-string,wrong-import-order
  10. from __future__ import absolute_import, print_function, unicode_literals
  11. import base64
  12. import errno
  13. import fnmatch
  14. import functools
  15. import inspect
  16. import logging
  17. import os
  18. import random
  19. import shutil
  20. import socket
  21. import string
  22. import subprocess
  23. import sys
  24. import tempfile
  25. import textwrap
  26. import threading
  27. import time
  28. import types
  29. import pytest
  30. import salt.ext.tornado.ioloop
  31. import salt.ext.tornado.web
  32. import salt.utils.files
  33. import salt.utils.platform
  34. import salt.utils.stringutils
  35. import salt.utils.versions
  36. from pytestsalt.utils import get_unused_localhost_port
  37. from salt.ext import six
  38. from salt.ext.six.moves import builtins, range
  39. from tests.support.mock import patch
  40. from tests.support.runtests import RUNTIME_VARS
  41. from tests.support.sminion import create_sminion
  42. from tests.support.unit import SkipTest, _id, skip
  43. log = logging.getLogger(__name__)
  44. HAS_SYMLINKS = None
  45. PRE_PYTEST_SKIP_OR_NOT = "PRE_PYTEST_DONT_SKIP" not in os.environ
  46. PRE_PYTEST_SKIP_REASON = (
  47. "PRE PYTEST - This test was skipped before running under pytest"
  48. )
  49. PRE_PYTEST_SKIP = pytest.mark.skipif(
  50. PRE_PYTEST_SKIP_OR_NOT, reason=PRE_PYTEST_SKIP_REASON
  51. )
  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 destructiveTest(caller):
  77. """
  78. Mark a test case as a destructive test for example adding or removing users
  79. from your system.
  80. .. code-block:: python
  81. class MyTestCase(TestCase):
  82. @destructiveTest
  83. def test_create_user(self):
  84. pass
  85. """
  86. # Late import
  87. from tests.support.runtests import RUNTIME_VARS
  88. if RUNTIME_VARS.PYTEST_SESSION:
  89. setattr(caller, "__destructive_test__", True)
  90. if inspect.isclass(caller):
  91. # We're decorating a class
  92. old_setup = getattr(caller, "setUp", None)
  93. def setUp(self, *args, **kwargs):
  94. if os.environ.get("DESTRUCTIVE_TESTS", "False").lower() == "false":
  95. self.skipTest("Destructive tests are disabled")
  96. if old_setup is not None:
  97. old_setup(self, *args, **kwargs)
  98. caller.setUp = setUp
  99. return caller
  100. # We're simply decorating functions
  101. @functools.wraps(caller)
  102. def wrap(cls):
  103. if os.environ.get("DESTRUCTIVE_TESTS", "False").lower() == "false":
  104. cls.skipTest("Destructive tests are disabled")
  105. return caller(cls)
  106. return wrap
  107. def expensiveTest(caller):
  108. """
  109. Mark a test case as an expensive test, for example, a test which can cost
  110. money(Salt's cloud provider tests).
  111. .. code-block:: python
  112. class MyTestCase(TestCase):
  113. @expensiveTest
  114. def test_create_user(self):
  115. pass
  116. """
  117. # Late import
  118. from tests.support.runtests import RUNTIME_VARS
  119. if RUNTIME_VARS.PYTEST_SESSION:
  120. setattr(caller, "__expensive_test__", True)
  121. if inspect.isclass(caller):
  122. # We're decorating a class
  123. old_setup = getattr(caller, "setUp", None)
  124. def setUp(self, *args, **kwargs):
  125. if os.environ.get("EXPENSIVE_TESTS", "False").lower() == "false":
  126. self.skipTest("Expensive tests are disabled")
  127. if old_setup is not None:
  128. old_setup(self, *args, **kwargs)
  129. caller.setUp = setUp
  130. return caller
  131. # We're simply decorating functions
  132. @functools.wraps(caller)
  133. def wrap(cls):
  134. if os.environ.get("EXPENSIVE_TESTS", "False").lower() == "false":
  135. cls.skipTest("Expensive tests are disabled")
  136. return caller(cls)
  137. return wrap
  138. def flaky(caller=None, condition=True, attempts=4):
  139. """
  140. Mark a test as flaky. The test will attempt to run five times,
  141. looking for a successful run. After an immediate second try,
  142. it will use an exponential backoff starting with one second.
  143. .. code-block:: python
  144. class MyTestCase(TestCase):
  145. @flaky
  146. def test_sometimes_works(self):
  147. pass
  148. """
  149. if caller is None:
  150. return functools.partial(flaky, condition=condition, attempts=attempts)
  151. if isinstance(condition, bool) and condition is False:
  152. # Don't even decorate
  153. return caller
  154. elif callable(condition):
  155. if condition() is False:
  156. # Don't even decorate
  157. return caller
  158. if inspect.isclass(caller):
  159. attrs = [n for n in dir(caller) if n.startswith("test_")]
  160. for attrname in attrs:
  161. try:
  162. function = getattr(caller, attrname)
  163. if not inspect.isfunction(function) and not inspect.ismethod(function):
  164. continue
  165. setattr(
  166. caller,
  167. attrname,
  168. flaky(caller=function, condition=condition, attempts=attempts),
  169. )
  170. except Exception as exc: # pylint: disable=broad-except
  171. log.exception(exc)
  172. continue
  173. return caller
  174. @functools.wraps(caller)
  175. def wrap(cls):
  176. for attempt in range(0, attempts):
  177. try:
  178. if attempt > 0:
  179. # Run through setUp again
  180. # We only run it after the first iteration(>0) because the regular
  181. # test runner will have already ran setUp the first time
  182. setup = getattr(cls, "setUp", None)
  183. if callable(setup):
  184. setup()
  185. return caller(cls)
  186. except SkipTest as exc:
  187. cls.skipTest(exc.args[0])
  188. except Exception as exc: # pylint: disable=broad-except
  189. exc_info = sys.exc_info()
  190. if isinstance(exc, SkipTest):
  191. six.reraise(*exc_info)
  192. if not isinstance(exc, AssertionError) and log.isEnabledFor(
  193. logging.DEBUG
  194. ):
  195. log.exception(exc, exc_info=exc_info)
  196. if attempt >= attempts - 1:
  197. # We won't try to run tearDown once the attempts are exhausted
  198. # because the regular test runner will do that for us
  199. six.reraise(*exc_info)
  200. # Run through tearDown again
  201. teardown = getattr(cls, "tearDown", None)
  202. if callable(teardown):
  203. teardown()
  204. backoff_time = attempt ** 2
  205. log.info("Found Exception. Waiting %s seconds to retry.", backoff_time)
  206. time.sleep(backoff_time)
  207. return cls
  208. return wrap
  209. def requires_sshd_server(caller):
  210. """
  211. Mark a test as requiring the tests SSH daemon running.
  212. .. code-block:: python
  213. class MyTestCase(TestCase):
  214. @requiresSshdServer
  215. def test_create_user(self):
  216. pass
  217. """
  218. if inspect.isclass(caller):
  219. # We're decorating a class
  220. old_setup = getattr(caller, "setUp", None)
  221. def setUp(self, *args, **kwargs):
  222. if os.environ.get("SSH_DAEMON_RUNNING", "False").lower() == "false":
  223. self.skipTest("SSH tests are disabled")
  224. if old_setup is not None:
  225. old_setup(self, *args, **kwargs)
  226. caller.setUp = setUp
  227. return caller
  228. # We're simply decorating functions
  229. @functools.wraps(caller)
  230. def wrap(cls):
  231. if os.environ.get("SSH_DAEMON_RUNNING", "False").lower() == "false":
  232. cls.skipTest("SSH tests are disabled")
  233. return caller(cls)
  234. return wrap
  235. class RedirectStdStreams(object):
  236. """
  237. Temporarily redirect system output to file like objects.
  238. Default is to redirect to `os.devnull`, which just mutes output, `stdout`
  239. and `stderr`.
  240. """
  241. def __init__(self, stdout=None, stderr=None):
  242. # Late import
  243. import salt.utils.files
  244. if stdout is None:
  245. # pylint: disable=resource-leakage
  246. stdout = salt.utils.files.fopen(os.devnull, "w")
  247. # pylint: enable=resource-leakage
  248. if stderr is None:
  249. # pylint: disable=resource-leakage
  250. stderr = salt.utils.files.fopen(os.devnull, "w")
  251. # pylint: enable=resource-leakage
  252. self.__stdout = stdout
  253. self.__stderr = stderr
  254. self.__redirected = False
  255. self.patcher = patch.multiple(sys, stderr=self.__stderr, stdout=self.__stdout)
  256. def __enter__(self):
  257. self.redirect()
  258. return self
  259. def __exit__(self, exc_type, exc_value, traceback):
  260. self.unredirect()
  261. def redirect(self):
  262. self.old_stdout = sys.stdout
  263. self.old_stdout.flush()
  264. self.old_stderr = sys.stderr
  265. self.old_stderr.flush()
  266. self.patcher.start()
  267. self.__redirected = True
  268. def unredirect(self):
  269. if not self.__redirected:
  270. return
  271. try:
  272. self.__stdout.flush()
  273. self.__stdout.close()
  274. except ValueError:
  275. # already closed?
  276. pass
  277. try:
  278. self.__stderr.flush()
  279. self.__stderr.close()
  280. except ValueError:
  281. # already closed?
  282. pass
  283. self.patcher.stop()
  284. def flush(self):
  285. if self.__redirected:
  286. try:
  287. self.__stdout.flush()
  288. except Exception: # pylint: disable=broad-except
  289. pass
  290. try:
  291. self.__stderr.flush()
  292. except Exception: # pylint: disable=broad-except
  293. pass
  294. class TstSuiteLoggingHandler(object):
  295. """
  296. Simple logging handler which can be used to test if certain logging
  297. messages get emitted or not:
  298. .. code-block:: python
  299. with TstSuiteLoggingHandler() as handler:
  300. # (...) Do what ever you wish here
  301. handler.messages # here are the emitted log messages
  302. """
  303. def __init__(self, level=0, format="%(levelname)s:%(message)s"):
  304. self.level = level
  305. self.format = format
  306. self.activated = False
  307. self.prev_logging_level = None
  308. def activate(self):
  309. class Handler(logging.Handler):
  310. def __init__(self, level):
  311. logging.Handler.__init__(self, level)
  312. self.messages = []
  313. def emit(self, record):
  314. self.messages.append(self.format(record))
  315. self.handler = Handler(self.level)
  316. formatter = logging.Formatter(self.format)
  317. self.handler.setFormatter(formatter)
  318. logging.root.addHandler(self.handler)
  319. self.activated = True
  320. # Make sure we're running with the lowest logging level with our
  321. # tests logging handler
  322. current_logging_level = logging.root.getEffectiveLevel()
  323. if current_logging_level > logging.DEBUG:
  324. self.prev_logging_level = current_logging_level
  325. logging.root.setLevel(0)
  326. def deactivate(self):
  327. if not self.activated:
  328. return
  329. logging.root.removeHandler(self.handler)
  330. # Restore previous logging level if changed
  331. if self.prev_logging_level is not None:
  332. logging.root.setLevel(self.prev_logging_level)
  333. @property
  334. def messages(self):
  335. if not self.activated:
  336. return []
  337. return self.handler.messages
  338. def clear(self):
  339. self.handler.messages = []
  340. def __enter__(self):
  341. self.activate()
  342. return self
  343. def __exit__(self, type, value, traceback):
  344. self.deactivate()
  345. self.activated = False
  346. # Mimic some handler attributes and methods
  347. @property
  348. def lock(self):
  349. if self.activated:
  350. return self.handler.lock
  351. def createLock(self):
  352. if self.activated:
  353. return self.handler.createLock()
  354. def acquire(self):
  355. if self.activated:
  356. return self.handler.acquire()
  357. def release(self):
  358. if self.activated:
  359. return self.handler.release()
  360. class ForceImportErrorOn(object):
  361. """
  362. This class is meant to be used in mock'ed test cases which require an
  363. ``ImportError`` to be raised.
  364. >>> import os.path
  365. >>> with ForceImportErrorOn('os.path'):
  366. ... import os.path
  367. ...
  368. Traceback (most recent call last):
  369. File "<stdin>", line 2, in <module>
  370. File "salttesting/helpers.py", line 263, in __import__
  371. 'Forced ImportError raised for {0!r}'.format(name)
  372. ImportError: Forced ImportError raised for 'os.path'
  373. >>>
  374. >>> with ForceImportErrorOn(('os', 'path')):
  375. ... import os.path
  376. ... sys.modules.pop('os', None)
  377. ... from os import path
  378. ...
  379. <module 'os' from '/usr/lib/python2.7/os.pyc'>
  380. Traceback (most recent call last):
  381. File "<stdin>", line 4, in <module>
  382. File "salttesting/helpers.py", line 288, in __fake_import__
  383. name, ', '.join(fromlist)
  384. ImportError: Forced ImportError raised for 'from os import path'
  385. >>>
  386. >>> with ForceImportErrorOn(('os', 'path'), 'os.path'):
  387. ... import os.path
  388. ... sys.modules.pop('os', None)
  389. ... from os import path
  390. ...
  391. Traceback (most recent call last):
  392. File "<stdin>", line 2, in <module>
  393. File "salttesting/helpers.py", line 281, in __fake_import__
  394. 'Forced ImportError raised for {0!r}'.format(name)
  395. ImportError: Forced ImportError raised for 'os.path'
  396. >>>
  397. """
  398. def __init__(self, *module_names):
  399. self.__module_names = {}
  400. for entry in module_names:
  401. if isinstance(entry, (list, tuple)):
  402. modname = entry[0]
  403. self.__module_names[modname] = set(entry[1:])
  404. else:
  405. self.__module_names[entry] = None
  406. self.__original_import = builtins.__import__
  407. self.patcher = patch.object(builtins, "__import__", self.__fake_import__)
  408. def patch_import_function(self):
  409. self.patcher.start()
  410. def restore_import_funtion(self):
  411. self.patcher.stop()
  412. def __fake_import__(
  413. self, name, globals_=None, locals_=None, fromlist=None, level=None
  414. ):
  415. if six.PY2:
  416. if globals_ is None:
  417. globals_ = {}
  418. if locals_ is None:
  419. locals_ = {}
  420. if level is None:
  421. if six.PY2:
  422. level = -1
  423. else:
  424. level = 0
  425. if fromlist is None:
  426. fromlist = []
  427. if name in self.__module_names:
  428. importerror_fromlist = self.__module_names.get(name)
  429. if importerror_fromlist is None:
  430. raise ImportError("Forced ImportError raised for {0!r}".format(name))
  431. if importerror_fromlist.intersection(set(fromlist)):
  432. raise ImportError(
  433. "Forced ImportError raised for {0!r}".format(
  434. "from {0} import {1}".format(name, ", ".join(fromlist))
  435. )
  436. )
  437. return self.__original_import(name, globals_, locals_, fromlist, level)
  438. def __enter__(self):
  439. self.patch_import_function()
  440. return self
  441. def __exit__(self, exc_type, exc_value, traceback):
  442. self.restore_import_funtion()
  443. class MockWraps(object):
  444. """
  445. Helper class to be used with the mock library.
  446. To be used in the ``wraps`` keyword of ``Mock`` or ``MagicMock`` where you
  447. want to trigger a side effect for X times, and afterwards, call the
  448. original and un-mocked method.
  449. As an example:
  450. >>> def original():
  451. ... print 'original'
  452. ...
  453. >>> def side_effect():
  454. ... print 'side effect'
  455. ...
  456. >>> mw = MockWraps(original, 2, side_effect)
  457. >>> mw()
  458. side effect
  459. >>> mw()
  460. side effect
  461. >>> mw()
  462. original
  463. >>>
  464. """
  465. def __init__(self, original, expected_failures, side_effect):
  466. self.__original = original
  467. self.__expected_failures = expected_failures
  468. self.__side_effect = side_effect
  469. self.__call_counter = 0
  470. def __call__(self, *args, **kwargs):
  471. try:
  472. if self.__call_counter < self.__expected_failures:
  473. if isinstance(self.__side_effect, types.FunctionType):
  474. return self.__side_effect()
  475. raise self.__side_effect
  476. return self.__original(*args, **kwargs)
  477. finally:
  478. self.__call_counter += 1
  479. def requires_network(only_local_network=False):
  480. """
  481. Simple decorator which is supposed to skip a test case in case there's no
  482. network connection to the internet.
  483. """
  484. def decorator(func):
  485. @functools.wraps(func)
  486. def wrapper(cls):
  487. has_local_network = False
  488. # First lets try if we have a local network. Inspired in
  489. # verify_socket
  490. try:
  491. pubsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  492. retsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  493. pubsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  494. pubsock.bind(("", 18000))
  495. pubsock.close()
  496. retsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  497. retsock.bind(("", 18001))
  498. retsock.close()
  499. has_local_network = True
  500. except socket.error:
  501. # I wonder if we just have IPV6 support?
  502. try:
  503. pubsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  504. retsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  505. pubsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  506. pubsock.bind(("", 18000))
  507. pubsock.close()
  508. retsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  509. retsock.bind(("", 18001))
  510. retsock.close()
  511. has_local_network = True
  512. except socket.error:
  513. # Let's continue
  514. pass
  515. if only_local_network is True:
  516. if has_local_network is False:
  517. # Since we're only supposed to check local network, and no
  518. # local network was detected, skip the test
  519. cls.skipTest("No local network was detected")
  520. return func(cls)
  521. if os.environ.get("NO_INTERNET"):
  522. cls.skipTest("Environment variable NO_INTERNET is set.")
  523. # We are using the google.com DNS records as numerical IPs to avoid
  524. # DNS lookups which could greatly slow down this check
  525. for addr in (
  526. "173.194.41.198",
  527. "173.194.41.199",
  528. "173.194.41.200",
  529. "173.194.41.201",
  530. "173.194.41.206",
  531. "173.194.41.192",
  532. "173.194.41.193",
  533. "173.194.41.194",
  534. "173.194.41.195",
  535. "173.194.41.196",
  536. "173.194.41.197",
  537. ):
  538. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  539. try:
  540. sock.settimeout(0.25)
  541. sock.connect((addr, 80))
  542. # We connected? Stop the loop
  543. break
  544. except socket.error:
  545. # Let's check the next IP
  546. continue
  547. else:
  548. cls.skipTest("No internet network connection was detected")
  549. finally:
  550. sock.close()
  551. return func(cls)
  552. return wrapper
  553. return decorator
  554. def with_system_user(
  555. username, on_existing="delete", delete=True, password=None, groups=None
  556. ):
  557. """
  558. Create and optionally destroy a system user to be used within a test
  559. case. The system user is created using the ``user`` salt module.
  560. The decorated testcase function must accept 'username' as an argument.
  561. :param username: The desired username for the system user.
  562. :param on_existing: What to do when the desired username is taken. The
  563. available options are:
  564. * nothing: Do nothing, act as if the user was created.
  565. * delete: delete and re-create the existing user
  566. * skip: skip the test case
  567. """
  568. if on_existing not in ("nothing", "delete", "skip"):
  569. raise RuntimeError(
  570. "The value of 'on_existing' can only be one of, "
  571. "'nothing', 'delete' and 'skip'"
  572. )
  573. if not isinstance(delete, bool):
  574. raise RuntimeError("The value of 'delete' can only be 'True' or 'False'")
  575. def decorator(func):
  576. @functools.wraps(func)
  577. def wrap(cls):
  578. # Let's add the user to the system.
  579. log.debug("Creating system user {0!r}".format(username))
  580. kwargs = {"timeout": 60, "groups": groups}
  581. if salt.utils.platform.is_windows():
  582. kwargs.update({"password": password})
  583. create_user = cls.run_function("user.add", [username], **kwargs)
  584. if not create_user:
  585. log.debug("Failed to create system user")
  586. # The user was not created
  587. if on_existing == "skip":
  588. cls.skipTest("Failed to create system user {0!r}".format(username))
  589. if on_existing == "delete":
  590. log.debug("Deleting the system user {0!r}".format(username))
  591. delete_user = cls.run_function(
  592. "user.delete", [username, True, True]
  593. )
  594. if not delete_user:
  595. cls.skipTest(
  596. "A user named {0!r} already existed on the "
  597. "system and re-creating it was not possible".format(
  598. username
  599. )
  600. )
  601. log.debug("Second time creating system user {0!r}".format(username))
  602. create_user = cls.run_function("user.add", [username], **kwargs)
  603. if not create_user:
  604. cls.skipTest(
  605. "A user named {0!r} already existed, was deleted "
  606. "as requested, but re-creating it was not possible".format(
  607. username
  608. )
  609. )
  610. failure = None
  611. try:
  612. try:
  613. return func(cls, username)
  614. except Exception as exc: # pylint: disable=W0703
  615. log.error(
  616. "Running {0!r} raised an exception: {1}".format(func, exc),
  617. exc_info=True,
  618. )
  619. # Store the original exception details which will be raised
  620. # a little further down the code
  621. failure = sys.exc_info()
  622. finally:
  623. if delete:
  624. delete_user = cls.run_function(
  625. "user.delete", [username, True, True], timeout=60
  626. )
  627. if not delete_user:
  628. if failure is None:
  629. log.warning(
  630. "Although the actual test-case did not fail, "
  631. "deleting the created system user {0!r} "
  632. "afterwards did.".format(username)
  633. )
  634. else:
  635. log.warning(
  636. "The test-case failed and also did the removal"
  637. " of the system user {0!r}".format(username)
  638. )
  639. if failure is not None:
  640. # If an exception was thrown, raise it
  641. six.reraise(failure[0], failure[1], failure[2])
  642. return wrap
  643. return decorator
  644. def with_system_group(group, on_existing="delete", delete=True):
  645. """
  646. Create and optionally destroy a system group to be used within a test
  647. case. The system user is crated using the ``group`` salt module.
  648. The decorated testcase function must accept 'group' as an argument.
  649. :param group: The desired group name for the system user.
  650. :param on_existing: What to do when the desired username is taken. The
  651. available options are:
  652. * nothing: Do nothing, act as if the group was created
  653. * delete: delete and re-create the existing user
  654. * skip: skip the test case
  655. """
  656. if on_existing not in ("nothing", "delete", "skip"):
  657. raise RuntimeError(
  658. "The value of 'on_existing' can only be one of, "
  659. "'nothing', 'delete' and 'skip'"
  660. )
  661. if not isinstance(delete, bool):
  662. raise RuntimeError("The value of 'delete' can only be 'True' or 'False'")
  663. def decorator(func):
  664. @functools.wraps(func)
  665. def wrap(cls):
  666. # Let's add the user to the system.
  667. log.debug("Creating system group {0!r}".format(group))
  668. create_group = cls.run_function("group.add", [group])
  669. if not create_group:
  670. log.debug("Failed to create system group")
  671. # The group was not created
  672. if on_existing == "skip":
  673. cls.skipTest("Failed to create system group {0!r}".format(group))
  674. if on_existing == "delete":
  675. log.debug("Deleting the system group {0!r}".format(group))
  676. delete_group = cls.run_function("group.delete", [group])
  677. if not delete_group:
  678. cls.skipTest(
  679. "A group named {0!r} already existed on the "
  680. "system and re-creating it was not possible".format(group)
  681. )
  682. log.debug("Second time creating system group {0!r}".format(group))
  683. create_group = cls.run_function("group.add", [group])
  684. if not create_group:
  685. cls.skipTest(
  686. "A group named {0!r} already existed, was deleted "
  687. "as requested, but re-creating it was not possible".format(
  688. group
  689. )
  690. )
  691. failure = None
  692. try:
  693. try:
  694. return func(cls, group)
  695. except Exception as exc: # pylint: disable=W0703
  696. log.error(
  697. "Running {0!r} raised an exception: {1}".format(func, exc),
  698. exc_info=True,
  699. )
  700. # Store the original exception details which will be raised
  701. # a little further down the code
  702. failure = sys.exc_info()
  703. finally:
  704. if delete:
  705. delete_group = cls.run_function("group.delete", [group])
  706. if not delete_group:
  707. if failure is None:
  708. log.warning(
  709. "Although the actual test-case did not fail, "
  710. "deleting the created system group {0!r} "
  711. "afterwards did.".format(group)
  712. )
  713. else:
  714. log.warning(
  715. "The test-case failed and also did the removal"
  716. " of the system group {0!r}".format(group)
  717. )
  718. if failure is not None:
  719. # If an exception was thrown, raise it
  720. six.reraise(failure[0], failure[1], failure[2])
  721. return wrap
  722. return decorator
  723. def with_system_user_and_group(username, group, on_existing="delete", delete=True):
  724. """
  725. Create and optionally destroy a system user and group to be used within a
  726. test case. The system user is crated using the ``user`` salt module, and
  727. the system group is created with the ``group`` salt module.
  728. The decorated testcase function must accept both the 'username' and 'group'
  729. arguments.
  730. :param username: The desired username for the system user.
  731. :param group: The desired name for the system group.
  732. :param on_existing: What to do when the desired username is taken. The
  733. available options are:
  734. * nothing: Do nothing, act as if the user was created.
  735. * delete: delete and re-create the existing user
  736. * skip: skip the test case
  737. """
  738. if on_existing not in ("nothing", "delete", "skip"):
  739. raise RuntimeError(
  740. "The value of 'on_existing' can only be one of, "
  741. "'nothing', 'delete' and 'skip'"
  742. )
  743. if not isinstance(delete, bool):
  744. raise RuntimeError("The value of 'delete' can only be 'True' or 'False'")
  745. def decorator(func):
  746. @functools.wraps(func)
  747. def wrap(cls):
  748. # Let's add the user to the system.
  749. log.debug("Creating system user {0!r}".format(username))
  750. create_user = cls.run_function("user.add", [username])
  751. log.debug("Creating system group {0!r}".format(group))
  752. create_group = cls.run_function("group.add", [group])
  753. if not create_user:
  754. log.debug("Failed to create system user")
  755. # The user was not created
  756. if on_existing == "skip":
  757. cls.skipTest("Failed to create system user {0!r}".format(username))
  758. if on_existing == "delete":
  759. log.debug("Deleting the system user {0!r}".format(username))
  760. delete_user = cls.run_function(
  761. "user.delete", [username, True, True]
  762. )
  763. if not delete_user:
  764. cls.skipTest(
  765. "A user named {0!r} already existed on the "
  766. "system and re-creating it was not possible".format(
  767. username
  768. )
  769. )
  770. log.debug("Second time creating system user {0!r}".format(username))
  771. create_user = cls.run_function("user.add", [username])
  772. if not create_user:
  773. cls.skipTest(
  774. "A user named {0!r} already existed, was deleted "
  775. "as requested, but re-creating it was not possible".format(
  776. username
  777. )
  778. )
  779. if not create_group:
  780. log.debug("Failed to create system group")
  781. # The group was not created
  782. if on_existing == "skip":
  783. cls.skipTest("Failed to create system group {0!r}".format(group))
  784. if on_existing == "delete":
  785. log.debug("Deleting the system group {0!r}".format(group))
  786. delete_group = cls.run_function("group.delete", [group])
  787. if not delete_group:
  788. cls.skipTest(
  789. "A group named {0!r} already existed on the "
  790. "system and re-creating it was not possible".format(group)
  791. )
  792. log.debug("Second time creating system group {0!r}".format(group))
  793. create_group = cls.run_function("group.add", [group])
  794. if not create_group:
  795. cls.skipTest(
  796. "A group named {0!r} already existed, was deleted "
  797. "as requested, but re-creating it was not possible".format(
  798. group
  799. )
  800. )
  801. failure = None
  802. try:
  803. try:
  804. return func(cls, username, group)
  805. except Exception as exc: # pylint: disable=W0703
  806. log.error(
  807. "Running {0!r} raised an exception: {1}".format(func, exc),
  808. exc_info=True,
  809. )
  810. # Store the original exception details which will be raised
  811. # a little further down the code
  812. failure = sys.exc_info()
  813. finally:
  814. if delete:
  815. delete_user = cls.run_function(
  816. "user.delete", [username, True, True]
  817. )
  818. delete_group = cls.run_function("group.delete", [group])
  819. if not delete_user:
  820. if failure is None:
  821. log.warning(
  822. "Although the actual test-case did not fail, "
  823. "deleting the created system user {0!r} "
  824. "afterwards did.".format(username)
  825. )
  826. else:
  827. log.warning(
  828. "The test-case failed and also did the removal"
  829. " of the system user {0!r}".format(username)
  830. )
  831. if not delete_group:
  832. if failure is None:
  833. log.warning(
  834. "Although the actual test-case did not fail, "
  835. "deleting the created system group {0!r} "
  836. "afterwards did.".format(group)
  837. )
  838. else:
  839. log.warning(
  840. "The test-case failed and also did the removal"
  841. " of the system group {0!r}".format(group)
  842. )
  843. if failure is not None:
  844. # If an exception was thrown, raise it
  845. six.reraise(failure[0], failure[1], failure[2])
  846. return wrap
  847. return decorator
  848. class WithTempfile(object):
  849. def __init__(self, **kwargs):
  850. self.create = kwargs.pop("create", True)
  851. if "dir" not in kwargs:
  852. kwargs["dir"] = RUNTIME_VARS.TMP
  853. if "prefix" not in kwargs:
  854. kwargs["prefix"] = "__salt.test."
  855. self.kwargs = kwargs
  856. def __call__(self, func):
  857. self.func = func
  858. return functools.wraps(func)(
  859. # pylint: disable=unnecessary-lambda
  860. lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs)
  861. # pylint: enable=unnecessary-lambda
  862. )
  863. def wrap(self, testcase, *args, **kwargs):
  864. name = salt.utils.files.mkstemp(**self.kwargs)
  865. if not self.create:
  866. os.remove(name)
  867. try:
  868. return self.func(testcase, name, *args, **kwargs)
  869. finally:
  870. try:
  871. os.remove(name)
  872. except OSError:
  873. pass
  874. with_tempfile = WithTempfile
  875. class WithTempdir(object):
  876. def __init__(self, **kwargs):
  877. self.create = kwargs.pop("create", True)
  878. if "dir" not in kwargs:
  879. kwargs["dir"] = RUNTIME_VARS.TMP
  880. self.kwargs = kwargs
  881. def __call__(self, func):
  882. self.func = func
  883. return functools.wraps(func)(
  884. # pylint: disable=unnecessary-lambda
  885. lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs)
  886. # pylint: enable=unnecessary-lambda
  887. )
  888. def wrap(self, testcase, *args, **kwargs):
  889. tempdir = tempfile.mkdtemp(**self.kwargs)
  890. if not self.create:
  891. os.rmdir(tempdir)
  892. try:
  893. return self.func(testcase, tempdir, *args, **kwargs)
  894. finally:
  895. shutil.rmtree(tempdir, ignore_errors=True)
  896. with_tempdir = WithTempdir
  897. def requires_system_grains(func):
  898. """
  899. Function decorator which loads and passes the system's grains to the test
  900. case.
  901. """
  902. @functools.wraps(func)
  903. def decorator(*args, **kwargs):
  904. if not hasattr(requires_system_grains, "__grains__"):
  905. # Late import
  906. from tests.support.sminion import build_minion_opts
  907. opts = build_minion_opts(minion_id="runtests-internal-sminion")
  908. requires_system_grains.__grains__ = salt.loader.grains(opts)
  909. kwargs["grains"] = requires_system_grains.__grains__
  910. return func(*args, **kwargs)
  911. return decorator
  912. @requires_system_grains
  913. def runs_on(grains=None, **kwargs):
  914. """
  915. Skip the test if grains don't match the values passed into **kwargs
  916. if a kwarg value is a list then skip if the grains don't match any item in the list
  917. """
  918. def decorator(caller):
  919. @functools.wraps(caller)
  920. def wrapper(cls):
  921. reason = kwargs.pop("reason", None)
  922. for kw, value in kwargs.items():
  923. if isinstance(value, list):
  924. if not any(
  925. str(grains.get(kw)).lower() != str(v).lower() for v in value
  926. ):
  927. if reason is None:
  928. reason = "This test does not run on {}={}".format(
  929. kw, grains.get(kw)
  930. )
  931. raise SkipTest(reason)
  932. else:
  933. if str(grains.get(kw)).lower() != str(value).lower():
  934. if reason is None:
  935. reason = "This test runs on {}={}, not {}".format(
  936. kw, value, grains.get(kw)
  937. )
  938. raise SkipTest(reason)
  939. return caller(cls)
  940. return wrapper
  941. return decorator
  942. @requires_system_grains
  943. def not_runs_on(grains=None, **kwargs):
  944. """
  945. Reverse of `runs_on`.
  946. Skip the test if any grains match the values passed into **kwargs
  947. if a kwarg value is a list then skip if the grains match any item in the list
  948. """
  949. def decorator(caller):
  950. @functools.wraps(caller)
  951. def wrapper(cls):
  952. reason = kwargs.pop("reason", None)
  953. for kw, value in kwargs.items():
  954. if isinstance(value, list):
  955. if any(
  956. str(grains.get(kw)).lower() == str(v).lower() for v in value
  957. ):
  958. if reason is None:
  959. reason = "This test does not run on {}={}".format(
  960. kw, grains.get(kw)
  961. )
  962. raise SkipTest(reason)
  963. else:
  964. if str(grains.get(kw)).lower() == str(value).lower():
  965. if reason is None:
  966. reason = "This test does not run on {}={}, got {}".format(
  967. kw, value, grains.get(kw)
  968. )
  969. raise SkipTest(reason)
  970. return caller(cls)
  971. return wrapper
  972. return decorator
  973. def _check_required_sminion_attributes(sminion_attr, *required_items):
  974. """
  975. :param sminion_attr: The name of the sminion attribute to check, such as 'functions' or 'states'
  976. :param required_items: The items that must be part of the designated sminion attribute for the decorated test
  977. :return The packages that are not available
  978. """
  979. # Late import
  980. from tests.support.sminion import create_sminion
  981. required_salt_items = set(required_items)
  982. sminion = create_sminion(minion_id="runtests-internal-sminion")
  983. available_items = list(getattr(sminion, sminion_attr))
  984. not_available_items = set()
  985. name = "__not_available_{items}s__".format(items=sminion_attr)
  986. if not hasattr(sminion, name):
  987. setattr(sminion, name, set())
  988. cached_not_available_items = getattr(sminion, name)
  989. for not_available_item in cached_not_available_items:
  990. if not_available_item in required_salt_items:
  991. not_available_items.add(not_available_item)
  992. required_salt_items.remove(not_available_item)
  993. for required_item_name in required_salt_items:
  994. search_name = required_item_name
  995. if "." not in search_name:
  996. search_name += ".*"
  997. if not fnmatch.filter(available_items, search_name):
  998. not_available_items.add(required_item_name)
  999. cached_not_available_items.add(required_item_name)
  1000. return not_available_items
  1001. def requires_salt_states(*names):
  1002. """
  1003. Makes sure the passed salt state is available. Skips the test if not
  1004. .. versionadded:: 3000
  1005. """
  1006. not_available = _check_required_sminion_attributes("states", *names)
  1007. def decorator(caller):
  1008. if inspect.isclass(caller):
  1009. # We're decorating a class
  1010. old_setup = getattr(caller, "setUp", None)
  1011. def setUp(self, *args, **kwargs):
  1012. if not_available:
  1013. raise SkipTest("Unavailable salt states: {}".format(*not_available))
  1014. if old_setup is not None:
  1015. old_setup(self, *args, **kwargs)
  1016. caller.setUp = setUp
  1017. return caller
  1018. # We're simply decorating functions
  1019. @functools.wraps(caller)
  1020. def wrapper(cls):
  1021. if not_available:
  1022. raise SkipTest("Unavailable salt states: {}".format(*not_available))
  1023. return caller(cls)
  1024. return wrapper
  1025. return decorator
  1026. def requires_salt_modules(*names):
  1027. """
  1028. Makes sure the passed salt module is available. Skips the test if not
  1029. .. versionadded:: 0.5.2
  1030. """
  1031. not_available = _check_required_sminion_attributes("functions", *names)
  1032. def decorator(caller):
  1033. if inspect.isclass(caller):
  1034. # We're decorating a class
  1035. old_setup = getattr(caller, "setUp", None)
  1036. def setUp(self, *args, **kwargs):
  1037. if not_available:
  1038. raise SkipTest(
  1039. "Unavailable salt modules: {}".format(*not_available)
  1040. )
  1041. if old_setup is not None:
  1042. old_setup(self, *args, **kwargs)
  1043. caller.setUp = setUp
  1044. return caller
  1045. # We're simply decorating functions
  1046. @functools.wraps(caller)
  1047. def wrapper(cls):
  1048. if not_available:
  1049. raise SkipTest("Unavailable salt modules: {}".format(*not_available))
  1050. return caller(cls)
  1051. return wrapper
  1052. return decorator
  1053. def skip_if_binaries_missing(*binaries, **kwargs):
  1054. import salt.utils.path
  1055. if len(binaries) == 1:
  1056. if isinstance(binaries[0], (list, tuple, set, frozenset)):
  1057. binaries = binaries[0]
  1058. check_all = kwargs.pop("check_all", False)
  1059. message = kwargs.pop("message", None)
  1060. if kwargs:
  1061. raise RuntimeError(
  1062. "The only supported keyword argument is 'check_all' and "
  1063. "'message'. Invalid keyword arguments: {0}".format(", ".join(kwargs.keys()))
  1064. )
  1065. if check_all:
  1066. for binary in binaries:
  1067. if salt.utils.path.which(binary) is None:
  1068. return skip(
  1069. "{0}The {1!r} binary was not found".format(
  1070. message and "{0}. ".format(message) or "", binary
  1071. )
  1072. )
  1073. elif salt.utils.path.which_bin(binaries) is None:
  1074. return skip(
  1075. "{0}None of the following binaries was found: {1}".format(
  1076. message and "{0}. ".format(message) or "", ", ".join(binaries)
  1077. )
  1078. )
  1079. return _id
  1080. def skip_if_not_root(func):
  1081. # Late import
  1082. from tests.support.runtests import RUNTIME_VARS
  1083. if RUNTIME_VARS.PYTEST_SESSION:
  1084. setattr(func, "__skip_if_not_root__", True)
  1085. if not sys.platform.startswith("win"):
  1086. if os.getuid() != 0:
  1087. func.__unittest_skip__ = True
  1088. func.__unittest_skip_why__ = (
  1089. "You must be logged in as root to run this test"
  1090. )
  1091. else:
  1092. current_user = salt.utils.win_functions.get_current_user()
  1093. if current_user != "SYSTEM":
  1094. if not salt.utils.win_functions.is_admin(current_user):
  1095. func.__unittest_skip__ = True
  1096. func.__unittest_skip_why__ = (
  1097. "You must be logged in as an Administrator to run this test"
  1098. )
  1099. return func
  1100. def repeat(caller=None, condition=True, times=5):
  1101. """
  1102. Repeat a test X amount of times until the first failure.
  1103. .. code-block:: python
  1104. class MyTestCase(TestCase):
  1105. @repeat
  1106. def test_sometimes_works(self):
  1107. pass
  1108. """
  1109. if caller is None:
  1110. return functools.partial(repeat, condition=condition, times=times)
  1111. if isinstance(condition, bool) and condition is False:
  1112. # Don't even decorate
  1113. return caller
  1114. elif callable(condition):
  1115. if condition() is False:
  1116. # Don't even decorate
  1117. return caller
  1118. if inspect.isclass(caller):
  1119. attrs = [n for n in dir(caller) if n.startswith("test_")]
  1120. for attrname in attrs:
  1121. try:
  1122. function = getattr(caller, attrname)
  1123. if not inspect.isfunction(function) and not inspect.ismethod(function):
  1124. continue
  1125. setattr(
  1126. caller,
  1127. attrname,
  1128. repeat(caller=function, condition=condition, times=times),
  1129. )
  1130. except Exception as exc: # pylint: disable=broad-except
  1131. log.exception(exc)
  1132. continue
  1133. return caller
  1134. @functools.wraps(caller)
  1135. def wrap(cls):
  1136. result = None
  1137. for attempt in range(1, times + 1):
  1138. log.info("%s test run %d of %s times", cls, attempt, times)
  1139. caller(cls)
  1140. return cls
  1141. return wrap
  1142. def http_basic_auth(login_cb=lambda username, password: False):
  1143. """
  1144. A crude decorator to force a handler to request HTTP Basic Authentication
  1145. Example usage:
  1146. .. code-block:: python
  1147. @http_basic_auth(lambda u, p: u == 'foo' and p == 'bar')
  1148. class AuthenticatedHandler(salt.ext.tornado.web.RequestHandler):
  1149. pass
  1150. """
  1151. def wrapper(handler_class):
  1152. def wrap_execute(handler_execute):
  1153. def check_auth(handler, kwargs):
  1154. auth = handler.request.headers.get("Authorization")
  1155. if auth is None or not auth.startswith("Basic "):
  1156. # No username/password entered yet, we need to return a 401
  1157. # and set the WWW-Authenticate header to request login.
  1158. handler.set_status(401)
  1159. handler.set_header("WWW-Authenticate", "Basic realm=Restricted")
  1160. else:
  1161. # Strip the 'Basic ' from the beginning of the auth header
  1162. # leaving the base64-encoded secret
  1163. username, password = base64.b64decode(auth[6:]).split(":", 1)
  1164. if login_cb(username, password):
  1165. # Authentication successful
  1166. return
  1167. else:
  1168. # Authentication failed
  1169. handler.set_status(403)
  1170. handler._transforms = []
  1171. handler.finish()
  1172. def _execute(self, transforms, *args, **kwargs):
  1173. check_auth(self, kwargs)
  1174. return handler_execute(self, transforms, *args, **kwargs)
  1175. return _execute
  1176. handler_class._execute = wrap_execute(handler_class._execute)
  1177. return handler_class
  1178. return wrapper
  1179. def generate_random_name(prefix, size=6):
  1180. """
  1181. Generates a random name by combining the provided prefix with a randomly generated
  1182. ascii string.
  1183. .. versionadded:: 2018.3.0
  1184. prefix
  1185. The string to prefix onto the randomly generated ascii string.
  1186. size
  1187. The number of characters to generate. Default: 6.
  1188. """
  1189. salt.utils.versions.warn_until_date(
  1190. "20220101",
  1191. "Please replace your call 'generate_random_name({0})' with 'random_string({0}, lowercase=False)' as "
  1192. "'generate_random_name' will be removed after {{date}}".format(prefix),
  1193. )
  1194. return random_string(prefix, size=size, lowercase=False)
  1195. def random_string(prefix, size=6, uppercase=True, lowercase=True, digits=True):
  1196. """
  1197. Generates a random string.
  1198. ..versionadded: 3001
  1199. Args:
  1200. prefix(str): The prefix for the random string
  1201. size(int): The size of the random string
  1202. uppercase(bool): If true, include uppercased ascii chars in choice sample
  1203. lowercase(bool): If true, include lowercased ascii chars in choice sample
  1204. digits(bool): If true, include digits in choice sample
  1205. Returns:
  1206. str: The random string
  1207. """
  1208. if not any([uppercase, lowercase, digits]):
  1209. raise RuntimeError(
  1210. "At least one of 'uppercase', 'lowercase' or 'digits' needs to be true"
  1211. )
  1212. choices = []
  1213. if uppercase:
  1214. choices.extend(string.ascii_uppercase)
  1215. if lowercase:
  1216. choices.extend(string.ascii_lowercase)
  1217. if digits:
  1218. choices.extend(string.digits)
  1219. return prefix + "".join(random.choice(choices) for _ in range(size))
  1220. class Webserver(object):
  1221. """
  1222. Starts a tornado webserver on 127.0.0.1 on a random available port
  1223. USAGE:
  1224. .. code-block:: python
  1225. from tests.support.helpers import Webserver
  1226. webserver = Webserver('/path/to/web/root')
  1227. webserver.start()
  1228. webserver.stop()
  1229. """
  1230. def __init__(self, root=None, port=None, wait=5, handler=None):
  1231. """
  1232. root
  1233. Root directory of webserver. If not passed, it will default to the
  1234. location of the base environment of the integration suite's file
  1235. roots (tests/integration/files/file/base/)
  1236. port
  1237. Port on which to listen. If not passed, a random one will be chosen
  1238. at the time the start() function is invoked.
  1239. wait : 5
  1240. Number of seconds to wait for the socket to be open before raising
  1241. an exception
  1242. handler
  1243. Can be used to use a subclass of tornado.web.StaticFileHandler,
  1244. such as when enforcing authentication with the http_basic_auth
  1245. decorator.
  1246. """
  1247. if port is not None and not isinstance(port, six.integer_types):
  1248. raise ValueError("port must be an integer")
  1249. if root is None:
  1250. root = RUNTIME_VARS.BASE_FILES
  1251. try:
  1252. self.root = os.path.realpath(root)
  1253. except AttributeError:
  1254. raise ValueError("root must be a string")
  1255. self.port = port
  1256. self.wait = wait
  1257. self.handler = (
  1258. handler if handler is not None else salt.ext.tornado.web.StaticFileHandler
  1259. )
  1260. self.web_root = None
  1261. def target(self):
  1262. """
  1263. Threading target which stands up the tornado application
  1264. """
  1265. self.ioloop = salt.ext.tornado.ioloop.IOLoop()
  1266. self.ioloop.make_current()
  1267. if self.handler == salt.ext.tornado.web.StaticFileHandler:
  1268. self.application = salt.ext.tornado.web.Application(
  1269. [(r"/(.*)", self.handler, {"path": self.root})]
  1270. )
  1271. else:
  1272. self.application = salt.ext.tornado.web.Application(
  1273. [(r"/(.*)", self.handler)]
  1274. )
  1275. self.application.listen(self.port)
  1276. self.ioloop.start()
  1277. @property
  1278. def listening(self):
  1279. if self.port is None:
  1280. return False
  1281. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  1282. return sock.connect_ex(("127.0.0.1", self.port)) == 0
  1283. def url(self, path):
  1284. """
  1285. Convenience function which, given a file path, will return a URL that
  1286. points to that path. If the path is relative, it will just be appended
  1287. to self.web_root.
  1288. """
  1289. if self.web_root is None:
  1290. raise RuntimeError("Webserver instance has not been started")
  1291. err_msg = (
  1292. "invalid path, must be either a relative path or a path "
  1293. "within {0}".format(self.root)
  1294. )
  1295. try:
  1296. relpath = (
  1297. path if not os.path.isabs(path) else os.path.relpath(path, self.root)
  1298. )
  1299. if relpath.startswith(".." + os.sep):
  1300. raise ValueError(err_msg)
  1301. return "/".join((self.web_root, relpath))
  1302. except AttributeError:
  1303. raise ValueError(err_msg)
  1304. def start(self):
  1305. """
  1306. Starts the webserver
  1307. """
  1308. if self.port is None:
  1309. self.port = get_unused_localhost_port()
  1310. self.web_root = "http://127.0.0.1:{0}".format(self.port)
  1311. self.server_thread = threading.Thread(target=self.target)
  1312. self.server_thread.daemon = True
  1313. self.server_thread.start()
  1314. for idx in range(self.wait + 1):
  1315. if self.listening:
  1316. break
  1317. if idx != self.wait:
  1318. time.sleep(1)
  1319. else:
  1320. raise Exception(
  1321. "Failed to start tornado webserver on 127.0.0.1:{0} within "
  1322. "{1} seconds".format(self.port, self.wait)
  1323. )
  1324. def stop(self):
  1325. """
  1326. Stops the webserver
  1327. """
  1328. self.ioloop.add_callback(self.ioloop.stop)
  1329. self.server_thread.join()
  1330. class SaveRequestsPostHandler(salt.ext.tornado.web.RequestHandler):
  1331. """
  1332. Save all requests sent to the server.
  1333. """
  1334. received_requests = []
  1335. def post(self, *args): # pylint: disable=arguments-differ
  1336. """
  1337. Handle the post
  1338. """
  1339. self.received_requests.append(self.request)
  1340. def data_received(self): # pylint: disable=arguments-differ
  1341. """
  1342. Streaming not used for testing
  1343. """
  1344. raise NotImplementedError()
  1345. class MirrorPostHandler(salt.ext.tornado.web.RequestHandler):
  1346. """
  1347. Mirror a POST body back to the client
  1348. """
  1349. def post(self, *args): # pylint: disable=arguments-differ
  1350. """
  1351. Handle the post
  1352. """
  1353. body = self.request.body
  1354. log.debug("Incoming body: %s Incoming args: %s", body, args)
  1355. self.write(body)
  1356. def data_received(self): # pylint: disable=arguments-differ
  1357. """
  1358. Streaming not used for testing
  1359. """
  1360. raise NotImplementedError()
  1361. def dedent(text, linesep=os.linesep):
  1362. """
  1363. A wrapper around textwrap.dedent that also sets line endings.
  1364. """
  1365. linesep = salt.utils.stringutils.to_unicode(linesep)
  1366. unicode_text = textwrap.dedent(salt.utils.stringutils.to_unicode(text))
  1367. clean_text = linesep.join(unicode_text.splitlines())
  1368. if unicode_text.endswith("\n"):
  1369. clean_text += linesep
  1370. if not isinstance(text, six.text_type):
  1371. return salt.utils.stringutils.to_bytes(clean_text)
  1372. return clean_text
  1373. class PatchedEnviron(object):
  1374. def __init__(self, **kwargs):
  1375. self.cleanup_keys = kwargs.pop("__cleanup__", ())
  1376. self.kwargs = kwargs
  1377. self.original_environ = None
  1378. def __enter__(self):
  1379. self.original_environ = os.environ.copy()
  1380. for key in self.cleanup_keys:
  1381. os.environ.pop(key, None)
  1382. # Make sure there are no unicode characters in the self.kwargs if we're
  1383. # on Python 2. These are being added to `os.environ` and causing
  1384. # problems
  1385. if sys.version_info < (3,):
  1386. kwargs = self.kwargs.copy()
  1387. clean_kwargs = {}
  1388. for k in self.kwargs:
  1389. key = k
  1390. if isinstance(key, six.text_type):
  1391. key = key.encode("utf-8")
  1392. if isinstance(self.kwargs[k], six.text_type):
  1393. kwargs[k] = kwargs[k].encode("utf-8")
  1394. clean_kwargs[key] = kwargs[k]
  1395. self.kwargs = clean_kwargs
  1396. os.environ.update(**self.kwargs)
  1397. return self
  1398. def __exit__(self, *args):
  1399. os.environ.clear()
  1400. os.environ.update(self.original_environ)
  1401. patched_environ = PatchedEnviron
  1402. class VirtualEnv(object):
  1403. def __init__(self, venv_dir=None):
  1404. self.venv_dir = venv_dir or tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  1405. if salt.utils.platform.is_windows():
  1406. self.venv_python = os.path.join(self.venv_dir, "Scripts", "python.exe")
  1407. else:
  1408. self.venv_python = os.path.join(self.venv_dir, "bin", "python")
  1409. def __enter__(self):
  1410. try:
  1411. self._create_virtualenv()
  1412. except subprocess.CalledProcessError:
  1413. raise AssertionError("Failed to create virtualenv")
  1414. return self
  1415. def __exit__(self, *args):
  1416. shutil.rmtree(self.venv_dir, ignore_errors=True)
  1417. def install(self, *args):
  1418. subprocess.check_call([self.venv_python, "-m", "pip", "install"] + list(args))
  1419. def _get_real_python(self):
  1420. """
  1421. The reason why the virtualenv creation is proxied by this function is mostly
  1422. because under windows, we can't seem to properly create a virtualenv off of
  1423. another virtualenv(we can on linux) and also because, we really don't want to
  1424. test virtualenv creation off of another virtualenv, we want a virtualenv created
  1425. from the original python.
  1426. Also, on windows, we must also point to the virtualenv binary outside the existing
  1427. virtualenv because it will fail otherwise
  1428. """
  1429. try:
  1430. if salt.utils.platform.is_windows():
  1431. return os.path.join(sys.real_prefix, os.path.basename(sys.executable))
  1432. else:
  1433. python_binary_names = [
  1434. "python{}.{}".format(*sys.version_info),
  1435. "python{}".format(*sys.version_info),
  1436. "python",
  1437. ]
  1438. for binary_name in python_binary_names:
  1439. python = os.path.join(sys.real_prefix, "bin", binary_name)
  1440. if os.path.exists(python):
  1441. break
  1442. else:
  1443. raise AssertionError(
  1444. "Couldn't find a python binary name under '{}' matching: {}".format(
  1445. os.path.join(sys.real_prefix, "bin"), python_binary_names
  1446. )
  1447. )
  1448. return python
  1449. except AttributeError:
  1450. return sys.executable
  1451. def _create_virtualenv(self):
  1452. sminion = create_sminion()
  1453. sminion.functions.virtualenv.create(
  1454. self.venv_dir, python=self._get_real_python()
  1455. )