1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
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.
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.
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/>.
21
from charmhelpers.fetch import apt_install
22
from charmhelpers.core.hookenv import (
27
from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES
30
from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
32
# python-jinja2 may not be installed yet, or we're running unittests.
33
FileSystemLoader = ChoiceLoader = Environment = exceptions = None
36
class OSConfigException(Exception):
40
def get_loader(templates_dir, os_release):
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
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::
51
hooks/charmhelpers/contrib/openstack/templates
53
:param templates_dir (str): Base template directory containing release
55
:param os_release (str): OpenStack release codename to construct template
57
:returns: jinja2.ChoiceLoader constructed with a list of
58
jinja2.FilesystemLoaders, ordered in descending
59
order by OpenStack release.
61
tmpl_dirs = [(rel, os.path.join(templates_dir, rel))
62
for rel in six.itervalues(OPENSTACK_CODENAMES)]
64
if not os.path.isdir(templates_dir):
65
log('Templates directory not found @ %s.' % templates_dir,
67
raise OSConfigException
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))
76
for rel, tmpl_dir in tmpl_dirs:
77
if os.path.isdir(tmpl_dir):
78
loaders.insert(0, FileSystemLoader(tmpl_dir))
81
log('Creating choice loader with dirs: %s' %
82
[l.searchpath for l in loaders], level=INFO)
83
return ChoiceLoader(loaders)
86
class OSConfigTemplate(object):
88
Associates a config file template with a list of context generators.
89
Responsible for constructing a template context based on those generators.
91
def __init__(self, config_file, contexts):
92
self.config_file = config_file
94
if hasattr(contexts, '__call__'):
95
self.contexts = [contexts]
97
self.contexts = contexts
99
self._complete_contexts = []
103
for context in self.contexts:
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]
113
def complete_contexts(self):
115
Return a list of interfaces that have atisfied contexts.
117
if self._complete_contexts:
118
return self._complete_contexts
120
return self._complete_contexts
123
class OSConfigRenderer(object):
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
132
# import some common context generates from charmhelpers
133
from charmhelpers.contrib.openstack import context
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
151
**OpenStack Releases and template loading**
153
When the object is instantiated, it is associated with a specific OS
154
release. This dictates how the template loader will be constructed.
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.
162
For the example above, '/tmp/templates' contains the following structure::
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
169
Since it was registered with the grizzly release, it first seraches
170
the grizzly directory for nova.conf, then the templates dir.
172
When writing api-paste.ini, it will find the template in the grizzly
175
If the object were created with folsom, it would fall back to the
176
base templates dir for its api-paste.ini template.
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
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.
187
**Context generators**
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.
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,
200
raise OSConfigException
202
self.templates_dir = templates_dir
203
self.openstack_release = openstack_release
205
self._tmpl_env = None
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')
213
def register(self, config_file, contexts):
215
Register a config file with a list of context generators to be called
218
self.templates[config_file] = OSConfigTemplate(config_file=config_file,
220
log('Registered config file: %s' % config_file, level=INFO)
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)
227
def _get_template(self, template):
229
template = self._tmpl_env.get_template(template)
230
log('Loaded template from %s' % template.filename, level=INFO)
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()
239
_tmpl = os.path.basename(config_file)
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:])
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),
255
log('Rendering from template: %s' % _tmpl, level=INFO)
256
return template.render(ctxt)
258
def write(self, config_file):
260
Write a single config file, raises if config file is not registered.
262
if config_file not in self.templates:
263
log('Config not registered: %s' % config_file, level=ERROR)
264
raise OSConfigException
266
_out = self.render(config_file)
268
with open(config_file, 'wb') as out:
271
log('Wrote template %s.' % config_file, level=INFO)
275
Write out all registered config files.
277
[self.write(k) for k in six.iterkeys(self.templates)]
279
def set_release(self, openstack_release):
281
Resets the template environment and generates a new template loader
282
based on a the new openstack release.
284
self._tmpl_env = None
285
self.openstack_release = openstack_release
288
def complete_contexts(self):
290
Returns a list of context interfaces that yield a complete context.
293
[interfaces.extend(i.complete_contexts())
294
for i in six.itervalues(self.templates)]