1
0

helpers.py 57 KB

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