~canonical-ci-engineering/charms/trusty/vivid-edge-core-image-watcher/trunk

« back to all changes in this revision

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

  • Committer: Celso Providelo
  • Date: 2015-03-25 03:51:13 UTC
  • Revision ID: celso.providelo@canonical.com-20150325035113-a6vs913dmt3g2jho
Forking adt-cloud-worker + string replacing.

Show diffs side-by-side

added added

removed removed

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