summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Plugins/Cfg/CfgGenshiGenerator.py
blob: dc128bbe90a8b596ce8efca3f338de2b72cee97d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
""" The CfgGenshiGenerator allows you to use the `Genshi
<http://genshi.edgewall.org>`_ templating system to generate
:ref:`server-plugins-generators-cfg` files. """

import re
import sys
import logging
import traceback
import Bcfg2.Server.Plugin
from Bcfg2.Server.Plugins.Cfg import CfgGenerator

logger = logging.getLogger(__name__)

try:
    import genshi.core
    from genshi.template import TemplateLoader, NewTextTemplate
    from genshi.template.eval import UndefinedError
    have_genshi = True
except ImportError:
    TemplateLoader = None
    have_genshi = False

def removecomment(stream):
    """ A Genshi filter that removes comments from the stream.  This
    function is a generator.

    :param stream: The Genshi stream to remove comments from
    :type stream: genshi.core.Stream
    :returns: tuple of ``(kind, data, pos)``, as when iterating
              through a Genshi stream
    """
    for kind, data, pos in stream:
        if kind is genshi.core.COMMENT:
            continue
        yield kind, data, pos


class CfgGenshiGenerator(CfgGenerator):
    """ The CfgGenshiGenerator allows you to use the `Genshi
    <http://genshi.edgewall.org>`_ templating system to generate
    :ref:`server-plugins-generators-cfg` files. """

    #: Handle .genshi files
    __extensions__ = ['genshi']
    
    #: ``__loader_cls__`` is the class that will be instantiated to
    #: load the template files.  It must implement one public function,
    #: ``load()``, as :class:`genshi.template.TemplateLoader`.
    __loader_cls__ = TemplateLoader

    #: Ignore ``.genshi_include`` files so they can be used with the
    #: Genshi ``{% include ... %}`` directive without raising warnings.
    __ignore__ = ["genshi_include"]

    #: Error-handling in Genshi is pretty obtuse.  This regex is used
    #: to extract the first line of the code block that raised an
    #: exception in a Genshi template so we can provide a decent error
    #: message that actually tells the end user where an error
    #: occurred.
    pyerror_re = re.compile('<\w+ u?[\'"](.*?)\s*\.\.\.[\'"]>')

    def __init__(self, fname, spec, encoding):
        CfgGenerator.__init__(self, fname, spec, encoding)
        if not have_genshi:
            msg = "Cfg: Genshi is not available: %s" % fname
            logger.error(msg)
            raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
        self.loader = self.__loader_cls__()
        self.template = None
    __init__.__doc__ = CfgGenerator.__init__.__doc__

    def get_data(self, entry, metadata):
        fname = entry.get('realname', entry.get('name'))
        stream = \
            self.template.generate(name=fname,
                                   metadata=metadata,
                                   path=self.name).filter(removecomment)
        try:
            try:
                return stream.render('text', encoding=self.encoding,
                                     strip_whitespace=False)
            except TypeError:
                return stream.render('text', encoding=self.encoding)
        except UndefinedError:
            # a failure in a genshi expression _other_ than %{ python ... %}
            err = sys.exc_info()[1]
            stack = traceback.extract_tb(sys.exc_info()[2])
            for quad in stack:
                if quad[0] == self.name:
                    logger.error("Cfg: Error rendering %s at '%s': %s: %s" %
                                 (fname, quad[2], err.__class__.__name__, err))
                    break
            raise
        except:
            # a failure in a %{ python ... %} block -- the snippet in
            # the traceback is just the beginning of the block.
            err = sys.exc_info()[1]
            stack = traceback.extract_tb(sys.exc_info()[2])
            (filename, lineno, func, text) = stack[-1]
            # this is horrible, and I deeply apologize to whoever gets
            # to maintain this after I go to the Great Beer Garden in
            # the Sky.  genshi is incredibly opaque about what's being
            # executed, so the only way I can find to determine which
            # {% python %} block is being executed -- if there are
            # multiples -- is to iterate through them and match the
            # snippet of the first line that's in the traceback with
            # the first non-empty line of the block.
            execs = [contents
                     for etype, contents, loc in self.template.stream
                     if etype == self.template.EXEC]
            contents = None
            if len(execs) == 1:
                contents = execs[0]
            elif len(execs) > 1:
                match = pyerror_re.match(func)
                if match:
                    firstline = match.group(0)
                    for pyblock in execs:
                        if pyblock.startswith(firstline):
                            contents = pyblock
                            break
            # else, no EXEC blocks -- WTF?
            if contents:
                # we now have the bogus block, but we need to get the
                # offending line.  To get there, we do (line number
                # given in the exception) - (firstlineno from the
                # internal genshi code object of the snippet) + 1 =
                # (line number of the line with an error within the
                # block, with all multiple line breaks elided to a
                # single line break)
                real_lineno = lineno - contents.code.co_firstlineno
                src = re.sub(r'\n\n+', '\n', contents.source).splitlines()
                logger.error("Cfg: Error rendering %s at '%s': %s: %s" %
                             (fname, src[real_lineno], err.__class__.__name__,
                              err))
            raise
    get_data.__doc__ = CfgGenerator.get_data.__doc__

    def handle_event(self, event):
        if event.code2str() == 'deleted':
            return
        CfgGenerator.handle_event(self, event)
        try:
            self.template = self.loader.load(self.name, cls=NewTextTemplate,
                                             encoding=self.encoding)
        except Exception:
            msg = "Cfg: Could not load template %s: %s" % (self.name,
                                                           sys.exc_info()[1])
            logger.error(msg)
            raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
    handle_event.__doc__ = CfgGenerator.handle_event.__doc__