1
0

mixins.py 26 KB

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