Add support for jinja as userdata template engine
If the userdata starts with the text '## template:jinja', Jinja2 templating engine will be used to render that userdata. This feature leverages the get_instance_data metadata service implementation and tries to be as close as possible with the cloud-init's implementation. Change-Id: I79cb0066f7d65ae27867ac01b443e9be432b4dc3
This commit is contained in:
parent
c31c5b99f1
commit
493e57c195
@ -26,6 +26,7 @@ from cloudbaseinit.plugins.common import execcmd
|
|||||||
from cloudbaseinit.plugins.common.userdataplugins import factory
|
from cloudbaseinit.plugins.common.userdataplugins import factory
|
||||||
from cloudbaseinit.plugins.common import userdatautils
|
from cloudbaseinit.plugins.common import userdatautils
|
||||||
from cloudbaseinit.utils import encoding
|
from cloudbaseinit.utils import encoding
|
||||||
|
from cloudbaseinit.utils.template_engine import factory as template_factory
|
||||||
from cloudbaseinit.utils import x509constants
|
from cloudbaseinit.utils import x509constants
|
||||||
|
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ class UserDataPlugin(base.BasePlugin):
|
|||||||
self._write_userdata(user_data, user_data_path)
|
self._write_userdata(user_data, user_data_path)
|
||||||
|
|
||||||
if CONF.process_userdata:
|
if CONF.process_userdata:
|
||||||
return self._process_user_data(user_data)
|
return self._process_user_data(user_data, service)
|
||||||
return base.PLUGIN_EXECUTION_DONE, False
|
return base.PLUGIN_EXECUTION_DONE, False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -100,7 +101,7 @@ class UserDataPlugin(base.BasePlugin):
|
|||||||
"The user data content is "
|
"The user data content is "
|
||||||
"either invalid or empty.")
|
"either invalid or empty.")
|
||||||
|
|
||||||
def _process_user_data(self, user_data):
|
def _process_user_data(self, user_data, service):
|
||||||
plugin_status = base.PLUGIN_EXECUTION_DONE
|
plugin_status = base.PLUGIN_EXECUTION_DONE
|
||||||
reboot = False
|
reboot = False
|
||||||
headers = self._get_headers(user_data)
|
headers = self._get_headers(user_data)
|
||||||
@ -122,7 +123,7 @@ class UserDataPlugin(base.BasePlugin):
|
|||||||
|
|
||||||
return plugin_status, reboot
|
return plugin_status, reboot
|
||||||
else:
|
else:
|
||||||
return self._process_non_multi_part(user_data)
|
return self._process_non_multi_part(user_data, service)
|
||||||
|
|
||||||
def _process_part(self, part, user_data_plugins, user_handlers):
|
def _process_part(self, part, user_data_plugins, user_handlers):
|
||||||
ret_val = None
|
ret_val = None
|
||||||
@ -186,8 +187,16 @@ class UserDataPlugin(base.BasePlugin):
|
|||||||
LOG.debug("Calling part handler \"__end__\" event")
|
LOG.debug("Calling part handler \"__end__\" event")
|
||||||
handler_func(None, "__end__", None, None)
|
handler_func(None, "__end__", None, None)
|
||||||
|
|
||||||
def _process_non_multi_part(self, user_data):
|
def _process_non_multi_part(self, user_data, service):
|
||||||
ret_val = None
|
ret_val = None
|
||||||
|
|
||||||
|
template_engine = template_factory.get_template_engine(user_data)
|
||||||
|
if template_engine:
|
||||||
|
user_data = template_engine.render(
|
||||||
|
service.get_instance_data(),
|
||||||
|
user_data
|
||||||
|
)
|
||||||
|
|
||||||
if user_data.startswith(b'#cloud-config'):
|
if user_data.startswith(b'#cloud-config'):
|
||||||
user_data_plugins = factory.load_plugins()
|
user_data_plugins = factory.load_plugins()
|
||||||
cloud_config_plugin = user_data_plugins.get('text/cloud-config')
|
cloud_config_plugin = user_data_plugins.get('text/cloud-config')
|
||||||
|
@ -162,8 +162,10 @@ class UserDataPluginTest(unittest.TestCase):
|
|||||||
mock_part = mock.MagicMock()
|
mock_part = mock.MagicMock()
|
||||||
mock_parse_mime.return_value = [mock_part]
|
mock_parse_mime.return_value = [mock_part]
|
||||||
mock_process_part.return_value = (base.PLUGIN_EXECUTION_DONE, reboot)
|
mock_process_part.return_value = (base.PLUGIN_EXECUTION_DONE, reboot)
|
||||||
|
mock_service = mock.MagicMock()
|
||||||
|
|
||||||
response = self._userdata._process_user_data(user_data=user_data)
|
response = self._userdata._process_user_data(user_data=user_data,
|
||||||
|
service=mock_service)
|
||||||
|
|
||||||
if user_data.startswith(b'Content-Type: multipart'):
|
if user_data.startswith(b'Content-Type: multipart'):
|
||||||
mock_load_plugins.assert_called_once_with()
|
mock_load_plugins.assert_called_once_with()
|
||||||
@ -172,7 +174,8 @@ class UserDataPluginTest(unittest.TestCase):
|
|||||||
mock_load_plugins(), {})
|
mock_load_plugins(), {})
|
||||||
self.assertEqual((base.PLUGIN_EXECUTION_DONE, reboot), response)
|
self.assertEqual((base.PLUGIN_EXECUTION_DONE, reboot), response)
|
||||||
else:
|
else:
|
||||||
mock_process_non_multi_part.assert_called_once_with(user_data)
|
mock_process_non_multi_part.assert_called_once_with(user_data,
|
||||||
|
mock_service)
|
||||||
self.assertEqual(mock_process_non_multi_part.return_value,
|
self.assertEqual(mock_process_non_multi_part.return_value,
|
||||||
response)
|
response)
|
||||||
|
|
||||||
@ -313,8 +316,9 @@ class UserDataPluginTest(unittest.TestCase):
|
|||||||
'.execute_user_data_script')
|
'.execute_user_data_script')
|
||||||
def test_process_non_multi_part(self, mock_execute_user_data_script):
|
def test_process_non_multi_part(self, mock_execute_user_data_script):
|
||||||
user_data = b'fake'
|
user_data = b'fake'
|
||||||
|
service = mock.MagicMock()
|
||||||
status, reboot = self._userdata._process_non_multi_part(
|
status, reboot = self._userdata._process_non_multi_part(
|
||||||
user_data=user_data)
|
user_data=user_data, service=service)
|
||||||
mock_execute_user_data_script.assert_called_once_with(user_data)
|
mock_execute_user_data_script.assert_called_once_with(user_data)
|
||||||
self.assertEqual(status, 1)
|
self.assertEqual(status, 1)
|
||||||
self.assertFalse(reboot)
|
self.assertFalse(reboot)
|
||||||
@ -329,10 +333,11 @@ class UserDataPluginTest(unittest.TestCase):
|
|||||||
b2NhbGhvc3QwHhcNMTUwNjE1MTAyODUxWhcNMjUwNjEyMTAyODUxWjAbMRkwFwYD
|
b2NhbGhvc3QwHhcNMTUwNjE1MTAyODUxWhcNMjUwNjEyMTAyODUxWjAbMRkwFwYD
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
''').encode()
|
''').encode()
|
||||||
|
service = mock.MagicMock()
|
||||||
with testutils.LogSnatcher('cloudbaseinit.plugins.'
|
with testutils.LogSnatcher('cloudbaseinit.plugins.'
|
||||||
'common.userdata') as snatcher:
|
'common.userdata') as snatcher:
|
||||||
status, reboot = self._userdata._process_non_multi_part(
|
status, reboot = self._userdata._process_non_multi_part(
|
||||||
user_data=user_data)
|
user_data=user_data, service=service)
|
||||||
|
|
||||||
expected_logging = ['Found X509 certificate in userdata']
|
expected_logging = ['Found X509 certificate in userdata']
|
||||||
self.assertFalse(mock_execute_user_data_script.called)
|
self.assertFalse(mock_execute_user_data_script.called)
|
||||||
@ -340,25 +345,54 @@ class UserDataPluginTest(unittest.TestCase):
|
|||||||
self.assertEqual(1, status)
|
self.assertEqual(1, status)
|
||||||
self.assertFalse(reboot)
|
self.assertFalse(reboot)
|
||||||
|
|
||||||
|
@mock.patch('cloudbaseinit.utils.template_engine.factory.'
|
||||||
|
'get_template_engine')
|
||||||
@mock.patch('cloudbaseinit.plugins.common.userdataplugins.factory.'
|
@mock.patch('cloudbaseinit.plugins.common.userdataplugins.factory.'
|
||||||
'load_plugins')
|
'load_plugins')
|
||||||
def test_process_non_multi_part_cloud_config(self, mock_load_plugins):
|
def _test_process_non_multi_part_cloud_config(self, mock_load_plugins,
|
||||||
user_data = b'#cloud-config'
|
mock_load_templates,
|
||||||
|
user_data,
|
||||||
|
expected_userdata,
|
||||||
|
template_renderer=None):
|
||||||
|
mock_service = mock.MagicMock()
|
||||||
mock_return_value = mock.sentinel.return_value
|
mock_return_value = mock.sentinel.return_value
|
||||||
mock_cloud_config_plugin = mock.Mock()
|
mock_cloud_config_plugin = mock.Mock()
|
||||||
mock_cloud_config_plugin.process.return_value = mock_return_value
|
mock_cloud_config_plugin.process.return_value = mock_return_value
|
||||||
mock_load_plugins.return_value = {
|
mock_load_plugins.return_value = {
|
||||||
'text/cloud-config': mock_cloud_config_plugin}
|
'text/cloud-config': mock_cloud_config_plugin}
|
||||||
|
mock_load_templates.return_value = template_renderer
|
||||||
status, reboot = self._userdata._process_non_multi_part(
|
status, reboot = self._userdata._process_non_multi_part(
|
||||||
user_data=user_data)
|
user_data=user_data, service=mock_service)
|
||||||
|
|
||||||
|
if template_renderer:
|
||||||
|
mock_load_plugins.assert_called_once_with()
|
||||||
|
|
||||||
|
(mock_cloud_config_plugin
|
||||||
|
.process_non_multipart
|
||||||
|
.assert_called_once_with(expected_userdata))
|
||||||
|
|
||||||
mock_load_plugins.assert_called_once_with()
|
|
||||||
(mock_cloud_config_plugin
|
|
||||||
.process_non_multipart
|
|
||||||
.assert_called_once_with(user_data))
|
|
||||||
self.assertEqual(status, 1)
|
self.assertEqual(status, 1)
|
||||||
self.assertFalse(reboot)
|
self.assertFalse(reboot)
|
||||||
|
|
||||||
|
def test_process_non_multi_part_cloud_config(self):
|
||||||
|
user_data = b'#cloud-config'
|
||||||
|
self._test_process_non_multi_part_cloud_config(
|
||||||
|
user_data=user_data, expected_userdata=user_data)
|
||||||
|
|
||||||
|
def test_process_non_multi_part_cloud_config_jinja(self):
|
||||||
|
user_data = b'## template:jinja\n#cloud-config'
|
||||||
|
expected_userdata = b'#cloud-config'
|
||||||
|
mock_template_renderer = mock.MagicMock()
|
||||||
|
mock_template_renderer.render.return_value = expected_userdata
|
||||||
|
self._test_process_non_multi_part_cloud_config(
|
||||||
|
user_data=user_data, expected_userdata=expected_userdata,
|
||||||
|
template_renderer=mock_template_renderer)
|
||||||
|
|
||||||
|
def test_process_non_multi_part_no_valid_template(self):
|
||||||
|
user_data = b'## template:none'
|
||||||
|
self._test_process_non_multi_part_cloud_config(
|
||||||
|
user_data=user_data, expected_userdata=user_data)
|
||||||
|
|
||||||
|
|
||||||
class TestCloudConfig(unittest.TestCase):
|
class TestCloudConfig(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
# Copyright 2019 Cloudbase Solutions Srl
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import ddt
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from cloudbaseinit.utils.template_engine import base_template as bt
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class TestBaseTemplateEngine(unittest.TestCase):
|
||||||
|
|
||||||
|
@ddt.data((b'', b''),
|
||||||
|
(b'## template:jinja test', b''),
|
||||||
|
(b'## template:jinja \ntest', b'test'))
|
||||||
|
@ddt.unpack
|
||||||
|
def test_remove_template_definition(self, template, expected_output):
|
||||||
|
output = bt.BaseTemplateEngine.remove_template_definition(template)
|
||||||
|
self.assertEqual(expected_output, output)
|
58
cloudbaseinit/tests/utils/template_engine/test_factory.py
Normal file
58
cloudbaseinit/tests/utils/template_engine/test_factory.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Copyright 2019 Cloudbase Solutions Srl
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
try:
|
||||||
|
import unittest.mock as mock
|
||||||
|
except ImportError:
|
||||||
|
import mock
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from cloudbaseinit.utils.template_engine import factory
|
||||||
|
|
||||||
|
|
||||||
|
class FakeLoaderError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestTemplateFactory(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_get_template_engine_empty(self):
|
||||||
|
fake_userdata = b''
|
||||||
|
result = factory.get_template_engine(fake_userdata)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
def test_get_template_engine_no_match(self):
|
||||||
|
fake_userdata = b'no match'
|
||||||
|
result = factory.get_template_engine(fake_userdata)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
def test_get_template_engine_not_supported(self):
|
||||||
|
fake_userdata = b'## template:fake'
|
||||||
|
result = factory.get_template_engine(fake_userdata)
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
@mock.patch('cloudbaseinit.utils.classloader.ClassLoader')
|
||||||
|
def test_get_template_engine(self, mock_class_loader):
|
||||||
|
fake_userdata = b'## template:jinja'
|
||||||
|
mock_load_class = mock_class_loader.return_value.load_class
|
||||||
|
self.assertEqual(mock_load_class.return_value.return_value,
|
||||||
|
factory.get_template_engine(fake_userdata))
|
||||||
|
|
||||||
|
@mock.patch('cloudbaseinit.utils.classloader.ClassLoader')
|
||||||
|
def test_get_template_engine_class_not_found(self, mock_class_loader):
|
||||||
|
fake_userdata = b'## template:jinja'
|
||||||
|
mock_class_loader.return_value.load_class.side_effect = (
|
||||||
|
FakeLoaderError)
|
||||||
|
self.assertRaises(FakeLoaderError,
|
||||||
|
factory.get_template_engine, fake_userdata)
|
@ -0,0 +1,82 @@
|
|||||||
|
# Copyright 2019 Cloudbase Solutions Srl
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
try:
|
||||||
|
import unittest.mock as mock
|
||||||
|
except ImportError:
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from cloudbaseinit.utils.template_engine.jinja2_template import (
|
||||||
|
Jinja2TemplateEngine)
|
||||||
|
|
||||||
|
|
||||||
|
class TestJinja2TemplateEngine(unittest.TestCase):
|
||||||
|
|
||||||
|
@mock.patch('cloudbaseinit.utils.template_engine.base_template'
|
||||||
|
'.BaseTemplateEngine.remove_template_definition')
|
||||||
|
def _test_jinja_render_template(self, mock_remove_header,
|
||||||
|
fake_instance_data, expected_result,
|
||||||
|
fake_template = b'{{v1.local_hostname}}'):
|
||||||
|
|
||||||
|
mock_remove_header.return_value = fake_template
|
||||||
|
|
||||||
|
output = Jinja2TemplateEngine().render(fake_instance_data,
|
||||||
|
fake_template)
|
||||||
|
|
||||||
|
self.assertEqual(expected_result, output)
|
||||||
|
|
||||||
|
def test_jinja_render_template(self):
|
||||||
|
fake_instance_data = {
|
||||||
|
'v1': {
|
||||||
|
'local_hostname': 'fake_hostname'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expected_result = b'fake_hostname'
|
||||||
|
self._test_jinja_render_template(
|
||||||
|
fake_instance_data=fake_instance_data,
|
||||||
|
expected_result=expected_result)
|
||||||
|
|
||||||
|
def test_jinja_render_template_missing_variable(self):
|
||||||
|
fake_instance_data = {
|
||||||
|
'v1': {
|
||||||
|
'localhostname': 'fake_hostname'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expected_result = b'CI_MISSING_JINJA_VAR/local_hostname'
|
||||||
|
self._test_jinja_render_template(
|
||||||
|
fake_instance_data=fake_instance_data,
|
||||||
|
expected_result=expected_result)
|
||||||
|
|
||||||
|
def test_jinja_render_template_multiple_variables(self):
|
||||||
|
fake_instance_data = {
|
||||||
|
'v1': {
|
||||||
|
'localhostname': 'fake_hostname'
|
||||||
|
},
|
||||||
|
'ds': {
|
||||||
|
'meta_data': {
|
||||||
|
'hostname': 'fake_hostname'
|
||||||
|
},
|
||||||
|
'meta-data': {
|
||||||
|
'hostname': 'fake_hostname'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fake_template = b'{{ds.meta_data.hostname}}'
|
||||||
|
expected_result = b'fake_hostname'
|
||||||
|
self._test_jinja_render_template(
|
||||||
|
fake_instance_data=fake_instance_data,
|
||||||
|
expected_result=expected_result,
|
||||||
|
fake_template=fake_template)
|
0
cloudbaseinit/utils/template_engine/__init__.py
Normal file
0
cloudbaseinit/utils/template_engine/__init__.py
Normal file
60
cloudbaseinit/utils/template_engine/base_template.py
Normal file
60
cloudbaseinit/utils/template_engine/base_template.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Copyright 2019 Cloudbase Solutions Srl
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import re
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class BaseTemplateEngine(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._template_matcher = re.compile(r"##\s*template:(.*)", re.I)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_template_type(self):
|
||||||
|
"""Return the template type for the class loader"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def render(self, data, template):
|
||||||
|
"""Renders the template according to the data dictionary
|
||||||
|
|
||||||
|
The data variable is a dict which contains the key-values
|
||||||
|
that will be used to render the template.
|
||||||
|
|
||||||
|
The template is an encoded string which can contain special
|
||||||
|
constructions that will be used by the template engine.
|
||||||
|
|
||||||
|
The return value will be an encoded string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def load(self, data):
|
||||||
|
"""Returns True if the template header matches, False otherwise"""
|
||||||
|
template_type_matcher = self._template_matcher.match(data.decode())
|
||||||
|
template_type = template_type_matcher.group(1).lower().strip()
|
||||||
|
if self.get_template_type() == template_type:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_template_definition(raw_template):
|
||||||
|
# Remove the first line, as it contains the template definition
|
||||||
|
template_split = raw_template.split(b"\n", 1)
|
||||||
|
|
||||||
|
if len(template_split) == 2:
|
||||||
|
# return the template without the header
|
||||||
|
return template_split[1]
|
||||||
|
|
||||||
|
# the template has just one line, return empty encoded string
|
||||||
|
return b''
|
37
cloudbaseinit/utils/template_engine/factory.py
Normal file
37
cloudbaseinit/utils/template_engine/factory.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Copyright 2019 Cloudbase Solutions Srl
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from cloudbaseinit.utils import classloader
|
||||||
|
from oslo_log import log as oslo_logging
|
||||||
|
|
||||||
|
TEMPLATE_ENGINE_CLASS_PATHS = ["cloudbaseinit.utils.template_engine"
|
||||||
|
".jinja2_template.Jinja2TemplateEngine"]
|
||||||
|
LOG = oslo_logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_engine(user_data):
|
||||||
|
"""Returns the first template engine that loads correctly"""
|
||||||
|
|
||||||
|
cl = classloader.ClassLoader()
|
||||||
|
for class_path in TEMPLATE_ENGINE_CLASS_PATHS:
|
||||||
|
tpl_engine = cl.load_class(class_path)()
|
||||||
|
try:
|
||||||
|
if tpl_engine.load(user_data):
|
||||||
|
LOG.info("Using template engine: %s"
|
||||||
|
% tpl_engine.get_template_type())
|
||||||
|
return tpl_engine
|
||||||
|
except Exception as ex:
|
||||||
|
LOG.error("Failed to load template engine '%s'" % class_path)
|
||||||
|
LOG.exception(ex)
|
||||||
|
return
|
50
cloudbaseinit/utils/template_engine/jinja2_template.py
Normal file
50
cloudbaseinit/utils/template_engine/jinja2_template.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Copyright 2019 Cloudbase Solutions Srl
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
import jinja2
|
||||||
|
|
||||||
|
from cloudbaseinit.utils.template_engine import base_template
|
||||||
|
from jinja2 import runtime
|
||||||
|
|
||||||
|
MISSING_JINJA_VARIABLE = u'CI_MISSING_JINJA_VAR/'
|
||||||
|
|
||||||
|
|
||||||
|
@runtime.implements_to_string
|
||||||
|
class MissingJinjaVariable(jinja2.DebugUndefined):
|
||||||
|
"""Missing Jinja2 variable class."""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return u'%s%s' % (MISSING_JINJA_VARIABLE, self._undefined_name)
|
||||||
|
|
||||||
|
|
||||||
|
class Jinja2TemplateEngine(base_template.BaseTemplateEngine):
|
||||||
|
def get_template_type(self):
|
||||||
|
return 'jinja'
|
||||||
|
|
||||||
|
def render(self, data, raw_template):
|
||||||
|
"""Renders the template using Jinja2 template engine
|
||||||
|
|
||||||
|
The data variable is a dict which contains the key-values
|
||||||
|
that will be used to render the template.
|
||||||
|
|
||||||
|
The template is an encoded string which can contain special
|
||||||
|
constructions that will be used by the template engine.
|
||||||
|
|
||||||
|
The return value will be an encoded string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = self.remove_template_definition(raw_template).decode()
|
||||||
|
jinja_template = jinja2.Template(template,
|
||||||
|
trim_blocks=True,
|
||||||
|
undefined=MissingJinjaVariable,)
|
||||||
|
return jinja_template.render(**data).encode()
|
@ -12,6 +12,7 @@ netifaces
|
|||||||
PyYAML
|
PyYAML
|
||||||
requests
|
requests
|
||||||
untangle==1.1.1
|
untangle==1.1.1
|
||||||
|
jinja2
|
||||||
pywin32;sys_platform=="win32"
|
pywin32;sys_platform=="win32"
|
||||||
comtypes;sys_platform=="win32"
|
comtypes;sys_platform=="win32"
|
||||||
pymi;sys_platform=="win32"
|
pymi;sys_platform=="win32"
|
||||||
|
@ -10,3 +10,4 @@ stestr>=2.0.0
|
|||||||
openstackdocstheme>=1.11.0 # Apache-2.0
|
openstackdocstheme>=1.11.0 # Apache-2.0
|
||||||
# releasenotes
|
# releasenotes
|
||||||
reno>=1.8.0 # Apache-2.0
|
reno>=1.8.0 # Apache-2.0
|
||||||
|
ddt
|
||||||
|
Loading…
x
Reference in New Issue
Block a user