~openstack-charmers/charms/precise/glance/ceilometer-support

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/openstack/templating.py

  • Committer: James Page
  • Date: 2013-10-15 08:33:43 UTC
  • mfrom: (29.2.200 glance)
  • Revision ID: james.page@canonical.com-20131015083343-8y6lee574knme3n4
Merge of python-redux and havana support

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import os
 
2
 
 
3
from charmhelpers.fetch import apt_install
 
4
 
 
5
from charmhelpers.core.hookenv import (
 
6
    log,
 
7
    ERROR,
 
8
    INFO
 
9
)
 
10
 
 
11
from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES
 
12
 
 
13
try:
 
14
    from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
 
15
except ImportError:
 
16
    # python-jinja2 may not be installed yet, or we're running unittests.
 
17
    FileSystemLoader = ChoiceLoader = Environment = exceptions = None
 
18
 
 
19
 
 
20
class OSConfigException(Exception):
 
21
    pass
 
22
 
 
23
 
 
24
def get_loader(templates_dir, os_release):
 
25
    """
 
26
    Create a jinja2.ChoiceLoader containing template dirs up to
 
27
    and including os_release.  If directory template directory
 
28
    is missing at templates_dir, it will be omitted from the loader.
 
29
    templates_dir is added to the bottom of the search list as a base
 
30
    loading dir.
 
31
 
 
32
    A charm may also ship a templates dir with this module
 
33
    and it will be appended to the bottom of the search list, eg:
 
34
    hooks/charmhelpers/contrib/openstack/templates.
 
35
 
 
36
    :param templates_dir: str: Base template directory containing release
 
37
                               sub-directories.
 
38
    :param os_release   : str: OpenStack release codename to construct template
 
39
                               loader.
 
40
 
 
41
    :returns            : jinja2.ChoiceLoader constructed with a list of
 
42
                          jinja2.FilesystemLoaders, ordered in descending
 
43
                          order by OpenStack release.
 
44
    """
 
45
    tmpl_dirs = [(rel, os.path.join(templates_dir, rel))
 
46
                 for rel in OPENSTACK_CODENAMES.itervalues()]
 
47
 
 
48
    if not os.path.isdir(templates_dir):
 
49
        log('Templates directory not found @ %s.' % templates_dir,
 
50
            level=ERROR)
 
51
        raise OSConfigException
 
52
 
 
53
    # the bottom contains tempaltes_dir and possibly a common templates dir
 
54
    # shipped with the helper.
 
55
    loaders = [FileSystemLoader(templates_dir)]
 
56
    helper_templates = os.path.join(os.path.dirname(__file__), 'templates')
 
57
    if os.path.isdir(helper_templates):
 
58
        loaders.append(FileSystemLoader(helper_templates))
 
59
 
 
60
    for rel, tmpl_dir in tmpl_dirs:
 
61
        if os.path.isdir(tmpl_dir):
 
62
            loaders.insert(0, FileSystemLoader(tmpl_dir))
 
63
        if rel == os_release:
 
64
            break
 
65
    log('Creating choice loader with dirs: %s' %
 
66
        [l.searchpath for l in loaders], level=INFO)
 
67
    return ChoiceLoader(loaders)
 
68
 
 
69
 
 
70
class OSConfigTemplate(object):
 
71
    """
 
72
    Associates a config file template with a list of context generators.
 
73
    Responsible for constructing a template context based on those generators.
 
74
    """
 
75
    def __init__(self, config_file, contexts):
 
76
        self.config_file = config_file
 
77
 
 
78
        if hasattr(contexts, '__call__'):
 
79
            self.contexts = [contexts]
 
80
        else:
 
81
            self.contexts = contexts
 
82
 
 
83
        self._complete_contexts = []
 
84
 
 
85
    def context(self):
 
86
        ctxt = {}
 
87
        for context in self.contexts:
 
88
            _ctxt = context()
 
89
            if _ctxt:
 
90
                ctxt.update(_ctxt)
 
91
                # track interfaces for every complete context.
 
92
                [self._complete_contexts.append(interface)
 
93
                 for interface in context.interfaces
 
94
                 if interface not in self._complete_contexts]
 
95
        return ctxt
 
96
 
 
97
    def complete_contexts(self):
 
98
        '''
 
99
        Return a list of interfaces that have atisfied contexts.
 
100
        '''
 
101
        if self._complete_contexts:
 
102
            return self._complete_contexts
 
103
        self.context()
 
104
        return self._complete_contexts
 
105
 
 
106
 
 
107
class OSConfigRenderer(object):
 
108
    """
 
109
    This class provides a common templating system to be used by OpenStack
 
110
    charms.  It is intended to help charms share common code and templates,
 
111
    and ease the burden of managing config templates across multiple OpenStack
 
112
    releases.
 
113
 
 
114
    Basic usage:
 
115
        # import some common context generates from charmhelpers
 
116
        from charmhelpers.contrib.openstack import context
 
117
 
 
118
        # Create a renderer object for a specific OS release.
 
119
        configs = OSConfigRenderer(templates_dir='/tmp/templates',
 
120
                                   openstack_release='folsom')
 
121
        # register some config files with context generators.
 
122
        configs.register(config_file='/etc/nova/nova.conf',
 
123
                         contexts=[context.SharedDBContext(),
 
124
                                   context.AMQPContext()])
 
125
        configs.register(config_file='/etc/nova/api-paste.ini',
 
126
                         contexts=[context.IdentityServiceContext()])
 
127
        configs.register(config_file='/etc/haproxy/haproxy.conf',
 
128
                         contexts=[context.HAProxyContext()])
 
129
        # write out a single config
 
130
        configs.write('/etc/nova/nova.conf')
 
131
        # write out all registered configs
 
132
        configs.write_all()
 
133
 
 
134
    Details:
 
135
 
 
136
    OpenStack Releases and template loading
 
137
    ---------------------------------------
 
138
    When the object is instantiated, it is associated with a specific OS
 
139
    release.  This dictates how the template loader will be constructed.
 
140
 
 
141
    The constructed loader attempts to load the template from several places
 
142
    in the following order:
 
143
        - from the most recent OS release-specific template dir (if one exists)
 
144
        - the base templates_dir
 
145
        - a template directory shipped in the charm with this helper file.
 
146
 
 
147
 
 
148
    For the example above, '/tmp/templates' contains the following structure:
 
149
        /tmp/templates/nova.conf
 
150
        /tmp/templates/api-paste.ini
 
151
        /tmp/templates/grizzly/api-paste.ini
 
152
        /tmp/templates/havana/api-paste.ini
 
153
 
 
154
    Since it was registered with the grizzly release, it first seraches
 
155
    the grizzly directory for nova.conf, then the templates dir.
 
156
 
 
157
    When writing api-paste.ini, it will find the template in the grizzly
 
158
    directory.
 
159
 
 
160
    If the object were created with folsom, it would fall back to the
 
161
    base templates dir for its api-paste.ini template.
 
162
 
 
163
    This system should help manage changes in config files through
 
164
    openstack releases, allowing charms to fall back to the most recently
 
165
    updated config template for a given release
 
166
 
 
167
    The haproxy.conf, since it is not shipped in the templates dir, will
 
168
    be loaded from the module directory's template directory, eg
 
169
    $CHARM/hooks/charmhelpers/contrib/openstack/templates.  This allows
 
170
    us to ship common templates (haproxy, apache) with the helpers.
 
171
 
 
172
    Context generators
 
173
    ---------------------------------------
 
174
    Context generators are used to generate template contexts during hook
 
175
    execution.  Doing so may require inspecting service relations, charm
 
176
    config, etc.  When registered, a config file is associated with a list
 
177
    of generators.  When a template is rendered and written, all context
 
178
    generates are called in a chain to generate the context dictionary
 
179
    passed to the jinja2 template. See context.py for more info.
 
180
    """
 
181
    def __init__(self, templates_dir, openstack_release):
 
182
        if not os.path.isdir(templates_dir):
 
183
            log('Could not locate templates dir %s' % templates_dir,
 
184
                level=ERROR)
 
185
            raise OSConfigException
 
186
 
 
187
        self.templates_dir = templates_dir
 
188
        self.openstack_release = openstack_release
 
189
        self.templates = {}
 
190
        self._tmpl_env = None
 
191
 
 
192
        if None in [Environment, ChoiceLoader, FileSystemLoader]:
 
193
            # if this code is running, the object is created pre-install hook.
 
194
            # jinja2 shouldn't get touched until the module is reloaded on next
 
195
            # hook execution, with proper jinja2 bits successfully imported.
 
196
            apt_install('python-jinja2')
 
197
 
 
198
    def register(self, config_file, contexts):
 
199
        """
 
200
        Register a config file with a list of context generators to be called
 
201
        during rendering.
 
202
        """
 
203
        self.templates[config_file] = OSConfigTemplate(config_file=config_file,
 
204
                                                       contexts=contexts)
 
205
        log('Registered config file: %s' % config_file, level=INFO)
 
206
 
 
207
    def _get_tmpl_env(self):
 
208
        if not self._tmpl_env:
 
209
            loader = get_loader(self.templates_dir, self.openstack_release)
 
210
            self._tmpl_env = Environment(loader=loader)
 
211
 
 
212
    def _get_template(self, template):
 
213
        self._get_tmpl_env()
 
214
        template = self._tmpl_env.get_template(template)
 
215
        log('Loaded template from %s' % template.filename, level=INFO)
 
216
        return template
 
217
 
 
218
    def render(self, config_file):
 
219
        if config_file not in self.templates:
 
220
            log('Config not registered: %s' % config_file, level=ERROR)
 
221
            raise OSConfigException
 
222
        ctxt = self.templates[config_file].context()
 
223
 
 
224
        _tmpl = os.path.basename(config_file)
 
225
        try:
 
226
            template = self._get_template(_tmpl)
 
227
        except exceptions.TemplateNotFound:
 
228
            # if no template is found with basename, try looking for it
 
229
            # using a munged full path, eg:
 
230
            #   /etc/apache2/apache2.conf -> etc_apache2_apache2.conf
 
231
            _tmpl = '_'.join(config_file.split('/')[1:])
 
232
            try:
 
233
                template = self._get_template(_tmpl)
 
234
            except exceptions.TemplateNotFound as e:
 
235
                log('Could not load template from %s by %s or %s.' %
 
236
                    (self.templates_dir, os.path.basename(config_file), _tmpl),
 
237
                    level=ERROR)
 
238
                raise e
 
239
 
 
240
        log('Rendering from template: %s' % _tmpl, level=INFO)
 
241
        return template.render(ctxt)
 
242
 
 
243
    def write(self, config_file):
 
244
        """
 
245
        Write a single config file, raises if config file is not registered.
 
246
        """
 
247
        if config_file not in self.templates:
 
248
            log('Config not registered: %s' % config_file, level=ERROR)
 
249
            raise OSConfigException
 
250
 
 
251
        _out = self.render(config_file)
 
252
 
 
253
        with open(config_file, 'wb') as out:
 
254
            out.write(_out)
 
255
 
 
256
        log('Wrote template %s.' % config_file, level=INFO)
 
257
 
 
258
    def write_all(self):
 
259
        """
 
260
        Write out all registered config files.
 
261
        """
 
262
        [self.write(k) for k in self.templates.iterkeys()]
 
263
 
 
264
    def set_release(self, openstack_release):
 
265
        """
 
266
        Resets the template environment and generates a new template loader
 
267
        based on a the new openstack release.
 
268
        """
 
269
        self._tmpl_env = None
 
270
        self.openstack_release = openstack_release
 
271
        self._get_tmpl_env()
 
272
 
 
273
    def complete_contexts(self):
 
274
        '''
 
275
        Returns a list of context interfaces that yield a complete context.
 
276
        '''
 
277
        interfaces = []
 
278
        [interfaces.extend(i.complete_contexts())
 
279
         for i in self.templates.itervalues()]
 
280
        return interfaces