mixins.py 27 KB

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