~anton-skriptsov/charms/trusty/cinder-nexentaedge/trunk

« back to all changes in this revision

Viewing changes to charms/trusty/cinder-nedge/hooks/charmhelpers/contrib/openstack/templating.py

  • Committer: anton.skriptsov at nexenta
  • Date: 2015-11-12 18:47:44 UTC
  • Revision ID: anton.skriptsov@nexenta.com-20151112184744-8nrnbppt6m3jpewl
remove

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