1
0

helpers.py 60 KB

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