Source code for cobbler.template_api

"""
Cobbler provides builtin methods for use in Cheetah templates. $SNIPPET is one
such function and is now used to implement Cobbler's SNIPPET:: syntax.

Written by Daniel Guernsey <danpg102@gmail.com>
Contributions by Michael DeHaan <michael.dehaan AT gmail>
US Government work; No explicit copyright attached to this file.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301  USA
"""
import logging
import os.path
import re
from typing import Match, Optional, TextIO, Tuple, Union

from Cheetah.Template import Template

from cobbler import utils


# This class is defined using the Cheetah language. Using the 'compile' function we can compile the source directly into
# a Python class. This class will allow us to define the cheetah builtins.

logger = logging.getLogger()


[docs]def read_macro_file(location='/etc/cobbler/cheetah_macros'): if not os.path.exists(location): raise FileNotFoundError("Cobbler Cheetah Macros File must exist!") with open(location, "r") as macro_file: return macro_file.read()
[docs]def generate_cheetah_macros(): try: macro_file = read_macro_file() return Template.compile( source=macro_file, moduleName="cobbler.template_api", className="CheetahMacros") except FileNotFoundError: logger.warning("Cheetah Macros file note found. Using empty template.") return Template.compile(source="")
[docs]class CobblerTemplate(generate_cheetah_macros()): """ This class will allow us to include any pure python builtin functions. It derives from the cheetah-compiled class above. This way, we can include both types (cheetah and pure python) of builtins in the same base template. We don't need to override __init__ """ def __init__(self, **kwargs): """ Constructor for this derived class. We include two additional default templates. :param kwargs: These arguments get passed to the super constructor of this class. """ # This part (see 'Template' below for the other part) handles the actual inclusion of the file contents. We # still need to make the snippet's namespace (searchList) available to the template calling SNIPPET (done in # the other part). # This function can be used in two ways: # Cheetah syntax: # - $SNIPPET('my_snippet') # - SNIPPET syntax: # - SNIPPET::my_snippet # This follows all of the rules of snippets and advanced snippets. First it searches for a per-system snippet, # then a per-profile snippet, then a general snippet. If none is found, a comment explaining the error is # substituted. self.BuiltinTemplate = Template.compile(source="\n".join([ "#def SNIPPET($file)", "#set $snippet = $read_snippet($file)", "#if $snippet", "#include source=$snippet", "#else", "# Error: no snippet data for $file", "#end if", "#end def", ]) + "\n") super().__init__(**kwargs) # OK, so this function gets called by Cheetah.Template.Template.__init__ to compile the template into a class. This # is probably a kludge, but it add a baseclass argument to the standard compile (see Cheetah's compile docstring) # and returns the resulting class. This argument, of course, points to this class. Now any methods entered here (or # in the base class above) will be accessible to all cheetah templates compiled by Cobbler.
[docs] @classmethod def compile(cls, *args, **kwargs) -> bytes: """ Compile a cheetah template with Cobbler modifications. Modifications include ``SNIPPET::`` syntax replacement and inclusion of Cobbler builtin methods. Please be aware that you cannot use the ``baseclass`` attribute of Cheetah anymore due to the fact that we are using it in our implementation to enable the Cheetah Macros. :param args: These just get passed right to Cheetah. :param kwargs: We just execute our own preprocessors and remove them and let afterwards handle Cheetah the rest. :return: The compiled template. """ def replacer(match: Match): return "$SNIPPET('%s')" % match.group(1) def preprocess(source: Optional[str], file: Union[TextIO, str]) -> Tuple[str, Union[TextIO, str]]: # Normally, the cheetah compiler worries about this, but we need to preprocess the actual source. if source is None: if hasattr(file, 'read'): source = file.read() else: if os.path.exists(file): with open(file, "r") as f: source = "#errorCatcher Echo\n" + f.read() else: source = "# Unable to read %s\n" % file # Stop Cheetah from throwing a fit. file = None snippet_regex = re.compile(r'SNIPPET::([A-Za-z0-9_\-/.]+)') results = snippet_regex.sub(replacer, source) return results, file preprocessors = [preprocess] if 'preprocessors' in kwargs: preprocessors.extend(kwargs['preprocessors']) kwargs['preprocessors'] = preprocessors # Now let Cheetah do the actual compilation return super().compile(*args, **kwargs)
[docs] def read_snippet(self, file: str) -> Optional[str]: """ Locate the appropriate snippet for the current system and profile and read its contents. This file could be located in a remote location. This will first check for a per-system snippet, a per-profile snippet, a distro snippet, and a general snippet. :param file: The name of the file to read. Depending on the context this gets expanded automatically. :return: None (if the snippet file was not found) or the string with the read snippet. :raises AttributeError: Raised in case ``autoinstall_snippets_dir`` is missing. :raises FileNotFoundError: Raised in case some files are not found. """ if not self.varExists('autoinstall_snippets_dir'): raise AttributeError("\"autoinstall_snippets_dir\" is required to find snippets") for snippet_class in ('system', 'profile', 'distro'): if self.varExists('%s_name' % snippet_class): full_path = '%s/per_%s/%s/%s' % (self.getVar('autoinstall_snippets_dir'), snippet_class, file, self.getVar('%s_name' % snippet_class)) try: contents = utils.read_file_contents(full_path, fetch_if_remote=True) return contents except FileNotFoundError: pass try: full_path = '%s/%s' % (self.getVar('autoinstall_snippets_dir'), file) return "#errorCatcher ListErrors\n" + utils.read_file_contents(full_path, fetch_if_remote=True) except FileNotFoundError: return None
[docs] def SNIPPET(self, file: str): """ Include the contents of the named snippet here. This is equivalent to the #include directive in Cheetah, except that it searches for system and profile specific snippets, and it includes the snippet's namespace. This may be a little frobby, but it's really cool. This is a pure python portion of SNIPPET that appends the snippet's searchList to the caller's searchList. This makes any #defs within a given snippet available to the template that included the snippet. :param file: The snippet file to read and include in the template. :return: The updated template. """ # First, do the actual inclusion. Cheetah (when processing #include) will track the inclusion in # self._CHEETAH__cheetahIncludes result = self.BuiltinTemplate.SNIPPET(self, file) # Now do our dirty work: locate the new include, and append its searchList to ours. We have to compute the full # path again? Eww. # This weird method is getting even weirder, the cheetah includes keys are no longer filenames but actual # contents of snippets. Regardless this seems to work and hopefully it will be ok. snippet_contents = self.read_snippet(file) if snippet_contents: # Only include what we don't already have. Because Cheetah passes our searchList into included templates, # the snippet's searchList will include this templates searchList. We need to avoid duplicating entries. childList = self._CHEETAH__cheetahIncludes[snippet_contents].searchList() myList = self.searchList() for childElem in childList: if childElem not in myList: myList.append(childElem) return result
# pylint: disable=R0201
[docs] def sedesc(self, value: str) -> str: """ Escape a string for use in sed. This function is used by several cheetah methods in cheetah_macros. It can be used by the end user as well. Example: Replace all instances of ``/etc/banner`` with a value stored in ``$new_banner`` ..code:: sed 's/$sedesc("/etc/banner")/$sedesc($new_banner)/' :param value: The phrase to escape. :return: The escaped phrase. """ def escchar(character: str) -> str: if character in '/^.[]$()|*+?{}\\': return '\\' + character return character return ''.join([escchar(c) for c in value])