Files
charm-keystone/hooks/keystone_context.py
Liam Young ccf153981f Add default_domain_id for Keystone v3 deploys
The default_domain_id is used to specify a domain when the client
hasn't explicitly set one. It defaults to 'default' which is fine
for liberty and previous because the id of the default domain is,
 oddly, 'default' rather than a uuid. On Mitaka and higher it is
a uuid so when keystone assumes the default domains id is 'default'
it fails.

Change-Id: Iaa5e6a07a229815cf2281858cb68a4e120aa2af3
Closes-Bug: 1626889
2016-09-27 08:45:47 +00:00

316 lines
10 KiB
Python

# Copyright 2016 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import hashlib
import math
import os
from base64 import b64decode
from charmhelpers.core.host import (
mkdir,
write_file,
service_restart,
)
from charmhelpers.contrib.openstack import context
from charmhelpers.contrib.hahelpers.cluster import (
DC_RESOURCE_NAME,
determine_apache_port,
determine_api_port,
is_elected_leader,
)
from charmhelpers.core.hookenv import (
config,
log,
DEBUG,
INFO,
)
from charmhelpers.core.strutils import (
bool_from_string,
)
from charmhelpers.contrib.hahelpers.apache import install_ca_cert
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
def is_cert_provided_in_config():
ca = config('ssl_ca')
cert = config('ssl_cert')
key = config('ssl_key')
return bool(ca and cert and key)
class ApacheSSLContext(context.ApacheSSLContext):
interfaces = ['https']
external_ports = []
service_namespace = 'keystone'
def __call__(self):
# late import to work around circular dependency
from keystone_utils import (
determine_ports,
update_hash_from_path,
)
ssl_paths = [CA_CERT_PATH,
os.path.join('/etc/apache2/ssl/',
self.service_namespace)]
self.external_ports = determine_ports()
before = hashlib.sha256()
for path in ssl_paths:
update_hash_from_path(before, path)
ret = super(ApacheSSLContext, self).__call__()
after = hashlib.sha256()
for path in ssl_paths:
update_hash_from_path(after, path)
# Ensure that apache2 is restarted if these change
if before.hexdigest() != after.hexdigest():
service_restart('apache2')
return ret
def configure_cert(self, cn):
from keystone_utils import (
SSH_USER,
get_ca,
ensure_permissions,
is_ssl_cert_master,
)
# Ensure ssl dir exists whether master or not
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
perms = 0o755
mkdir(path=ssl_dir, owner=SSH_USER, group='keystone', perms=perms)
# Ensure accessible by keystone ssh user and group (for sync)
ensure_permissions(ssl_dir, user=SSH_USER, group='keystone',
perms=perms)
if not is_cert_provided_in_config() and not is_ssl_cert_master():
log("Not ssl-cert-master - skipping apache cert config until "
"master is elected", level=INFO)
return
log("Creating apache ssl certs in %s" % (ssl_dir), level=INFO)
cert = config('ssl_cert')
key = config('ssl_key')
if not (cert and key):
ca = get_ca(user=SSH_USER)
cert, key = ca.get_cert_and_key(common_name=cn)
else:
cert = b64decode(cert)
key = b64decode(key)
write_file(path=os.path.join(ssl_dir, 'cert_{}'.format(cn)),
content=cert, owner=SSH_USER, group='keystone', perms=0o644)
write_file(path=os.path.join(ssl_dir, 'key_{}'.format(cn)),
content=key, owner=SSH_USER, group='keystone', perms=0o644)
def configure_ca(self):
from keystone_utils import (
SSH_USER,
get_ca,
ensure_permissions,
is_ssl_cert_master,
)
if not is_cert_provided_in_config() and not is_ssl_cert_master():
log("Not ssl-cert-master - skipping apache ca config until "
"master is elected", level=INFO)
return
ca_cert = config('ssl_ca')
if ca_cert is None:
ca = get_ca(user=SSH_USER)
ca_cert = ca.get_ca_bundle()
else:
ca_cert = b64decode(ca_cert)
# Ensure accessible by keystone ssh user and group (unison)
install_ca_cert(ca_cert)
ensure_permissions(CA_CERT_PATH, user=SSH_USER, group='keystone',
perms=0o0644)
def canonical_names(self):
addresses = self.get_network_addresses()
addrs = []
for address, endpoint in addresses:
addrs.append(endpoint)
return list(set(addrs))
class HAProxyContext(context.HAProxyContext):
interfaces = []
def __call__(self):
'''
Extends the main charmhelpers HAProxyContext with a port mapping
specific to this charm.
Also used to extend nova.conf context with correct api_listening_ports
'''
from keystone_utils import api_port
ctxt = super(HAProxyContext, self).__call__()
# determine which port api processes should bind to, depending
# on existence of haproxy + apache frontends
listen_ports = {}
listen_ports['admin_port'] = api_port('keystone-admin')
listen_ports['public_port'] = api_port('keystone-public')
# Apache ports
a_admin_port = determine_apache_port(api_port('keystone-admin'),
singlenode_mode=True)
a_public_port = determine_apache_port(api_port('keystone-public'),
singlenode_mode=True)
port_mapping = {
'admin-port': [
api_port('keystone-admin'), a_admin_port],
'public-port': [
api_port('keystone-public'), a_public_port],
}
# for haproxy.conf
ctxt['service_ports'] = port_mapping
# for keystone.conf
ctxt['listen_ports'] = listen_ports
return ctxt
class KeystoneContext(context.OSContextGenerator):
interfaces = []
def __call__(self):
from keystone_utils import (
api_port, set_admin_token, endpoint_url, resolve_address,
PUBLIC, ADMIN, PKI_CERTS_DIR, ensure_pki_cert_paths,
get_admin_domain_id, get_default_domain_id
)
ctxt = {}
ctxt['token'] = set_admin_token(config('admin-token'))
ctxt['api_version'] = int(config('preferred-api-version'))
ctxt['admin_role'] = config('admin-role')
if ctxt['api_version'] > 2:
ctxt['admin_domain_id'] = (
get_admin_domain_id() or 'admin_domain_id')
# default is the default for default_domain_id
ctxt['default_domain_id'] = (
get_default_domain_id() or 'default')
ctxt['admin_port'] = determine_api_port(api_port('keystone-admin'),
singlenode_mode=True)
ctxt['public_port'] = determine_api_port(api_port('keystone-public'),
singlenode_mode=True)
ctxt['debug'] = config('debug')
ctxt['verbose'] = config('verbose')
ctxt['token_expiration'] = config('token-expiration')
ctxt['identity_backend'] = config('identity-backend')
ctxt['assignment_backend'] = config('assignment-backend')
if config('identity-backend') == 'ldap':
ctxt['ldap_server'] = config('ldap-server')
ctxt['ldap_user'] = config('ldap-user')
ctxt['ldap_password'] = config('ldap-password')
ctxt['ldap_suffix'] = config('ldap-suffix')
ctxt['ldap_readonly'] = config('ldap-readonly')
ldap_flags = config('ldap-config-flags')
if ldap_flags:
flags = context.config_flags_parser(ldap_flags)
ctxt['ldap_config_flags'] = flags
enable_pki = config('enable-pki')
if enable_pki and bool_from_string(enable_pki):
log("Enabling PKI", level=DEBUG)
ctxt['token_provider'] = 'pki'
ensure_pki_cert_paths()
certs = os.path.join(PKI_CERTS_DIR, 'certs')
privates = os.path.join(PKI_CERTS_DIR, 'privates')
ctxt.update({'certfile': os.path.join(certs, 'signing_cert.pem'),
'keyfile': os.path.join(privates, 'signing_key.pem'),
'ca_certs': os.path.join(certs, 'ca.pem'),
'ca_key': os.path.join(certs, 'ca_key.pem')})
# Base endpoint URL's which are used in keystone responses
# to unauthenticated requests to redirect clients to the
# correct auth URL.
ctxt['public_endpoint'] = endpoint_url(
resolve_address(PUBLIC),
api_port('keystone-public')).replace('v2.0', '')
ctxt['admin_endpoint'] = endpoint_url(
resolve_address(ADMIN),
api_port('keystone-admin')).replace('v2.0', '')
return ctxt
class KeystoneLoggingContext(context.OSContextGenerator):
def __call__(self):
ctxt = {}
debug = config('debug')
if debug:
ctxt['root_level'] = 'DEBUG'
log_level = config('log-level')
log_level_accepted_params = ['WARNING', 'INFO', 'DEBUG', 'ERROR']
if log_level in log_level_accepted_params:
ctxt['log_level'] = config('log-level')
else:
log("log-level must be one of the following states "
"(WARNING, INFO, DEBUG, ERROR) keeping the current state.")
ctxt['log_level'] = None
return ctxt
class TokenFlushContext(context.OSContextGenerator):
def __call__(self):
ctxt = {
'token_flush': is_elected_leader(DC_RESOURCE_NAME)
}
return ctxt
class WSGIWorkerConfigContext(context.WorkerConfigContext):
def __call__(self):
from keystone_utils import (
determine_usr_bin, determine_python_path,
)
multiplier = config('worker-multiplier') or 1
total_processes = self.num_cpus * multiplier
ctxt = {
"public_processes": int(math.ceil(0.75 * total_processes)),
"admin_processes": int(math.ceil(0.25 * total_processes)),
# Keystone install guide suggests 1 but offers no science
"public_threads": 1,
"admin_threads": 1,
"usr_bin": determine_usr_bin(),
"python_path": determine_python_path(),
}
return ctxt