test_mine.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: Rupesh Tare <rupesht@saltstack.com>
  4. :codeauthor: Herbert Buurman <herbert.buurman@ogd.nl>
  5. """
  6. # Import Python Libs
  7. from __future__ import absolute_import, print_function, unicode_literals
  8. # Import Salt Libs
  9. import salt.modules.mine as mine
  10. import salt.utils.mine
  11. from salt.utils.odict import OrderedDict
  12. # Import Salt Testing Libs
  13. from tests.support.mixins import LoaderModuleMockMixin
  14. from tests.support.mock import MagicMock, patch
  15. from tests.support.unit import TestCase
  16. class FakeCache(object):
  17. def __init__(self):
  18. self.data = {}
  19. def store(self, bank, key, value):
  20. self.data[bank, key] = value
  21. return "FakeCache:StoreSuccess!"
  22. def fetch(self, bank, key):
  23. return self.data.get((bank, key), {})
  24. def debug(self):
  25. print(__name__ + ":FakeCache dump:\n" "{}".format(self.data))
  26. class MineTestCase(TestCase, LoaderModuleMockMixin):
  27. """
  28. Test cases for salt.modules.mine
  29. """
  30. def setUp(self):
  31. self.kernel_ret = "Linux!"
  32. self.foo_ret = "baz"
  33. self.ip_ret = "2001:db8::1:3"
  34. self.cache = FakeCache()
  35. def setup_loader_modules(self):
  36. mock_match = MagicMock(return_value="webserver")
  37. return {
  38. mine: {
  39. "__salt__": {
  40. "match.glob": mock_match,
  41. "match.pcre": mock_match,
  42. "match.list": mock_match,
  43. "match.grain": mock_match,
  44. "match.grain_pcre": mock_match,
  45. "match.ipcidr": mock_match,
  46. "match.compound": mock_match,
  47. "match.pillar": mock_match,
  48. "match.pillar_pcre": mock_match,
  49. "data.get": lambda key: self.cache.fetch("minions/webserver", key),
  50. "data.update": lambda key, value: self.cache.store(
  51. "minions/webserver", key, value
  52. ),
  53. }
  54. }
  55. }
  56. def test_get_local_empty(self):
  57. """
  58. Tests getting function data from the local mine that does not exist.
  59. """
  60. with patch.dict(mine.__opts__, {"file_client": "local", "id": "webserver"}):
  61. ret_classic = mine.get("*", "funky.doodle")
  62. ret_dict = mine.get("*", ["funky.doodle"])
  63. self.assertEqual(ret_classic, {})
  64. self.assertEqual(ret_dict, {})
  65. def test_get_local_classic(self):
  66. """
  67. Tests getting function data from the local mine that was stored without minion-side ACL.
  68. This verifies backwards compatible reads from a salt mine.
  69. """
  70. # Prefill minion cache with a non-ACL value
  71. self.cache.store("minions/webserver", "mine_cache", {"foobard": "barfood"})
  72. with patch.dict(mine.__opts__, {"file_client": "local", "id": "webserver"}):
  73. ret_classic = mine.get("*", "foobard")
  74. ret_dict = mine.get("*", ["foobard"])
  75. self.assertEqual(ret_classic, {"webserver": "barfood"})
  76. self.assertEqual(ret_dict, {"foobard": {"webserver": "barfood"}})
  77. def test_send_get_local(self):
  78. """
  79. Tests sending an item to the mine in the minion's local cache,
  80. and then immediately fetching it again (since tests are executed unordered).
  81. Also verify that the stored mine cache does not use ACL data structure
  82. without allow_tgt passed.
  83. """
  84. with patch.dict(
  85. mine.__opts__, {"file_client": "local", "id": "webserver"}
  86. ), patch.dict(
  87. mine.__salt__,
  88. {
  89. "network.ip_addrs": MagicMock(return_value=self.ip_ret),
  90. "foo.bar": MagicMock(return_value=self.foo_ret),
  91. },
  92. ):
  93. ret = mine.send("ip_addr", mine_function="network.ip_addrs")
  94. mine.send("foo.bar")
  95. self.assertEqual(ret, "FakeCache:StoreSuccess!")
  96. self.assertEqual(
  97. self.cache.fetch("minions/webserver", "mine_cache"),
  98. {"ip_addr": self.ip_ret, "foo.bar": self.foo_ret},
  99. )
  100. with patch.dict(mine.__opts__, {"file_client": "local", "id": "webserver"}):
  101. ret_single = mine.get("*", "ip_addr")
  102. ret_single_dict = mine.get("*", ["ip_addr"])
  103. ret_multi = mine.get("*", "ip_addr,foo.bar")
  104. ret_multi2 = mine.get("*", ["ip_addr", "foo.bar"])
  105. self.assertEqual(ret_single, {"webserver": self.ip_ret})
  106. self.assertEqual(ret_single_dict, {"ip_addr": {"webserver": self.ip_ret}})
  107. self.assertEqual(
  108. ret_multi,
  109. {
  110. "ip_addr": {"webserver": self.ip_ret},
  111. "foo.bar": {"webserver": self.foo_ret},
  112. },
  113. )
  114. self.assertEqual(ret_multi, ret_multi2)
  115. def test_send_get_acl_local(self):
  116. """
  117. Tests sending an item to the mine in the minion's local cache,
  118. including ACL information (useless when only working locally, but hey),
  119. and then immediately fetching it again (since tests are executed unordered).
  120. Also verify that the stored mine cache has the correct structure (with ACL)
  121. when using allow_tgt and no ACL without allow_tgt.
  122. """
  123. with patch.dict(
  124. mine.__opts__, {"file_client": "local", "id": "webserver"}
  125. ), patch.dict(
  126. mine.__salt__,
  127. {
  128. "network.ip_addrs": MagicMock(return_value=self.ip_ret),
  129. "foo.bar": MagicMock(return_value=self.foo_ret),
  130. },
  131. ):
  132. ret = mine.send(
  133. "ip_addr",
  134. mine_function="network.ip_addrs",
  135. allow_tgt="web*",
  136. allow_tgt_type="glob",
  137. )
  138. mine.send("foo.bar")
  139. self.assertEqual(ret, "FakeCache:StoreSuccess!")
  140. self.assertEqual(
  141. self.cache.fetch("minions/webserver", "mine_cache"),
  142. {
  143. "ip_addr": {
  144. salt.utils.mine.MINE_ITEM_ACL_DATA: self.ip_ret,
  145. salt.utils.mine.MINE_ITEM_ACL_ID: salt.utils.mine.MINE_ITEM_ACL_VERSION,
  146. "allow_tgt": "web*",
  147. "allow_tgt_type": "glob",
  148. },
  149. "foo.bar": self.foo_ret,
  150. },
  151. )
  152. with patch.dict(mine.__opts__, {"file_client": "local", "id": "webserver"}):
  153. ret_single = mine.get("*", "ip_addr")
  154. self.assertEqual(ret_single, {"webserver": self.ip_ret})
  155. def test_send_master(self):
  156. """
  157. Tests sending an item to the mine stored on the master.
  158. This is done by capturing the load that is sent to the master.
  159. """
  160. with patch.object(
  161. mine, "_mine_send", MagicMock(side_effect=lambda x, y: x)
  162. ), patch.dict(
  163. mine.__salt__, {"foo.bar": MagicMock(return_value=self.foo_ret)}
  164. ), patch.dict(
  165. mine.__opts__, {"file_client": "remote", "id": "foo"}
  166. ):
  167. ret = mine.send("foo.bar")
  168. self.assertEqual(
  169. ret,
  170. {
  171. "id": "foo",
  172. "cmd": "_mine",
  173. "data": {"foo.bar": self.foo_ret},
  174. "clear": False,
  175. },
  176. )
  177. def test_send_master_acl(self):
  178. """
  179. Tests sending an item to the mine stored on the master. Now with ACL.
  180. This is done by capturing the load that is sent to the master.
  181. """
  182. with patch.object(
  183. mine, "_mine_send", MagicMock(side_effect=lambda x, y: x)
  184. ), patch.dict(
  185. mine.__salt__, {"foo.bar": MagicMock(return_value=self.foo_ret)}
  186. ), patch.dict(
  187. mine.__opts__, {"file_client": "remote", "id": "foo"}
  188. ):
  189. ret = mine.send("foo.bar", allow_tgt="roles:web", allow_tgt_type="grains")
  190. self.assertEqual(
  191. ret,
  192. {
  193. "id": "foo",
  194. "cmd": "_mine",
  195. "data": {
  196. "foo.bar": {
  197. salt.utils.mine.MINE_ITEM_ACL_DATA: self.foo_ret,
  198. salt.utils.mine.MINE_ITEM_ACL_ID: salt.utils.mine.MINE_ITEM_ACL_VERSION,
  199. "allow_tgt": "roles:web",
  200. "allow_tgt_type": "grains",
  201. },
  202. },
  203. "clear": False,
  204. },
  205. )
  206. def test_get_master(self):
  207. """
  208. Tests loading a mine item from the mine stored on the master.
  209. """
  210. mock_load = {
  211. "tgt_type": "qux",
  212. "tgt": self.foo_ret,
  213. "cmd": "_mine_get",
  214. "fun": "foo.bar",
  215. "id": "foo",
  216. }
  217. with patch.object(
  218. mine, "_mine_get", MagicMock(return_value=mock_load)
  219. ), patch.dict(mine.__opts__, {"file_client": "remote", "id": "foo"}):
  220. # Verify the correct load
  221. self.assertEqual(mine.get("*", "foo.bar"), mock_load)
  222. def test_get_master_exclude_minion(self):
  223. """
  224. Tests the exclude_minion-parameter for mine.get
  225. """
  226. _mine_get_ret = OrderedDict([("webserver", "value")])
  227. with patch.object(
  228. mine, "_mine_get", MagicMock(return_value=_mine_get_ret)
  229. ), patch.dict(mine.__opts__, {"file_client": "remote", "id": "webserver"}):
  230. self.assertEqual(
  231. mine.get("*", "foo.bar", exclude_minion=False), {"webserver": "value"}
  232. )
  233. self.assertEqual(mine.get("*", "foo.bar", exclude_minion=True), {})
  234. def test_update_local(self):
  235. """
  236. Tests the ``update``-function on the minion's local cache.
  237. Updates mine functions from pillar+config only.
  238. """
  239. config_mine_functions = {
  240. "ip_addr": {"mine_function": "network.ip_addrs"},
  241. "network.ip_addrs": [],
  242. "kernel": [
  243. {"mine_function": "grains.get"},
  244. "kernel",
  245. {"allow_tgt": "web*"},
  246. ],
  247. "foo.bar": {"allow_tgt": "G@roles:webserver", "allow_tgt_type": "compound"},
  248. }
  249. with patch.dict(
  250. mine.__opts__, {"file_client": "local", "id": "webserver"}
  251. ), patch.dict(
  252. mine.__salt__,
  253. {
  254. "config.merge": MagicMock(return_value=config_mine_functions),
  255. "grains.get": lambda x: self.kernel_ret,
  256. "network.ip_addrs": MagicMock(return_value=self.ip_ret),
  257. "foo.bar": MagicMock(return_value=self.foo_ret),
  258. },
  259. ):
  260. ret = mine.update()
  261. self.assertEqual(ret, "FakeCache:StoreSuccess!")
  262. # Check if the mine entries have been stored properly in the FakeCache.
  263. self.assertEqual(
  264. self.cache.fetch("minions/webserver", "mine_cache"),
  265. {
  266. "ip_addr": self.ip_ret,
  267. "network.ip_addrs": self.ip_ret,
  268. "foo.bar": {
  269. salt.utils.mine.MINE_ITEM_ACL_DATA: self.foo_ret,
  270. salt.utils.mine.MINE_ITEM_ACL_ID: salt.utils.mine.MINE_ITEM_ACL_VERSION,
  271. "allow_tgt": "G@roles:webserver",
  272. "allow_tgt_type": "compound",
  273. },
  274. "kernel": {
  275. salt.utils.mine.MINE_ITEM_ACL_DATA: self.kernel_ret,
  276. salt.utils.mine.MINE_ITEM_ACL_ID: salt.utils.mine.MINE_ITEM_ACL_VERSION,
  277. "allow_tgt": "web*",
  278. },
  279. },
  280. )
  281. def test_update_local_specific(self):
  282. """
  283. Tests the ``update``-function on the minion's local cache.
  284. Updates mine functions from kwargs only.
  285. """
  286. manual_mine_functions = {
  287. "ip_addr": {"mine_function": "network.ip_addrs"},
  288. "network.ip_addrs": [],
  289. "kernel": [
  290. {"mine_function": "grains.get"},
  291. "kernel",
  292. {"allow_tgt": "web*"},
  293. ],
  294. "foo.bar": {"allow_tgt": "G@roles:webserver", "allow_tgt_type": "compound"},
  295. }
  296. with patch.dict(
  297. mine.__opts__, {"file_client": "local", "id": "webserver"}
  298. ), patch.dict(
  299. mine.__salt__,
  300. {
  301. "config.merge": MagicMock(return_value={}),
  302. "grains.get": lambda x: "Linux!!",
  303. "network.ip_addrs": MagicMock(return_value=self.ip_ret),
  304. "foo.bar": MagicMock(return_value=self.foo_ret),
  305. },
  306. ):
  307. ret = mine.update(mine_functions=manual_mine_functions)
  308. self.assertEqual(ret, "FakeCache:StoreSuccess!")
  309. # Check if the mine entries have been stored properly in the FakeCache.
  310. self.assertEqual(
  311. self.cache.fetch("minions/webserver", "mine_cache"),
  312. {
  313. "ip_addr": self.ip_ret,
  314. "network.ip_addrs": self.ip_ret,
  315. "foo.bar": {
  316. salt.utils.mine.MINE_ITEM_ACL_DATA: self.foo_ret,
  317. salt.utils.mine.MINE_ITEM_ACL_ID: salt.utils.mine.MINE_ITEM_ACL_VERSION,
  318. "allow_tgt": "G@roles:webserver",
  319. "allow_tgt_type": "compound",
  320. },
  321. "kernel": {
  322. salt.utils.mine.MINE_ITEM_ACL_DATA: "Linux!!",
  323. salt.utils.mine.MINE_ITEM_ACL_ID: salt.utils.mine.MINE_ITEM_ACL_VERSION,
  324. "allow_tgt": "web*",
  325. },
  326. },
  327. )
  328. def test_update_master(self):
  329. """
  330. Tests whether the ``update``-function sends the correct data to the master.
  331. """
  332. config_mine_functions = {
  333. "ip_addr": {"mine_function": "network.ip_addrs"},
  334. "network.ip_addrs": [],
  335. "kernel": [{"mine_function": "grains.get"}, "kernel"],
  336. "foo.bar": {},
  337. }
  338. mock_load = {
  339. "id": "webserver",
  340. "cmd": "_mine",
  341. "data": {
  342. "ip_addr": self.ip_ret,
  343. "network.ip_addrs": self.ip_ret,
  344. "foo.bar": self.foo_ret,
  345. "kernel": self.kernel_ret,
  346. },
  347. "clear": False,
  348. }
  349. with patch.object(
  350. mine, "_mine_send", MagicMock(side_effect=lambda x, y: x)
  351. ), patch.dict(
  352. mine.__opts__, {"file_client": "remote", "id": "webserver"}
  353. ), patch.dict(
  354. mine.__salt__,
  355. {
  356. "config.merge": MagicMock(return_value=config_mine_functions),
  357. "grains.get": lambda x: self.kernel_ret,
  358. "network.ip_addrs": MagicMock(return_value=self.ip_ret),
  359. "foo.bar": MagicMock(return_value=self.foo_ret),
  360. },
  361. ):
  362. # Verify the correct load
  363. self.assertEqual(mine.update(), mock_load)
  364. def test_delete_local(self):
  365. """
  366. Tests the ``delete``-function on the minion's local cache.
  367. """
  368. # Prefill minion cache with a non-ACL value
  369. self.cache.store("minions/webserver", "mine_cache", {"foobard": "barfood"})
  370. with patch.dict(mine.__opts__, {"file_client": "local", "id": "webserver"}):
  371. ret = mine.delete("foobard")
  372. self.assertEqual(self.cache.fetch("minions/webserver", "mine_cache"), {})
  373. def test_delete_master(self):
  374. """
  375. Tests whether the ``delete``-function sends the correct data to the master.
  376. """
  377. # Prefill minion cache with a non-ACL value
  378. self.cache.store("minions/webserver", "mine_cache", {"foobard": "barfood"})
  379. mock_load = {
  380. "cmd": "_mine_delete",
  381. "fun": "foobard",
  382. "id": "foo",
  383. }
  384. with patch.object(
  385. mine, "_mine_send", MagicMock(side_effect=lambda x, y: x)
  386. ), patch.dict(mine.__opts__, {"file_client": "remote", "id": "foo"}):
  387. # Verify the correct load
  388. self.assertEqual(mine.delete("foobard"), mock_load)
  389. def test_flush_local(self):
  390. """
  391. Tests the ``flush``-function on the minion's local cache.
  392. """
  393. # Prefill minion cache with a non-ACL value
  394. self.cache.store("minions/webserver", "mine_cache", {"foobard": "barfood"})
  395. with patch.dict(mine.__opts__, {"file_client": "local", "id": "webserver"}):
  396. ret = mine.flush()
  397. self.assertEqual(self.cache.fetch("minions/webserver", "mine_cache"), {})
  398. def test_flush_master(self):
  399. """
  400. Tests whether the ``flush``-function sends the correct data to the master.
  401. """
  402. mock_load = {"cmd": "_mine_flush", "id": "foo"}
  403. with patch.object(
  404. mine, "_mine_send", MagicMock(side_effect=lambda x, y: x)
  405. ), patch.dict(mine.__opts__, {"file_client": "remote", "id": "foo"}):
  406. # Verify the correct load
  407. self.assertEqual(mine.flush(), mock_load)
  408. def test_valid(self):
  409. """
  410. Tests the ``valid``-function.
  411. Note that mine functions defined as list are returned in dict format.
  412. Mine functions that do not exist in __salt__ are not returned.
  413. """
  414. config_mine_functions = {
  415. "network.ip_addrs": [],
  416. "kernel": [{"mine_function": "grains.get"}, "kernel"],
  417. "fubar": [{"mine_function": "does.not_exist"}],
  418. }
  419. with patch.dict(
  420. mine.__salt__,
  421. {
  422. "config.merge": MagicMock(return_value=config_mine_functions),
  423. "network.ip_addrs": lambda: True,
  424. "grains.get": lambda: True,
  425. },
  426. ):
  427. self.assertEqual(
  428. mine.valid(),
  429. {"network.ip_addrs": [], "kernel": {"grains.get": ["kernel"]}},
  430. )
  431. def test_get_docker(self):
  432. """
  433. Test for Get all mine data for 'docker.ps' and run an
  434. aggregation.
  435. """
  436. ps_response = {
  437. "localhost": {
  438. "host": {
  439. "interfaces": {
  440. "docker0": {
  441. "hwaddr": "88:99:00:00:99:99",
  442. "inet": [
  443. {
  444. "address": "172.17.42.1",
  445. "broadcast": None,
  446. "label": "docker0",
  447. "netmask": "255.255.0.0",
  448. }
  449. ],
  450. "inet6": [
  451. {
  452. "address": "ffff::eeee:aaaa:bbbb:8888",
  453. "prefixlen": "64",
  454. }
  455. ],
  456. "up": True,
  457. },
  458. "eth0": {
  459. "hwaddr": "88:99:00:99:99:99",
  460. "inet": [
  461. {
  462. "address": "192.168.0.1",
  463. "broadcast": "192.168.0.255",
  464. "label": "eth0",
  465. "netmask": "255.255.255.0",
  466. }
  467. ],
  468. "inet6": [
  469. {
  470. "address": "ffff::aaaa:aaaa:bbbb:8888",
  471. "prefixlen": "64",
  472. }
  473. ],
  474. "up": True,
  475. },
  476. }
  477. },
  478. "abcdefhjhi1234567899": { # container Id
  479. "Ports": [
  480. {
  481. "IP": "0.0.0.0", # we bind on every interfaces
  482. "PrivatePort": 80,
  483. "PublicPort": 80,
  484. "Type": "tcp",
  485. }
  486. ],
  487. "Image": "image:latest",
  488. "Info": {"Id": "abcdefhjhi1234567899"},
  489. },
  490. }
  491. }
  492. with patch.object(mine, "get", return_value=ps_response):
  493. ret = mine.get_docker()
  494. # Sort ifaces since that will change between py2 and py3
  495. ret["image:latest"]["ipv4"][80] = sorted(ret["image:latest"]["ipv4"][80])
  496. self.assertEqual(
  497. ret,
  498. {
  499. "image:latest": {
  500. "ipv4": {80: sorted(["172.17.42.1:80", "192.168.0.1:80"])}
  501. }
  502. },
  503. )
  504. def test_get_docker_with_container_id(self):
  505. """
  506. Test for Get all mine data for 'docker.ps' and run an
  507. aggregation.
  508. """
  509. ps_response = {
  510. "localhost": {
  511. "host": {
  512. "interfaces": {
  513. "docker0": {
  514. "hwaddr": "88:99:00:00:99:99",
  515. "inet": [
  516. {
  517. "address": "172.17.42.1",
  518. "broadcast": None,
  519. "label": "docker0",
  520. "netmask": "255.255.0.0",
  521. }
  522. ],
  523. "inet6": [
  524. {
  525. "address": "ffff::eeee:aaaa:bbbb:8888",
  526. "prefixlen": "64",
  527. }
  528. ],
  529. "up": True,
  530. },
  531. "eth0": {
  532. "hwaddr": "88:99:00:99:99:99",
  533. "inet": [
  534. {
  535. "address": "192.168.0.1",
  536. "broadcast": "192.168.0.255",
  537. "label": "eth0",
  538. "netmask": "255.255.255.0",
  539. }
  540. ],
  541. "inet6": [
  542. {
  543. "address": "ffff::aaaa:aaaa:bbbb:8888",
  544. "prefixlen": "64",
  545. }
  546. ],
  547. "up": True,
  548. },
  549. }
  550. },
  551. "abcdefhjhi1234567899": { # container Id
  552. "Ports": [
  553. {
  554. "IP": "0.0.0.0", # we bind on every interfaces
  555. "PrivatePort": 80,
  556. "PublicPort": 80,
  557. "Type": "tcp",
  558. }
  559. ],
  560. "Image": "image:latest",
  561. "Info": {"Id": "abcdefhjhi1234567899"},
  562. },
  563. }
  564. }
  565. with patch.object(mine, "get", return_value=ps_response):
  566. ret = mine.get_docker(with_container_id=True)
  567. # Sort ifaces since that will change between py2 and py3
  568. ret["image:latest"]["ipv4"][80] = sorted(ret["image:latest"]["ipv4"][80])
  569. self.assertEqual(
  570. ret,
  571. {
  572. "image:latest": {
  573. "ipv4": {
  574. 80: sorted(
  575. [
  576. ("172.17.42.1:80", "abcdefhjhi1234567899"),
  577. ("192.168.0.1:80", "abcdefhjhi1234567899"),
  578. ]
  579. )
  580. }
  581. }
  582. },
  583. )