Files
charm-openstack-dashboard/hooks/horizon_contexts.py
Liam Young 94df23fbc5 Add support for Keystone API version 3
This change enables the charm to configure the Openstack Dashboard
to support Keystone v3 integration. Mitaka is the earliest release
to support Dashboard and v3 integration so v3 integration should
only be enabled on Mitaka or above.

A new identity policy template now ships with the charm which is
specifically for v3 integration.

Both the local settings file and the new v3 policy file need the
admin domain id. This is now passed to the charm from Keystone via
the identity service relation.

The openstack-dashboard package uses
django.contrib.sessions.backends.signed_cookies for session
management but cookies are not large enough to store domain scoped
tokens so a different session management engine is needed. This patch
adds the option to relate the charm to a database backend. If the
relation is present then the charm uses the
django.contrib.sessions.backends.cached_db session engine. This
stores the session information in the database and also caches the
session information locally using memcache.

For details on Dashboard and v3 integration see
https://wiki.openstack.org/wiki/Horizon/DomainWorkFlow

Change-Id: I24f514e29811752d7c0c5347a1157d9778297738
Partial-Bug: 1595685
2016-06-30 08:31:12 +00:00

263 lines
8.9 KiB
Python

# vim: set ts=4:et
from charmhelpers.core.hookenv import (
config,
relation_ids,
related_units,
relation_get,
local_unit,
unit_get,
log,
WARNING,
ERROR,
)
from charmhelpers.contrib.openstack.context import (
OSContextGenerator,
HAProxyContext,
context_complete
)
from charmhelpers.contrib.openstack.utils import get_host_ip
from charmhelpers.contrib.hahelpers.apache import (
get_ca_cert,
get_cert,
install_ca_cert,
)
from charmhelpers.contrib.network.ip import (
get_ipv6_addr,
format_ipv6_addr,
)
from charmhelpers.core.host import pwgen
from base64 import b64decode
import os
VALID_ENDPOINT_TYPES = {
'PUBLICURL': 'publicURL',
'INTERNALURL': 'internalURL',
'ADMINURL': 'adminURL',
}
class HorizonHAProxyContext(HAProxyContext):
def __call__(self):
'''
Horizon specific HAProxy context; haproxy is used all the time
in the openstack dashboard charm so a single instance just
self refers
'''
cluster_hosts = {}
l_unit = local_unit().replace('/', '-')
if config('prefer-ipv6'):
cluster_hosts[l_unit] = get_ipv6_addr(exc_list=[config('vip')])[0]
else:
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
log('Ensuring haproxy enabled in /etc/default/haproxy.')
with open('/etc/default/haproxy', 'w') as out:
out.write('ENABLED=1\n')
ctxt = {
'units': cluster_hosts,
'service_ports': {
'dash_insecure': [80, 70],
'dash_secure': [443, 433]
},
'prefer_ipv6': config('prefer-ipv6')
}
return ctxt
# NOTE: this is a stripped-down version of
# contrib.openstack.IdentityServiceContext
class IdentityServiceContext(OSContextGenerator):
interfaces = ['identity-service']
def normalize(self, endpoint_type):
"""Normalizes the endpoint type values.
:param endpoint_type (string): the endpoint type to normalize.
:raises: Exception if the endpoint type is not valid.
:return (string): the normalized form of the endpoint type.
"""
normalized_form = VALID_ENDPOINT_TYPES.get(endpoint_type.upper(), None)
if not normalized_form:
msg = ('Endpoint type specified %s is not a valid'
' endpoint type' % endpoint_type)
log(msg, ERROR)
raise Exception(msg)
return normalized_form
def __call__(self):
log('Generating template context for identity-service')
ctxt = {}
regions = set()
for rid in relation_ids('identity-service'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
serv_host = rdata.get('service_host')
serv_host = format_ipv6_addr(serv_host) or serv_host
region = rdata.get('region')
local_ctxt = {
'service_port': rdata.get('service_port'),
'service_host': serv_host,
'service_protocol':
rdata.get('service_protocol') or 'http',
'api_version': rdata.get('api_version', '2')
}
# If using keystone v3 the context is incomplete without the
# admin domain id
if local_ctxt['api_version'] == '3':
local_ctxt['admin_domain_id'] = rdata.get(
'admin_domain_id')
if not context_complete(local_ctxt):
continue
# Update the service endpoint and title for each available
# region in order to support multi-region deployments
if region is not None:
endpoint = ("%(service_protocol)s://%(service_host)s"
":%(service_port)s/v2.0") % local_ctxt
for reg in region.split():
regions.add((endpoint, reg))
if len(ctxt) == 0:
ctxt = local_ctxt
if len(regions) > 1:
avail_regions = map(lambda r: {'endpoint': r[0], 'title': r[1]},
regions)
ctxt['regions'] = sorted(avail_regions)
# Allow the endpoint types to be specified via a config parameter.
# The config parameter accepts either:
# 1. a single endpoint type to be specified, in which case the
# primary endpoint is configured
# 2. a list of endpoint types, in which case the primary endpoint
# is taken as the first entry and the secondary endpoint is
# taken as the second entry. All subsequent entries are ignored.
ep_types = config('endpoint-type')
if ep_types:
ep_types = [self.normalize(e) for e in ep_types.split(',')]
ctxt['primary_endpoint'] = ep_types[0]
if len(ep_types) > 1:
ctxt['secondary_endpoint'] = ep_types[1]
return ctxt
class HorizonContext(OSContextGenerator):
def __call__(self):
''' Provide all configuration for Horizon '''
ctxt = {
'compress_offline': config('offline-compression') in ['yes', True],
'debug': config('debug') in ['yes', True],
'default_role': config('default-role'),
"webroot": config('webroot'),
"ubuntu_theme": config('ubuntu-theme') in ['yes', True],
"secret": config('secret') or pwgen(),
'support_profile': config('profile')
if config('profile') in ['cisco'] else None,
"neutron_network_lb": config("neutron-network-lb"),
"neutron_network_firewall": config("neutron-network-firewall"),
"neutron_network_vpn": config("neutron-network-vpn"),
"cinder_backup": config("cinder-backup"),
}
return ctxt
class ApacheContext(OSContextGenerator):
def __call__(self):
''' Grab cert and key from configuraton for SSL config '''
ctxt = {
'http_port': 70,
'https_port': 433
}
if config('enforce-ssl'):
# NOTE(dosaboy): if ssl is not configured we shouldn't allow this
if all(get_cert()):
if config('vip'):
addr = config('vip')
elif config('prefer-ipv6'):
addr = format_ipv6_addr(get_ipv6_addr()[0])
else:
addr = get_host_ip(unit_get('private-address'))
ctxt['ssl_addr'] = addr
else:
log("Enforce ssl redirect requested but ssl not configured - "
"skipping redirect", level=WARNING)
return ctxt
class ApacheSSLContext(OSContextGenerator):
def __call__(self):
''' Grab cert and key from configuration for SSL config '''
ca_cert = get_ca_cert()
if ca_cert:
install_ca_cert(b64decode(ca_cert))
ssl_cert, ssl_key = get_cert()
if all([ssl_cert, ssl_key]):
with open('/etc/ssl/certs/dashboard.cert', 'w') as cert_out:
cert_out.write(b64decode(ssl_cert))
with open('/etc/ssl/private/dashboard.key', 'w') as key_out:
key_out.write(b64decode(ssl_key))
os.chmod('/etc/ssl/private/dashboard.key', 0600)
ctxt = {
'ssl_configured': True,
'ssl_cert': '/etc/ssl/certs/dashboard.cert',
'ssl_key': '/etc/ssl/private/dashboard.key',
}
else:
# Use snakeoil ones by default
ctxt = {
'ssl_configured': False,
}
return ctxt
class RouterSettingContext(OSContextGenerator):
def __call__(self):
''' Enable/Disable Router Tab on horizon '''
ctxt = {
'disable_router': False if config('profile') in ['cisco'] else True
}
return ctxt
class LocalSettingsContext(OSContextGenerator):
def __call__(self):
''' Additional config stanzas to be appended to local_settings.py '''
relations = []
for rid in relation_ids("plugin"):
try:
unit = related_units(rid)[0]
except IndexError:
pass
else:
rdata = relation_get(unit=unit, rid=rid)
if set(('local-settings', 'priority')) <= set(rdata.keys()):
relations.append((unit, rdata))
ctxt = {
'settings': [
'# {0}\n{1}'.format(u, rd['local-settings'])
for u, rd in sorted(relations,
key=lambda r: r[1]['priority'])]
}
return ctxt