test_pyobjects.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. import logging
  2. import os
  3. import shutil
  4. import tempfile
  5. import textwrap
  6. import uuid
  7. import jinja2
  8. import salt.config
  9. import salt.state
  10. import salt.utils.files
  11. from salt.template import compile_template
  12. from salt.utils.odict import OrderedDict
  13. from salt.utils.pyobjects import (
  14. DuplicateState,
  15. InvalidFunction,
  16. Registry,
  17. SaltObject,
  18. State,
  19. StateFactory,
  20. )
  21. from tests.support.helpers import slowTest
  22. from tests.support.runtests import RUNTIME_VARS
  23. from tests.support.unit import TestCase
  24. log = logging.getLogger(__name__)
  25. class MapBuilder:
  26. def build_map(self, template=None):
  27. """
  28. Build from a specific template or just use a default if no template
  29. is passed to this function.
  30. """
  31. map_prefix = textwrap.dedent(
  32. """\
  33. #!pyobjects
  34. from salt.utils.pyobjects import StateFactory
  35. Service = StateFactory('service')
  36. {% macro priority(value) %}
  37. priority = {{ value }}
  38. {% endmacro %}
  39. class Samba(Map):
  40. """
  41. )
  42. map_suffix = textwrap.dedent(
  43. """\
  44. with Pkg.installed("samba", names=[Samba.server, Samba.client]):
  45. Service.running("samba", name=Samba.service)
  46. """
  47. )
  48. map_data = {
  49. "debian": " class Debian:\n"
  50. " server = 'samba'\n"
  51. " client = 'samba-client'\n"
  52. " service = 'samba'\n",
  53. "centos": " class RougeChapeau:\n"
  54. " __match__ = 'RedHat'\n"
  55. " server = 'samba'\n"
  56. " client = 'samba'\n"
  57. " service = 'smb'\n",
  58. "ubuntu": " class Ubuntu:\n"
  59. " __grain__ = 'os'\n"
  60. " service = 'smbd'\n",
  61. }
  62. if template is None:
  63. template = textwrap.dedent(
  64. """\
  65. {{ ubuntu }}
  66. {{ centos }}
  67. {{ debian }}
  68. """
  69. )
  70. full_template = map_prefix + template + map_suffix
  71. ret = jinja2.Template(full_template).render(**map_data)
  72. log.debug("built map: \n%s", ret)
  73. return ret
  74. class StateTests(TestCase):
  75. @classmethod
  76. def setUpClass(cls):
  77. cls.File = StateFactory("file")
  78. @classmethod
  79. def tearDownClass(cls):
  80. cls.File = None
  81. def setUp(self):
  82. Registry.empty()
  83. self.pydmesg_expected = {
  84. "file.managed": [
  85. {"group": "root"},
  86. {"mode": "0755"},
  87. {"require": [{"file": "/usr/local/bin"}]},
  88. {"source": "salt://debian/files/pydmesg.py"},
  89. {"user": "root"},
  90. ]
  91. }
  92. self.pydmesg_salt_expected = OrderedDict(
  93. [("/usr/local/bin/pydmesg", self.pydmesg_expected)]
  94. )
  95. self.pydmesg_kwargs = dict(
  96. user="root",
  97. group="root",
  98. mode="0755",
  99. source="salt://debian/files/pydmesg.py",
  100. )
  101. def tearDown(self):
  102. self.pydmesg_expected = self.pydmesg_salt_expected = self.pydmesg_kwargs = None
  103. def test_serialization(self):
  104. f = State(
  105. "/usr/local/bin/pydmesg",
  106. "file",
  107. "managed",
  108. require=self.File("/usr/local/bin"),
  109. **self.pydmesg_kwargs
  110. )
  111. self.assertEqual(f(), self.pydmesg_expected)
  112. def test_factory_serialization(self):
  113. self.File.managed(
  114. "/usr/local/bin/pydmesg",
  115. require=self.File("/usr/local/bin"),
  116. **self.pydmesg_kwargs
  117. )
  118. self.assertEqual(
  119. Registry.states["/usr/local/bin/pydmesg"], self.pydmesg_expected
  120. )
  121. def test_context_manager(self):
  122. with self.File("/usr/local/bin"):
  123. pydmesg = self.File.managed("/usr/local/bin/pydmesg", **self.pydmesg_kwargs)
  124. self.assertEqual(
  125. Registry.states["/usr/local/bin/pydmesg"], self.pydmesg_expected
  126. )
  127. with pydmesg:
  128. self.File.managed("/tmp/something", owner="root")
  129. self.assertEqual(
  130. Registry.states["/tmp/something"],
  131. {
  132. "file.managed": [
  133. {"owner": "root"},
  134. {
  135. "require": [
  136. {"file": "/usr/local/bin"},
  137. {"file": "/usr/local/bin/pydmesg"},
  138. ]
  139. },
  140. ]
  141. },
  142. )
  143. def test_salt_data(self):
  144. self.File.managed(
  145. "/usr/local/bin/pydmesg",
  146. require=self.File("/usr/local/bin"),
  147. **self.pydmesg_kwargs
  148. )
  149. self.assertEqual(
  150. Registry.states["/usr/local/bin/pydmesg"], self.pydmesg_expected
  151. )
  152. self.assertEqual(Registry.salt_data(), self.pydmesg_salt_expected)
  153. self.assertEqual(Registry.states, OrderedDict())
  154. def test_duplicates(self):
  155. def add_dup():
  156. self.File.managed("dup", name="/dup")
  157. add_dup()
  158. self.assertRaises(DuplicateState, add_dup)
  159. Service = StateFactory("service")
  160. Service.running("dup", name="dup-service")
  161. self.assertEqual(
  162. Registry.states,
  163. OrderedDict(
  164. [
  165. (
  166. "dup",
  167. OrderedDict(
  168. [
  169. ("file.managed", [{"name": "/dup"}]),
  170. ("service.running", [{"name": "dup-service"}]),
  171. ]
  172. ),
  173. )
  174. ]
  175. ),
  176. )
  177. class RendererMixin:
  178. """
  179. This is a mixin that adds a ``.render()`` method to render a template
  180. It must come BEFORE ``TestCase`` in the declaration of your test case
  181. class so that our setUp & tearDown get invoked first, and super can
  182. trigger the methods in the ``TestCase`` class.
  183. """
  184. def setUp(self, *args, **kwargs):
  185. super().setUp(*args, **kwargs)
  186. self.root_dir = tempfile.mkdtemp("pyobjects_test_root", dir=RUNTIME_VARS.TMP)
  187. self.state_tree_dir = os.path.join(self.root_dir, "state_tree")
  188. self.cache_dir = os.path.join(self.root_dir, "cachedir")
  189. if not os.path.isdir(self.root_dir):
  190. os.makedirs(self.root_dir)
  191. if not os.path.isdir(self.state_tree_dir):
  192. os.makedirs(self.state_tree_dir)
  193. if not os.path.isdir(self.cache_dir):
  194. os.makedirs(self.cache_dir)
  195. self.config = salt.config.minion_config(None)
  196. self.config["root_dir"] = self.root_dir
  197. self.config["state_events"] = False
  198. self.config["id"] = "match"
  199. self.config["file_client"] = "local"
  200. self.config["file_roots"] = dict(base=[self.state_tree_dir])
  201. self.config["cachedir"] = self.cache_dir
  202. self.config["test"] = False
  203. def tearDown(self, *args, **kwargs):
  204. shutil.rmtree(self.root_dir)
  205. del self.config
  206. super().tearDown(*args, **kwargs)
  207. def write_template_file(self, filename, content):
  208. full_path = os.path.join(self.state_tree_dir, filename)
  209. with salt.utils.files.fopen(full_path, "w") as f:
  210. f.write(content)
  211. return full_path
  212. def render(self, template, opts=None, filename=None):
  213. if opts:
  214. self.config.update(opts)
  215. if not filename:
  216. filename = ".".join([str(uuid.uuid4()), "sls"])
  217. full_path = self.write_template_file(filename, template)
  218. state = salt.state.State(self.config)
  219. return compile_template(
  220. full_path,
  221. state.rend,
  222. state.opts["renderer"],
  223. state.opts["renderer_blacklist"],
  224. state.opts["renderer_whitelist"],
  225. )
  226. class RendererTests(RendererMixin, StateTests, MapBuilder):
  227. @classmethod
  228. def setUpClass(cls):
  229. cls.recursive_map_template = textwrap.dedent(
  230. """\
  231. #!pyobjects
  232. from salt://map.sls import Samba
  233. class CustomSamba(Samba):
  234. pass
  235. """
  236. )
  237. cls.recursive_import_template = textwrap.dedent(
  238. """\
  239. #!pyobjects
  240. from salt://recursive_map.sls import CustomSamba
  241. Pkg.removed("samba-imported", names=[CustomSamba.server, CustomSamba.client])"""
  242. )
  243. cls.File = StateFactory("file")
  244. @classmethod
  245. def tearDownClass(cls):
  246. cls.File = None
  247. @slowTest
  248. def test_basic(self):
  249. basic_template = textwrap.dedent(
  250. """\
  251. #!pyobjects
  252. File.directory('/tmp', mode='1777', owner='root', group='root')
  253. """
  254. )
  255. ret = self.render(basic_template)
  256. self.assertEqual(
  257. ret,
  258. OrderedDict(
  259. [
  260. (
  261. "/tmp",
  262. {
  263. "file.directory": [
  264. {"group": "root"},
  265. {"mode": "1777"},
  266. {"owner": "root"},
  267. ]
  268. },
  269. ),
  270. ]
  271. ),
  272. )
  273. self.assertEqual(Registry.states, OrderedDict())
  274. @slowTest
  275. def test_invalid_function(self):
  276. def _test():
  277. invalid_template = textwrap.dedent(
  278. """\
  279. #!pyobjects
  280. File.fail('/tmp')
  281. """
  282. )
  283. self.render(invalid_template)
  284. self.assertRaises(InvalidFunction, _test)
  285. @slowTest
  286. def test_include(self):
  287. include_template = textwrap.dedent(
  288. """\
  289. #!pyobjects
  290. include('http')
  291. """
  292. )
  293. ret = self.render(include_template)
  294. self.assertEqual(ret, OrderedDict([("include", ["http"])]))
  295. @slowTest
  296. def test_extend(self):
  297. extend_template = textwrap.dedent(
  298. """\
  299. #!pyobjects
  300. include('http')
  301. from salt.utils.pyobjects import StateFactory
  302. Service = StateFactory('service')
  303. Service.running(extend('apache'), watch=[{'file': '/etc/file'}])
  304. """
  305. )
  306. ret = self.render(
  307. extend_template, {"grains": {"os_family": "Debian", "os": "Debian"}}
  308. )
  309. self.assertEqual(
  310. ret,
  311. OrderedDict(
  312. [
  313. ("include", ["http"]),
  314. (
  315. "extend",
  316. OrderedDict(
  317. [
  318. (
  319. "apache",
  320. {
  321. "service.running": [
  322. {"watch": [{"file": "/etc/file"}]}
  323. ]
  324. },
  325. ),
  326. ]
  327. ),
  328. ),
  329. ]
  330. ),
  331. )
  332. @slowTest
  333. def test_sls_imports(self):
  334. def render_and_assert(template):
  335. ret = self.render(
  336. template, {"grains": {"os_family": "Debian", "os": "Debian"}}
  337. )
  338. self.assertEqual(
  339. ret,
  340. OrderedDict(
  341. [
  342. (
  343. "samba-imported",
  344. {"pkg.removed": [{"names": ["samba", "samba-client"]}]},
  345. )
  346. ]
  347. ),
  348. )
  349. self.write_template_file("map.sls", self.build_map())
  350. import_template = textwrap.dedent(
  351. """\
  352. #!pyobjects
  353. import salt://map.sls
  354. Pkg.removed("samba-imported", names=[map.Samba.server, map.Samba.client])
  355. """
  356. )
  357. render_and_assert(import_template)
  358. from_import_template = textwrap.dedent(
  359. """\
  360. #!pyobjects
  361. # this spacing is like this on purpose to ensure it's stripped properly
  362. from salt://map.sls import Samba
  363. Pkg.removed("samba-imported", names=[Samba.server, Samba.client])
  364. """
  365. )
  366. render_and_assert(from_import_template)
  367. import_as_template = textwrap.dedent(
  368. """\
  369. #!pyobjects
  370. from salt://map.sls import Samba as Other
  371. Pkg.removed("samba-imported", names=[Other.server, Other.client])
  372. """
  373. )
  374. render_and_assert(import_as_template)
  375. self.write_template_file("recursive_map.sls", self.recursive_map_template)
  376. render_and_assert(self.recursive_import_template)
  377. @slowTest
  378. def test_import_scope(self):
  379. self.write_template_file("map.sls", self.build_map())
  380. self.write_template_file("recursive_map.sls", self.recursive_map_template)
  381. def do_render():
  382. scope_test_import_template = textwrap.dedent(
  383. """\
  384. #!pyobjects
  385. from salt://recursive_map.sls import CustomSamba
  386. # since we import CustomSamba we should shouldn't be able to see Samba
  387. Pkg.removed("samba-imported", names=[Samba.server, Samba.client])"""
  388. )
  389. ret = self.render(
  390. scope_test_import_template,
  391. {"grains": {"os_family": "Debian", "os": "Debian"}},
  392. )
  393. self.assertRaises(NameError, do_render)
  394. @slowTest
  395. def test_random_password(self):
  396. """Test for https://github.com/saltstack/salt/issues/21796"""
  397. random_password_template = textwrap.dedent(
  398. """\
  399. #!pyobjects
  400. import random, string
  401. password = ''.join([random.SystemRandom().choice(
  402. string.ascii_letters + string.digits) for _ in range(20)])
  403. """
  404. )
  405. ret = self.render(random_password_template)
  406. @slowTest
  407. def test_import_random_password(self):
  408. """Import test for https://github.com/saltstack/salt/issues/21796"""
  409. random_password_template = textwrap.dedent(
  410. """\
  411. #!pyobjects
  412. import random, string
  413. password = ''.join([random.SystemRandom().choice(
  414. string.ascii_letters + string.digits) for _ in range(20)])
  415. """
  416. )
  417. self.write_template_file("password.sls", random_password_template)
  418. random_password_import_template = textwrap.dedent(
  419. """\
  420. #!pyobjects
  421. from salt://password.sls import password
  422. """
  423. )
  424. ret = self.render(random_password_import_template)
  425. @slowTest
  426. def test_requisite_implicit_list(self):
  427. """Ensure that the implicit list characteristic works as expected"""
  428. requisite_implicit_list_template = textwrap.dedent(
  429. """\
  430. #!pyobjects
  431. from salt.utils.pyobjects import StateFactory
  432. Service = StateFactory('service')
  433. with Pkg.installed("pkg"):
  434. Service.running("service", watch=File("file"), require=Cmd("cmd"))
  435. """
  436. )
  437. ret = self.render(
  438. requisite_implicit_list_template,
  439. {"grains": {"os_family": "Debian", "os": "Debian"}},
  440. )
  441. self.assertEqual(
  442. ret,
  443. OrderedDict(
  444. [
  445. ("pkg", OrderedDict([("pkg.installed", [])])),
  446. (
  447. "service",
  448. OrderedDict(
  449. [
  450. (
  451. "service.running",
  452. [
  453. {"require": [{"cmd": "cmd"}, {"pkg": "pkg"}]},
  454. {"watch": [{"file": "file"}]},
  455. ],
  456. )
  457. ]
  458. ),
  459. ),
  460. ]
  461. ),
  462. )
  463. class MapTests(RendererMixin, TestCase, MapBuilder):
  464. maxDiff = None
  465. debian_grains = {"os_family": "Debian", "os": "Debian"}
  466. ubuntu_grains = {"os_family": "Debian", "os": "Ubuntu"}
  467. centos_grains = {"os_family": "RedHat", "os": "CentOS"}
  468. debian_attrs = ("samba", "samba-client", "samba")
  469. ubuntu_attrs = ("samba", "samba-client", "smbd")
  470. centos_attrs = ("samba", "samba", "smb")
  471. def samba_with_grains(self, template, grains):
  472. return self.render(template, {"grains": grains})
  473. def assert_equal(self, ret, server, client, service):
  474. self.assertDictEqual(
  475. ret,
  476. OrderedDict(
  477. [
  478. (
  479. "samba",
  480. OrderedDict(
  481. [
  482. ("pkg.installed", [{"names": [server, client]}]),
  483. (
  484. "service.running",
  485. [
  486. {"name": service},
  487. {"require": [{"pkg": "samba"}]},
  488. ],
  489. ),
  490. ]
  491. ),
  492. )
  493. ]
  494. ),
  495. )
  496. def assert_not_equal(self, ret, server, client, service):
  497. try:
  498. self.assert_equal(ret, server, client, service)
  499. except AssertionError:
  500. pass
  501. else:
  502. raise AssertionError("both dicts are equal")
  503. @slowTest
  504. def test_map(self):
  505. """
  506. Test declarative ordering
  507. """
  508. # With declarative ordering, the ubuntu-specific service name should
  509. # override the one inherited from debian.
  510. template = self.build_map(
  511. textwrap.dedent(
  512. """\
  513. {{ debian }}
  514. {{ centos }}
  515. {{ ubuntu }}
  516. """
  517. )
  518. )
  519. ret = self.samba_with_grains(template, self.debian_grains)
  520. self.assert_equal(ret, *self.debian_attrs)
  521. ret = self.samba_with_grains(template, self.ubuntu_grains)
  522. self.assert_equal(ret, *self.ubuntu_attrs)
  523. ret = self.samba_with_grains(template, self.centos_grains)
  524. self.assert_equal(ret, *self.centos_attrs)
  525. # Switching the order, debian should still work fine but ubuntu should
  526. # no longer match, since the debian service name should override the
  527. # ubuntu one.
  528. template = self.build_map(
  529. textwrap.dedent(
  530. """\
  531. {{ ubuntu }}
  532. {{ debian }}
  533. """
  534. )
  535. )
  536. ret = self.samba_with_grains(template, self.debian_grains)
  537. self.assert_equal(ret, *self.debian_attrs)
  538. ret = self.samba_with_grains(template, self.ubuntu_grains)
  539. self.assert_not_equal(ret, *self.ubuntu_attrs)
  540. @slowTest
  541. def test_map_with_priority(self):
  542. """
  543. With declarative ordering, the debian service name would override the
  544. ubuntu one since debian comes second. This will test overriding this
  545. behavior using the priority attribute.
  546. """
  547. template = self.build_map(
  548. textwrap.dedent(
  549. """\
  550. {{ priority(('os_family', 'os')) }}
  551. {{ ubuntu }}
  552. {{ centos }}
  553. {{ debian }}
  554. """
  555. )
  556. )
  557. ret = self.samba_with_grains(template, self.debian_grains)
  558. self.assert_equal(ret, *self.debian_attrs)
  559. ret = self.samba_with_grains(template, self.ubuntu_grains)
  560. self.assert_equal(ret, *self.ubuntu_attrs)
  561. ret = self.samba_with_grains(template, self.centos_grains)
  562. self.assert_equal(ret, *self.centos_attrs)
  563. class SaltObjectTests(TestCase):
  564. def test_salt_object(self):
  565. def attr_fail():
  566. Salt.fail.blah()
  567. def times2(x):
  568. return x * 2
  569. __salt__ = {"math.times2": times2}
  570. Salt = SaltObject(__salt__)
  571. self.assertRaises(AttributeError, attr_fail)
  572. self.assertEqual(Salt.math.times2, times2)
  573. self.assertEqual(Salt.math.times2(2), 4)