375 lines
14 KiB
Python
375 lines
14 KiB
Python
|
|
import os
|
|
import unittest
|
|
|
|
from mock import patch, call, MagicMock
|
|
|
|
import charmhelpers.contrib.openstack.templating as templating
|
|
|
|
from jinja2.exceptions import TemplateNotFound
|
|
|
|
import six
|
|
if not six.PY3:
|
|
builtin_open = '__builtin__.open'
|
|
else:
|
|
builtin_open = 'builtins.open'
|
|
|
|
|
|
class FakeContextGenerator(object):
|
|
interfaces = None
|
|
|
|
def set(self, interfaces, context):
|
|
self.interfaces = interfaces
|
|
self.context = context
|
|
|
|
def __call__(self):
|
|
return self.context
|
|
|
|
|
|
class FakeLoader(object):
|
|
def set(self, template):
|
|
self.template = template
|
|
|
|
def get(self, name):
|
|
return self.template
|
|
|
|
|
|
class MockFSLoader(object):
|
|
def __init__(self, dirs):
|
|
self.searchpath = [dirs]
|
|
|
|
|
|
class MockChoiceLoader(object):
|
|
def __init__(self, loaders):
|
|
self.loaders = loaders
|
|
|
|
|
|
def MockTemplate():
|
|
templ = MagicMock()
|
|
templ.render = MagicMock()
|
|
return templ
|
|
|
|
|
|
class TemplatingTests(unittest.TestCase):
|
|
def setUp(self):
|
|
path = os.path.dirname(__file__)
|
|
self.loader = FakeLoader()
|
|
self.context = FakeContextGenerator()
|
|
|
|
self.addCleanup(patch.object(templating, 'apt_install').start().stop())
|
|
self.addCleanup(patch.object(templating, 'log').start().stop())
|
|
|
|
templating.FileSystemLoader = MockFSLoader
|
|
templating.ChoiceLoader = MockChoiceLoader
|
|
templating.Environment = MagicMock
|
|
|
|
self.renderer = templating.OSConfigRenderer(templates_dir=path,
|
|
openstack_release='folsom')
|
|
|
|
@patch.object(templating, 'apt_install')
|
|
def test_initializing_a_render_ensures_jinja2_present(self, apt):
|
|
'''Creatinga new renderer object installs jinja2 if needed'''
|
|
# temp. undo the patching from setUp
|
|
templating.FileSystemLoader = None
|
|
templating.ChoiceLoader = None
|
|
templating.Environment = None
|
|
templating.OSConfigRenderer(templates_dir='/tmp',
|
|
openstack_release='foo')
|
|
templating.FileSystemLoader = MockFSLoader
|
|
templating.ChoiceLoader = MockChoiceLoader
|
|
templating.Environment = MagicMock
|
|
if six.PY2:
|
|
apt.assert_called_with('python-jinja2')
|
|
else:
|
|
apt.assert_called_with('python3-jinja2')
|
|
|
|
def test_create_renderer_invalid_templates_dir(self):
|
|
'''Ensure OSConfigRenderer checks templates_dir'''
|
|
self.assertRaises(templating.OSConfigException,
|
|
templating.OSConfigRenderer,
|
|
templates_dir='/tmp/foooo0',
|
|
openstack_release='grizzly')
|
|
|
|
def test_render_unregistered_config(self):
|
|
'''Ensure cannot render an unregistered config file'''
|
|
self.assertRaises(templating.OSConfigException,
|
|
self.renderer.render,
|
|
config_file='/tmp/foo')
|
|
|
|
def test_write_unregistered_config(self):
|
|
'''Ensure cannot write an unregistered config file'''
|
|
self.assertRaises(templating.OSConfigException,
|
|
self.renderer.write,
|
|
config_file='/tmp/foo')
|
|
|
|
def test_render_complete_context(self):
|
|
'''It renders a template when provided a complete context'''
|
|
self.loader.set('{{ foo }}')
|
|
self.context.set(interfaces=['fooservice'], context={'foo': 'bar'})
|
|
self.renderer.register('/tmp/foo', [self.context])
|
|
with patch.object(self.renderer, '_get_template') as _get_t:
|
|
fake_tmpl = MockTemplate()
|
|
_get_t.return_value = fake_tmpl
|
|
self.renderer.render('/tmp/foo')
|
|
fake_tmpl.render.assert_called_with(self.context())
|
|
self.assertIn('fooservice', self.renderer.complete_contexts())
|
|
|
|
def test_render_incomplete_context_with_template(self):
|
|
'''It renders a template when provided an incomplete context'''
|
|
self.context.set(interfaces=['fooservice'], context={})
|
|
self.renderer.register('/tmp/foo', [self.context])
|
|
with patch.object(self.renderer, '_get_template') as _get_t:
|
|
fake_tmpl = MockTemplate()
|
|
_get_t.return_value = fake_tmpl
|
|
self.renderer.render('/tmp/foo')
|
|
fake_tmpl.render.assert_called_with({})
|
|
self.assertNotIn('fooservice', self.renderer.complete_contexts())
|
|
|
|
def test_render_template_registered_but_not_found(self):
|
|
'''It loads a template by basename of config file first'''
|
|
path = os.path.dirname(__file__)
|
|
renderer = templating.OSConfigRenderer(templates_dir=path,
|
|
openstack_release='folsom')
|
|
e = TemplateNotFound('')
|
|
renderer._get_template = MagicMock()
|
|
renderer._get_template.side_effect = e
|
|
renderer.register('/etc/nova/nova.conf', contexts=[])
|
|
self.assertRaises(
|
|
TemplateNotFound, renderer.render, '/etc/nova/nova.conf')
|
|
|
|
def test_render_template_by_basename_first(self):
|
|
'''It loads a template by basename of config file first'''
|
|
path = os.path.dirname(__file__)
|
|
renderer = templating.OSConfigRenderer(templates_dir=path,
|
|
openstack_release='folsom')
|
|
renderer._get_template = MagicMock()
|
|
renderer.register('/etc/nova/nova.conf', contexts=[])
|
|
renderer.render('/etc/nova/nova.conf')
|
|
self.assertEquals(1, len(renderer._get_template.call_args_list))
|
|
self.assertEquals(
|
|
[call('nova.conf')], renderer._get_template.call_args_list)
|
|
|
|
def test_render_template_by_munged_full_path_last(self):
|
|
'''It loads a template by full path of config file second'''
|
|
path = os.path.dirname(__file__)
|
|
renderer = templating.OSConfigRenderer(templates_dir=path,
|
|
openstack_release='folsom')
|
|
tmp = MagicMock()
|
|
tmp.render = MagicMock()
|
|
e = TemplateNotFound('')
|
|
renderer._get_template = MagicMock()
|
|
renderer._get_template.side_effect = [e, tmp]
|
|
renderer.register('/etc/nova/nova.conf', contexts=[])
|
|
renderer.render('/etc/nova/nova.conf')
|
|
self.assertEquals(2, len(renderer._get_template.call_args_list))
|
|
self.assertEquals(
|
|
[call('nova.conf'), call('etc_nova_nova.conf')],
|
|
renderer._get_template.call_args_list)
|
|
|
|
def test_render_template_by_basename(self):
|
|
'''It renders template if it finds it by config file basename'''
|
|
|
|
@patch(builtin_open)
|
|
@patch.object(templating, 'get_loader')
|
|
def test_write_out_config(self, loader, _open):
|
|
'''It writes a templated config when provided a complete context'''
|
|
self.context.set(interfaces=['fooservice'], context={'foo': 'bar'})
|
|
self.renderer.register('/tmp/foo', [self.context])
|
|
with patch.object(self.renderer, '_get_template') as _get_t:
|
|
fake_tmpl = MockTemplate()
|
|
_get_t.return_value = fake_tmpl
|
|
self.renderer.write('/tmp/foo')
|
|
_open.assert_called_with('/tmp/foo', 'wb')
|
|
|
|
def test_write_all(self):
|
|
'''It writes out all configuration files at once'''
|
|
self.context.set(interfaces=['fooservice'], context={'foo': 'bar'})
|
|
self.renderer.register('/tmp/foo', [self.context])
|
|
self.renderer.register('/tmp/bar', [self.context])
|
|
ex_calls = [
|
|
call('/tmp/bar'),
|
|
call('/tmp/foo'),
|
|
]
|
|
with patch.object(self.renderer, 'write') as _write:
|
|
self.renderer.write_all()
|
|
self.assertEquals(sorted(ex_calls), sorted(_write.call_args_list))
|
|
pass
|
|
|
|
@patch.object(templating, 'get_loader')
|
|
def test_reset_template_loader_for_new_os_release(self, loader):
|
|
self.loader.set('')
|
|
self.context.set(interfaces=['fooservice'], context={})
|
|
loader.return_value = MockFSLoader('/tmp/foo')
|
|
self.renderer.register('/tmp/foo', [self.context])
|
|
self.renderer.render('/tmp/foo')
|
|
loader.assert_called_with(os.path.dirname(__file__), 'folsom')
|
|
self.renderer.set_release(openstack_release='grizzly')
|
|
self.renderer.render('/tmp/foo')
|
|
loader.assert_called_with(os.path.dirname(__file__), 'grizzly')
|
|
|
|
@patch.object(templating, 'get_loader')
|
|
def test_incomplete_context_not_reported_complete(self, loader):
|
|
'''It does not recognize an incomplete context as a complete context'''
|
|
self.context.set(interfaces=['fooservice'], context={})
|
|
self.renderer.register('/tmp/foo', [self.context])
|
|
self.assertNotIn('fooservice', self.renderer.complete_contexts())
|
|
|
|
@patch.object(templating, 'get_loader')
|
|
def test_complete_context_reported_complete(self, loader):
|
|
'''It recognizes a complete context as a complete context'''
|
|
self.context.set(interfaces=['fooservice'], context={'foo': 'bar'})
|
|
self.renderer.register('/tmp/foo', [self.context])
|
|
self.assertIn('fooservice', self.renderer.complete_contexts())
|
|
|
|
@patch('os.path.isdir')
|
|
def test_get_loader_no_templates_dir(self, isdir):
|
|
'''Ensure getting loader fails with no template dir'''
|
|
isdir.return_value = False
|
|
self.assertRaises(templating.OSConfigException,
|
|
templating.get_loader,
|
|
templates_dir='/tmp/foo', os_release='foo')
|
|
|
|
@patch('os.path.isdir')
|
|
def test_get_loader_all_search_paths(self, isdir):
|
|
'''Ensure loader reverse searches of all release template dirs'''
|
|
isdir.return_value = True
|
|
choice_loader = templating.get_loader('/tmp/foo',
|
|
os_release='icehouse')
|
|
dirs = [l.searchpath for l in choice_loader.loaders]
|
|
|
|
common_tmplts = os.path.join(os.path.dirname(templating.__file__),
|
|
'templates')
|
|
expected = [['/tmp/foo/icehouse'],
|
|
['/tmp/foo/havana'],
|
|
['/tmp/foo/grizzly'],
|
|
['/tmp/foo/folsom'],
|
|
['/tmp/foo/essex'],
|
|
['/tmp/foo/diablo'],
|
|
['/tmp/foo'],
|
|
[common_tmplts]]
|
|
self.assertEquals(dirs, expected)
|
|
|
|
@patch('os.path.isdir')
|
|
def test_get_loader_some_search_paths(self, isdir):
|
|
'''Ensure loader reverse searches of some release template dirs'''
|
|
isdir.return_value = True
|
|
choice_loader = templating.get_loader('/tmp/foo', os_release='grizzly')
|
|
dirs = [l.searchpath for l in choice_loader.loaders]
|
|
|
|
common_tmplts = os.path.join(os.path.dirname(templating.__file__),
|
|
'templates')
|
|
|
|
expected = [['/tmp/foo/grizzly'],
|
|
['/tmp/foo/folsom'],
|
|
['/tmp/foo/essex'],
|
|
['/tmp/foo/diablo'],
|
|
['/tmp/foo'],
|
|
[common_tmplts]]
|
|
self.assertEquals(dirs, expected)
|
|
|
|
def test_register_template_with_list_of_contexts(self):
|
|
'''Ensure registering a template with a list of context generators'''
|
|
def _c1():
|
|
pass
|
|
|
|
def _c2():
|
|
pass
|
|
tmpl = templating.OSConfigTemplate(config_file='/tmp/foo',
|
|
contexts=[_c1, _c2])
|
|
self.assertEquals(tmpl.contexts, [_c1, _c2])
|
|
|
|
def test_register_template_with_single_context(self):
|
|
'''Ensure registering a template with a single non-list context'''
|
|
def _c1():
|
|
pass
|
|
tmpl = templating.OSConfigTemplate(config_file='/tmp/foo',
|
|
contexts=_c1)
|
|
self.assertEquals(tmpl.contexts, [_c1])
|
|
|
|
|
|
class TemplatingStringTests(unittest.TestCase):
|
|
def setUp(self):
|
|
path = os.path.dirname(__file__)
|
|
self.loader = FakeLoader()
|
|
self.context = FakeContextGenerator()
|
|
|
|
self.addCleanup(patch.object(templating,
|
|
'apt_install').start().stop())
|
|
self.addCleanup(patch.object(templating, 'log').start().stop())
|
|
|
|
templating.FileSystemLoader = MockFSLoader
|
|
templating.ChoiceLoader = MockChoiceLoader
|
|
|
|
self.config_file = '/etc/confd/extensible.d/drop-in.conf'
|
|
self.config_template = 'use: {{ fake_key }}'
|
|
self.renderer = templating.OSConfigRenderer(templates_dir=path,
|
|
openstack_release='folsom')
|
|
|
|
def test_render_template_from_string_full_context(self):
|
|
'''
|
|
Test rendering a specified config file with a string template
|
|
and a context.
|
|
'''
|
|
|
|
context = {'fake_key': 'fake_val'}
|
|
self.context.set(
|
|
interfaces=['fooservice'],
|
|
context=context
|
|
)
|
|
|
|
expected_output = 'use: {}'.format(context['fake_key'])
|
|
|
|
self.renderer.register(
|
|
config_file=self.config_file,
|
|
contexts=[self.context],
|
|
config_template=self.config_template
|
|
)
|
|
|
|
# should return a string given we render from an in-memory
|
|
# template source
|
|
output = self.renderer.render(self.config_file)
|
|
|
|
self.assertEquals(output, expected_output)
|
|
|
|
def test_render_template_from_string_incomplete_context(self):
|
|
'''
|
|
Test rendering a specified config file with a string template
|
|
and a context.
|
|
'''
|
|
|
|
self.context.set(
|
|
interfaces=['fooservice'],
|
|
context={}
|
|
)
|
|
|
|
expected_output = 'use: '
|
|
|
|
self.renderer.register(
|
|
config_file=self.config_file,
|
|
contexts=[self.context],
|
|
config_template=self.config_template
|
|
)
|
|
|
|
# should return a string given we render from an in-memory
|
|
# template source
|
|
output = self.renderer.render(self.config_file)
|
|
|
|
self.assertEquals(output, expected_output)
|
|
|
|
def test_register_string_template_with_single_context(self):
|
|
'''Template rendering from a provided string with a context'''
|
|
def _c1():
|
|
pass
|
|
|
|
config_file = '/etc/confdir/custom-drop-in.conf'
|
|
config_template = 'use: {{ key_available_in_c1 }}'
|
|
tmpl = templating.OSConfigTemplate(
|
|
config_file=config_file,
|
|
contexts=_c1,
|
|
config_template=config_template
|
|
)
|
|
|
|
self.assertEquals(tmpl.contexts, [_c1])
|
|
self.assertEquals(tmpl.config_file, config_file)
|
|
self.assertEquals(tmpl.config_template, config_template)
|