from .schedule import HostAssignment
from .inventory import Inventory, SpecStore, HostCache
from .upgrade import CEPH_UPGRADE_ORDER, CephadmUpgrade
+from .template import TemplateMgr
try:
import remoto
'iscsi': self.iscsi_service,
}
+ self.template = TemplateMgr()
+
def shutdown(self):
self.log.debug('shutdown')
self._worker_pool.close()
--- /dev/null
+import copy
+from typing import Optional
+
+from jinja2 import Environment, PackageLoader, select_autoescape, StrictUndefined
+from jinja2 import exceptions as j2_exceptions
+
+
+class TemplateError(Exception):
+ pass
+
+
+class UndefinedError(TemplateError):
+ pass
+
+
+class TemplateNotFoundError(TemplateError):
+ pass
+
+
+class TemplateEngine:
+ def render(self, name: str, context: Optional[dict] = None) -> str:
+ raise NotImplementedError()
+
+
+class Jinja2Engine(TemplateEngine):
+ def __init__(self):
+ self.env = Environment(
+ loader=PackageLoader('cephadm', 'templates'),
+ autoescape=select_autoescape(['html', 'xml']),
+ trim_blocks=True,
+ lstrip_blocks=True,
+ undefined=StrictUndefined
+ )
+
+ def render(self, name: str, context: Optional[dict] = None) -> str:
+ try:
+ template = self.env.get_template(name)
+ if context is None:
+ return template.render()
+ return template.render(context)
+ except j2_exceptions.UndefinedError as e:
+ raise UndefinedError(e.message)
+ except j2_exceptions.TemplateNotFound as e:
+ raise TemplateNotFoundError(e.message)
+
+
+class TemplateMgr:
+ def __init__(self):
+ self.engine = Jinja2Engine()
+ self.base_context = {
+ 'cephadm_managed': 'This file is generated by cephadm.'
+ }
+
+ def render(self, name: str, context: Optional[dict] = None, managed_context=True) -> str:
+ """Render a string from a template with context.
+
+ :param name: template name. e.g. services/nfs/ganesha.conf.j2
+ :type name: str
+ :param context: a dictionary that contains values to be used in the template, defaults
+ to None
+ :type context: Optional[dict], optional
+ :param managed_context: to inject default context like managed header or not, defaults
+ to True
+ :type managed_context: bool, optional
+ :return: the templated string
+ :rtype: str
+ """
+ ctx = {}
+ if managed_context:
+ ctx = copy.deepcopy(self.base_context)
+ if context is not None:
+ ctx = {**ctx, **context}
+ return self.engine.render(name, ctx)
--- /dev/null
+import pathlib
+
+import pytest
+
+from cephadm.template import TemplateMgr, UndefinedError, TemplateNotFoundError
+
+
+def test_render(fs):
+ template_base = (pathlib.Path(__file__).parent / '../templates').resolve()
+ fake_template = template_base / 'foo/bar'
+ fs.create_file(fake_template, contents='{{ cephadm_managed }}{{ var }}')
+
+ template_mgr = TemplateMgr()
+ value = 'test'
+
+ # with base context
+ expected_text = '{}{}'.format(template_mgr.base_context['cephadm_managed'], value)
+ assert template_mgr.render('foo/bar', {'var': value}) == expected_text
+
+ # without base context
+ with pytest.raises(UndefinedError):
+ template_mgr.render('foo/bar', {'var': value}, managed_context=False)
+
+ # override the base context
+ context = {
+ 'cephadm_managed': 'abc',
+ 'var': value
+ }
+ assert template_mgr.render('foo/bar', context) == 'abc{}'.format(value)
+
+ # template not found
+ with pytest.raises(TemplateNotFoundError):
+ template_mgr.render('foo/bar/2', {})