helpers.py 60 KB

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