Here it is. See the patch description for details. Please note that the
header at the top of the new file, templateapi.py, may need to change once I
hear back from Aaron Lippold.

~
Dan
From ba93eaca03e4caade534959eadf5f4e6fedbc75e Mon Sep 17 00:00:00 2001
From: Dan Guernsey <[EMAIL PROTECTED]>
Date: Mon, 4 Aug 2008 01:05:27 -0500
Subject: [PATCH] Added builtins and recursive SNIPPETS

A new framework has been added to support builtin methods. They can be Cheetah methods, pure Python methods, or a mixture of both. This framework along with
some builtin method definitions are all located in a new file: templateapi.py.
One of the new builtin methods is SNIPPET, which is a mixture of cheetah (to do the actual inclusion) and pure python (to locate the proper snippet and
merge the namespaces), implements the new $SNIPPET('my_snippet') syntax for snippets. I've also included code that will replace the old syntax SNIPPET::
with the new syntax in all cheetah templates processed by cobbler. This means that for any template included into the kickstart template (whether
via the
Because SNIPPET:: is now handled in templateapi.py, all snippet processing code in templar.py has been removed.
The namespace merging is done to allow #def's in child snippets to be visible to the parent template. This only applies to $SNIPPET, or SNIPPET::, not
---
 cobbler/templar.py     |   96 +---------------
 cobbler/templateapi.py |  312 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 313 insertions(+), 95 deletions(-)
 create mode 100644 cobbler/templateapi.py

diff --git a/cobbler/templar.py b/cobbler/templar.py
index b1b285d..08f4112 100644
--- a/cobbler/templar.py
+++ b/cobbler/templar.py
@@ -18,7 +18,7 @@ import os
 import os.path
 import glob
 from cexceptions import *
-from Cheetah.Template import Template
+from templateapi import Template
 from utils import *
 import utils
 
@@ -51,14 +51,6 @@ class Templar:
         # template syntax.
         raw_data = raw_data.replace("TEMPLATE::","$")
 
-        # replace snippets with the proper Cheetah include directives prior to processing.
-        # see Wiki for full details on how snippets operate.
-        snippet_results = ""
-        for line in raw_data.split("\n"):
-             line = self.replace_snippets(line,subject)
-             snippet_results = "\n".join((snippet_results, line))
-        raw_data = snippet_results
-
         # HACK:  the ksmeta field may contain nfs://server:/mount in which
         # case this is likely WRONG for kickstart, which needs the NFS
         # directive instead.  Do this to make the templates work.
@@ -109,89 +101,3 @@ class Templar:
             fd.close()
 
         return data_out
-
-    def replace_snippets(self,line,subject):
-        """
-        Replace all SNIPPET:: syntaxes on a line with the
-        results of evaluating the snippet, taking care not
-        to replace tabs with spaces or anything like that
-        """
-        tokens = line.split(None)
-        for t in tokens:
-           if t.startswith("SNIPPET::"):
-               snippet_name = t.replace("SNIPPET::","")
-               line = line.replace(t,self.eval_snippet(snippet_name,subject))
-        return line
-
-    def eval_snippet(self,name,subject):
-        """
-        Replace SNIPPET::foo with contents of files:
-            Use /var/lib/cobbler/snippets/per_system/$name/$sysname
-                /var/lib/cobbler/snippets/per_profile/$name/$proname
-                /var/lib/cobbler/snippets/$name
-            in order... (first one wins)
-        """
-
-        sd = self.settings.snippetsdir
-        default_path = "%s/%s" % (sd,name)  
-
-        if subject is None:
-            if os.path.exists(default_path):
-                return self.slurp(default_path)
-            else:
-                return self.slurp(None)
-            
-
-        if subject.COLLECTION_TYPE == "system":
-            profile  = self.api.find_profile(name=subject.profile)
-            sys_path = "%s/per_system/%s/%s" % (sd,name,subject.name) 
-            pro_path = "%s/per_profile/%s/%s" % (sd,name,profile.name) 
-            if os.path.exists(sys_path):
-                return self.slurp(sys_path)
-            elif os.path.exists(pro_path):
-                return self.slurp(pro_path)
-            elif os.path.exists(default_path):
-                return self.slurp(default_path)
-            else:
-                return self.slurp(None)
-
-        if subject.COLLECTION_TYPE == "profile":
-            pro_path = "%s/per_profile/%s/%s" % (sd,name,subject.name) 
-            if os.path.exists(pro_path):
-                return self.slurp(pro_path)
-            elif os.path.exists(default_path):
-                return self.slurp(default_path)
-            else:
-                return self.slurp(None)
-
-        return self.slurp(None)
-
-    def slurp(self,filename):
-        """
-        Get the contents of a filename but if none is specified
-        just include some generic error text for the rendered
-        template.
-        """
-
-        if filename is None:
-            return "# error: no snippet data found"
-
-        # disabling this as it requires restarting cobblerd after
-        # making changes to snippets.  not good.  Since kickstart
-        # templates are now generated dynamically and we don't need
-        # to load all snippets to parse any given template, this should
-        # be ok, leaving this in as a footnote should we need it later.
-        #
-        ## potentially eliminate a ton of system calls if syncing
-        ## thousands of systems that use the same template
-        #if self.cache.has_key(filename):
-        #    
-        #    return self.cache[filename]
-
-        fd = open(filename,"r")
-        data = fd.read()
-        # self.cache[filename] = data
-        fd.close()
-
-        return data
-
diff --git a/cobbler/templateapi.py b/cobbler/templateapi.py
new file mode 100644
index 0000000..ab4bc78
--- /dev/null
+++ b/cobbler/templateapi.py
@@ -0,0 +1,312 @@
+"""
+Cobbler provides builtin methods for use in Cheetah templates. $SNIPPET is one
+such function and is now used to implement Cobbler's SNIPPET:: syntax.
+
+Copyright 2008, The Defense Information Systems Agency
+Daniel Guernsey <[EMAIL PROTECTED]>
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+import Cheetah.Template
+
+# 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.
+BuiltinTemplate = Cheetah.Template.Template.compile(source="\n".join([
+    # This is the SNIPPET function I proposed. 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).
+    
+    # TODO: Should this be in its own file? If so, should it go into
+    # /var/lib/cobbler or should it go with cobbler's site-package?
+    # I would suggest the site-package.
+    "#def SNIPPET($file)",
+        "#set $fullpath = $find_snippet($file)",
+        "#if $fullpath",
+            "#include $fullpath",
+        "#else",
+            "# Error: no snippet data",
+        "#end if",
+    "#end def",
+    
+    # Comment every line containing the pattern
+    "#def comment_lines($filename, $pattern, $commentchar='#')",
+        "perl -npe 's/^(.*${pattern}.*)$/${commentchar}\${1}/' -i '$filename'",
+    "#end def",
+    
+    # Comments every line which contains only the exact pattern.
+    "#def comment_lines_exact($filename, $pattern, $commentchar='#')",
+        "perl -npe 's/^(${pattern})$/${commentchar}\${1}/' -f '$filename'",
+    "#end def",
+    
+    # Uncomments every (commented) line containing the pattern
+    # Patterns should not contain the #
+    "#def uncomment_lines($filename, $pattern, $commentchar='#')",
+        "perl -npe 's/^[ \t]*${commentchar}(.*${pattern}.*)$/\${1}/' -i '$filename'",
+    "#end def",
+    
+    # Nullify (by changing to 'true') all instances of a given sh command. This
+    # does understand lines with multiple commands (separated by ';') and also
+    # knows to ignore comments. Consider other options before using this
+    # method.
+    "#def delete_command($filename, $pattern)",
+        "sed -nr '",
+        "    h",
+        "    s/^([^#]*)(#?.*)$/\1/",
+        "    s/((^|;)[ \t]*)${pattern}([ \t]*($|;))/\1true\3/g",
+        "    s/((^|;)[ \t]*)${pattern}([ \t]*($|;))/\1true\3/g",
+        "    x",
+        "    s/^([^#]*)(#?.*)$/\2/",
+        "    H",
+        "    x",
+        "    s/\\n//",
+        "    p",
+        "' -i '$filename'",
+    "#end def",
+    
+    # Replace a configuration parameter value, or add it if it doesn't exist.
+    # Assumes format is [param_name] [value]
+    "#def set_config_value($filename, $param_name, $value)",
+        "if [ -n \"\$(grep -Ee '^[ \t]*${param_name}[ \t]+' '$filename')\" ]",
+        "then",
+        "    perl -npe 's/^([ \t]*${param_name}[ \t]+)[\x21-\x7E]*([ \t]*(#.*)?)$/\${1}${sedesc($value)}\${2}/' -i '$filename'",
+        "else",
+        "    echo '$param_name $value' >> '$filename'",
+        "fi",
+    "#end def",
+    
+    # Replace a configuration parameter value, or add it if it doesn't exist.
+    # Assues format is [param_name] [delimiter] [value], where [delimiter] is
+    # usually '='.
+    "#def set_config_value_delim($filename, $param_name, $delim, $value)",
+        "if [ -n \"\$(grep -Ee '^[ \t]*${param_name}[ \t]*${delim}[ \t]*' '$filename')\" ]",
+        "then",
+        "    perl -npe 's/^([ \t]*${param_name}[ \t]*${delim}[ \t]*)[\x21-\x7E]*([ \t]*(#.*)?)$/${1}${sedesc($value)}${2}/' -i '$filename'",
+        "else",
+        "    echo '$param_name$delim$value' >> '$filename'",
+        "fi",
+    "#end def",
+    
+    # Copy a file from the server to the client.
+    "#def copy_over_file($serverfile, $clientfile)",
+        "cat << 'EOF' > '$clientfile'",
+        "#include $files + $serverfile",
+        "EOF",
+    "#end def",
+    
+    # Copy a file from the server and append the contents to a file on the
+    # client.
+    "#def copy_over_file($serverfile, $clientfile)",
+        "cat << 'EOF' >> '$clientfile'",
+        "#include $files + $serverfile",
+        "EOF",
+    "#end def",
+    
+    # Convenience function: Copy/append several files at once. This accepts a
+    # list of tuples. The first element indicates whether to overwrite ('w') or
+    # append ('a'). The second element is the file name on both the server and
+    # the client (a '/' is prepended on the client side).
+    "#def copy_files($filelist)",
+        "#for $thisfile in $filelist",
+            "#if $thisfile[0] == 'a'",
+                "$copy_append_file($thisfile[1], '/' + $thisfile[1])",
+            "#else",
+                "$copy_over_file($thisfile[1], '/' + $thisfile[1])",
+            "#end if",
+        "#end for",
+    "#end def",
+    
+    # Append some content to the todo file. NOTE: $todofile must be defined
+    # before using this (unless you want unexpected results). Be sure to end
+    # the content with 'EOF'
+    "#def TODO()",
+        "cat << 'EOF' >> '$todofile'",
+    "#end def",
+    
+    # Set the owner, group, and permissions for several files. Assignment can
+    # be plain ('p') or recursive. If recursive you can assign everything ('r')
+    # or just files ('f'). This method takes a list of tuples. The first element
+    # of each indicates which style. The remaining elements are owner, group,
+    # and mode respectively. If 'f' is used, an additional element is a find
+    # pattern that can further restrict assignments (use '*' if no additional
+    # restrict is desired).
+    "#def set_permissions($filelist)",
+        "#for $file in $filelist",
+            "#if $file[0] == 'p'",
+                "#if $file[1] != '' and $file[2] != ''",
+                    "chown '$file[1]:$file[2]' '$file[4]'",
+                "#else",
+                    "#if $file[1] != ''",
+                        "chown '$file[1]' '$file[4]'",
+                    "#end if",
+                    "#if $file[2] != ''",
+                        "chgrp '$file[2]' '$file[4]'",
+                    "#end if",
+                "#end if",
+                "#if $file[3] != ''",
+                    "chmod '$file[3]' '$file[4]'",
+                "#end if",
+            "#elif $file[0] == 'r'",
+                "#if $file[1] != '' and $file[2] != ''",
+                    "chown -R '$file[1]:$file[2]' '$file[4]'",
+                "#else",
+                    "#if $file[1] != ''",
+                        "chown -R '$file[1]' '$file[4]'",
+                    "#end if",
+                    "#if $file[2] != ''",
+                        "chgrp -R '$file[2]' '$file[4]'",
+                    "#end if",
+                "#end if",
+                "#if $file[3] != ''",
+                    "chmod -R '$file[3]' '$file[4]'",
+                "#end if",
+            "#elif $file[0] == 'f'",
+                "#if $file[1] != '' and $file[2] != ''",
+                    "find $file[4] -name '$file[5]' -type f -exec chown -R '$file[1]:$file[2]' {} \\;",
+                "#else",
+                    "#if $file[1] != ''",
+                        "find $file[4] -name '$file[5]' -type f -exec chown -R '$file[1]' {} \\;",
+                    "#end if",
+                    "#if $file[2] != ''",
+                        "find $file[4] -name '$file[5]' -type f -exec chgrp -R '$file[2]' {} \\;",
+                    "#end if",
+                "#end if",
+                "#if $file[3] != ''",
+                    "find $file[4] -name '$file[5]' -type f -exec chmod -R '$file[3]' {} \\;",
+                "#end if",
+            "#end if",
+        "#end for",
+    "#end def",
+    
+    # Cheeseball an entire directory.
+    "#def includeall($dir)",
+        "#import os",
+        "#for $file in $os.listdir($snippetsdir + $dir)",
+            "#include $snippetsdir + $dir + '/' + $file",
+        "#end for",
+    "#end def",
+
+]) + "\n")
+
+# 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.
+class Template(BuiltinTemplate):
+    # We don't need to override __init__
+    
+    # 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.
+    def compile(klass, *args, **kwargs):
+        """
+        Compile a cheetah template with cobbler modifications. Modifications
+        include SNIPPET:: syntax replacement and inclusion of cobbler builtin
+        methods.
+        """
+        # We can do the SNIPPET:: syntax replacements here, effectively making
+        # it recursive. Any cheetah template compiled by cobbler will have this
+        # replacement
+        def replace_token(token):
+            if token.startswith('SNIPPET::'):
+                snippet_name = token.replace('SNIPPET::', '')
+                return "$SNIPPET('%s')" % snippet_name
+            else:
+                return token
+        def replace_line(line):
+            return ' '.join([replace_token(token) for token in line.split(' ')])
+        def preprocess(source, file):
+            # Normally, the cheetah compiler worries about this, but we need to
+            # preprocess the actual source
+            if source is None:
+                if isinstance(file, (str, unicode)):
+                    f = open(file)
+                    source = f.read()
+                    f.close()
+                elif hasattr(file, 'read'):
+                    source = file.read()
+                file = None # Stop Cheetah from throwing a fit.
+            return ('\n'.join([replace_line(line) for line in source.split('\n')]), file)
+        preprocessors = [preprocess]
+        if kwargs.has_key('preprocessors'):
+            preprocessors.extend(kwargs['preprocessors'])
+        kwargs['preprocessors'] = preprocessors
+        
+        # Instruct Cheetah to use this class as the base for all cheetah templates
+        if not kwargs.has_key('baseclass'):
+            kwargs['baseclass'] = Template
+        
+        # Now let Cheetah do the actual compilation
+        return Cheetah.Template.Template.compile(*args, **kwargs)
+    compile = classmethod(compile)
+    
+    def find_snippet(self, file):
+        """
+        Locate the appropriate snippet for the current system and profile.
+        This will first check for a per_system snippet, a per_profile snippet,
+        and a general snippet. If no snippet is located, it returns None.
+        """
+        import os.path
+        if self.varExists('system_name'):
+            fullpath = '%s/per_system/%s/%s' % (self.getVar('snippetsdir'), file, self.getVar('system_name'))
+            if os.path.exists(fullpath):
+                return fullpath
+        if self.varExists('profile_name'):
+            fullpath = '%s/per_profile/%s/%s' % (self.getVar('snippetsdir'), file, self.getVar('profile_name'))
+            if os.path.exists(fullpath):
+                return fullpath
+        fullpath = '%s/%s' % (self.getVar('snippetsdir'), file)
+        if os.path.exists(fullpath):
+            return fullpath
+        return None
+    
+    # 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.
+    def SNIPPET(self, file):
+        """
+        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.
+        """
+        # First, do the actual inclusion. Cheetah (when processing #include)
+        # will track the inclusion in self._CHEETAH__cheetahIncludes
+        result = 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.
+        fullpath = self.find_snippet(file);
+        if fullpath:
+            # 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[fullpath].searchList()
+            myList = self.searchList()
+            for childElem in childList:
+                if not childElem in myList:
+                    myList.append(childElem)
+        
+        return result
+    
+    def sedesc(self, value):
+        """
+	Escape a string for use in sed.
+	"""
+        def escchar(c):
+            if c in '/^.[]$()|*+?{}\\':
+                return '\\' + c
+            else:
+                return c
+        return ''.join([escchar for c in value])
-- 
1.5.6.3

_______________________________________________
cobbler mailing list
[email protected]
https://fedorahosted.org/mailman/listinfo/cobbler

Reply via email to