1
0

helpers.py 58 KB

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