mixins.py 31 KB

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