]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
cephadm: workaround issues running cephadm with relative path 54487/head
authorJohn Mulligan <jmulligan@redhat.com>
Tue, 14 Nov 2023 00:15:25 +0000 (19:15 -0500)
committerJohn Mulligan <jmulligan@redhat.com>
Wed, 15 Nov 2023 18:32:48 +0000 (13:32 -0500)
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 <jmulligan@redhat.com>
src/cephadm/cephadmlib/templating.py

index 3b7c6f9657e0911f4f67eda0df65218712b426e2..e6e8d5e0ea2c2a82863107c2a39b1ebe45489e2f 100644 (file)
@@ -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(