1
0

test_pyobjects.py 17 KB

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