~nottrobin/charms/trusty/wsgi-app/trunk

« back to all changes in this revision

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

  • Committer: Robin Winslow
  • Date: 2014-12-02 22:54:40 UTC
  • Revision ID: robin@robinwinslow.co.uk-20141202225440-ruuctvfe7pdh1dd8
Try reverting most of charmhelpers to the old version

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
import os
2
 
 
3
 
import six
4
 
 
5
 
from charmhelpers.fetch import apt_install
6
 
from charmhelpers.core.hookenv import (
7
 
    log,
8
 
    ERROR,
9
 
    INFO
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
 
 
35
 
        hooks/charmhelpers/contrib/openstack/templates
36
 
 
37
 
    :param templates_dir (str): Base template directory containing release
38
 
        sub-directories.
39
 
    :param os_release (str): OpenStack release codename to construct template
40
 
        loader.
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 six.itervalues(OPENSTACK_CODENAMES)]
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
 
 
116
 
        # import some common context generates from charmhelpers
117
 
        from charmhelpers.contrib.openstack import context
118
 
 
119
 
        # Create a renderer object for a specific OS release.
120
 
        configs = OSConfigRenderer(templates_dir='/tmp/templates',
121
 
                                   openstack_release='folsom')
122
 
        # register some config files with context generators.
123
 
        configs.register(config_file='/etc/nova/nova.conf',
124
 
                         contexts=[context.SharedDBContext(),
125
 
                                   context.AMQPContext()])
126
 
        configs.register(config_file='/etc/nova/api-paste.ini',
127
 
                         contexts=[context.IdentityServiceContext()])
128
 
        configs.register(config_file='/etc/haproxy/haproxy.conf',
129
 
                         contexts=[context.HAProxyContext()])
130
 
        # write out a single config
131
 
        configs.write('/etc/nova/nova.conf')
132
 
        # write out all registered configs
133
 
        configs.write_all()
134
 
 
135
 
    **OpenStack Releases and template loading**
136
 
 
137
 
    When the object is instantiated, it is associated with a specific OS
138
 
    release.  This dictates how the template loader will be constructed.
139
 
 
140
 
    The constructed loader attempts to load the template from several places
141
 
    in the following order:
142
 
    - from the most recent OS release-specific template dir (if one exists)
143
 
    - the base templates_dir
144
 
    - a template directory shipped in the charm with this helper file.
145
 
 
146
 
    For the example above, '/tmp/templates' contains the following structure::
147
 
 
148
 
        /tmp/templates/nova.conf
149
 
        /tmp/templates/api-paste.ini
150
 
        /tmp/templates/grizzly/api-paste.ini
151
 
        /tmp/templates/havana/api-paste.ini
152
 
 
153
 
    Since it was registered with the grizzly release, it first seraches
154
 
    the grizzly directory for nova.conf, then the templates dir.
155
 
 
156
 
    When writing api-paste.ini, it will find the template in the grizzly
157
 
    directory.
158
 
 
159
 
    If the object were created with folsom, it would fall back to the
160
 
    base templates dir for its api-paste.ini template.
161
 
 
162
 
    This system should help manage changes in config files through
163
 
    openstack releases, allowing charms to fall back to the most recently
164
 
    updated config template for a given release
165
 
 
166
 
    The haproxy.conf, since it is not shipped in the templates dir, will
167
 
    be loaded from the module directory's template directory, eg
168
 
    $CHARM/hooks/charmhelpers/contrib/openstack/templates.  This allows
169
 
    us to ship common templates (haproxy, apache) with the helpers.
170
 
 
171
 
    **Context generators**
172
 
 
173
 
    Context generators are used to generate template contexts during hook
174
 
    execution.  Doing so may require inspecting service relations, charm
175
 
    config, etc.  When registered, a config file is associated with a list
176
 
    of generators.  When a template is rendered and written, all context
177
 
    generates are called in a chain to generate the context dictionary
178
 
    passed to the jinja2 template. See context.py for more info.
179
 
    """
180
 
    def __init__(self, templates_dir, openstack_release):
181
 
        if not os.path.isdir(templates_dir):
182
 
            log('Could not locate templates dir %s' % templates_dir,
183
 
                level=ERROR)
184
 
            raise OSConfigException
185
 
 
186
 
        self.templates_dir = templates_dir
187
 
        self.openstack_release = openstack_release
188
 
        self.templates = {}
189
 
        self._tmpl_env = None
190
 
 
191
 
        if None in [Environment, ChoiceLoader, FileSystemLoader]:
192
 
            # if this code is running, the object is created pre-install hook.
193
 
            # jinja2 shouldn't get touched until the module is reloaded on next
194
 
            # hook execution, with proper jinja2 bits successfully imported.
195
 
            apt_install('python-jinja2')
196
 
 
197
 
    def register(self, config_file, contexts):
198
 
        """
199
 
        Register a config file with a list of context generators to be called
200
 
        during rendering.
201
 
        """
202
 
        self.templates[config_file] = OSConfigTemplate(config_file=config_file,
203
 
                                                       contexts=contexts)
204
 
        log('Registered config file: %s' % config_file, level=INFO)
205
 
 
206
 
    def _get_tmpl_env(self):
207
 
        if not self._tmpl_env:
208
 
            loader = get_loader(self.templates_dir, self.openstack_release)
209
 
            self._tmpl_env = Environment(loader=loader)
210
 
 
211
 
    def _get_template(self, template):
212
 
        self._get_tmpl_env()
213
 
        template = self._tmpl_env.get_template(template)
214
 
        log('Loaded template from %s' % template.filename, level=INFO)
215
 
        return template
216
 
 
217
 
    def render(self, config_file):
218
 
        if config_file not in self.templates:
219
 
            log('Config not registered: %s' % config_file, level=ERROR)
220
 
            raise OSConfigException
221
 
        ctxt = self.templates[config_file].context()
222
 
 
223
 
        _tmpl = os.path.basename(config_file)
224
 
        try:
225
 
            template = self._get_template(_tmpl)
226
 
        except exceptions.TemplateNotFound:
227
 
            # if no template is found with basename, try looking for it
228
 
            # using a munged full path, eg:
229
 
            #   /etc/apache2/apache2.conf -> etc_apache2_apache2.conf
230
 
            _tmpl = '_'.join(config_file.split('/')[1:])
231
 
            try:
232
 
                template = self._get_template(_tmpl)
233
 
            except exceptions.TemplateNotFound as e:
234
 
                log('Could not load template from %s by %s or %s.' %
235
 
                    (self.templates_dir, os.path.basename(config_file), _tmpl),
236
 
                    level=ERROR)
237
 
                raise e
238
 
 
239
 
        log('Rendering from template: %s' % _tmpl, level=INFO)
240
 
        return template.render(ctxt)
241
 
 
242
 
    def write(self, config_file):
243
 
        """
244
 
        Write a single config file, raises if config file is not registered.
245
 
        """
246
 
        if config_file not in self.templates:
247
 
            log('Config not registered: %s' % config_file, level=ERROR)
248
 
            raise OSConfigException
249
 
 
250
 
        _out = self.render(config_file)
251
 
 
252
 
        with open(config_file, 'wb') as out:
253
 
            out.write(_out)
254
 
 
255
 
        log('Wrote template %s.' % config_file, level=INFO)
256
 
 
257
 
    def write_all(self):
258
 
        """
259
 
        Write out all registered config files.
260
 
        """
261
 
        [self.write(k) for k in six.iterkeys(self.templates)]
262
 
 
263
 
    def set_release(self, openstack_release):
264
 
        """
265
 
        Resets the template environment and generates a new template loader
266
 
        based on a the new openstack release.
267
 
        """
268
 
        self._tmpl_env = None
269
 
        self.openstack_release = openstack_release
270
 
        self._get_tmpl_env()
271
 
 
272
 
    def complete_contexts(self):
273
 
        '''
274
 
        Returns a list of context interfaces that yield a complete context.
275
 
        '''
276
 
        interfaces = []
277
 
        [interfaces.extend(i.complete_contexts())
278
 
         for i in six.itervalues(self.templates)]
279
 
        return interfaces