696 lines
22 KiB
Python
696 lines
22 KiB
Python
import json
|
|
import os
|
|
import time
|
|
|
|
from base64 import b64decode
|
|
|
|
from subprocess import (
|
|
check_call
|
|
)
|
|
|
|
|
|
from charmhelpers.fetch import (
|
|
apt_install,
|
|
filter_installed_packages,
|
|
)
|
|
|
|
from charmhelpers.core.hookenv import (
|
|
config,
|
|
local_unit,
|
|
log,
|
|
relation_get,
|
|
relation_ids,
|
|
related_units,
|
|
unit_get,
|
|
unit_private_ip,
|
|
ERROR,
|
|
)
|
|
|
|
from charmhelpers.contrib.hahelpers.cluster import (
|
|
determine_apache_port,
|
|
determine_api_port,
|
|
https,
|
|
is_clustered
|
|
)
|
|
|
|
from charmhelpers.contrib.hahelpers.apache import (
|
|
get_cert,
|
|
get_ca_cert,
|
|
)
|
|
|
|
from charmhelpers.contrib.openstack.neutron import (
|
|
neutron_plugin_attribute,
|
|
)
|
|
|
|
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
|
|
|
|
|
|
class OSContextError(Exception):
|
|
pass
|
|
|
|
|
|
def ensure_packages(packages):
|
|
'''Install but do not upgrade required plugin packages'''
|
|
required = filter_installed_packages(packages)
|
|
if required:
|
|
apt_install(required, fatal=True)
|
|
|
|
|
|
def context_complete(ctxt):
|
|
_missing = []
|
|
for k, v in ctxt.iteritems():
|
|
if v is None or v == '':
|
|
_missing.append(k)
|
|
if _missing:
|
|
log('Missing required data: %s' % ' '.join(_missing), level='INFO')
|
|
return False
|
|
return True
|
|
|
|
|
|
def config_flags_parser(config_flags):
|
|
if config_flags.find('==') >= 0:
|
|
log("config_flags is not in expected format (key=value)",
|
|
level=ERROR)
|
|
raise OSContextError
|
|
# strip the following from each value.
|
|
post_strippers = ' ,'
|
|
# we strip any leading/trailing '=' or ' ' from the string then
|
|
# split on '='.
|
|
split = config_flags.strip(' =').split('=')
|
|
limit = len(split)
|
|
flags = {}
|
|
for i in xrange(0, limit - 1):
|
|
current = split[i]
|
|
next = split[i + 1]
|
|
vindex = next.rfind(',')
|
|
if (i == limit - 2) or (vindex < 0):
|
|
value = next
|
|
else:
|
|
value = next[:vindex]
|
|
|
|
if i == 0:
|
|
key = current
|
|
else:
|
|
# if this not the first entry, expect an embedded key.
|
|
index = current.rfind(',')
|
|
if index < 0:
|
|
log("invalid config value(s) at index %s" % (i),
|
|
level=ERROR)
|
|
raise OSContextError
|
|
key = current[index + 1:]
|
|
|
|
# Add to collection.
|
|
flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
|
|
return flags
|
|
|
|
|
|
class OSContextGenerator(object):
|
|
interfaces = []
|
|
|
|
def __call__(self):
|
|
raise NotImplementedError
|
|
|
|
|
|
class SharedDBContext(OSContextGenerator):
|
|
interfaces = ['shared-db']
|
|
|
|
def __init__(self,
|
|
database=None, user=None, relation_prefix=None, ssl_dir=None):
|
|
'''
|
|
Allows inspecting relation for settings prefixed with relation_prefix.
|
|
This is useful for parsing access for multiple databases returned via
|
|
the shared-db interface (eg, nova_password, quantum_password)
|
|
'''
|
|
self.relation_prefix = relation_prefix
|
|
self.database = database
|
|
self.user = user
|
|
self.ssl_dir = ssl_dir
|
|
|
|
def __call__(self):
|
|
self.database = self.database or config('database')
|
|
self.user = self.user or config('database-user')
|
|
if None in [self.database, self.user]:
|
|
log('Could not generate shared_db context. '
|
|
'Missing required charm config options. '
|
|
'(database name and user)')
|
|
raise OSContextError
|
|
ctxt = {}
|
|
|
|
password_setting = 'password'
|
|
if self.relation_prefix:
|
|
password_setting = self.relation_prefix + '_password'
|
|
|
|
for rid in relation_ids('shared-db'):
|
|
for unit in related_units(rid):
|
|
rdata = relation_get(rid=rid, unit=unit)
|
|
ctxt = {
|
|
'database_host': rdata.get('db_host'),
|
|
'database': self.database,
|
|
'database_user': self.user,
|
|
'database_password': rdata.get(password_setting)
|
|
}
|
|
if context_complete(ctxt):
|
|
db_ssl(rdata, ctxt, self.ssl_dir)
|
|
return ctxt
|
|
return {}
|
|
|
|
|
|
class PostgresqlDBContext(OSContextGenerator):
|
|
interfaces = ['pgsql-db']
|
|
|
|
def __init__(self, database=None):
|
|
self.database = database
|
|
|
|
def __call__(self):
|
|
self.database = self.database or config('database')
|
|
if self.database is None:
|
|
log('Could not generate postgresql_db context. '
|
|
'Missing required charm config options. '
|
|
'(database name)')
|
|
raise OSContextError
|
|
ctxt = {}
|
|
|
|
for rid in relation_ids(self.interfaces[0]):
|
|
for unit in related_units(rid):
|
|
ctxt = {
|
|
'database_host': relation_get('host', rid=rid, unit=unit),
|
|
'database': self.database,
|
|
'database_user': relation_get('user', rid=rid, unit=unit),
|
|
'database_password': relation_get('password', rid=rid, unit=unit),
|
|
'database_type': 'postgresql',
|
|
}
|
|
if context_complete(ctxt):
|
|
return ctxt
|
|
return {}
|
|
|
|
|
|
def db_ssl(rdata, ctxt, ssl_dir):
|
|
if 'ssl_ca' in rdata and ssl_dir:
|
|
ca_path = os.path.join(ssl_dir, 'db-client.ca')
|
|
with open(ca_path, 'w') as fh:
|
|
fh.write(b64decode(rdata['ssl_ca']))
|
|
ctxt['database_ssl_ca'] = ca_path
|
|
elif 'ssl_ca' in rdata:
|
|
log("Charm not setup for ssl support but ssl ca found")
|
|
return ctxt
|
|
if 'ssl_cert' in rdata:
|
|
cert_path = os.path.join(
|
|
ssl_dir, 'db-client.cert')
|
|
if not os.path.exists(cert_path):
|
|
log("Waiting 1m for ssl client cert validity")
|
|
time.sleep(60)
|
|
with open(cert_path, 'w') as fh:
|
|
fh.write(b64decode(rdata['ssl_cert']))
|
|
ctxt['database_ssl_cert'] = cert_path
|
|
key_path = os.path.join(ssl_dir, 'db-client.key')
|
|
with open(key_path, 'w') as fh:
|
|
fh.write(b64decode(rdata['ssl_key']))
|
|
ctxt['database_ssl_key'] = key_path
|
|
return ctxt
|
|
|
|
|
|
class IdentityServiceContext(OSContextGenerator):
|
|
interfaces = ['identity-service']
|
|
|
|
def __call__(self):
|
|
log('Generating template context for identity-service')
|
|
ctxt = {}
|
|
|
|
for rid in relation_ids('identity-service'):
|
|
for unit in related_units(rid):
|
|
rdata = relation_get(rid=rid, unit=unit)
|
|
ctxt = {
|
|
'service_port': rdata.get('service_port'),
|
|
'service_host': rdata.get('service_host'),
|
|
'auth_host': rdata.get('auth_host'),
|
|
'auth_port': rdata.get('auth_port'),
|
|
'admin_tenant_name': rdata.get('service_tenant'),
|
|
'admin_user': rdata.get('service_username'),
|
|
'admin_password': rdata.get('service_password'),
|
|
'service_protocol':
|
|
rdata.get('service_protocol') or 'http',
|
|
'auth_protocol':
|
|
rdata.get('auth_protocol') or 'http',
|
|
}
|
|
if context_complete(ctxt):
|
|
return ctxt
|
|
return {}
|
|
|
|
|
|
class AMQPContext(OSContextGenerator):
|
|
interfaces = ['amqp']
|
|
|
|
def __init__(self, ssl_dir=None):
|
|
self.ssl_dir = ssl_dir
|
|
|
|
def __call__(self):
|
|
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'):
|
|
ha_vip_only = False
|
|
for unit in related_units(rid):
|
|
if relation_get('clustered', rid=rid, unit=unit):
|
|
ctxt['clustered'] = True
|
|
ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
|
|
unit=unit)
|
|
else:
|
|
ctxt['rabbitmq_host'] = relation_get('private-address',
|
|
rid=rid, unit=unit)
|
|
ctxt.update({
|
|
'rabbitmq_user': username,
|
|
'rabbitmq_password': relation_get('password', rid=rid,
|
|
unit=unit),
|
|
'rabbitmq_virtual_host': vhost,
|
|
})
|
|
|
|
ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
|
|
if ssl_port:
|
|
ctxt['rabbit_ssl_port'] = ssl_port
|
|
ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
|
|
if ssl_ca:
|
|
ctxt['rabbit_ssl_ca'] = ssl_ca
|
|
|
|
if relation_get('ha_queues', rid=rid, unit=unit) is not None:
|
|
ctxt['rabbitmq_ha_queues'] = True
|
|
|
|
ha_vip_only = relation_get('ha-vip-only',
|
|
rid=rid, unit=unit) is not None
|
|
|
|
if context_complete(ctxt):
|
|
if 'rabbit_ssl_ca' in ctxt:
|
|
if not self.ssl_dir:
|
|
log(("Charm not setup for ssl support "
|
|
"but ssl ca found"))
|
|
break
|
|
ca_path = os.path.join(
|
|
self.ssl_dir, 'rabbit-client-ca.pem')
|
|
with open(ca_path, 'w') as fh:
|
|
fh.write(b64decode(ctxt['rabbit_ssl_ca']))
|
|
ctxt['rabbit_ssl_ca'] = ca_path
|
|
# Sufficient information found = break out!
|
|
break
|
|
# Used for active/active rabbitmq >= grizzly
|
|
if ('clustered' not in ctxt or ha_vip_only) \
|
|
and len(related_units(rid)) > 1:
|
|
rabbitmq_hosts = []
|
|
for unit in related_units(rid):
|
|
rabbitmq_hosts.append(relation_get('private-address',
|
|
rid=rid, unit=unit))
|
|
ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
|
|
if not context_complete(ctxt):
|
|
return {}
|
|
else:
|
|
return ctxt
|
|
|
|
|
|
class CephContext(OSContextGenerator):
|
|
interfaces = ['ceph']
|
|
|
|
def __call__(self):
|
|
'''This generates context for /etc/ceph/ceph.conf templates'''
|
|
if not relation_ids('ceph'):
|
|
return {}
|
|
|
|
log('Generating template context for ceph')
|
|
|
|
mon_hosts = []
|
|
auth = None
|
|
key = None
|
|
use_syslog = str(config('use-syslog')).lower()
|
|
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)
|
|
key = relation_get('key', rid=rid, unit=unit)
|
|
|
|
ctxt = {
|
|
'mon_hosts': ' '.join(mon_hosts),
|
|
'auth': auth,
|
|
'key': key,
|
|
'use_syslog': use_syslog
|
|
}
|
|
|
|
if not os.path.isdir('/etc/ceph'):
|
|
os.mkdir('/etc/ceph')
|
|
|
|
if not context_complete(ctxt):
|
|
return {}
|
|
|
|
ensure_packages(['ceph-common'])
|
|
|
|
return ctxt
|
|
|
|
|
|
class HAProxyContext(OSContextGenerator):
|
|
interfaces = ['cluster']
|
|
|
|
def __call__(self):
|
|
'''
|
|
Builds half a context for the haproxy template, which describes
|
|
all peers to be included in the cluster. Each charm needs to include
|
|
its own context generator that describes the port mapping.
|
|
'''
|
|
if not relation_ids('cluster'):
|
|
return {}
|
|
|
|
cluster_hosts = {}
|
|
l_unit = local_unit().replace('/', '-')
|
|
cluster_hosts[l_unit] = unit_get('private-address')
|
|
|
|
for rid in relation_ids('cluster'):
|
|
for unit in related_units(rid):
|
|
_unit = unit.replace('/', '-')
|
|
addr = relation_get('private-address', rid=rid, unit=unit)
|
|
cluster_hosts[_unit] = addr
|
|
|
|
ctxt = {
|
|
'units': cluster_hosts,
|
|
}
|
|
if len(cluster_hosts.keys()) > 1:
|
|
# Enable haproxy when we have enough peers.
|
|
log('Ensuring haproxy enabled in /etc/default/haproxy.')
|
|
with open('/etc/default/haproxy', 'w') as out:
|
|
out.write('ENABLED=1\n')
|
|
return ctxt
|
|
log('HAProxy context is incomplete, this unit has no peers.')
|
|
return {}
|
|
|
|
|
|
class ImageServiceContext(OSContextGenerator):
|
|
interfaces = ['image-service']
|
|
|
|
def __call__(self):
|
|
'''
|
|
Obtains the glance API server from the image-service relation. Useful
|
|
in nova and cinder (currently).
|
|
'''
|
|
log('Generating template context for image-service.')
|
|
rids = relation_ids('image-service')
|
|
if not rids:
|
|
return {}
|
|
for rid in rids:
|
|
for unit in related_units(rid):
|
|
api_server = relation_get('glance-api-server',
|
|
rid=rid, unit=unit)
|
|
if api_server:
|
|
return {'glance_api_servers': api_server}
|
|
log('ImageService context is incomplete. '
|
|
'Missing required relation data.')
|
|
return {}
|
|
|
|
|
|
class ApacheSSLContext(OSContextGenerator):
|
|
|
|
"""
|
|
Generates a context for an apache vhost configuration that configures
|
|
HTTPS reverse proxying for one or many endpoints. Generated context
|
|
looks something like:
|
|
{
|
|
'namespace': 'cinder',
|
|
'private_address': 'iscsi.mycinderhost.com',
|
|
'endpoints': [(8776, 8766), (8777, 8767)]
|
|
}
|
|
|
|
The endpoints list consists of a tuples mapping external ports
|
|
to internal ports.
|
|
"""
|
|
interfaces = ['https']
|
|
|
|
# charms should inherit this context and set external ports
|
|
# and service namespace accordingly.
|
|
external_ports = []
|
|
service_namespace = None
|
|
|
|
def enable_modules(self):
|
|
cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
|
|
check_call(cmd)
|
|
|
|
def configure_cert(self):
|
|
if not os.path.isdir('/etc/apache2/ssl'):
|
|
os.mkdir('/etc/apache2/ssl')
|
|
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
|
|
if not os.path.isdir(ssl_dir):
|
|
os.mkdir(ssl_dir)
|
|
cert, key = get_cert()
|
|
with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
|
|
cert_out.write(b64decode(cert))
|
|
with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
|
|
key_out.write(b64decode(key))
|
|
ca_cert = get_ca_cert()
|
|
if ca_cert:
|
|
with open(CA_CERT_PATH, 'w') as ca_out:
|
|
ca_out.write(b64decode(ca_cert))
|
|
check_call(['update-ca-certificates'])
|
|
|
|
def __call__(self):
|
|
if isinstance(self.external_ports, basestring):
|
|
self.external_ports = [self.external_ports]
|
|
if (not self.external_ports or not https()):
|
|
return {}
|
|
|
|
self.configure_cert()
|
|
self.enable_modules()
|
|
|
|
ctxt = {
|
|
'namespace': self.service_namespace,
|
|
'private_address': unit_get('private-address'),
|
|
'endpoints': []
|
|
}
|
|
if is_clustered():
|
|
ctxt['private_address'] = config('vip')
|
|
for api_port in self.external_ports:
|
|
ext_port = determine_apache_port(api_port)
|
|
int_port = determine_api_port(api_port)
|
|
portmap = (int(ext_port), int(int_port))
|
|
ctxt['endpoints'].append(portmap)
|
|
return ctxt
|
|
|
|
|
|
class NeutronContext(OSContextGenerator):
|
|
interfaces = []
|
|
|
|
@property
|
|
def plugin(self):
|
|
return None
|
|
|
|
@property
|
|
def network_manager(self):
|
|
return None
|
|
|
|
@property
|
|
def packages(self):
|
|
return neutron_plugin_attribute(
|
|
self.plugin, 'packages', self.network_manager)
|
|
|
|
@property
|
|
def neutron_security_groups(self):
|
|
return None
|
|
|
|
def _ensure_packages(self):
|
|
[ensure_packages(pkgs) for pkgs in self.packages]
|
|
|
|
def _save_flag_file(self):
|
|
if self.network_manager == 'quantum':
|
|
_file = '/etc/nova/quantum_plugin.conf'
|
|
else:
|
|
_file = '/etc/nova/neutron_plugin.conf'
|
|
with open(_file, 'wb') as out:
|
|
out.write(self.plugin + '\n')
|
|
|
|
def ovs_ctxt(self):
|
|
driver = neutron_plugin_attribute(self.plugin, 'driver',
|
|
self.network_manager)
|
|
config = neutron_plugin_attribute(self.plugin, 'config',
|
|
self.network_manager)
|
|
ovs_ctxt = {
|
|
'core_plugin': driver,
|
|
'neutron_plugin': 'ovs',
|
|
'neutron_security_groups': self.neutron_security_groups,
|
|
'local_ip': unit_private_ip(),
|
|
'config': config
|
|
}
|
|
|
|
return ovs_ctxt
|
|
|
|
def nvp_ctxt(self):
|
|
driver = neutron_plugin_attribute(self.plugin, 'driver',
|
|
self.network_manager)
|
|
config = neutron_plugin_attribute(self.plugin, 'config',
|
|
self.network_manager)
|
|
nvp_ctxt = {
|
|
'core_plugin': driver,
|
|
'neutron_plugin': 'nvp',
|
|
'neutron_security_groups': self.neutron_security_groups,
|
|
'local_ip': unit_private_ip(),
|
|
'config': config
|
|
}
|
|
|
|
return nvp_ctxt
|
|
|
|
def neutron_ctxt(self):
|
|
if https():
|
|
proto = 'https'
|
|
else:
|
|
proto = 'http'
|
|
if is_clustered():
|
|
host = config('vip')
|
|
else:
|
|
host = unit_get('private-address')
|
|
url = '%s://%s:%s' % (proto, host, '9696')
|
|
ctxt = {
|
|
'network_manager': self.network_manager,
|
|
'neutron_url': url,
|
|
}
|
|
return ctxt
|
|
|
|
def __call__(self):
|
|
self._ensure_packages()
|
|
|
|
if self.network_manager not in ['quantum', 'neutron']:
|
|
return {}
|
|
|
|
if not self.plugin:
|
|
return {}
|
|
|
|
ctxt = self.neutron_ctxt()
|
|
|
|
if self.plugin == 'ovs':
|
|
ctxt.update(self.ovs_ctxt())
|
|
elif self.plugin == 'nvp':
|
|
ctxt.update(self.nvp_ctxt())
|
|
|
|
alchemy_flags = config('neutron-alchemy-flags')
|
|
if alchemy_flags:
|
|
flags = config_flags_parser(alchemy_flags)
|
|
ctxt['neutron_alchemy_flags'] = flags
|
|
|
|
self._save_flag_file()
|
|
return ctxt
|
|
|
|
|
|
class OSConfigFlagContext(OSContextGenerator):
|
|
|
|
"""
|
|
Responsible for adding user-defined config-flags in charm config to a
|
|
template context.
|
|
|
|
NOTE: the value of config-flags may be a comma-separated list of
|
|
key=value pairs and some Openstack config files support
|
|
comma-separated lists as values.
|
|
"""
|
|
|
|
def __call__(self):
|
|
config_flags = config('config-flags')
|
|
if not config_flags:
|
|
return {}
|
|
|
|
flags = config_flags_parser(config_flags)
|
|
return {'user_config_flags': flags}
|
|
|
|
|
|
class SubordinateConfigContext(OSContextGenerator):
|
|
|
|
"""
|
|
Responsible for inspecting relations to subordinates that
|
|
may be exporting required config via a json blob.
|
|
|
|
The subordinate interface allows subordinates to export their
|
|
configuration requirements to the principle for multiple config
|
|
files and multiple serivces. Ie, a subordinate that has interfaces
|
|
to both glance and nova may export to following yaml blob as json:
|
|
|
|
glance:
|
|
/etc/glance/glance-api.conf:
|
|
sections:
|
|
DEFAULT:
|
|
- [key1, value1]
|
|
/etc/glance/glance-registry.conf:
|
|
MYSECTION:
|
|
- [key2, value2]
|
|
nova:
|
|
/etc/nova/nova.conf:
|
|
sections:
|
|
DEFAULT:
|
|
- [key3, value3]
|
|
|
|
|
|
It is then up to the principle charms to subscribe this context to
|
|
the service+config file it is interestd in. Configuration data will
|
|
be available in the template context, in glance's case, as:
|
|
ctxt = {
|
|
... other context ...
|
|
'subordinate_config': {
|
|
'DEFAULT': {
|
|
'key1': 'value1',
|
|
},
|
|
'MYSECTION': {
|
|
'key2': 'value2',
|
|
},
|
|
}
|
|
}
|
|
|
|
"""
|
|
|
|
def __init__(self, service, config_file, interface):
|
|
"""
|
|
:param service : Service name key to query in any subordinate
|
|
data found
|
|
:param config_file : Service's config file to query sections
|
|
:param interface : Subordinate interface to inspect
|
|
"""
|
|
self.service = service
|
|
self.config_file = config_file
|
|
self.interface = interface
|
|
|
|
def __call__(self):
|
|
ctxt = {}
|
|
for rid in relation_ids(self.interface):
|
|
for unit in related_units(rid):
|
|
sub_config = relation_get('subordinate_configuration',
|
|
rid=rid, unit=unit)
|
|
if sub_config and sub_config != '':
|
|
try:
|
|
sub_config = json.loads(sub_config)
|
|
except:
|
|
log('Could not parse JSON from subordinate_config '
|
|
'setting from %s' % rid, level=ERROR)
|
|
continue
|
|
|
|
if self.service not in sub_config:
|
|
log('Found subordinate_config on %s but it contained'
|
|
'nothing for %s service' % (rid, self.service))
|
|
continue
|
|
|
|
sub_config = sub_config[self.service]
|
|
if self.config_file not in sub_config:
|
|
log('Found subordinate_config on %s but it contained'
|
|
'nothing for %s' % (rid, self.config_file))
|
|
continue
|
|
|
|
sub_config = sub_config[self.config_file]
|
|
for k, v in sub_config.iteritems():
|
|
ctxt[k] = v
|
|
|
|
if not ctxt:
|
|
ctxt['sections'] = {}
|
|
|
|
return ctxt
|
|
|
|
|
|
class SyslogContext(OSContextGenerator):
|
|
|
|
def __call__(self):
|
|
ctxt = {
|
|
'use_syslog': config('use-syslog')
|
|
}
|
|
return ctxt
|