3
from charmhelpers.fetch import apt_install
5
from charmhelpers.core.hookenv import (
11
from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES
14
from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
16
# python-jinja2 may not be installed yet, or we're running unittests.
17
FileSystemLoader = ChoiceLoader = Environment = exceptions = None
20
class OSConfigException(Exception):
24
def get_loader(templates_dir, os_release):
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
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.
36
:param templates_dir: str: Base template directory containing release
38
:param os_release : str: OpenStack release codename to construct template
41
:returns : jinja2.ChoiceLoader constructed with a list of
42
jinja2.FilesystemLoaders, ordered in descending
43
order by OpenStack release.
45
tmpl_dirs = [(rel, os.path.join(templates_dir, rel))
46
for rel in OPENSTACK_CODENAMES.itervalues()]
48
if not os.path.isdir(templates_dir):
49
log('Templates directory not found @ %s.' % templates_dir,
51
raise OSConfigException
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))
60
for rel, tmpl_dir in tmpl_dirs:
61
if os.path.isdir(tmpl_dir):
62
loaders.insert(0, FileSystemLoader(tmpl_dir))
65
log('Creating choice loader with dirs: %s' %
66
[l.searchpath for l in loaders], level=INFO)
67
return ChoiceLoader(loaders)
70
class OSConfigTemplate(object):
72
Associates a config file template with a list of context generators.
73
Responsible for constructing a template context based on those generators.
75
def __init__(self, config_file, contexts):
76
self.config_file = config_file
78
if hasattr(contexts, '__call__'):
79
self.contexts = [contexts]
81
self.contexts = contexts
83
self._complete_contexts = []
87
for context in self.contexts:
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]
97
def complete_contexts(self):
99
Return a list of interfaces that have atisfied contexts.
101
if self._complete_contexts:
102
return self._complete_contexts
104
return self._complete_contexts
107
class OSConfigRenderer(object):
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
115
# import some common context generates from charmhelpers
116
from charmhelpers.contrib.openstack import context
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
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.
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.
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
154
Since it was registered with the grizzly release, it first seraches
155
the grizzly directory for nova.conf, then the templates dir.
157
When writing api-paste.ini, it will find the template in the grizzly
160
If the object were created with folsom, it would fall back to the
161
base templates dir for its api-paste.ini template.
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
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.
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.
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,
185
raise OSConfigException
187
self.templates_dir = templates_dir
188
self.openstack_release = openstack_release
190
self._tmpl_env = None
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')
198
def register(self, config_file, contexts):
200
Register a config file with a list of context generators to be called
203
self.templates[config_file] = OSConfigTemplate(config_file=config_file,
205
log('Registered config file: %s' % config_file, level=INFO)
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)
212
def _get_template(self, template):
214
template = self._tmpl_env.get_template(template)
215
log('Loaded template from %s' % template.filename, level=INFO)
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()
224
_tmpl = os.path.basename(config_file)
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:])
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),
240
log('Rendering from template: %s' % _tmpl, level=INFO)
241
return template.render(ctxt)
243
def write(self, config_file):
245
Write a single config file, raises if config file is not registered.
247
if config_file not in self.templates:
248
log('Config not registered: %s' % config_file, level=ERROR)
249
raise OSConfigException
251
_out = self.render(config_file)
253
with open(config_file, 'wb') as out:
256
log('Wrote template %s.' % config_file, level=INFO)
260
Write out all registered config files.
262
[self.write(k) for k in self.templates.iterkeys()]
264
def set_release(self, openstack_release):
266
Resets the template environment and generates a new template loader
267
based on a the new openstack release.
269
self._tmpl_env = None
270
self.openstack_release = openstack_release
273
def complete_contexts(self):
275
Returns a list of context interfaces that yield a complete context.
278
[interfaces.extend(i.complete_contexts())
279
for i in self.templates.itervalues()]