From 4377fbd851bc8614780e61d2e1d22d9a552e2c02 Mon Sep 17 00:00:00 2001 From: John Mulligan Date: Mon, 13 Nov 2023 19:15:25 -0500 Subject: [PATCH] cephadm: workaround issues running cephadm with relative path Implement a workaround for the jinja2 package loader not correctly finding a template inside the cephadmlib package when run as a zipapp. See docstring in the shim class for more details. Signed-off-by: John Mulligan --- src/cephadm/cephadmlib/templating.py | 54 ++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/cephadm/cephadmlib/templating.py b/src/cephadm/cephadmlib/templating.py index 3b7c6f9657e09..e6e8d5e0ea2c2 100644 --- a/src/cephadm/cephadmlib/templating.py +++ b/src/cephadm/cephadmlib/templating.py @@ -1,10 +1,14 @@ # templating.py - functions to wrap string/file templating libs import enum +import os +import posixpath +import zipimport -from typing import Any, Optional, IO +from typing import Any, Optional, IO, Tuple, Callable, cast import jinja2 +import jinja2.loaders from .context import CephadmContext @@ -25,6 +29,52 @@ class Templates(str, enum.Enum): return repr(self.value) +class _PackageLoader(jinja2.PackageLoader): + """Workaround for PackageLoader when using cephadm with relative paths. + + It was found that running the cephadm zipapp from a local dir (like: + `./cephadm`) instead of an absolute path (like: `/usr/sbin/cephadm`) caused + the PackageLoader to fail to load the template. After investigation it was + found to relate to how the PackageLoader tries to normalize paths and yet + the zipimporter type did not have a normalized path (/home/foo/./cephadm + and /home/foo/cephadm respectively). When a full absolute path is passed + to zipimporter's get_data method it uses the (non normalized) .archive + property to strip the prefix from the argument. When the argument is a + normalized path - the prefix fails to match and is not stripped and then + the full path fails to match any value in the archive. + + This shim subclass of jinja2.PackageLoader customizes the code path used to + load files from the zipimporter so that we try to do the prefix handling + all with normalized paths and only path the relative paths to the + zipimporter function. + """ + + def get_source( + self, environment: jinja2.Environment, template: str + ) -> Tuple[str, str, Optional[Callable[[], bool]]]: + if isinstance(self._loader, zipimport.zipimporter): + return self._get_archive_source(template) + return super().get_source(environment, template) + + def _get_archive_source(self, template: str) -> Tuple[str, str, None]: + assert isinstance(self._loader, zipimport.zipimporter) + path = arelpath = os.path.normpath( + posixpath.join( + self._template_root, + *jinja2.loaders.split_template_path(template) + ) + ) + archive_path = os.path.normpath(self._loader.archive) + if arelpath.startswith(archive_path + '/'): + plen = len(archive_path) + 1 + arelpath = arelpath[plen:] + try: + source = cast(bytes, self._loader.get_data(arelpath)) + except OSError as e: + raise jinja2.TemplateNotFound(template) from e + return source.decode(self.encoding), path, None + + class Templater: """Cephadm's generic templater class. Based on jinja2.""" @@ -44,7 +94,7 @@ class Templater: @property def _loader(self) -> jinja2.BaseLoader: if self._jinja2_loader is None: - self._jinja2_loader = jinja2.PackageLoader(self._pkg, self._dir) + self._jinja2_loader = _PackageLoader(self._pkg, self._dir) return self._jinja2_loader def render_str( -- 2.39.5