1
0

helpers.py 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736
  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(password=password)
  626. hashed_password = "'{0}'".format(hashed_password)
  627. add_pwd = cls.run_function(
  628. "shadow.set_password", [username, hashed_password]
  629. )
  630. failure = None
  631. try:
  632. try:
  633. return func(cls, username)
  634. except Exception as exc: # pylint: disable=W0703
  635. log.error(
  636. "Running {0!r} raised an exception: {1}".format(func, exc),
  637. exc_info=True,
  638. )
  639. # Store the original exception details which will be raised
  640. # a little further down the code
  641. failure = sys.exc_info()
  642. finally:
  643. if delete:
  644. delete_user = cls.run_function(
  645. "user.delete", [username, True, True], timeout=60
  646. )
  647. if not delete_user:
  648. if failure is None:
  649. log.warning(
  650. "Although the actual test-case did not fail, "
  651. "deleting the created system user {0!r} "
  652. "afterwards did.".format(username)
  653. )
  654. else:
  655. log.warning(
  656. "The test-case failed and also did the removal"
  657. " of the system user {0!r}".format(username)
  658. )
  659. if failure is not None:
  660. # If an exception was thrown, raise it
  661. six.reraise(failure[0], failure[1], failure[2])
  662. return wrap
  663. return decorator
  664. def with_system_group(group, on_existing="delete", delete=True):
  665. """
  666. Create and optionally destroy a system group to be used within a test
  667. case. The system user is crated using the ``group`` salt module.
  668. The decorated testcase function must accept 'group' as an argument.
  669. :param group: The desired group name for the system user.
  670. :param on_existing: What to do when the desired username is taken. The
  671. available options are:
  672. * nothing: Do nothing, act as if the group was created
  673. * delete: delete and re-create the existing user
  674. * skip: skip the test case
  675. """
  676. if on_existing not in ("nothing", "delete", "skip"):
  677. raise RuntimeError(
  678. "The value of 'on_existing' can only be one of, "
  679. "'nothing', 'delete' and 'skip'"
  680. )
  681. if not isinstance(delete, bool):
  682. raise RuntimeError("The value of 'delete' can only be 'True' or 'False'")
  683. def decorator(func):
  684. @functools.wraps(func)
  685. def wrap(cls):
  686. # Let's add the user to the system.
  687. log.debug("Creating system group {0!r}".format(group))
  688. create_group = cls.run_function("group.add", [group])
  689. if not create_group:
  690. log.debug("Failed to create system group")
  691. # The group was not created
  692. if on_existing == "skip":
  693. cls.skipTest("Failed to create system group {0!r}".format(group))
  694. if on_existing == "delete":
  695. log.debug("Deleting the system group {0!r}".format(group))
  696. delete_group = cls.run_function("group.delete", [group])
  697. if not delete_group:
  698. cls.skipTest(
  699. "A group named {0!r} already existed on the "
  700. "system and re-creating it was not possible".format(group)
  701. )
  702. log.debug("Second time creating system group {0!r}".format(group))
  703. create_group = cls.run_function("group.add", [group])
  704. if not create_group:
  705. cls.skipTest(
  706. "A group named {0!r} already existed, was deleted "
  707. "as requested, but re-creating it was not possible".format(
  708. group
  709. )
  710. )
  711. failure = None
  712. try:
  713. try:
  714. return func(cls, group)
  715. except Exception as exc: # pylint: disable=W0703
  716. log.error(
  717. "Running {0!r} raised an exception: {1}".format(func, exc),
  718. exc_info=True,
  719. )
  720. # Store the original exception details which will be raised
  721. # a little further down the code
  722. failure = sys.exc_info()
  723. finally:
  724. if delete:
  725. delete_group = cls.run_function("group.delete", [group])
  726. if not delete_group:
  727. if failure is None:
  728. log.warning(
  729. "Although the actual test-case did not fail, "
  730. "deleting the created system group {0!r} "
  731. "afterwards did.".format(group)
  732. )
  733. else:
  734. log.warning(
  735. "The test-case failed and also did the removal"
  736. " of the system group {0!r}".format(group)
  737. )
  738. if failure is not None:
  739. # If an exception was thrown, raise it
  740. six.reraise(failure[0], failure[1], failure[2])
  741. return wrap
  742. return decorator
  743. def with_system_user_and_group(username, group, on_existing="delete", delete=True):
  744. """
  745. Create and optionally destroy a system user and group to be used within a
  746. test case. The system user is crated using the ``user`` salt module, and
  747. the system group is created with the ``group`` salt module.
  748. The decorated testcase function must accept both the 'username' and 'group'
  749. arguments.
  750. :param username: The desired username for the system user.
  751. :param group: The desired name for the system group.
  752. :param on_existing: What to do when the desired username is taken. The
  753. available options are:
  754. * nothing: Do nothing, act as if the user was created.
  755. * delete: delete and re-create the existing user
  756. * skip: skip the test case
  757. """
  758. if on_existing not in ("nothing", "delete", "skip"):
  759. raise RuntimeError(
  760. "The value of 'on_existing' can only be one of, "
  761. "'nothing', 'delete' and 'skip'"
  762. )
  763. if not isinstance(delete, bool):
  764. raise RuntimeError("The value of 'delete' can only be 'True' or 'False'")
  765. def decorator(func):
  766. @functools.wraps(func)
  767. def wrap(cls):
  768. # Let's add the user to the system.
  769. log.debug("Creating system user {0!r}".format(username))
  770. create_user = cls.run_function("user.add", [username])
  771. log.debug("Creating system group {0!r}".format(group))
  772. create_group = cls.run_function("group.add", [group])
  773. if not create_user:
  774. log.debug("Failed to create system user")
  775. # The user was not created
  776. if on_existing == "skip":
  777. cls.skipTest("Failed to create system user {0!r}".format(username))
  778. if on_existing == "delete":
  779. log.debug("Deleting the system user {0!r}".format(username))
  780. delete_user = cls.run_function(
  781. "user.delete", [username, True, True]
  782. )
  783. if not delete_user:
  784. cls.skipTest(
  785. "A user named {0!r} already existed on the "
  786. "system and re-creating it was not possible".format(
  787. username
  788. )
  789. )
  790. log.debug("Second time creating system user {0!r}".format(username))
  791. create_user = cls.run_function("user.add", [username])
  792. if not create_user:
  793. cls.skipTest(
  794. "A user named {0!r} already existed, was deleted "
  795. "as requested, but re-creating it was not possible".format(
  796. username
  797. )
  798. )
  799. if not create_group:
  800. log.debug("Failed to create system group")
  801. # The group was not created
  802. if on_existing == "skip":
  803. cls.skipTest("Failed to create system group {0!r}".format(group))
  804. if on_existing == "delete":
  805. log.debug("Deleting the system group {0!r}".format(group))
  806. delete_group = cls.run_function("group.delete", [group])
  807. if not delete_group:
  808. cls.skipTest(
  809. "A group named {0!r} already existed on the "
  810. "system and re-creating it was not possible".format(group)
  811. )
  812. log.debug("Second time creating system group {0!r}".format(group))
  813. create_group = cls.run_function("group.add", [group])
  814. if not create_group:
  815. cls.skipTest(
  816. "A group named {0!r} already existed, was deleted "
  817. "as requested, but re-creating it was not possible".format(
  818. group
  819. )
  820. )
  821. failure = None
  822. try:
  823. try:
  824. return func(cls, username, group)
  825. except Exception as exc: # pylint: disable=W0703
  826. log.error(
  827. "Running {0!r} raised an exception: {1}".format(func, exc),
  828. exc_info=True,
  829. )
  830. # Store the original exception details which will be raised
  831. # a little further down the code
  832. failure = sys.exc_info()
  833. finally:
  834. if delete:
  835. delete_user = cls.run_function(
  836. "user.delete", [username, True, True]
  837. )
  838. delete_group = cls.run_function("group.delete", [group])
  839. if not delete_user:
  840. if failure is None:
  841. log.warning(
  842. "Although the actual test-case did not fail, "
  843. "deleting the created system user {0!r} "
  844. "afterwards did.".format(username)
  845. )
  846. else:
  847. log.warning(
  848. "The test-case failed and also did the removal"
  849. " of the system user {0!r}".format(username)
  850. )
  851. if not delete_group:
  852. if failure is None:
  853. log.warning(
  854. "Although the actual test-case did not fail, "
  855. "deleting the created system group {0!r} "
  856. "afterwards did.".format(group)
  857. )
  858. else:
  859. log.warning(
  860. "The test-case failed and also did the removal"
  861. " of the system group {0!r}".format(group)
  862. )
  863. if failure is not None:
  864. # If an exception was thrown, raise it
  865. six.reraise(failure[0], failure[1], failure[2])
  866. return wrap
  867. return decorator
  868. class WithTempfile(object):
  869. def __init__(self, **kwargs):
  870. self.create = kwargs.pop("create", True)
  871. if "dir" not in kwargs:
  872. kwargs["dir"] = RUNTIME_VARS.TMP
  873. if "prefix" not in kwargs:
  874. kwargs["prefix"] = "__salt.test."
  875. self.kwargs = kwargs
  876. def __call__(self, func):
  877. self.func = func
  878. return functools.wraps(func)(
  879. # pylint: disable=unnecessary-lambda
  880. lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs)
  881. # pylint: enable=unnecessary-lambda
  882. )
  883. def wrap(self, testcase, *args, **kwargs):
  884. name = salt.utils.files.mkstemp(**self.kwargs)
  885. if not self.create:
  886. os.remove(name)
  887. try:
  888. return self.func(testcase, name, *args, **kwargs)
  889. finally:
  890. try:
  891. os.remove(name)
  892. except OSError:
  893. pass
  894. with_tempfile = WithTempfile
  895. class WithTempdir(object):
  896. def __init__(self, **kwargs):
  897. self.create = kwargs.pop("create", True)
  898. if "dir" not in kwargs:
  899. kwargs["dir"] = RUNTIME_VARS.TMP
  900. self.kwargs = kwargs
  901. def __call__(self, func):
  902. self.func = func
  903. return functools.wraps(func)(
  904. # pylint: disable=unnecessary-lambda
  905. lambda testcase, *args, **kwargs: self.wrap(testcase, *args, **kwargs)
  906. # pylint: enable=unnecessary-lambda
  907. )
  908. def wrap(self, testcase, *args, **kwargs):
  909. tempdir = tempfile.mkdtemp(**self.kwargs)
  910. if not self.create:
  911. os.rmdir(tempdir)
  912. try:
  913. return self.func(testcase, tempdir, *args, **kwargs)
  914. finally:
  915. shutil.rmtree(tempdir, ignore_errors=True)
  916. with_tempdir = WithTempdir
  917. def requires_system_grains(func):
  918. """
  919. Function decorator which loads and passes the system's grains to the test
  920. case.
  921. """
  922. @functools.wraps(func)
  923. def decorator(*args, **kwargs):
  924. if not hasattr(requires_system_grains, "__grains__"):
  925. # Late import
  926. from tests.support.sminion import build_minion_opts
  927. opts = build_minion_opts(minion_id="runtests-internal-sminion")
  928. requires_system_grains.__grains__ = salt.loader.grains(opts)
  929. kwargs["grains"] = requires_system_grains.__grains__
  930. return func(*args, **kwargs)
  931. return decorator
  932. @requires_system_grains
  933. def runs_on(grains=None, **kwargs):
  934. """
  935. Skip the test if grains don't match the values passed into **kwargs
  936. if a kwarg value is a list then skip if the grains don't match any item in the list
  937. """
  938. reason = kwargs.pop("reason", None)
  939. for kw, value in kwargs.items():
  940. if isinstance(value, list):
  941. if not any(str(grains.get(kw)).lower() != str(v).lower() for v in value):
  942. if reason is None:
  943. reason = "This test does not run on {}={}".format(
  944. kw, grains.get(kw)
  945. )
  946. return skip(reason)
  947. else:
  948. if str(grains.get(kw)).lower() != str(value).lower():
  949. if reason is None:
  950. reason = "This test runs on {}={}, not {}".format(
  951. kw, value, grains.get(kw)
  952. )
  953. return skip(reason)
  954. return _id
  955. @requires_system_grains
  956. def not_runs_on(grains=None, **kwargs):
  957. """
  958. Reverse of `runs_on`.
  959. Skip the test if any grains match the values passed into **kwargs
  960. if a kwarg value is a list then skip if the grains match any item in the list
  961. """
  962. reason = kwargs.pop("reason", None)
  963. for kw, value in kwargs.items():
  964. if isinstance(value, list):
  965. if any(str(grains.get(kw)).lower() == str(v).lower() for v in value):
  966. if reason is None:
  967. reason = "This test does not run on {}={}".format(
  968. kw, grains.get(kw)
  969. )
  970. return skip(reason)
  971. else:
  972. if str(grains.get(kw)).lower() == str(value).lower():
  973. if reason is None:
  974. reason = "This test does not run on {}={}, got {}".format(
  975. kw, value, grains.get(kw)
  976. )
  977. return skip(reason)
  978. return _id
  979. def _check_required_sminion_attributes(sminion_attr, *required_items):
  980. """
  981. :param sminion_attr: The name of the sminion attribute to check, such as 'functions' or 'states'
  982. :param required_items: The items that must be part of the designated sminion attribute for the decorated test
  983. :return The packages that are not available
  984. """
  985. # Late import
  986. from tests.support.sminion import create_sminion
  987. required_salt_items = set(required_items)
  988. sminion = create_sminion(minion_id="runtests-internal-sminion")
  989. available_items = list(getattr(sminion, sminion_attr))
  990. not_available_items = set()
  991. name = "__not_available_{items}s__".format(items=sminion_attr)
  992. if not hasattr(sminion, name):
  993. setattr(sminion, name, set())
  994. cached_not_available_items = getattr(sminion, name)
  995. for not_available_item in cached_not_available_items:
  996. if not_available_item in required_salt_items:
  997. not_available_items.add(not_available_item)
  998. required_salt_items.remove(not_available_item)
  999. for required_item_name in required_salt_items:
  1000. search_name = required_item_name
  1001. if "." not in search_name:
  1002. search_name += ".*"
  1003. if not fnmatch.filter(available_items, search_name):
  1004. not_available_items.add(required_item_name)
  1005. cached_not_available_items.add(required_item_name)
  1006. return not_available_items
  1007. def requires_salt_states(*names):
  1008. """
  1009. Makes sure the passed salt state is available. Skips the test if not
  1010. .. versionadded:: 3000
  1011. """
  1012. not_available = _check_required_sminion_attributes("states", *names)
  1013. if not_available:
  1014. return skip("Unavailable salt states: {}".format(*not_available))
  1015. return _id
  1016. def requires_salt_modules(*names):
  1017. """
  1018. Makes sure the passed salt module is available. Skips the test if not
  1019. .. versionadded:: 0.5.2
  1020. """
  1021. not_available = _check_required_sminion_attributes("functions", *names)
  1022. if not_available:
  1023. return skip("Unavailable salt modules: {}".format(*not_available))
  1024. return _id
  1025. def skip_if_binaries_missing(*binaries, **kwargs):
  1026. import salt.utils.path
  1027. if len(binaries) == 1:
  1028. if isinstance(binaries[0], (list, tuple, set, frozenset)):
  1029. binaries = binaries[0]
  1030. check_all = kwargs.pop("check_all", False)
  1031. message = kwargs.pop("message", None)
  1032. if kwargs:
  1033. raise RuntimeError(
  1034. "The only supported keyword argument is 'check_all' and "
  1035. "'message'. Invalid keyword arguments: {0}".format(", ".join(kwargs.keys()))
  1036. )
  1037. if check_all:
  1038. for binary in binaries:
  1039. if salt.utils.path.which(binary) is None:
  1040. return skip(
  1041. "{0}The {1!r} binary was not found".format(
  1042. message and "{0}. ".format(message) or "", binary
  1043. )
  1044. )
  1045. elif salt.utils.path.which_bin(binaries) is None:
  1046. return skip(
  1047. "{0}None of the following binaries was found: {1}".format(
  1048. message and "{0}. ".format(message) or "", ", ".join(binaries)
  1049. )
  1050. )
  1051. return _id
  1052. def skip_if_not_root(func):
  1053. # Late import
  1054. from tests.support.runtests import RUNTIME_VARS
  1055. if RUNTIME_VARS.PYTEST_SESSION:
  1056. setattr(func, "__skip_if_not_root__", True)
  1057. if not sys.platform.startswith("win"):
  1058. if os.getuid() != 0:
  1059. func.__unittest_skip__ = True
  1060. func.__unittest_skip_why__ = (
  1061. "You must be logged in as root to run this test"
  1062. )
  1063. else:
  1064. current_user = salt.utils.win_functions.get_current_user()
  1065. if current_user != "SYSTEM":
  1066. if not salt.utils.win_functions.is_admin(current_user):
  1067. func.__unittest_skip__ = True
  1068. func.__unittest_skip_why__ = (
  1069. "You must be logged in as an Administrator to run this test"
  1070. )
  1071. return func
  1072. def repeat(caller=None, condition=True, times=5):
  1073. """
  1074. Repeat a test X amount of times until the first failure.
  1075. .. code-block:: python
  1076. class MyTestCase(TestCase):
  1077. @repeat
  1078. def test_sometimes_works(self):
  1079. pass
  1080. """
  1081. if caller is None:
  1082. return functools.partial(repeat, condition=condition, times=times)
  1083. if isinstance(condition, bool) and condition is False:
  1084. # Don't even decorate
  1085. return caller
  1086. elif callable(condition):
  1087. if condition() is False:
  1088. # Don't even decorate
  1089. return caller
  1090. if inspect.isclass(caller):
  1091. attrs = [n for n in dir(caller) if n.startswith("test_")]
  1092. for attrname in attrs:
  1093. try:
  1094. function = getattr(caller, attrname)
  1095. if not inspect.isfunction(function) and not inspect.ismethod(function):
  1096. continue
  1097. setattr(
  1098. caller,
  1099. attrname,
  1100. repeat(caller=function, condition=condition, times=times),
  1101. )
  1102. except Exception as exc: # pylint: disable=broad-except
  1103. log.exception(exc)
  1104. continue
  1105. return caller
  1106. @functools.wraps(caller)
  1107. def wrap(cls):
  1108. result = None
  1109. for attempt in range(1, times + 1):
  1110. log.info("%s test run %d of %s times", cls, attempt, times)
  1111. caller(cls)
  1112. return cls
  1113. return wrap
  1114. def http_basic_auth(login_cb=lambda username, password: False):
  1115. """
  1116. A crude decorator to force a handler to request HTTP Basic Authentication
  1117. Example usage:
  1118. .. code-block:: python
  1119. @http_basic_auth(lambda u, p: u == 'foo' and p == 'bar')
  1120. class AuthenticatedHandler(salt.ext.tornado.web.RequestHandler):
  1121. pass
  1122. """
  1123. def wrapper(handler_class):
  1124. def wrap_execute(handler_execute):
  1125. def check_auth(handler, kwargs):
  1126. auth = handler.request.headers.get("Authorization")
  1127. if auth is None or not auth.startswith("Basic "):
  1128. # No username/password entered yet, we need to return a 401
  1129. # and set the WWW-Authenticate header to request login.
  1130. handler.set_status(401)
  1131. handler.set_header("WWW-Authenticate", "Basic realm=Restricted")
  1132. else:
  1133. # Strip the 'Basic ' from the beginning of the auth header
  1134. # leaving the base64-encoded secret
  1135. username, password = base64.b64decode(auth[6:]).split(":", 1)
  1136. if login_cb(username, password):
  1137. # Authentication successful
  1138. return
  1139. else:
  1140. # Authentication failed
  1141. handler.set_status(403)
  1142. handler._transforms = []
  1143. handler.finish()
  1144. def _execute(self, transforms, *args, **kwargs):
  1145. check_auth(self, kwargs)
  1146. return handler_execute(self, transforms, *args, **kwargs)
  1147. return _execute
  1148. handler_class._execute = wrap_execute(handler_class._execute)
  1149. return handler_class
  1150. return wrapper
  1151. def generate_random_name(prefix, size=6):
  1152. """
  1153. Generates a random name by combining the provided prefix with a randomly generated
  1154. ascii string.
  1155. .. versionadded:: 2018.3.0
  1156. prefix
  1157. The string to prefix onto the randomly generated ascii string.
  1158. size
  1159. The number of characters to generate. Default: 6.
  1160. """
  1161. salt.utils.versions.warn_until_date(
  1162. "20220101",
  1163. "Please replace your call 'generate_random_name({0})' with 'random_string({0}, lowercase=False)' as "
  1164. "'generate_random_name' will be removed after {{date}}".format(prefix),
  1165. )
  1166. return random_string(prefix, size=size, lowercase=False)
  1167. def random_string(prefix, size=6, uppercase=True, lowercase=True, digits=True):
  1168. """
  1169. Generates a random string.
  1170. ..versionadded: 3001
  1171. Args:
  1172. prefix(str): The prefix for the random string
  1173. size(int): The size of the random string
  1174. uppercase(bool): If true, include uppercased ascii chars in choice sample
  1175. lowercase(bool): If true, include lowercased ascii chars in choice sample
  1176. digits(bool): If true, include digits in choice sample
  1177. Returns:
  1178. str: The random string
  1179. """
  1180. if not any([uppercase, lowercase, digits]):
  1181. raise RuntimeError(
  1182. "At least one of 'uppercase', 'lowercase' or 'digits' needs to be true"
  1183. )
  1184. choices = []
  1185. if uppercase:
  1186. choices.extend(string.ascii_uppercase)
  1187. if lowercase:
  1188. choices.extend(string.ascii_lowercase)
  1189. if digits:
  1190. choices.extend(string.digits)
  1191. return prefix + "".join(random.choice(choices) for _ in range(size))
  1192. class Webserver(object):
  1193. """
  1194. Starts a tornado webserver on 127.0.0.1 on a random available port
  1195. USAGE:
  1196. .. code-block:: python
  1197. from tests.support.helpers import Webserver
  1198. webserver = Webserver('/path/to/web/root')
  1199. webserver.start()
  1200. webserver.stop()
  1201. """
  1202. def __init__(self, root=None, port=None, wait=5, handler=None):
  1203. """
  1204. root
  1205. Root directory of webserver. If not passed, it will default to the
  1206. location of the base environment of the integration suite's file
  1207. roots (tests/integration/files/file/base/)
  1208. port
  1209. Port on which to listen. If not passed, a random one will be chosen
  1210. at the time the start() function is invoked.
  1211. wait : 5
  1212. Number of seconds to wait for the socket to be open before raising
  1213. an exception
  1214. handler
  1215. Can be used to use a subclass of tornado.web.StaticFileHandler,
  1216. such as when enforcing authentication with the http_basic_auth
  1217. decorator.
  1218. """
  1219. if port is not None and not isinstance(port, six.integer_types):
  1220. raise ValueError("port must be an integer")
  1221. if root is None:
  1222. root = RUNTIME_VARS.BASE_FILES
  1223. try:
  1224. self.root = os.path.realpath(root)
  1225. except AttributeError:
  1226. raise ValueError("root must be a string")
  1227. self.port = port
  1228. self.wait = wait
  1229. self.handler = (
  1230. handler if handler is not None else salt.ext.tornado.web.StaticFileHandler
  1231. )
  1232. self.web_root = None
  1233. def target(self):
  1234. """
  1235. Threading target which stands up the tornado application
  1236. """
  1237. self.ioloop = salt.ext.tornado.ioloop.IOLoop()
  1238. self.ioloop.make_current()
  1239. if self.handler == salt.ext.tornado.web.StaticFileHandler:
  1240. self.application = salt.ext.tornado.web.Application(
  1241. [(r"/(.*)", self.handler, {"path": self.root})]
  1242. )
  1243. else:
  1244. self.application = salt.ext.tornado.web.Application(
  1245. [(r"/(.*)", self.handler)]
  1246. )
  1247. self.application.listen(self.port)
  1248. self.ioloop.start()
  1249. @property
  1250. def listening(self):
  1251. if self.port is None:
  1252. return False
  1253. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  1254. return sock.connect_ex(("127.0.0.1", self.port)) == 0
  1255. def url(self, path):
  1256. """
  1257. Convenience function which, given a file path, will return a URL that
  1258. points to that path. If the path is relative, it will just be appended
  1259. to self.web_root.
  1260. """
  1261. if self.web_root is None:
  1262. raise RuntimeError("Webserver instance has not been started")
  1263. err_msg = (
  1264. "invalid path, must be either a relative path or a path "
  1265. "within {0}".format(self.root)
  1266. )
  1267. try:
  1268. relpath = (
  1269. path if not os.path.isabs(path) else os.path.relpath(path, self.root)
  1270. )
  1271. if relpath.startswith(".." + os.sep):
  1272. raise ValueError(err_msg)
  1273. return "/".join((self.web_root, relpath))
  1274. except AttributeError:
  1275. raise ValueError(err_msg)
  1276. def start(self):
  1277. """
  1278. Starts the webserver
  1279. """
  1280. if self.port is None:
  1281. self.port = get_unused_localhost_port()
  1282. self.web_root = "http://127.0.0.1:{0}".format(self.port)
  1283. self.server_thread = threading.Thread(target=self.target)
  1284. self.server_thread.daemon = True
  1285. self.server_thread.start()
  1286. for idx in range(self.wait + 1):
  1287. if self.listening:
  1288. break
  1289. if idx != self.wait:
  1290. time.sleep(1)
  1291. else:
  1292. raise Exception(
  1293. "Failed to start tornado webserver on 127.0.0.1:{0} within "
  1294. "{1} seconds".format(self.port, self.wait)
  1295. )
  1296. def stop(self):
  1297. """
  1298. Stops the webserver
  1299. """
  1300. self.ioloop.add_callback(self.ioloop.stop)
  1301. self.server_thread.join()
  1302. class SaveRequestsPostHandler(salt.ext.tornado.web.RequestHandler):
  1303. """
  1304. Save all requests sent to the server.
  1305. """
  1306. received_requests = []
  1307. def post(self, *args): # pylint: disable=arguments-differ
  1308. """
  1309. Handle the post
  1310. """
  1311. self.received_requests.append(self.request)
  1312. def data_received(self): # pylint: disable=arguments-differ
  1313. """
  1314. Streaming not used for testing
  1315. """
  1316. raise NotImplementedError()
  1317. class MirrorPostHandler(salt.ext.tornado.web.RequestHandler):
  1318. """
  1319. Mirror a POST body back to the client
  1320. """
  1321. def post(self, *args): # pylint: disable=arguments-differ
  1322. """
  1323. Handle the post
  1324. """
  1325. body = self.request.body
  1326. log.debug("Incoming body: %s Incoming args: %s", body, args)
  1327. self.write(body)
  1328. def data_received(self): # pylint: disable=arguments-differ
  1329. """
  1330. Streaming not used for testing
  1331. """
  1332. raise NotImplementedError()
  1333. def dedent(text, linesep=os.linesep):
  1334. """
  1335. A wrapper around textwrap.dedent that also sets line endings.
  1336. """
  1337. linesep = salt.utils.stringutils.to_unicode(linesep)
  1338. unicode_text = textwrap.dedent(salt.utils.stringutils.to_unicode(text))
  1339. clean_text = linesep.join(unicode_text.splitlines())
  1340. if unicode_text.endswith("\n"):
  1341. clean_text += linesep
  1342. if not isinstance(text, six.text_type):
  1343. return salt.utils.stringutils.to_bytes(clean_text)
  1344. return clean_text
  1345. class PatchedEnviron(object):
  1346. def __init__(self, **kwargs):
  1347. self.cleanup_keys = kwargs.pop("__cleanup__", ())
  1348. self.kwargs = kwargs
  1349. self.original_environ = None
  1350. def __enter__(self):
  1351. self.original_environ = os.environ.copy()
  1352. for key in self.cleanup_keys:
  1353. os.environ.pop(key, None)
  1354. # Make sure there are no unicode characters in the self.kwargs if we're
  1355. # on Python 2. These are being added to `os.environ` and causing
  1356. # problems
  1357. if sys.version_info < (3,):
  1358. kwargs = self.kwargs.copy()
  1359. clean_kwargs = {}
  1360. for k in self.kwargs:
  1361. key = k
  1362. if isinstance(key, six.text_type):
  1363. key = key.encode("utf-8")
  1364. if isinstance(self.kwargs[k], six.text_type):
  1365. kwargs[k] = kwargs[k].encode("utf-8")
  1366. clean_kwargs[key] = kwargs[k]
  1367. self.kwargs = clean_kwargs
  1368. os.environ.update(**self.kwargs)
  1369. return self
  1370. def __exit__(self, *args):
  1371. os.environ.clear()
  1372. os.environ.update(self.original_environ)
  1373. patched_environ = PatchedEnviron
  1374. class VirtualEnv(object):
  1375. def __init__(self, venv_dir=None):
  1376. self.venv_dir = venv_dir or tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  1377. if salt.utils.platform.is_windows():
  1378. self.venv_python = os.path.join(self.venv_dir, "Scripts", "python.exe")
  1379. else:
  1380. self.venv_python = os.path.join(self.venv_dir, "bin", "python")
  1381. self.venv_bin_dir = os.path.dirname(self.venv_python)
  1382. def __enter__(self):
  1383. try:
  1384. self._create_virtualenv()
  1385. except subprocess.CalledProcessError:
  1386. raise AssertionError("Failed to create virtualenv")
  1387. return self
  1388. def __exit__(self, *args):
  1389. salt.utils.files.rm_rf(self.venv_dir)
  1390. def install(self, *args, **kwargs):
  1391. return self.run(self.venv_python, "-m", "pip", "install", *args, **kwargs)
  1392. def run(self, *args, **kwargs):
  1393. check = kwargs.pop("check", True)
  1394. kwargs.setdefault("cwd", self.venv_dir)
  1395. kwargs.setdefault("stdout", subprocess.PIPE)
  1396. kwargs.setdefault("stderr", subprocess.PIPE)
  1397. kwargs.setdefault("universal_newlines", True)
  1398. proc = subprocess.run(args, check=False, **kwargs)
  1399. ret = ProcessResult(proc.returncode, proc.stdout, proc.stderr, proc.args)
  1400. log.debug(ret)
  1401. if check is True:
  1402. proc.check_returncode()
  1403. return ret
  1404. def _get_real_python(self):
  1405. """
  1406. The reason why the virtualenv creation is proxied by this function is mostly
  1407. because under windows, we can't seem to properly create a virtualenv off of
  1408. another virtualenv(we can on linux) and also because, we really don't want to
  1409. test virtualenv creation off of another virtualenv, we want a virtualenv created
  1410. from the original python.
  1411. Also, on windows, we must also point to the virtualenv binary outside the existing
  1412. virtualenv because it will fail otherwise
  1413. """
  1414. try:
  1415. if salt.utils.platform.is_windows():
  1416. return os.path.join(sys.real_prefix, os.path.basename(sys.executable))
  1417. else:
  1418. python_binary_names = [
  1419. "python{}.{}".format(*sys.version_info),
  1420. "python{}".format(*sys.version_info),
  1421. "python",
  1422. ]
  1423. for binary_name in python_binary_names:
  1424. python = os.path.join(sys.real_prefix, "bin", binary_name)
  1425. if os.path.exists(python):
  1426. break
  1427. else:
  1428. raise AssertionError(
  1429. "Couldn't find a python binary name under '{}' matching: {}".format(
  1430. os.path.join(sys.real_prefix, "bin"), python_binary_names
  1431. )
  1432. )
  1433. return python
  1434. except AttributeError:
  1435. return sys.executable
  1436. def _create_virtualenv(self):
  1437. sminion = create_sminion()
  1438. sminion.functions.virtualenv.create(
  1439. self.venv_dir, python=self._get_real_python()
  1440. )
  1441. @contextmanager
  1442. def change_cwd(path):
  1443. """
  1444. Context manager helper to change CWD for a with code block and restore
  1445. it at the end
  1446. """
  1447. old_cwd = os.getcwd()
  1448. try:
  1449. os.chdir(path)
  1450. # Do stuff
  1451. yield
  1452. finally:
  1453. # Restore Old CWD
  1454. os.chdir(old_cwd)