1
0

mixins.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  4. =============
  5. Class Mix-Ins
  6. =============
  7. Some reusable class Mixins
  8. '''
  9. # pylint: disable=repr-flag-used-in-string
  10. # Import python libs
  11. from __future__ import absolute_import, print_function
  12. import os
  13. import sys
  14. import time
  15. import types
  16. import atexit
  17. import pprint
  18. import logging
  19. import tempfile
  20. import functools
  21. import subprocess
  22. import multiprocessing
  23. # Import Salt Testing Libs
  24. from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch
  25. from tests.support.runtests import RUNTIME_VARS
  26. from tests.support.paths import CODE_DIR
  27. # Import salt libs
  28. import salt.config
  29. import salt.utils.event
  30. import salt.utils.files
  31. import salt.utils.functools
  32. import salt.utils.path
  33. import salt.utils.stringutils
  34. import salt.utils.yaml
  35. import salt.version
  36. import salt.exceptions
  37. import salt.utils.process
  38. from salt.utils.verify import verify_env
  39. from salt.utils.immutabletypes import freeze
  40. from salt._compat import ElementTree as etree
  41. # Import 3rd-party libs
  42. from salt.ext import six
  43. from salt.ext.six.moves import zip # pylint: disable=import-error,redefined-builtin
  44. from salt.ext.six.moves.queue import Empty # pylint: disable=import-error,no-name-in-module
  45. log = logging.getLogger(__name__)
  46. class CheckShellBinaryNameAndVersionMixin(object):
  47. '''
  48. Simple class mix-in to subclass in companion to :class:`ShellTestCase<tests.support.case.ShellTestCase>` which
  49. adds a test case to verify proper version report from Salt's CLI tools.
  50. '''
  51. _call_binary_ = None
  52. _call_binary_expected_version_ = None
  53. def test_version_includes_binary_name(self):
  54. if getattr(self, '_call_binary_', None) is None:
  55. self.skipTest('\'_call_binary_\' not defined.')
  56. if self._call_binary_expected_version_ is None:
  57. # Late import
  58. self._call_binary_expected_version_ = salt.version.__version__
  59. out = '\n'.join(self.run_script(self._call_binary_, '--version'))
  60. self.assertIn(self._call_binary_, out)
  61. self.assertIn(self._call_binary_expected_version_, out)
  62. class AdaptedConfigurationTestCaseMixin(object):
  63. __slots__ = ()
  64. @staticmethod
  65. def get_temp_config(config_for, **config_overrides):
  66. rootdir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  67. conf_dir = os.path.join(rootdir, 'conf')
  68. for key in ('cachedir', 'pki_dir', 'sock_dir'):
  69. if key not in config_overrides:
  70. config_overrides[key] = key
  71. if 'log_file' not in config_overrides:
  72. config_overrides['log_file'] = 'logs/{}.log'.format(config_for)
  73. if 'user' not in config_overrides:
  74. config_overrides['user'] = RUNTIME_VARS.RUNNING_TESTS_USER
  75. config_overrides['root_dir'] = rootdir
  76. cdict = AdaptedConfigurationTestCaseMixin.get_config(config_for, from_scratch=True)
  77. if config_for in ('master', 'client_config'):
  78. rdict = salt.config.apply_master_config(config_overrides, cdict)
  79. if config_for == 'minion':
  80. rdict = salt.config.apply_minion_config(config_overrides, cdict)
  81. verify_env([os.path.join(rdict['pki_dir'], 'minions'),
  82. os.path.join(rdict['pki_dir'], 'minions_pre'),
  83. os.path.join(rdict['pki_dir'], 'minions_rejected'),
  84. os.path.join(rdict['pki_dir'], 'minions_denied'),
  85. os.path.join(rdict['cachedir'], 'jobs'),
  86. os.path.join(rdict['cachedir'], 'raet'),
  87. os.path.join(rdict['cachedir'], 'tokens'),
  88. os.path.join(rdict['root_dir'], 'cache', 'tokens'),
  89. os.path.join(rdict['pki_dir'], 'accepted'),
  90. os.path.join(rdict['pki_dir'], 'rejected'),
  91. os.path.join(rdict['pki_dir'], 'pending'),
  92. os.path.dirname(rdict['log_file']),
  93. rdict['sock_dir'],
  94. conf_dir
  95. ],
  96. RUNTIME_VARS.RUNNING_TESTS_USER,
  97. root_dir=rdict['root_dir'],
  98. )
  99. rdict['config_dir'] = conf_dir
  100. rdict['conf_file'] = os.path.join(conf_dir, config_for)
  101. with salt.utils.files.fopen(rdict['conf_file'], 'w') as wfh:
  102. salt.utils.yaml.safe_dump(rdict, wfh, default_flow_style=False)
  103. return rdict
  104. @staticmethod
  105. def get_config(config_for, from_scratch=False):
  106. if from_scratch:
  107. if config_for in ('master', 'syndic_master'):
  108. return salt.config.master_config(
  109. AdaptedConfigurationTestCaseMixin.get_config_file_path(config_for)
  110. )
  111. elif config_for in ('minion', 'sub_minion'):
  112. return salt.config.minion_config(
  113. AdaptedConfigurationTestCaseMixin.get_config_file_path(config_for)
  114. )
  115. elif config_for in ('syndic',):
  116. return salt.config.syndic_config(
  117. AdaptedConfigurationTestCaseMixin.get_config_file_path(config_for),
  118. AdaptedConfigurationTestCaseMixin.get_config_file_path('minion')
  119. )
  120. elif config_for == 'client_config':
  121. return salt.config.client_config(
  122. AdaptedConfigurationTestCaseMixin.get_config_file_path('master')
  123. )
  124. if config_for not in RUNTIME_VARS.RUNTIME_CONFIGS:
  125. if config_for in ('master', 'syndic_master'):
  126. RUNTIME_VARS.RUNTIME_CONFIGS[config_for] = freeze(
  127. salt.config.master_config(
  128. AdaptedConfigurationTestCaseMixin.get_config_file_path(config_for)
  129. )
  130. )
  131. elif config_for in ('minion', 'sub_minion'):
  132. RUNTIME_VARS.RUNTIME_CONFIGS[config_for] = freeze(
  133. salt.config.minion_config(
  134. AdaptedConfigurationTestCaseMixin.get_config_file_path(config_for)
  135. )
  136. )
  137. elif config_for in ('syndic',):
  138. RUNTIME_VARS.RUNTIME_CONFIGS[config_for] = freeze(
  139. salt.config.syndic_config(
  140. AdaptedConfigurationTestCaseMixin.get_config_file_path(config_for),
  141. AdaptedConfigurationTestCaseMixin.get_config_file_path('minion')
  142. )
  143. )
  144. elif config_for == 'client_config':
  145. RUNTIME_VARS.RUNTIME_CONFIGS[config_for] = freeze(
  146. salt.config.client_config(
  147. AdaptedConfigurationTestCaseMixin.get_config_file_path('master')
  148. )
  149. )
  150. return RUNTIME_VARS.RUNTIME_CONFIGS[config_for]
  151. @staticmethod
  152. def get_config_dir():
  153. return RUNTIME_VARS.TMP_CONF_DIR
  154. @staticmethod
  155. def get_config_file_path(filename):
  156. if filename == 'syndic_master':
  157. return os.path.join(RUNTIME_VARS.TMP_SYNDIC_MASTER_CONF_DIR, 'master')
  158. if filename == 'syndic':
  159. return os.path.join(RUNTIME_VARS.TMP_SYNDIC_MINION_CONF_DIR, 'minion')
  160. if filename == 'sub_minion':
  161. return os.path.join(RUNTIME_VARS.TMP_SUB_MINION_CONF_DIR, 'minion')
  162. return os.path.join(RUNTIME_VARS.TMP_CONF_DIR, filename)
  163. @property
  164. def master_opts(self):
  165. '''
  166. Return the options used for the master
  167. '''
  168. return self.get_config('master')
  169. @property
  170. def minion_opts(self):
  171. '''
  172. Return the options used for the minion
  173. '''
  174. return self.get_config('minion')
  175. @property
  176. def sub_minion_opts(self):
  177. '''
  178. Return the options used for the sub_minion
  179. '''
  180. return self.get_config('sub_minion')
  181. class SaltClientTestCaseMixin(AdaptedConfigurationTestCaseMixin):
  182. '''
  183. Mix-in class that provides a ``client`` attribute which returns a Salt
  184. :class:`LocalClient<salt:salt.client.LocalClient>`.
  185. .. code-block:: python
  186. class LocalClientTestCase(TestCase, SaltClientTestCaseMixin):
  187. def test_check_pub_data(self):
  188. just_minions = {'minions': ['m1', 'm2']}
  189. jid_no_minions = {'jid': '1234', 'minions': []}
  190. valid_pub_data = {'minions': ['m1', 'm2'], 'jid': '1234'}
  191. self.assertRaises(EauthAuthenticationError,
  192. self.client._check_pub_data, None)
  193. self.assertDictEqual({},
  194. self.client._check_pub_data(just_minions),
  195. 'Did not handle lack of jid correctly')
  196. self.assertDictEqual(
  197. {},
  198. self.client._check_pub_data({'jid': '0'}),
  199. 'Passing JID of zero is not handled gracefully')
  200. '''
  201. _salt_client_config_file_name_ = 'master'
  202. @property
  203. def client(self):
  204. # Late import
  205. import salt.client
  206. if 'runtime_client' not in RUNTIME_VARS.RUNTIME_CONFIGS:
  207. mopts = self.get_config(self._salt_client_config_file_name_, from_scratch=True)
  208. RUNTIME_VARS.RUNTIME_CONFIGS['runtime_client'] = salt.client.get_local_client(mopts=mopts)
  209. return RUNTIME_VARS.RUNTIME_CONFIGS['runtime_client']
  210. class ShellCaseCommonTestsMixin(CheckShellBinaryNameAndVersionMixin):
  211. _call_binary_expected_version_ = salt.version.__version__
  212. def test_salt_with_git_version(self):
  213. if getattr(self, '_call_binary_', None) is None:
  214. self.skipTest('\'_call_binary_\' not defined.')
  215. from salt.version import __version_info__, SaltStackVersion
  216. git = salt.utils.path.which('git')
  217. if not git:
  218. self.skipTest('The git binary is not available')
  219. opts = {
  220. 'stdout': subprocess.PIPE,
  221. 'stderr': subprocess.PIPE,
  222. 'cwd': CODE_DIR,
  223. }
  224. if not salt.utils.platform.is_windows():
  225. opts['close_fds'] = True
  226. # Let's get the output of git describe
  227. process = subprocess.Popen(
  228. [git, 'describe', '--tags', '--first-parent', '--match', 'v[0-9]*'],
  229. **opts
  230. )
  231. out, err = process.communicate()
  232. if process.returncode != 0:
  233. process = subprocess.Popen(
  234. [git, 'describe', '--tags', '--match', 'v[0-9]*'],
  235. **opts
  236. )
  237. out, err = process.communicate()
  238. if not out:
  239. self.skipTest(
  240. 'Failed to get the output of \'git describe\'. '
  241. 'Error: \'{0}\''.format(
  242. salt.utils.stringutils.to_str(err)
  243. )
  244. )
  245. parsed_version = SaltStackVersion.parse(out)
  246. if parsed_version.info < __version_info__:
  247. self.skipTest(
  248. 'We\'re likely about to release a new version. This test '
  249. 'would fail. Parsed(\'{0}\') < Expected(\'{1}\')'.format(
  250. parsed_version.info, __version_info__
  251. )
  252. )
  253. elif parsed_version.info != __version_info__:
  254. self.skipTest(
  255. 'In order to get the proper salt version with the '
  256. 'git hash you need to update salt\'s local git '
  257. 'tags. Something like: \'git fetch --tags\' or '
  258. '\'git fetch --tags upstream\' if you followed '
  259. 'salt\'s contribute documentation. The version '
  260. 'string WILL NOT include the git hash.'
  261. )
  262. out = '\n'.join(self.run_script(self._call_binary_, '--version'))
  263. self.assertIn(parsed_version.string, out)
  264. class _FixLoaderModuleMockMixinMroOrder(type):
  265. '''
  266. This metaclass will make sure that LoaderModuleMockMixin will always come as the first
  267. base class in order for LoaderModuleMockMixin.setUp to actually run
  268. '''
  269. def __new__(mcs, cls_name, cls_bases, cls_dict):
  270. if cls_name == 'LoaderModuleMockMixin':
  271. return super(_FixLoaderModuleMockMixinMroOrder, mcs).__new__(mcs, cls_name, cls_bases, cls_dict)
  272. bases = list(cls_bases)
  273. for idx, base in enumerate(bases):
  274. if base.__name__ == 'LoaderModuleMockMixin':
  275. bases.insert(0, bases.pop(idx))
  276. break
  277. # Create the class instance
  278. instance = super(_FixLoaderModuleMockMixinMroOrder, mcs).__new__(mcs, cls_name, tuple(bases), cls_dict)
  279. # Apply our setUp function decorator
  280. instance.setUp = LoaderModuleMockMixin.__setup_loader_modules_mocks__(instance.setUp)
  281. return instance
  282. class LoaderModuleMockMixin(six.with_metaclass(_FixLoaderModuleMockMixinMroOrder, object)):
  283. '''
  284. This class will setup salt loader dunders.
  285. Please check `set_up_loader_mocks` above
  286. '''
  287. # Define our setUp function decorator
  288. @staticmethod
  289. def __setup_loader_modules_mocks__(setup_func):
  290. @functools.wraps(setup_func)
  291. def wrapper(self):
  292. if NO_MOCK:
  293. self.skipTest(NO_MOCK_REASON)
  294. loader_modules_configs = self.setup_loader_modules()
  295. if not isinstance(loader_modules_configs, dict):
  296. raise RuntimeError(
  297. '{}.setup_loader_modules() must return a dictionary where the keys are the '
  298. 'modules that require loader mocking setup and the values, the global module '
  299. 'variables for each of the module being mocked. For example \'__salt__\', '
  300. '\'__opts__\', etc.'.format(self.__class__.__name__)
  301. )
  302. salt_dunders = (
  303. '__opts__', '__salt__', '__runner__', '__context__', '__utils__',
  304. '__ext_pillar__', '__thorium__', '__states__', '__serializers__', '__ret__',
  305. '__grains__', '__pillar__', '__sdb__',
  306. # Proxy is commented out on purpose since some code in salt expects a NameError
  307. # and is most of the time not a required dunder
  308. # '__proxy__'
  309. )
  310. for module, module_globals in six.iteritems(loader_modules_configs):
  311. if not isinstance(module, types.ModuleType):
  312. raise RuntimeError(
  313. 'The dictionary keys returned by {}.setup_loader_modules() '
  314. 'must be an imported module, not {}'.format(
  315. self.__class__.__name__,
  316. type(module)
  317. )
  318. )
  319. if not isinstance(module_globals, dict):
  320. raise RuntimeError(
  321. 'The dictionary values returned by {}.setup_loader_modules() '
  322. 'must be a dictionary, not {}'.format(
  323. self.__class__.__name__,
  324. type(module_globals)
  325. )
  326. )
  327. module_blacklisted_dunders = module_globals.pop('blacklisted_dunders', ())
  328. minion_funcs = {}
  329. if '__salt__' in module_globals and module_globals['__salt__'] == 'autoload':
  330. if '__opts__' not in module_globals:
  331. raise RuntimeError(
  332. 'You must provide \'__opts__\' on the {} module globals dictionary '
  333. 'to auto load the minion functions'.format(module.__name__)
  334. )
  335. import salt.loader
  336. ctx = {}
  337. if '__utils__' not in module_globals:
  338. utils = salt.loader.utils(module_globals['__opts__'],
  339. context=module_globals.get('__context__') or ctx)
  340. module_globals['__utils__'] = utils
  341. minion_funcs = salt.loader.minion_mods(
  342. module_globals['__opts__'],
  343. context=module_globals.get('__context__') or ctx,
  344. utils=module_globals.get('__utils__'),
  345. )
  346. module_globals['__salt__'] = minion_funcs
  347. for dunder_name in salt_dunders:
  348. if dunder_name not in module_globals:
  349. if dunder_name in module_blacklisted_dunders:
  350. continue
  351. module_globals[dunder_name] = {}
  352. sys_modules = module_globals.pop('sys.modules', None)
  353. if sys_modules is not None:
  354. if not isinstance(sys_modules, dict):
  355. raise RuntimeError(
  356. '\'sys.modules\' must be a dictionary not: {}'.format(
  357. type(sys_modules)
  358. )
  359. )
  360. patcher = patch.dict(sys.modules, sys_modules)
  361. patcher.start()
  362. def cleanup_sys_modules(patcher, sys_modules):
  363. patcher.stop()
  364. del patcher
  365. del sys_modules
  366. self.addCleanup(cleanup_sys_modules, patcher, sys_modules)
  367. for key in module_globals:
  368. if not hasattr(module, key):
  369. if key in salt_dunders:
  370. setattr(module, key, {})
  371. else:
  372. setattr(module, key, None)
  373. if module_globals:
  374. patcher = patch.multiple(module, **module_globals)
  375. patcher.start()
  376. def cleanup_module_globals(patcher, module_globals):
  377. patcher.stop()
  378. del patcher
  379. del module_globals
  380. self.addCleanup(cleanup_module_globals, patcher, module_globals)
  381. if minion_funcs:
  382. # Since we autoloaded the minion_funcs, let's namespace the functions with the globals
  383. # used to patch above
  384. import salt.utils
  385. for func in minion_funcs:
  386. minion_funcs[func] = salt.utils.functools.namespaced_function(
  387. minion_funcs[func],
  388. module_globals,
  389. preserve_context=True
  390. )
  391. return setup_func(self)
  392. return wrapper
  393. def setup_loader_modules(self):
  394. raise NotImplementedError(
  395. '\'{}.setup_loader_modules()\' must be implemented'.format(self.__class__.__name__)
  396. )
  397. class XMLEqualityMixin(object):
  398. def assertEqualXML(self, e1, e2):
  399. if six.PY3 and isinstance(e1, bytes):
  400. e1 = e1.decode('utf-8')
  401. if six.PY3 and isinstance(e2, bytes):
  402. e2 = e2.decode('utf-8')
  403. if isinstance(e1, six.string_types):
  404. e1 = etree.XML(e1)
  405. if isinstance(e2, six.string_types):
  406. e2 = etree.XML(e2)
  407. if e1.tag != e2.tag:
  408. return False
  409. if e1.text != e2.text:
  410. return False
  411. if e1.tail != e2.tail:
  412. return False
  413. if e1.attrib != e2.attrib:
  414. return False
  415. if len(e1) != len(e2):
  416. return False
  417. return all(self.assertEqualXML(c1, c2) for c1, c2 in zip(e1, e2))
  418. class SaltReturnAssertsMixin(object):
  419. def assertReturnSaltType(self, ret):
  420. try:
  421. self.assertTrue(isinstance(ret, dict))
  422. except AssertionError:
  423. raise AssertionError(
  424. '{0} is not dict. Salt returned: {1}'.format(
  425. type(ret).__name__, ret
  426. )
  427. )
  428. def assertReturnNonEmptySaltType(self, ret):
  429. self.assertReturnSaltType(ret)
  430. try:
  431. self.assertNotEqual(ret, {})
  432. except AssertionError:
  433. raise AssertionError(
  434. '{} is equal to {}. Salt returned an empty dictionary.'
  435. )
  436. def __return_valid_keys(self, keys):
  437. if isinstance(keys, tuple):
  438. # If it's a tuple, turn it into a list
  439. keys = list(keys)
  440. elif isinstance(keys, six.string_types):
  441. # If it's a string, make it a one item list
  442. keys = [keys]
  443. elif not isinstance(keys, list):
  444. # If we've reached here, it's a bad type passed to keys
  445. raise RuntimeError('The passed keys need to be a list')
  446. return keys
  447. def __getWithinSaltReturn(self, ret, keys):
  448. self.assertReturnNonEmptySaltType(ret)
  449. ret_data = []
  450. for part in six.itervalues(ret):
  451. keys = self.__return_valid_keys(keys)
  452. okeys = keys[:]
  453. try:
  454. ret_item = part[okeys.pop(0)]
  455. except (KeyError, TypeError):
  456. raise AssertionError(
  457. 'Could not get ret{0} from salt\'s return: {1}'.format(
  458. ''.join(['[\'{0}\']'.format(k) for k in keys]), part
  459. )
  460. )
  461. while okeys:
  462. try:
  463. ret_item = ret_item[okeys.pop(0)]
  464. except (KeyError, TypeError):
  465. raise AssertionError(
  466. 'Could not get ret{0} from salt\'s return: {1}'.format(
  467. ''.join(['[\'{0}\']'.format(k) for k in keys]), part
  468. )
  469. )
  470. ret_data.append(ret_item)
  471. return ret_data
  472. def assertSaltTrueReturn(self, ret):
  473. try:
  474. for saltret in self.__getWithinSaltReturn(ret, 'result'):
  475. self.assertTrue(saltret)
  476. except AssertionError:
  477. log.info('Salt Full Return:\n{0}'.format(pprint.pformat(ret)))
  478. try:
  479. raise AssertionError(
  480. '{result} is not True. Salt Comment:\n{comment}'.format(
  481. **(next(six.itervalues(ret)))
  482. )
  483. )
  484. except (AttributeError, IndexError):
  485. raise AssertionError(
  486. 'Failed to get result. Salt Returned:\n{0}'.format(
  487. pprint.pformat(ret)
  488. )
  489. )
  490. def assertSaltFalseReturn(self, ret):
  491. try:
  492. for saltret in self.__getWithinSaltReturn(ret, 'result'):
  493. self.assertFalse(saltret)
  494. except AssertionError:
  495. log.info('Salt Full Return:\n{0}'.format(pprint.pformat(ret)))
  496. try:
  497. raise AssertionError(
  498. '{result} is not False. Salt Comment:\n{comment}'.format(
  499. **(next(six.itervalues(ret)))
  500. )
  501. )
  502. except (AttributeError, IndexError):
  503. raise AssertionError(
  504. 'Failed to get result. Salt Returned: {0}'.format(ret)
  505. )
  506. def assertSaltNoneReturn(self, ret):
  507. try:
  508. for saltret in self.__getWithinSaltReturn(ret, 'result'):
  509. self.assertIsNone(saltret)
  510. except AssertionError:
  511. log.info('Salt Full Return:\n{0}'.format(pprint.pformat(ret)))
  512. try:
  513. raise AssertionError(
  514. '{result} is not None. Salt Comment:\n{comment}'.format(
  515. **(next(six.itervalues(ret)))
  516. )
  517. )
  518. except (AttributeError, IndexError):
  519. raise AssertionError(
  520. 'Failed to get result. Salt Returned: {0}'.format(ret)
  521. )
  522. def assertInSaltComment(self, in_comment, ret):
  523. for saltret in self.__getWithinSaltReturn(ret, 'comment'):
  524. self.assertIn(in_comment, saltret)
  525. def assertNotInSaltComment(self, not_in_comment, ret):
  526. for saltret in self.__getWithinSaltReturn(ret, 'comment'):
  527. self.assertNotIn(not_in_comment, saltret)
  528. def assertSaltCommentRegexpMatches(self, ret, pattern):
  529. return self.assertInSaltReturnRegexpMatches(ret, pattern, 'comment')
  530. def assertInSaltStateWarning(self, in_comment, ret):
  531. for saltret in self.__getWithinSaltReturn(ret, 'warnings'):
  532. self.assertIn(in_comment, saltret)
  533. def assertNotInSaltStateWarning(self, not_in_comment, ret):
  534. for saltret in self.__getWithinSaltReturn(ret, 'warnings'):
  535. self.assertNotIn(not_in_comment, saltret)
  536. def assertInSaltReturn(self, item_to_check, ret, keys):
  537. for saltret in self.__getWithinSaltReturn(ret, keys):
  538. self.assertIn(item_to_check, saltret)
  539. def assertNotInSaltReturn(self, item_to_check, ret, keys):
  540. for saltret in self.__getWithinSaltReturn(ret, keys):
  541. self.assertNotIn(item_to_check, saltret)
  542. def assertInSaltReturnRegexpMatches(self, ret, pattern, keys=()):
  543. for saltret in self.__getWithinSaltReturn(ret, keys):
  544. self.assertRegex(saltret, pattern)
  545. def assertSaltStateChangesEqual(self, ret, comparison, keys=()):
  546. keys = ['changes'] + self.__return_valid_keys(keys)
  547. for saltret in self.__getWithinSaltReturn(ret, keys):
  548. self.assertEqual(saltret, comparison)
  549. def assertSaltStateChangesNotEqual(self, ret, comparison, keys=()):
  550. keys = ['changes'] + self.__return_valid_keys(keys)
  551. for saltret in self.__getWithinSaltReturn(ret, keys):
  552. self.assertNotEqual(saltret, comparison)
  553. def _fetch_events(q):
  554. '''
  555. Collect events and store them
  556. '''
  557. def _clean_queue():
  558. print('Cleaning queue!')
  559. while not q.empty():
  560. queue_item = q.get()
  561. queue_item.task_done()
  562. atexit.register(_clean_queue)
  563. a_config = AdaptedConfigurationTestCaseMixin()
  564. event = salt.utils.event.get_event(
  565. 'minion',
  566. sock_dir=a_config.get_config('minion')['sock_dir'],
  567. opts=a_config.get_config('minion'),
  568. )
  569. # Wait for event bus to be connected
  570. while not event.connect_pull(30):
  571. time.sleep(1)
  572. # Notify parent process that the event bus is connected
  573. q.put('CONNECTED')
  574. while True:
  575. try:
  576. events = event.get_event(full=False)
  577. except Exception as exc:
  578. # This is broad but we'll see all kinds of issues right now
  579. # if we drop the proc out from under the socket while we're reading
  580. log.exception("Exception caught while getting events %r", exc)
  581. q.put(events)
  582. class SaltMinionEventAssertsMixin(object):
  583. '''
  584. Asserts to verify that a given event was seen
  585. '''
  586. def __new__(cls, *args, **kwargs):
  587. # We have to cross-call to re-gen a config
  588. cls.q = multiprocessing.Queue()
  589. cls.fetch_proc = salt.utils.process.SignalHandlingMultiprocessingProcess(
  590. target=_fetch_events, args=(cls.q,)
  591. )
  592. cls.fetch_proc.start()
  593. # Wait for the event bus to be connected
  594. msg = cls.q.get(block=True)
  595. if msg != 'CONNECTED':
  596. # Just in case something very bad happens
  597. raise RuntimeError('Unexpected message in test\'s event queue')
  598. return object.__new__(cls)
  599. def __exit__(self, *args, **kwargs):
  600. self.fetch_proc.join()
  601. def assertMinionEventFired(self, tag):
  602. #TODO
  603. raise salt.exceptions.NotImplemented('assertMinionEventFired() not implemented')
  604. def assertMinionEventReceived(self, desired_event, timeout=5, sleep_time=0.5):
  605. start = time.time()
  606. while True:
  607. try:
  608. event = self.q.get(False)
  609. except Empty:
  610. time.sleep(sleep_time)
  611. if time.time() - start >= timeout:
  612. break
  613. continue
  614. if isinstance(event, dict):
  615. event.pop('_stamp')
  616. if desired_event == event:
  617. self.fetch_proc.terminate()
  618. return True
  619. if time.time() - start >= timeout:
  620. break
  621. self.fetch_proc.terminate()
  622. raise AssertionError('Event {0} was not received by minion'.format(desired_event))