helpers.py 58 KB

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