From 64bb27f2f62010de44d50bc1a876436159770b56 Mon Sep 17 00:00:00 2001 From: Kiefer Chang Date: Fri, 29 May 2020 17:22:07 +0800 Subject: [PATCH] mgr/cephadm: add template engine Jinja2 Signed-off-by: Kiefer Chang --- ceph.spec.in | 2 + debian/control | 3 +- src/pybind/mgr/cephadm/module.py | 3 + src/pybind/mgr/cephadm/template.py | 73 +++++++++++++++++++ src/pybind/mgr/cephadm/tests/test_template.py | 33 +++++++++ src/pybind/mgr/requirements.txt | 5 +- 6 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 src/pybind/mgr/cephadm/template.py create mode 100644 src/pybind/mgr/cephadm/tests/test_template.py diff --git a/ceph.spec.in b/ceph.spec.in index eda4c6a8da7ee..7595eac21733d 100644 --- a/ceph.spec.in +++ b/ceph.spec.in @@ -656,9 +656,11 @@ Requires: python%{python3_pkgversion}-remoto Requires: cephadm = %{_epoch_prefix}%{version}-%{release} %if 0%{?suse_version} Requires: openssh +Requires: python%{python3_pkgversion}-Jinja2 %endif %if 0%{?rhel} || 0%{?fedora} Requires: openssh-clients +Requires: python%{python3_pkgversion}-jinja2 %endif %description mgr-cephadm ceph-mgr-cephadm is a ceph-mgr module for orchestration functions using diff --git a/debian/control b/debian/control index d98b16111a45e..9abc2bd752dd0 100644 --- a/debian/control +++ b/debian/control @@ -351,7 +351,8 @@ Depends: ceph-mgr (= ${binary:Version}), python3-six, ${misc:Depends}, ${python:Depends}, - openssh-client + openssh-client, + python3-jinja2 Description: cephadm orchestrator module for ceph-mgr Ceph is a massively scalable, open-source, distributed storage system that runs on commodity hardware and delivers object, diff --git a/src/pybind/mgr/cephadm/module.py b/src/pybind/mgr/cephadm/module.py index 6fc2f14025782..c4b2eee356cf3 100644 --- a/src/pybind/mgr/cephadm/module.py +++ b/src/pybind/mgr/cephadm/module.py @@ -40,6 +40,7 @@ from .services.monitoring import GrafanaService, AlertmanagerService, Prometheus from .schedule import HostAssignment from .inventory import Inventory, SpecStore, HostCache from .upgrade import CEPH_UPGRADE_ORDER, CephadmUpgrade +from .template import TemplateMgr try: import remoto @@ -355,6 +356,8 @@ class CephadmOrchestrator(orchestrator.Orchestrator, MgrModule): 'iscsi': self.iscsi_service, } + self.template = TemplateMgr() + def shutdown(self): self.log.debug('shutdown') self._worker_pool.close() diff --git a/src/pybind/mgr/cephadm/template.py b/src/pybind/mgr/cephadm/template.py new file mode 100644 index 0000000000000..f8e2affd4cd7b --- /dev/null +++ b/src/pybind/mgr/cephadm/template.py @@ -0,0 +1,73 @@ +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) diff --git a/src/pybind/mgr/cephadm/tests/test_template.py b/src/pybind/mgr/cephadm/tests/test_template.py new file mode 100644 index 0000000000000..962b30673e108 --- /dev/null +++ b/src/pybind/mgr/cephadm/tests/test_template.py @@ -0,0 +1,33 @@ +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', {}) diff --git a/src/pybind/mgr/requirements.txt b/src/pybind/mgr/requirements.txt index 54615fccf79f4..7d7cb1fbe3838 100644 --- a/src/pybind/mgr/requirements.txt +++ b/src/pybind/mgr/requirements.txt @@ -6,4 +6,7 @@ pyyaml prettytable pyOpenSSL execnet -remoto \ No newline at end of file +remoto +Jinja2 +pyfakefs + -- 2.39.5