5
from charmhelpers.fetch import apt_install
6
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::
35
hooks/charmhelpers/contrib/openstack/templates
37
:param templates_dir (str): Base template directory containing release
39
: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 six.itervalues(OPENSTACK_CODENAMES)]
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
116
# import some common context generates from charmhelpers
117
from charmhelpers.contrib.openstack import context
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
135
**OpenStack Releases and template loading**
137
When the object is instantiated, it is associated with a specific OS
138
release. This dictates how the template loader will be constructed.
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.
146
For the example above, '/tmp/templates' contains the following structure::
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
153
Since it was registered with the grizzly release, it first seraches
154
the grizzly directory for nova.conf, then the templates dir.
156
When writing api-paste.ini, it will find the template in the grizzly
159
If the object were created with folsom, it would fall back to the
160
base templates dir for its api-paste.ini template.
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
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.
171
**Context generators**
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.
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,
184
raise OSConfigException
186
self.templates_dir = templates_dir
187
self.openstack_release = openstack_release
189
self._tmpl_env = None
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')
197
def register(self, config_file, contexts):
199
Register a config file with a list of context generators to be called
202
self.templates[config_file] = OSConfigTemplate(config_file=config_file,
204
log('Registered config file: %s' % config_file, level=INFO)
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)
211
def _get_template(self, template):
213
template = self._tmpl_env.get_template(template)
214
log('Loaded template from %s' % template.filename, level=INFO)
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()
223
_tmpl = os.path.basename(config_file)
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:])
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),
239
log('Rendering from template: %s' % _tmpl, level=INFO)
240
return template.render(ctxt)
242
def write(self, config_file):
244
Write a single config file, raises if config file is not registered.
246
if config_file not in self.templates:
247
log('Config not registered: %s' % config_file, level=ERROR)
248
raise OSConfigException
250
_out = self.render(config_file)
252
with open(config_file, 'wb') as out:
255
log('Wrote template %s.' % config_file, level=INFO)
259
Write out all registered config files.
261
[self.write(k) for k in six.iterkeys(self.templates)]
263
def set_release(self, openstack_release):
265
Resets the template environment and generates a new template loader
266
based on a the new openstack release.
268
self._tmpl_env = None
269
self.openstack_release = openstack_release
272
def complete_contexts(self):
274
Returns a list of context interfaces that yield a complete context.
277
[interfaces.extend(i.complete_contexts())
278
for i in six.itervalues(self.templates)]