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, apt_update
22
from charmhelpers.core.hookenv import (
27
from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES
30
from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
32
apt_update(fatal=True)
33
apt_install('python-jinja2', fatal=True)
34
from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
37
class OSConfigException(Exception):
41
def get_loader(templates_dir, os_release):
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
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::
52
hooks/charmhelpers/contrib/openstack/templates
54
:param templates_dir (str): Base template directory containing release
56
:param os_release (str): OpenStack release codename to construct template
58
:returns: jinja2.ChoiceLoader constructed with a list of
59
jinja2.FilesystemLoaders, ordered in descending
60
order by OpenStack release.
62
tmpl_dirs = [(rel, os.path.join(templates_dir, rel))
63
for rel in six.itervalues(OPENSTACK_CODENAMES)]
65
if not os.path.isdir(templates_dir):
66
log('Templates directory not found @ %s.' % templates_dir,
68
raise OSConfigException
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))
77
for rel, tmpl_dir in tmpl_dirs:
78
if os.path.isdir(tmpl_dir):
79
loaders.insert(0, FileSystemLoader(tmpl_dir))
82
log('Creating choice loader with dirs: %s' %
83
[l.searchpath for l in loaders], level=INFO)
84
return ChoiceLoader(loaders)
87
class OSConfigTemplate(object):
89
Associates a config file template with a list of context generators.
90
Responsible for constructing a template context based on those generators.
92
def __init__(self, config_file, contexts):
93
self.config_file = config_file
95
if hasattr(contexts, '__call__'):
96
self.contexts = [contexts]
98
self.contexts = contexts
100
self._complete_contexts = []
104
for context in self.contexts:
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]
114
def complete_contexts(self):
116
Return a list of interfaces that have satisfied contexts.
118
if self._complete_contexts:
119
return self._complete_contexts
121
return self._complete_contexts
124
class OSConfigRenderer(object):
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
133
# import some common context generates from charmhelpers
134
from charmhelpers.contrib.openstack import context
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
152
**OpenStack Releases and template loading**
154
When the object is instantiated, it is associated with a specific OS
155
release. This dictates how the template loader will be constructed.
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.
163
For the example above, '/tmp/templates' contains the following structure::
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
170
Since it was registered with the grizzly release, it first seraches
171
the grizzly directory for nova.conf, then the templates dir.
173
When writing api-paste.ini, it will find the template in the grizzly
176
If the object were created with folsom, it would fall back to the
177
base templates dir for its api-paste.ini template.
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
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.
188
**Context generators**
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.
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,
201
raise OSConfigException
203
self.templates_dir = templates_dir
204
self.openstack_release = openstack_release
206
self._tmpl_env = None
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')
214
def register(self, config_file, contexts):
216
Register a config file with a list of context generators to be called
219
self.templates[config_file] = OSConfigTemplate(config_file=config_file,
221
log('Registered config file: %s' % config_file, level=INFO)
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)
228
def _get_template(self, template):
230
template = self._tmpl_env.get_template(template)
231
log('Loaded template from %s' % template.filename, level=INFO)
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()
240
_tmpl = os.path.basename(config_file)
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:])
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),
256
log('Rendering from template: %s' % _tmpl, level=INFO)
257
return template.render(ctxt)
259
def write(self, config_file):
261
Write a single config file, raises if config file is not registered.
263
if config_file not in self.templates:
264
log('Config not registered: %s' % config_file, level=ERROR)
265
raise OSConfigException
267
_out = self.render(config_file)
269
with open(config_file, 'wb') as out:
272
log('Wrote template %s.' % config_file, level=INFO)
276
Write out all registered config files.
278
[self.write(k) for k in six.iterkeys(self.templates)]
280
def set_release(self, openstack_release):
282
Resets the template environment and generates a new template loader
283
based on a the new openstack release.
285
self._tmpl_env = None
286
self.openstack_release = openstack_release
289
def complete_contexts(self):
291
Returns a list of context interfaces that yield a complete context.
294
[interfaces.extend(i.complete_contexts())
295
for i in six.itervalues(self.templates)]
298
def get_incomplete_context_data(self, interfaces):
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}}
305
incomplete_context_data = {}
307
for i in six.itervalues(self.templates):
308
for context in i.contexts:
309
for interface in interfaces:
311
if interface in context.interfaces:
312
related = context.get_related()
313
missing_data = context.missing_data
315
incomplete_context_data[interface] = {'missing_data': missing_data}
317
if incomplete_context_data.get(interface):
318
incomplete_context_data[interface].update({'related': True})
320
incomplete_context_data[interface] = {'related': True}
322
incomplete_context_data[interface] = {'related': False}
323
return incomplete_context_data