salt/tests/unit/test_pyobjects.py
2017-06-28 19:48:46 -05:00

542 lines
16 KiB
Python

# -*- coding: utf-8 -*-
# Import Pytohn libs
from __future__ import absolute_import
import jinja2
import logging
import os
import shutil
import tempfile
import textwrap
import uuid
# Import Salt Testing libs
from tests.support.unit import TestCase
# Import Salt libs
import tests.integration as integration
import salt.config
import salt.state
import salt.utils
from salt.template import compile_template
from salt.utils.odict import OrderedDict
from salt.utils.pyobjects import (StateFactory, State, Registry,
SaltObject, InvalidFunction, DuplicateState)
log = logging.getLogger(__name__)
File = StateFactory('file')
Service = StateFactory('service')
pydmesg_expected = {
'file.managed': [
{'group': 'root'},
{'mode': '0755'},
{'require': [{'file': '/usr/local/bin'}]},
{'source': 'salt://debian/files/pydmesg.py'},
{'user': 'root'},
]
}
pydmesg_salt_expected = OrderedDict([
('/usr/local/bin/pydmesg', pydmesg_expected)
])
pydmesg_kwargs = dict(user='root', group='root', mode='0755',
source='salt://debian/files/pydmesg.py')
basic_template = '''#!pyobjects
File.directory('/tmp', mode='1777', owner='root', group='root')
'''
invalid_template = '''#!pyobjects
File.fail('/tmp')
'''
include_template = '''#!pyobjects
include('http')
'''
extend_template = '''#!pyobjects
include('http')
from salt.utils.pyobjects import StateFactory
Service = StateFactory('service')
Service.running(extend('apache'), watch=[{'file': '/etc/file'}])
'''
map_prefix = '''\
#!pyobjects
from salt.utils.pyobjects import StateFactory
Service = StateFactory('service')
{% macro priority(value) %}
priority = {{ value }}
{% endmacro %}
class Samba(Map):
'''
map_suffix = '''
with Pkg.installed("samba", names=[Samba.server, Samba.client]):
Service.running("samba", name=Samba.service)
'''
map_data = {
'debian': " class Debian:\n"
" server = 'samba'\n"
" client = 'samba-client'\n"
" service = 'samba'\n",
'centos': " class RougeChapeau:\n"
" __match__ = 'RedHat'\n"
" server = 'samba'\n"
" client = 'samba'\n"
" service = 'smb'\n",
'ubuntu': " class Ubuntu:\n"
" __grain__ = 'os'\n"
" service = 'smbd'\n"
}
import_template = '''#!pyobjects
import salt://map.sls
Pkg.removed("samba-imported", names=[map.Samba.server, map.Samba.client])
'''
recursive_map_template = '''#!pyobjects
from salt://map.sls import Samba
class CustomSamba(Samba):
pass
'''
recursive_import_template = '''#!pyobjects
from salt://recursive_map.sls import CustomSamba
Pkg.removed("samba-imported", names=[CustomSamba.server, CustomSamba.client])'''
scope_test_import_template = '''#!pyobjects
from salt://recursive_map.sls import CustomSamba
# since we import CustomSamba we should shouldn't be able to see Samba
Pkg.removed("samba-imported", names=[Samba.server, Samba.client])'''
from_import_template = '''#!pyobjects
# this spacing is like this on purpose to ensure it's stripped properly
from salt://map.sls import Samba
Pkg.removed("samba-imported", names=[Samba.server, Samba.client])
'''
import_as_template = '''#!pyobjects
from salt://map.sls import Samba as Other
Pkg.removed("samba-imported", names=[Other.server, Other.client])
'''
random_password_template = '''#!pyobjects
import random, string
password = ''.join([random.SystemRandom().choice(
string.ascii_letters + string.digits) for _ in range(20)])
'''
random_password_import_template = '''#!pyobjects
from salt://password.sls import password
'''
requisite_implicit_list_template = '''#!pyobjects
from salt.utils.pyobjects import StateFactory
Service = StateFactory('service')
with Pkg.installed("pkg"):
Service.running("service", watch=File("file"), require=Cmd("cmd"))
'''
class MapBuilder(object):
def build_map(self, template=None):
'''
Build from a specific template or just use a default if no template
is passed to this function.
'''
if template is None:
template = textwrap.dedent('''\
{{ ubuntu }}
{{ centos }}
{{ debian }}
''')
full_template = map_prefix + template + map_suffix
ret = jinja2.Template(full_template).render(**map_data)
log.debug('built map: \n%s', ret)
return ret
class StateTests(TestCase):
def setUp(self):
Registry.empty()
def test_serialization(self):
f = State('/usr/local/bin/pydmesg', 'file', 'managed',
require=File('/usr/local/bin'),
**pydmesg_kwargs)
self.assertEqual(f(), pydmesg_expected)
def test_factory_serialization(self):
File.managed('/usr/local/bin/pydmesg',
require=File('/usr/local/bin'),
**pydmesg_kwargs)
self.assertEqual(
Registry.states['/usr/local/bin/pydmesg'],
pydmesg_expected
)
def test_context_manager(self):
with File('/usr/local/bin'):
pydmesg = File.managed('/usr/local/bin/pydmesg', **pydmesg_kwargs)
self.assertEqual(
Registry.states['/usr/local/bin/pydmesg'],
pydmesg_expected
)
with pydmesg:
File.managed('/tmp/something', owner='root')
self.assertEqual(
Registry.states['/tmp/something'],
{
'file.managed': [
{'owner': 'root'},
{'require': [
{'file': '/usr/local/bin'},
{'file': '/usr/local/bin/pydmesg'}
]},
]
}
)
def test_salt_data(self):
File.managed('/usr/local/bin/pydmesg',
require=File('/usr/local/bin'),
**pydmesg_kwargs)
self.assertEqual(
Registry.states['/usr/local/bin/pydmesg'],
pydmesg_expected
)
self.assertEqual(
Registry.salt_data(),
pydmesg_salt_expected
)
self.assertEqual(
Registry.states,
OrderedDict()
)
def test_duplicates(self):
def add_dup():
File.managed('dup', name='/dup')
add_dup()
self.assertRaises(DuplicateState, add_dup)
Service.running('dup', name='dup-service')
self.assertEqual(
Registry.states,
OrderedDict([
('dup',
OrderedDict([
('file.managed', [
{'name': '/dup'}
]),
('service.running', [
{'name': 'dup-service'}
])
]))
])
)
class RendererMixin(object):
'''
This is a mixin that adds a ``.render()`` method to render a template
It must come BEFORE ``TestCase`` in the declaration of your test case
class so that our setUp & tearDown get invoked first, and super can
trigger the methods in the ``TestCase`` class.
'''
def setUp(self, *args, **kwargs):
super(RendererMixin, self).setUp(*args, **kwargs)
self.root_dir = tempfile.mkdtemp('pyobjects_test_root', dir=integration.TMP)
self.state_tree_dir = os.path.join(self.root_dir, 'state_tree')
self.cache_dir = os.path.join(self.root_dir, 'cachedir')
if not os.path.isdir(self.root_dir):
os.makedirs(self.root_dir)
if not os.path.isdir(self.state_tree_dir):
os.makedirs(self.state_tree_dir)
if not os.path.isdir(self.cache_dir):
os.makedirs(self.cache_dir)
self.config = salt.config.minion_config(None)
self.config['root_dir'] = self.root_dir
self.config['state_events'] = False
self.config['id'] = 'match'
self.config['file_client'] = 'local'
self.config['file_roots'] = dict(base=[self.state_tree_dir])
self.config['cachedir'] = self.cache_dir
self.config['test'] = False
def tearDown(self, *args, **kwargs):
shutil.rmtree(self.root_dir)
del self.config
super(RendererMixin, self).tearDown(*args, **kwargs)
def write_template_file(self, filename, content):
full_path = os.path.join(self.state_tree_dir, filename)
with salt.utils.fopen(full_path, 'w') as f:
f.write(content)
return full_path
def render(self, template, opts=None, filename=None):
if opts:
self.config.update(opts)
if not filename:
filename = ".".join([
str(uuid.uuid4()),
"sls"
])
full_path = self.write_template_file(filename, template)
state = salt.state.State(self.config)
return compile_template(full_path,
state.rend,
state.opts['renderer'],
state.opts['renderer_blacklist'],
state.opts['renderer_whitelist'])
class RendererTests(RendererMixin, StateTests, MapBuilder):
def test_basic(self):
ret = self.render(basic_template)
self.assertEqual(ret, OrderedDict([
('/tmp', {
'file.directory': [
{'group': 'root'},
{'mode': '1777'},
{'owner': 'root'}
]
}),
]))
self.assertEqual(Registry.states, OrderedDict())
def test_invalid_function(self):
def _test():
self.render(invalid_template)
self.assertRaises(InvalidFunction, _test)
def test_include(self):
ret = self.render(include_template)
self.assertEqual(ret, OrderedDict([
('include', ['http']),
]))
def test_extend(self):
ret = self.render(extend_template,
{'grains': {
'os_family': 'Debian',
'os': 'Debian'
}})
self.assertEqual(ret, OrderedDict([
('include', ['http']),
('extend', OrderedDict([
('apache', {
'service.running': [
{'watch': [{'file': '/etc/file'}]}
]
}),
])),
]))
def test_sls_imports(self):
def render_and_assert(template):
ret = self.render(template,
{'grains': {
'os_family': 'Debian',
'os': 'Debian'
}})
self.assertEqual(ret, OrderedDict([
('samba-imported', {
'pkg.removed': [
{'names': ['samba', 'samba-client']},
]
})
]))
self.write_template_file("map.sls", self.build_map())
render_and_assert(import_template)
render_and_assert(from_import_template)
render_and_assert(import_as_template)
self.write_template_file("recursive_map.sls", recursive_map_template)
render_and_assert(recursive_import_template)
def test_import_scope(self):
self.write_template_file("map.sls", self.build_map())
self.write_template_file("recursive_map.sls", recursive_map_template)
def do_render():
ret = self.render(scope_test_import_template,
{'grains': {
'os_family': 'Debian',
'os': 'Debian'
}})
self.assertRaises(NameError, do_render)
def test_random_password(self):
'''Test for https://github.com/saltstack/salt/issues/21796'''
ret = self.render(random_password_template)
def test_import_random_password(self):
'''Import test for https://github.com/saltstack/salt/issues/21796'''
self.write_template_file("password.sls", random_password_template)
ret = self.render(random_password_import_template)
def test_requisite_implicit_list(self):
'''Ensure that the implicit list characteristic works as expected'''
ret = self.render(requisite_implicit_list_template,
{'grains': {
'os_family': 'Debian',
'os': 'Debian'
}})
self.assertEqual(ret, OrderedDict([
('pkg', OrderedDict([
('pkg.installed', [])
])),
('service', OrderedDict([
('service.running', [
{'require': [{'cmd': 'cmd'}, {'pkg': 'pkg'}]},
{'watch': [{'file': 'file'}]},
])
]))
]))
class MapTests(RendererMixin, TestCase, MapBuilder):
maxDiff = None
debian_grains = {'os_family': 'Debian', 'os': 'Debian'}
ubuntu_grains = {'os_family': 'Debian', 'os': 'Ubuntu'}
centos_grains = {'os_family': 'RedHat', 'os': 'CentOS'}
debian_attrs = ('samba', 'samba-client', 'samba')
ubuntu_attrs = ('samba', 'samba-client', 'smbd')
centos_attrs = ('samba', 'samba', 'smb')
def samba_with_grains(self, template, grains):
return self.render(template, {'grains': grains})
def assert_equal(self, ret, server, client, service):
self.assertDictEqual(ret, OrderedDict([
('samba', OrderedDict([
('pkg.installed', [
{'names': [server, client]}
]),
('service.running', [
{'name': service},
{'require': [{'pkg': 'samba'}]}
])
]))
]))
def assert_not_equal(self, ret, server, client, service):
try:
self.assert_equal(ret, server, client, service)
except AssertionError:
pass
else:
raise AssertionError('both dicts are equal')
def test_map(self):
'''
Test declarative ordering
'''
# With declarative ordering, the ubuntu-specfic service name should
# override the one inherited from debian.
template = self.build_map(textwrap.dedent('''\
{{ debian }}
{{ centos }}
{{ ubuntu }}
'''))
ret = self.samba_with_grains(template, self.debian_grains)
self.assert_equal(ret, *self.debian_attrs)
ret = self.samba_with_grains(template, self.ubuntu_grains)
self.assert_equal(ret, *self.ubuntu_attrs)
ret = self.samba_with_grains(template, self.centos_grains)
self.assert_equal(ret, *self.centos_attrs)
# Switching the order, debian should still work fine but ubuntu should
# no longer match, since the debian service name should override the
# ubuntu one.
template = self.build_map(textwrap.dedent('''\
{{ ubuntu }}
{{ debian }}
'''))
ret = self.samba_with_grains(template, self.debian_grains)
self.assert_equal(ret, *self.debian_attrs)
ret = self.samba_with_grains(template, self.ubuntu_grains)
self.assert_not_equal(ret, *self.ubuntu_attrs)
def test_map_with_priority(self):
'''
With declarative ordering, the debian service name would override the
ubuntu one since debian comes second. This will test overriding this
behavior using the priority attribute.
'''
template = self.build_map(textwrap.dedent('''\
{{ priority(('os_family', 'os')) }}
{{ ubuntu }}
{{ centos }}
{{ debian }}
'''))
ret = self.samba_with_grains(template, self.debian_grains)
self.assert_equal(ret, *self.debian_attrs)
ret = self.samba_with_grains(template, self.ubuntu_grains)
self.assert_equal(ret, *self.ubuntu_attrs)
ret = self.samba_with_grains(template, self.centos_grains)
self.assert_equal(ret, *self.centos_attrs)
class SaltObjectTests(TestCase):
def test_salt_object(self):
def attr_fail():
Salt.fail.blah()
def times2(x):
return x*2
__salt__ = {
'math.times2': times2
}
Salt = SaltObject(__salt__)
self.assertRaises(AttributeError, attr_fail)
self.assertEqual(Salt.math.times2, times2)
self.assertEqual(Salt.math.times2(2), 4)