Add context.py and templating.py

This commit is contained in:
Andres Rodriguez 2013-06-20 12:56:58 -04:00
parent 6acd805142
commit b962c77d80
2 changed files with 306 additions and 0 deletions

View File

@ -0,0 +1,126 @@
from charmhelpers.core.hookenv import (
config,
log,
relation_get,
relation_ids,
related_units,
)
class OSContextError(Exception):
pass
def context_complete(ctxt):
_missing = []
for k, v in ctxt.iteritems():
if v is None or v == '':
_missing.append(k)
if _missing:
print 'Missing required data: %s' % ' '.join(_missing)
return False
return True
def shared_db(relation_id=None, unit_id=None):
log('Generating template context for shared-db')
conf = config()
try:
database = conf['database']
username = conf['database-user']
except KeyError as e:
log('Could not generate shared_db context. '
'Missing required charm config options: %s.' % e)
raise OSContextError
ctxt = {}
for rid in relation_ids('shared-db'):
for unit in related_units(rid):
ctxt = {
'database_host': relation_get('db_host', rid=rid, unit=unit),
'database': database,
'database_user': username,
'database_password': relation_get('password', rid=rid,
unit=unit)
}
if not context_complete(ctxt):
return {}
return ctxt
def identity_service(relation_id=None):
log('Generating template context for identity-service')
ctxt = {}
for rid in relation_ids('identity-service'):
for unit in related_units(rid):
ctxt = {
'service_port': relation_get('service_port', rid=rid,
unit=unit),
'service_host': relation_get('service_host', rid=rid,
unit=unit),
'auth_host': relation_get('auth_host', rid=rid, unit=unit),
'auth_port': relation_get('auth_port', rid=rid, unit=unit),
'admin_tenant_name': relation_get('service_tenant', rid=rid,
unit=unit),
'admin_user': relation_get('service_username', rid=rid,
unit=unit),
'admin_password': relation_get('service_password', rid=rid,
unit=unit),
# XXX: Hard-coded http.
'service_protocol': 'http',
'auth_protocol': 'http',
}
if not context_complete(ctxt):
return {}
return ctxt
def amqp(relation_id=None):
log('Generating template context for amqp')
conf = config()
try:
username = conf['rabbit-user']
vhost = conf['rabbit-vhost']
except KeyError as e:
log('Could not generate shared_db context. '
'Missing required charm config options: %s.' % e)
raise OSContextError
ctxt = {}
for rid in relation_ids('amqp'):
for unit in related_units(rid):
if relation_get('clustered', rid=rid, unit=unit):
rabbitmq_host = relation_get('vip', rid=rid, unit=unit)
else:
rabbitmq_host = relation_get('private-address',
rid=rid, unit=unit)
ctxt = {
'rabbitmq_host': rabbitmq_host,
'rabbitmq_user': username,
'rabbitmq_password': relation_get('password', rid=rid,
unit=unit),
'rabbitmq_virtual_host': vhost,
}
if not context_complete(ctxt):
return {}
return ctxt
def ceph():
'''This generates context for /etc/ceph/ceph.conf templates'''
log('Generating tmeplate context for ceph')
mon_hosts = []
auth = None
for rid in relation_ids('ceph'):
for unit in related_units(rid):
mon_hosts.append(relation_get('private-address', rid=rid,
unit=unit))
auth = relation_get('auth', rid=rid, unit=unit)
ctxt = {
'mon_hosts': ' '.join(mon_hosts),
'auth': auth,
}
if not context_complete(ctxt):
return {}
return ctxt

View File

@ -0,0 +1,180 @@
import logging
import os
try:
import jinja2
except ImportError:
pass
logging.basicConfig(level=logging.INFO)
"""
WIP Abstract templating system for the OpenStack charms.
The idea is that an openstack charm can register a number of config files
associated with common context generators. The context generators are
responsible for inspecting charm config/relation data/deployment state
and presenting correct context to the template. Generic context generators
could live somewhere in charmhelpers.contrib.openstack, and each
charm can implement their own specific ones as well.
Ideally a charm would register all its config files somewhere in its namespace,
eg cinder_utils.py:
from charmhelpers.contrib.openstack import templating, context
config_files = {
'/etc/cinder/cinder.conf': [context.shared_db,
context.amqp,
context.ceph],
'/etc/cinder/api-paste.ini': [context.identity_service]
}
configs = templating.OSConfigRenderer(template_dir='templates/')
[configs.register(k, v) for k, v in config_files.iteritems()]
Hooks can then render config files as need, eg:
def config_changed():
configs.render_all()
def db_changed():
configs.render('/etc/cinder/cinder.conf')
check_call(['cinder-manage', 'db', 'sync'])
This would look very similar for nova/glance/etc.
The OSTemplteLoader is responsible for creating a jinja2.ChoiceLoader that should
help reduce fragmentation of a charms' templates across OpenStack releases, so we
do not need to maintain many copies of templates or juggle symlinks. The constructed
loader lets the template be loaded from the most recent OS release-specific template
dir or a base template dir.
For example, say cinder has no changes in config structure across any OS releases,
all OS releases share the same templates from the base directory:
templates/api-paste.ini
templates/cinder.conf
Then, since Grizzly and beyond, cinder.conf's format has changed:
templates/api-paste.ini
templates/cinder.conf
templates/grizzly/cinder.conf
Grizzly and beyond will load from templates/grizzly, but any release prior will
load from templates/. If some change in Icehouse breaks config format again:
templates/api-paste.ini
templates/cinder.conf
templates/grizzly/cinder.conf
templates/icehouse/cinder.conf
Icehouse and beyond will load from icehouse/, Grizzly + Havan from grizzly/, previous
releases from the base templates/
"""
class OSConfigException(Exception):
pass
def get_loader(templates_dir, os_release):
"""
Create a jinja2.ChoiceLoader containing template dirs up to
and including os_release. If directory template directory
is missing at templates_dir, it will be omitted from the loader.
templates_dir is added to the bottom of the search list as a base
loading dir.
:param templates_dir: str: Base template directory containing release
sub-directories.
:param os_release : str: OpenStack release codename to construct template
loader.
:returns : jinja2.ChoiceLoader constructed with a list of
jinja2.FilesystemLoaders, ordered in descending
order by OpenStack release.
"""
tmpl_dirs = (
('essex', os.path.join(templates_dir, 'essex')),
('folsom', os.path.join(templates_dir, 'folsom')),
('grizzly', os.path.join(templates_dir, 'grizzly')),
('havana', os.path.join(templates_dir, 'havana')),
('icehouse', os.path.join(templates_dir, 'icehouse')),
)
if not os.path.isdir(templates_dir):
logging.error('Templates directory not found @ %s.' % templates_dir)
raise OSConfigException
loaders = [jinja2.FileSystemLoader(templates_dir)]
for rel, tmpl_dir in tmpl_dirs:
if os.path.isdir(tmpl_dir):
loaders.insert(0, jinja2.FileSystemLoader(tmpl_dir))
if rel == os_release:
break
logging.info('Creating choice loader with dirs: %s' %\
[l.searchpath for l in loaders])
return jinja2.ChoiceLoader(loaders)
class OSConfigTemplate(object):
def __init__(self, config_file, contexts):
self.config_file = config_file
if hasattr(contexts, '__call__'):
self.contexts = [contexts]
else:
self.contexts = contexts
def context(self):
ctxt = {}
for context in self.contexts:
_ctxt = context()
if _ctxt:
ctxt.update(_ctxt)
return ctxt
class OSConfigRenderer(object):
def __init__(self, templates_dir, openstack_release):
if not os.path.isdir(templates_dir):
logging.error('Could not locate templates dir %s' % templates_dir)
raise OSConfigException
self.templates_dir = templates_dir
self.openstack_release = openstack_release
self.templates = {}
self._tmpl_env = None
def register(self, config_file, contexts):
self.templates[config_file] = OSConfigTemplate(config_file=config_file,
contexts=contexts)
logging.info('Registered config file: %s' % config_file)
def _get_tmpl_env(self):
if not self._tmpl_env:
loader = get_loader(self.templates_dir, self.openstack_release)
self._tmpl_env = jinja2.Environment(loader=loader)
def render(self, config_file):
if config_file not in self.templates:
logging.error('Config not registered: %s' % config_file)
raise OSConfigException
ctxt = self.templates[config_file].context()
_tmpl = os.path.basename(config_file)
logging.info('Rendering from template: %s' % _tmpl)
self._get_tmpl_env()
_tmpl = self._tmpl_env.get_template(_tmpl)
logging.info('Loaded template from %s' % _tmpl.filename)
return _tmpl.render(ctxt)
def write(self, config_file):
if config_file not in self.templates:
logging.error('Config not registered: %s' % config_file)
raise OSConfigException
with open(config_file, 'wb') as out:
out.write(self.render(config_file))
logging.info('Wrote template %s.' % config_file)
def write_all(self):
[self.write(k) for k in self.templates.iterkeys()]