Remove support for PKI tokens and legacy charm managed certificates

These features are disabled by default, a majority of our
users provide certificates through configuration.

At present the cluster relation carries information required
for these features even when they are not enabled. This makes
processing of cluster relation changes unnecessarily heavy
and vulnerable to bugs.

Notice of deprecation and removal in next release was given
as part of the 18.05 release notes.

Change-Id: I8b07c7e0d5c2c623c115c83dc8aff230b554a986
Closes-Bug: #1755897
Related-Bug: #1744990
This commit is contained in:
Frode Nordahl 2018-04-12 12:10:58 +02:00
parent b75a180a68
commit 17b24e7fde
No known key found for this signature in database
GPG Key ID: 6A5D59A3BA48373F
7 changed files with 56 additions and 1910 deletions

View File

@ -12,18 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import hashlib
import os
import json
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 (
@ -38,116 +29,13 @@ from charmhelpers.core.hookenv import (
config,
log,
leader_get,
DEBUG,
INFO,
related_units,
relation_ids,
relation_get,
)
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():
cert = config('ssl_cert')
key = config('ssl_key')
return bool(cert and key)
class SSLContext(context.ApacheSSLContext):
def configure_cert(self, cn):
from keystone_utils import (
SSH_USER,
get_ca,
ensure_permissions,
is_ssl_cert_master,
KEYSTONE_USER,
)
# Ensure ssl dir exists whether master or not
perms = 0o775
mkdir(path=self.ssl_dir, owner=SSH_USER, group=KEYSTONE_USER,
perms=perms)
# Ensure accessible by keystone ssh user and group (for sync)
ensure_permissions(self.ssl_dir, user=SSH_USER, group=KEYSTONE_USER,
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" % (self.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(self.ssl_dir, 'cert_{}'.format(cn)),
content=cert, owner=SSH_USER, group=KEYSTONE_USER,
perms=0o640)
write_file(path=os.path.join(self.ssl_dir, 'key_{}'.format(cn)),
content=key, owner=SSH_USER, group=KEYSTONE_USER,
perms=0o640)
def configure_ca(self):
from keystone_utils import (
SSH_USER,
get_ca,
ensure_permissions,
is_ssl_cert_master,
KEYSTONE_USER,
)
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
cert = config('ssl_cert')
key = config('ssl_key')
ca_cert = config('ssl_ca')
if ca_cert:
ca_cert = b64decode(ca_cert)
elif not (cert and key):
# NOTE(hopem): if a cert and key are provided as config we don't
# mandate that a CA is also provided since it isn't necessarily
# needed. As a result we only generate a custom CA if we are also
# generating cert and key.
ca = get_ca(user=SSH_USER)
ca_cert = ca.get_ca_bundle()
if 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_USER, 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 ApacheSSLContext(SSLContext):
class ApacheSSLContext(context.ApacheSSLContext):
interfaces = ['https']
external_ports = []
service_namespace = 'keystone'
@ -157,31 +45,13 @@ class ApacheSSLContext(SSLContext):
# late import to work around circular dependency
from keystone_utils import (
determine_ports,
update_hash_from_path,
)
ssl_paths = [CA_CERT_PATH, self.ssl_dir]
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
return super(ApacheSSLContext, self).__call__()
class NginxSSLContext(SSLContext):
class NginxSSLContext(context.ApacheSSLContext):
interfaces = ['https']
external_ports = []
service_namespace = 'keystone'
@ -192,30 +62,14 @@ class NginxSSLContext(SSLContext):
# late import to work around circular dependency
from keystone_utils import (
determine_ports,
update_hash_from_path,
APACHE_SSL_DIR
)
ssl_paths = [CA_CERT_PATH, APACHE_SSL_DIR]
self.external_ports = determine_ports()
before = hashlib.sha256()
for path in ssl_paths:
update_hash_from_path(before, path)
ret = super(NginxSSLContext, self).__call__()
if not ret:
log("SSL not used", level='DEBUG')
return {}
after = hashlib.sha256()
for path in ssl_paths:
update_hash_from_path(after, path)
# Ensure that Nginx is restarted if these change
if before.hexdigest() != after.hexdigest():
service_restart('snap.keystone.nginx')
# Transform for use by Nginx
"""
{'endpoints': [(u'10.5.0.30', u'10.5.0.30', 4990, 4980),
@ -294,7 +148,7 @@ class KeystoneContext(context.OSContextGenerator):
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, ADMIN_DOMAIN,
PUBLIC, ADMIN, ADMIN_DOMAIN,
snap_install_requested, get_api_version,
)
ctxt = {}
@ -331,25 +185,6 @@ class KeystoneContext(context.OSContextGenerator):
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'
# NOTE(jamespage): Only check PKI configuration if the PKI
# token format is in use, which has been
# removed as of OpenStack Ocata.
ensure_pki_cert_paths()
certs = os.path.join(PKI_CERTS_DIR, 'certs')
privates = os.path.join(PKI_CERTS_DIR, 'privates')
ctxt['enable_signing'] = True
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')})
else:
ctxt['enable_signing'] = False
# Base endpoint URL's which are used in keystone responses
# to unauthenticated requests to redirect clients to the
# correct auth URL.

View File

@ -16,12 +16,10 @@
import hashlib
import json
import os
import sys
from subprocess import check_call
from charmhelpers.contrib import unison
from charmhelpers.core import unitdata
from charmhelpers.core.hookenv import (
@ -29,7 +27,6 @@ from charmhelpers.core.hookenv import (
UnregisteredHookError,
config,
log,
local_unit,
DEBUG,
INFO,
WARNING,
@ -44,17 +41,12 @@ from charmhelpers.core.hookenv import (
)
from charmhelpers.core.host import (
mkdir,
service_pause,
service_stop,
service_start,
service_restart,
)
from charmhelpers.core.strutils import (
bool_from_string,
)
from charmhelpers.fetch import (
apt_install, apt_update,
filter_installed_packages
@ -85,32 +77,18 @@ from keystone_utils import (
migrate_database,
save_script_rc,
post_snap_install,
synchronize_ca_if_changed,
register_configs,
restart_map,
services,
CLUSTER_RES,
KEYSTONE_CONF,
KEYSTONE_USER,
POLICY_JSON,
TOKEN_FLUSH_CRON_FILE,
SSH_USER,
setup_ipv6,
send_notifications,
check_peer_actions,
get_ssl_sync_request_units,
is_ssl_cert_master,
is_db_ready,
clear_ssl_synced_units,
is_db_initialised,
update_certs_if_available,
ensure_ssl_dir,
ensure_pki_dir_permissions,
ensure_permissions,
force_ssl_sync,
filter_null,
ensure_ssl_dirs,
ensure_pki_cert_paths,
is_service_present,
delete_service_entry,
assess_status,
@ -128,7 +106,6 @@ from keystone_utils import (
from charmhelpers.contrib.hahelpers.cluster import (
is_elected_leader,
get_hacluster_config,
peer_units,
https,
is_clustered,
)
@ -142,7 +119,6 @@ from charmhelpers.payload.execd import execd_preinstall
from charmhelpers.contrib.peerstorage import (
peer_retrieve_by_prefix,
peer_echo,
relation_get as relation_get_and_migrate,
)
from charmhelpers.contrib.openstack.ip import (
ADMIN,
@ -199,13 +175,9 @@ def install():
disable_unused_apache_sites()
service_pause('keystone')
unison.ensure_user(user=SSH_USER, group=SSH_USER)
unison.ensure_user(user=SSH_USER, group=KEYSTONE_USER)
@hooks.hook('config-changed')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
@synchronize_ca_if_changed(fatal=True)
@harden()
def config_changed():
if config('prefer-ipv6'):
@ -214,36 +186,21 @@ def config_changed():
sync_db_with_multi_ipv6_addresses(config('database'),
config('database-user'))
unison.ensure_user(user=SSH_USER, group=SSH_USER)
unison.ensure_user(user=SSH_USER, group=KEYSTONE_USER)
homedir = unison.get_homedir(SSH_USER)
if not os.path.isdir(homedir):
mkdir(homedir, SSH_USER, SSH_USER, 0o775)
if not config('action-managed-upgrade'):
if openstack_upgrade_available('keystone'):
status_set('maintenance', 'Running openstack upgrade')
do_openstack_upgrade_reexec(configs=CONFIGS)
for r_id in relation_ids('cluster'):
cluster_joined(rid=r_id, ssl_sync_request=False)
cluster_joined(rid=r_id)
config_changed_postupgrade()
@hooks.hook('config-changed-postupgrade')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
@synchronize_ca_if_changed(fatal=True)
@harden()
def config_changed_postupgrade():
# Ensure ssl dir exists and is unison-accessible
ensure_ssl_dir()
if not snap_install_requested():
check_call(['chmod', '-R', 'g+wrx', '/var/lib/keystone/'])
ensure_ssl_dirs()
save_script_rc()
release = os_release('keystone')
if run_in_apache(release=release):
@ -275,57 +232,14 @@ def config_changed_postupgrade():
if snap_install_requested() and not is_unit_paused_set():
service_restart('snap.keystone.*')
initialise_pki()
update_all_identity_relation_units()
update_all_domain_backends()
update_all_fid_backends()
# Ensure sync request is sent out (needed for any/all ssl change)
send_ssl_sync_request()
for r_id in relation_ids('ha'):
ha_joined(relation_id=r_id)
@synchronize_ca_if_changed(fatal=True)
def initialise_pki():
"""Create certs and keys required for token signing.
Used for PKI and signing token revocation list.
NOTE: keystone.conf [signing] section must be up-to-date prior to
executing this.
"""
if CompareOpenStackReleases(os_release('keystone-common')) >= 'pike':
# pike dropped support for PKI token; skip function
return
ensure_pki_cert_paths()
if not peer_units() or is_ssl_cert_master():
log("Ensuring PKI token certs created", level=DEBUG)
if snap_install_requested():
cmd = ['/snap/bin/keystone-manage', 'pki_setup',
'--keystone-user', KEYSTONE_USER,
'--keystone-group', KEYSTONE_USER]
_log_dir = '/var/snap/keystone/common/log'
else:
cmd = ['keystone-manage', 'pki_setup',
'--keystone-user', KEYSTONE_USER,
'--keystone-group', KEYSTONE_USER]
_log_dir = '/var/log/keystone'
check_call(cmd)
# Ensure logfile has keystone perms since we may have just created it
# with root.
ensure_permissions(_log_dir, user=KEYSTONE_USER,
group=KEYSTONE_USER, perms=0o744)
ensure_permissions('{}/keystone.log'.format(_log_dir),
user=KEYSTONE_USER, group=KEYSTONE_USER,
perms=0o644)
ensure_pki_dir_permissions()
@hooks.hook('shared-db-relation-joined')
def db_joined():
if config('prefer-ipv6'):
@ -377,11 +291,6 @@ def update_all_identity_relation_units(check_db_ready=True):
identity_credentials_changed(relation_id=rid, remote_unit=unit)
@synchronize_ca_if_changed(force=True)
def update_all_identity_relation_units_force_sync():
update_all_identity_relation_units()
def update_all_domain_backends():
"""Re-trigger hooks for all domain-backend relations/units"""
for rid in relation_ids('domain-backend'):
@ -431,7 +340,6 @@ def leader_init_db_if_ready(use_current_context=False):
@hooks.hook('shared-db-relation-changed')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
@synchronize_ca_if_changed()
def db_changed():
if 'shared-db' not in CONFIGS.complete_contexts():
log('shared-db relation incomplete. Peer not ready?')
@ -446,7 +354,6 @@ def db_changed():
@hooks.hook('identity-service-relation-changed')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
@synchronize_ca_if_changed()
def identity_changed(relation_id=None, remote_unit=None):
CONFIGS.write_all()
@ -528,59 +435,8 @@ def identity_credentials_changed(relation_id=None, remote_unit=None):
log('Deferring identity_credentials_changed() to service leader.')
def send_ssl_sync_request():
"""Set sync request on cluster relation.
Value set equals number of ssl configs currently enabled so that if they
change, we ensure that certs are synced. This setting is consumed by
cluster-relation-changed ssl master. We also clear the 'synced' set to
guarantee that a sync will occur.
Note the we do nothing if the setting is already applied.
"""
unit = local_unit().replace('/', '-')
# Start with core config (e.g. used for signing revoked token list)
ssl_config = 0b1
use_https = config('use-https')
if use_https and bool_from_string(use_https):
ssl_config ^= 0b10
https_service_endpoints = config('https-service-endpoints')
if (https_service_endpoints and
bool_from_string(https_service_endpoints)):
ssl_config ^= 0b100
enable_pki = config('enable-pki')
if enable_pki and bool_from_string(enable_pki):
ssl_config ^= 0b1000
key = 'ssl-sync-required-%s' % (unit)
settings = {key: ssl_config}
prev = 0b0
rid = None
for rid in relation_ids('cluster'):
for unit in related_units(rid):
_prev = relation_get(rid=rid, unit=unit, attribute=key) or 0b0
if _prev and _prev > prev:
prev = bin(_prev)
if rid and prev ^ ssl_config:
if is_leader():
clear_ssl_synced_units()
log("Setting %s=%s" % (key, bin(ssl_config)), level=DEBUG)
relation_set(relation_id=rid, relation_settings=settings)
@hooks.hook('cluster-relation-joined')
def cluster_joined(rid=None, ssl_sync_request=True):
unison.ssh_authorized_peers(user=SSH_USER,
group=SSH_USER,
peer_interface='cluster',
ensure_local_user=True)
def cluster_joined(rid=None):
settings = {}
for addr_type in ADDRESS_TYPES:
@ -594,57 +450,19 @@ def cluster_joined(rid=None, ssl_sync_request=True):
relation_set(relation_id=rid, relation_settings=settings)
if ssl_sync_request:
send_ssl_sync_request()
@hooks.hook('cluster-relation-changed')
@restart_on_change(restart_map(), stopstart=True)
@update_certs_if_available
def cluster_changed():
unison.ssh_authorized_peers(user=SSH_USER,
group=SSH_USER,
peer_interface='cluster',
ensure_local_user=True)
# NOTE(jamespage) re-echo passwords for peer storage
echo_whitelist = ['_passwd', 'identity-service:',
'db-initialised', 'ssl-cert-available-updates']
# Don't echo if leader since a re-election may be in progress.
if not is_leader():
echo_whitelist.append('ssl-cert-master')
echo_whitelist = ['_passwd', 'identity-service:', 'db-initialised']
log("Peer echo whitelist: %s" % (echo_whitelist), level=DEBUG)
peer_echo(includes=echo_whitelist, force=True)
check_peer_actions()
update_all_identity_relation_units()
initialise_pki()
if is_leader():
# Figure out if we need to mandate a sync
units = get_ssl_sync_request_units()
synced_units = relation_get_and_migrate(attribute='ssl-synced-units',
unit=local_unit())
diff = None
if synced_units:
synced_units = json.loads(synced_units)
diff = set(units).symmetric_difference(set(synced_units))
else:
units = None
if units and (not synced_units or diff):
log("New peers joined and need syncing - %s" %
(', '.join(units)), level=DEBUG)
update_all_identity_relation_units_force_sync()
else:
update_all_identity_relation_units()
if not is_leader() and is_ssl_cert_master():
# Force and sync and trigger a sync master re-election since we are not
# leader anymore.
force_ssl_sync()
else:
CONFIGS.write_all()
CONFIGS.write_all()
@hooks.hook('leader-elected')
@ -739,7 +557,6 @@ def ha_joined(relation_id=None):
@hooks.hook('ha-relation-changed')
@restart_on_change(restart_map(), restart_functions=restart_function_map())
@synchronize_ca_if_changed()
def ha_changed():
CONFIGS.write_all()
@ -747,10 +564,7 @@ def ha_changed():
if clustered:
log('Cluster configured, notifying other services and updating '
'keystone endpoint configuration')
if is_ssl_cert_master():
update_all_identity_relation_units_force_sync()
else:
update_all_identity_relation_units()
update_all_identity_relation_units()
@hooks.hook('identity-admin-relation-changed')
@ -808,7 +622,6 @@ def domain_backend_changed(relation_id=None, unit=None):
db.flush()
@synchronize_ca_if_changed(fatal=True)
def configure_https():
'''
Enables SSL API Apache config if appropriate and kicks identity-service
@ -831,17 +644,10 @@ def configure_https():
@hooks.hook('upgrade-charm')
@restart_on_change(restart_map(), stopstart=True)
@synchronize_ca_if_changed()
@harden()
def upgrade_charm():
status_set('maintenance', 'Installing apt packages')
apt_install(filter_installed_packages(determine_packages()))
unison.ssh_authorized_peers(user=SSH_USER,
group=SSH_USER,
peer_interface='cluster',
ensure_local_user=True)
ensure_ssl_dirs()
if run_in_apache():
disable_unused_apache_sites()

View File

@ -1,358 +0,0 @@
#!/usr/bin/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 os
import shutil
import subprocess
import tarfile
import tempfile
from charmhelpers.core.hookenv import (
log,
DEBUG,
)
CA_EXPIRY = '365'
ORG_NAME = 'Ubuntu'
ORG_UNIT = 'Ubuntu Cloud'
CA_BUNDLE = '/usr/local/share/ca-certificates/juju_ca_cert.crt'
CA_CONFIG = """
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = %(ca_dir)s
policy = policy_match
database = $dir/index.txt
serial = $dir/serial
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
certificate = $dir/cacert.pem
private_key = $dir/private/cacert.key
RANDFILE = $dir/private/.rand
default_md = default
[ req ]
default_bits = 1024
default_md = sha1
prompt = no
distinguished_name = ca_distinguished_name
x509_extensions = ca_extensions
[ ca_distinguished_name ]
organizationName = %(org_name)s
organizationalUnitName = %(org_unit_name)s Certificate Authority
commonName = %(common_name)s
[ policy_match ]
countryName = optional
stateOrProvinceName = optional
organizationName = match
organizationalUnitName = optional
commonName = supplied
[ ca_extensions ]
basicConstraints = critical,CA:true
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
keyUsage = cRLSign, keyCertSign
"""
SIGNING_CONFIG = """
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = %(ca_dir)s
policy = policy_match
database = $dir/index.txt
serial = $dir/serial
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
certificate = $dir/cacert.pem
private_key = $dir/private/cacert.key
RANDFILE = $dir/private/.rand
default_md = default
[ req ]
default_bits = 1024
default_md = sha1
prompt = no
distinguished_name = req_distinguished_name
x509_extensions = req_extensions
[ req_distinguished_name ]
organizationName = %(org_name)s
organizationalUnitName = %(org_unit_name)s Server Farm
[ policy_match ]
countryName = optional
stateOrProvinceName = optional
organizationName = match
organizationalUnitName = optional
commonName = supplied
[ req_extensions ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
keyUsage = digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage = serverAuth, clientAuth
"""
# Instance can be appended to this list to represent a singleton
CA_SINGLETON = []
def init_ca(ca_dir, common_name, org_name=ORG_NAME, org_unit_name=ORG_UNIT):
log('Ensuring certificate authority exists at %s.' % ca_dir, level=DEBUG)
if not os.path.exists(ca_dir):
log('Initializing new certificate authority at %s' % ca_dir,
level=DEBUG)
os.mkdir(ca_dir)
for i in ['certs', 'crl', 'newcerts', 'private']:
d = os.path.join(ca_dir, i)
if not os.path.exists(d):
log('Creating %s.' % d, level=DEBUG)
os.mkdir(d)
os.chmod(os.path.join(ca_dir, 'private'), 0o710)
if not os.path.isfile(os.path.join(ca_dir, 'serial')):
with open(os.path.join(ca_dir, 'serial'), 'wb') as out:
out.write('01\n')
if not os.path.isfile(os.path.join(ca_dir, 'index.txt')):
with open(os.path.join(ca_dir, 'index.txt'), 'wb') as out:
out.write('')
conf = os.path.join(ca_dir, 'ca.cnf')
if not os.path.isfile(conf):
log('Creating new CA config in %s' % ca_dir, level=DEBUG)
with open(conf, 'wb') as out:
out.write(CA_CONFIG % locals())
def root_ca_crt_key(ca_dir):
init = False
crt = os.path.join(ca_dir, 'cacert.pem')
key = os.path.join(ca_dir, 'private', 'cacert.key')
for f in [crt, key]:
if not os.path.isfile(f):
log('Missing %s, will re-initialize cert+key.' % f, level=DEBUG)
init = True
else:
log('Found %s.' % f, level=DEBUG)
if init:
conf = os.path.join(ca_dir, 'ca.cnf')
cmd = ['openssl', 'req', '-config', conf,
'-x509', '-nodes', '-newkey', 'rsa', '-days', '21360',
'-keyout', key, '-out', crt, '-outform', 'PEM']
subprocess.check_call(cmd)
return crt, key
def intermediate_ca_csr_key(ca_dir):
log('Creating new intermediate CSR.', level=DEBUG)
key = os.path.join(ca_dir, 'private', 'cacert.key')
csr = os.path.join(ca_dir, 'cacert.csr')
conf = os.path.join(ca_dir, 'ca.cnf')
cmd = ['openssl', 'req', '-config', conf, '-sha1', '-newkey', 'rsa',
'-nodes', '-keyout', key, '-out', csr, '-outform', 'PEM']
subprocess.check_call(cmd)
return csr, key
def sign_int_csr(ca_dir, csr, common_name):
log('Signing certificate request %s.' % csr, level=DEBUG)
crt_name = os.path.basename(csr).split('.')[0]
crt = os.path.join(ca_dir, 'certs', '%s.crt' % crt_name)
subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
conf = os.path.join(ca_dir, 'ca.cnf')
cmd = ['openssl', 'ca', '-batch', '-config', conf, '-extensions',
'ca_extensions', '-days', CA_EXPIRY, '-notext', '-in', csr, '-out',
crt, '-subj', subj, '-batch']
log("Executing: %s" % ' '.join(cmd), level=DEBUG)
subprocess.check_call(cmd)
return crt
def init_root_ca(ca_dir, common_name):
init_ca(ca_dir, common_name)
return root_ca_crt_key(ca_dir)
def init_intermediate_ca(ca_dir, common_name, root_ca_dir, org_name=ORG_NAME,
org_unit_name=ORG_UNIT):
init_ca(ca_dir, common_name)
if not os.path.isfile(os.path.join(ca_dir, 'cacert.pem')):
csr, key = intermediate_ca_csr_key(ca_dir)
crt = sign_int_csr(root_ca_dir, csr, common_name)
shutil.copy(crt, os.path.join(ca_dir, 'cacert.pem'))
else:
log('Intermediate CA certificate already exists.', level=DEBUG)
conf = os.path.join(ca_dir, 'signing.cnf')
if not os.path.isfile(conf):
log('Creating new signing config in %s' % ca_dir, level=DEBUG)
with open(conf, 'wb') as out:
out.write(SIGNING_CONFIG % locals())
def create_certificate(ca_dir, service):
common_name = service
subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
csr = os.path.join(ca_dir, 'certs', '%s.csr' % service)
key = os.path.join(ca_dir, 'certs', '%s.key' % service)
cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa', '-nodes', '-keyout',
key, '-out', csr, '-subj', subj]
subprocess.check_call(cmd)
crt = sign_int_csr(ca_dir, csr, common_name)
log('Signed new CSR, crt @ %s' % crt, level=DEBUG)
return
def update_bundle(bundle_file, new_bundle):
return
if os.path.isfile(bundle_file):
with open(bundle_file, 'r') as f:
current = f.read().strip()
if new_bundle == current:
log('CA Bundle @ %s is up to date.' % bundle_file, level=DEBUG)
return
log('Updating CA bundle @ %s.' % bundle_file, level=DEBUG)
with open(bundle_file, 'wb') as out:
out.write(new_bundle)
subprocess.check_call(['update-ca-certificates'])
def tar_directory(path):
cwd = os.getcwd()
parent = os.path.dirname(path)
directory = os.path.basename(path)
tmp = tempfile.TemporaryFile()
os.chdir(parent)
tarball = tarfile.TarFile(fileobj=tmp, mode='w')
tarball.add(directory)
tarball.close()
tmp.seek(0)
out = tmp.read()
tmp.close()
os.chdir(cwd)
return out
class JujuCA(object):
def __init__(self, name, ca_dir, root_ca_dir, user, group):
# Root CA
cn = '%s Certificate Authority' % name
root_crt, root_key = init_root_ca(root_ca_dir, cn)
# Intermediate CA
cn = '%s Intermediate Certificate Authority' % name
init_intermediate_ca(ca_dir, cn, root_ca_dir)
# Create dirs
cmd = ['chown', '-R', '%s.%s' % (user, group), ca_dir]
subprocess.check_call(cmd)
cmd = ['chown', '-R', '%s.%s' % (user, group), root_ca_dir]
subprocess.check_call(cmd)
self.ca_dir = ca_dir
self.root_ca_dir = root_ca_dir
self.user = user
self.group = group
update_bundle(CA_BUNDLE, self.get_ca_bundle())
def _sign_csr(self, csr, service, common_name):
subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
crt = os.path.join(self.ca_dir, 'certs', '%s.crt' % common_name)
conf = os.path.join(self.ca_dir, 'signing.cnf')
cmd = ['openssl', 'ca', '-config', conf, '-extensions',
'req_extensions', '-days', '365', '-notext', '-in', csr,
'-out', crt, '-batch', '-subj', subj]
subprocess.check_call(cmd)
return crt
def _create_certificate(self, service, common_name):
subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name)
csr = os.path.join(self.ca_dir, 'certs', '%s.csr' % service)
key = os.path.join(self.ca_dir, 'certs', '%s.key' % service)
cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa', '-nodes',
'-keyout', key, '-out', csr, '-subj', subj]
subprocess.check_call(cmd)
crt = self._sign_csr(csr, service, common_name)
cmd = ['chown', '-R', '%s.%s' % (self.user, self.group), self.ca_dir]
subprocess.check_call(cmd)
log('Signed new CSR, crt @ %s' % crt, level=DEBUG)
return crt, key
def get_key_path(self, cn):
return os.path.join(self.ca_dir, 'certs', '%s.key' % cn)
def get_cert_path(self, cn):
return os.path.join(self.ca_dir, 'certs', '%s.crt' % cn)
def get_cert_and_key(self, common_name):
keypath = self.get_key_path(common_name)
crtpath = self.get_cert_path(common_name)
if not os.path.isfile(crtpath):
log("Creating certificate and key for {}.".format(common_name),
level=DEBUG)
crtpath, keypath = self._create_certificate(common_name,
common_name)
with open(crtpath, 'r') as f:
crt = f.read()
with open(keypath, 'r') as f:
key = f.read()
return crt, key
@property
def ca_cert_path(self):
return os.path.join(self.ca_dir, 'cacert.pem')
@property
def ca_key_path(self):
return os.path.join(self.ca_dir, 'private', 'cacert.key')
@property
def root_ca_cert_path(self):
return os.path.join(self.root_ca_dir, 'cacert.pem')
@property
def root_ca_key_path(self):
return os.path.join(self.root_ca_dir, 'private', 'cacert.key')
def get_ca_bundle(self):
with open(self.ca_cert_path) as f:
int_cert = f.read()
with open(self.root_ca_cert_path) as f:
root_cert = f.read()
# NOTE: ordering of certs in bundle matters!
return int_cert + root_cert

View File

@ -14,24 +14,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import glob
import grp
import hashlib
import json
import os
import pwd
import re
import shutil
import subprocess
import tarfile
import threading
import time
import urlparse
import uuid
import sys
from itertools import chain
from base64 import b64encode
from collections import OrderedDict
from copy import deepcopy
@ -39,7 +30,6 @@ from charmhelpers.contrib.hahelpers.cluster import (
is_elected_leader,
determine_api_port,
https,
peer_units,
get_hacluster_config,
)
@ -79,8 +69,6 @@ from charmhelpers.core.strutils import (
bool_from_string,
)
import charmhelpers.contrib.unison as unison
from charmhelpers.core.decorators import (
retry_on_exception,
)
@ -98,9 +86,6 @@ from charmhelpers.core.hookenv import (
related_units,
DEBUG,
INFO,
WARNING,
ERROR,
is_leader,
)
from charmhelpers.fetch import (
@ -111,13 +96,11 @@ from charmhelpers.fetch import (
)
from charmhelpers.core.host import (
mkdir,
service_restart,
service_stop,
service_start,
service_restart,
pwgen,
lsb_release,
write_file,
CompareHostReleases,
)
@ -125,11 +108,9 @@ from charmhelpers.contrib.peerstorage import (
peer_store_and_set,
peer_store,
peer_retrieve,
relation_set as relation_set_and_migrate_to_leader,
)
import keystone_context
import keystone_ssl as ssl
TEMPLATES = 'templates/'
@ -145,7 +126,6 @@ BASE_PACKAGES = [
'python-psycopg2',
'python-six',
'pwgen',
'unison',
'uuid',
]
@ -154,13 +134,11 @@ BASE_PACKAGES_SNAP = [
'openssl',
'python-six',
'pwgen',
'unison',
'uuid',
]
VERSION_PACKAGE = 'keystone'
SSH_USER = 'juju_keystone'
if snap_install_requested():
SNAP_BASE_DIR = "/snap/keystone/current"
SNAP_COMMON_DIR = "/var/snap/keystone/common"
@ -182,17 +160,9 @@ if snap_install_requested():
STORED_DEFAULT_DOMAIN_ID = ("{}/keystone.default_domain_id"
"".format(SNAP_LIB_DIR))
SERVICE_PASSWD_PATH = '{}/services.passwd'.format(SNAP_LIB_DIR)
SSH_USER_HOME = '/home/{}'.format(SSH_USER)
SYNC_FLAGS_DIR = '{}/juju_sync_flags/'.format(SSH_USER_HOME)
SYNC_DIR = '{}/juju_sync/'.format(SSH_USER_HOME)
SSL_SYNC_ARCHIVE = os.path.join(SYNC_DIR, 'juju-ssl-sync.tar')
SSL_DIR = '{}/juju_ssl/'.format(SNAP_LIB_DIR)
PKI_CERTS_DIR = os.path.join(SSL_DIR, 'pki')
POLICY_JSON = ('{}/keystone.conf.d/policy.json'
''.format(SNAP_COMMON_KEYSTONE_DIR))
BASE_SERVICES = ['snap.keystone.uwsgi', 'snap.keystone.nginx']
APACHE_SSL_DIR = '{}/keystone'.format(SSL_DIR)
else:
APACHE_SSL_DIR = '/etc/apache2/ssl/keystone'
KEYSTONE_USER = 'keystone'
@ -206,12 +176,6 @@ else:
STORED_ADMIN_DOMAIN_ID = "/var/lib/keystone/keystone.admin_domain_id"
STORED_DEFAULT_DOMAIN_ID = "/var/lib/keystone/keystone.default_domain_id"
SERVICE_PASSWD_PATH = '/var/lib/keystone/services.passwd'
SYNC_FLAGS_DIR = '/var/lib/keystone/juju_sync_flags/'
SYNC_DIR = '/var/lib/keystone/juju_sync/'
SSL_SYNC_ARCHIVE = os.path.join(SYNC_DIR, 'juju-ssl-sync.tar')
SSL_DIR = '/var/lib/keystone/juju_ssl/'
PKI_CERTS_DIR = os.path.join(SSL_DIR, 'pki')
POLICY_JSON = '/etc/keystone/policy.json'
BASE_SERVICES = [
'keystone',
@ -223,11 +187,7 @@ APACHE_CONF = '/etc/apache2/sites-available/openstack_https_frontend'
APACHE_24_CONF = '/etc/apache2/sites-available/openstack_https_frontend.conf'
MEMCACHED_CONF = '/etc/memcached.conf'
SSL_CA_NAME = 'Ubuntu Cloud'
CLUSTER_RES = 'grp_ks_vips'
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
SSL_SYNC_SEMAPHORE = threading.Semaphore()
SSL_DIRS = [SSL_DIR, APACHE_SSL_DIR, CA_CERT_PATH]
ADMIN_DOMAIN = 'admin_domain'
ADMIN_PROJECT = 'admin'
DEFAULT_DOMAIN = 'default'
@ -1311,594 +1271,6 @@ def is_password_changed(username, passwd):
return (_passwd is None or passwd != _passwd)
def ensure_ssl_dirs():
"""Ensure unison has access to these dirs."""
for path in [SYNC_FLAGS_DIR, SYNC_DIR]:
if not os.path.isdir(path):
mkdir(path, SSH_USER, KEYSTONE_USER, 0o775)
else:
ensure_permissions(path, user=SSH_USER, group=KEYSTONE_USER,
perms=0o775)
def ensure_permissions(path, user=None, group=None, perms=None, recurse=False,
maxdepth=50):
"""Set chownand chmod for path
Note that -1 for uid or gid result in no change.
"""
if user:
uid = pwd.getpwnam(user).pw_uid
else:
uid = -1
if group:
gid = grp.getgrnam(group).gr_gid
else:
gid = -1
os.chown(path, uid, gid)
if perms:
os.chmod(path, perms)
if recurse:
if not maxdepth:
log("Max recursion depth reached - skipping further recursion")
return
paths = glob.glob("%s/*" % (path))
for path in paths:
ensure_permissions(path, user=user, group=group, perms=perms,
recurse=recurse, maxdepth=maxdepth - 1)
def check_peer_actions():
"""Honour service action requests from sync master.
Check for service action request flags, perform the action then delete the
flag.
"""
restart = relation_get(attribute='restart-services-trigger')
if restart and os.path.isdir(SYNC_FLAGS_DIR):
for flagfile in glob.glob(os.path.join(SYNC_FLAGS_DIR, '*')):
flag = os.path.basename(flagfile)
key = re.compile("^(.+)?\.(.+)?\.(.+)")
res = re.search(key, flag)
if res:
source = res.group(1)
service = res.group(2)
action = res.group(3)
else:
key = re.compile("^(.+)?\.(.+)?")
res = re.search(key, flag)
source = res.group(1)
action = res.group(2)
# Don't execute actions requested by this unit.
if local_unit().replace('.', '-') != source:
if action == 'restart':
log("Running action='%s' on service '%s'" %
(action, service), level=DEBUG)
service_restart(service)
elif action == 'start':
log("Running action='%s' on service '%s'" %
(action, service), level=DEBUG)
service_start(service)
elif action == 'stop':
log("Running action='%s' on service '%s'" %
(action, service), level=DEBUG)
service_stop(service)
elif action == 'update-ca-certificates':
log("Running %s" % (action), level=DEBUG)
subprocess.check_call(['update-ca-certificates'])
elif action == 'ensure-pki-permissions':
log("Running %s" % (action), level=DEBUG)
ensure_pki_dir_permissions()
else:
log("Unknown action flag=%s" % (flag), level=WARNING)
try:
os.remove(flagfile)
except:
pass
def create_peer_service_actions(action, services):
"""Mark remote services for action.
Default action is restart. These action will be picked up by peer units
e.g. we may need to restart services on peer units after certs have been
synced.
"""
for service in services:
flagfile = os.path.join(SYNC_FLAGS_DIR, '%s.%s.%s' %
(local_unit().replace('/', '-'),
service.strip(), action))
log("Creating action %s" % (flagfile), level=DEBUG)
write_file(flagfile, content='', owner=SSH_USER, group=KEYSTONE_USER,
perms=0o744)
def create_peer_actions(actions):
for action in actions:
action = "%s.%s" % (local_unit().replace('/', '-'), action)
flagfile = os.path.join(SYNC_FLAGS_DIR, action)
log("Creating action %s" % (flagfile), level=DEBUG)
write_file(flagfile, content='', owner=SSH_USER, group=KEYSTONE_USER,
perms=0o744)
@retry_on_exception(3, base_delay=2, exc_type=subprocess.CalledProcessError)
def unison_sync(paths_to_sync):
"""Do unison sync and retry a few times if it fails since peers may not be
ready for sync.
Returns list of synced units or None if one or more peers was not synced.
"""
log('Synchronizing CA (%s) to all peers.' % (', '.join(paths_to_sync)),
level=INFO)
keystone_gid = grp.getgrnam(KEYSTONE_USER).gr_gid
# NOTE(dosaboy): This will sync to all peers who have already provided
# their ssh keys. If any existing peers have not provided their keys yet,
# they will be silently ignored.
unison.sync_to_peers(peer_interface='cluster', paths=paths_to_sync,
user=SSH_USER, verbose=True, gid=keystone_gid,
fatal=True)
synced_units = peer_units()
if len(unison.collect_authed_hosts('cluster')) != len(synced_units):
log("Not all peer units synced due to missing public keys", level=INFO)
return None
else:
return synced_units
def get_ssl_sync_request_units():
"""Get list of units that have requested to be synced.
NOTE: this must be called from cluster relation context.
"""
units = []
for unit in related_units():
settings = relation_get(unit=unit) or {}
rkeys = settings.keys()
key = re.compile("^ssl-sync-required-(.+)")
for rkey in rkeys:
res = re.search(key, rkey)
if res:
units.append(res.group(1))
return units
def is_ssl_cert_master(votes=None):
"""Return True if this unit is ssl cert master."""
votes = votes or get_ssl_cert_master_votes()
set_votes = set(votes)
# Discard unknown votes
if 'unknown' in set_votes:
set_votes.remove('unknown')
# This is the elected ssl-cert-master leader
if len(set_votes) == 1 and set_votes == set([local_unit()]):
log("This unit is the elected ssl-cert-master "
"{}".format(votes), level=DEBUG)
return True
# Contested election
if len(set_votes) > 1:
log("Did not get consensus from peers on who is ssl-cert-master "
"{}".format(votes), level=DEBUG)
return False
# Neither the elected ssl-cert-master leader nor the juju leader
if not is_leader():
return False
# Only the juju elected leader continues
# Singleton
if not peer_units():
log("This unit is a singleton and thefore ssl-cert-master",
level=DEBUG)
return True
# Early in the process and juju leader
if not set_votes:
log("This unit is the juju leader and there are no votes yet, "
"becoming the ssl-cert-master",
level=DEBUG)
return True
elif (len(set_votes) == 1 and set_votes != set([local_unit()]) and
is_leader()):
log("This unit is the juju leader but not yet ssl-cert-master "
"(current votes = {})".format(set_votes), level=DEBUG)
return False
# Should never reach here
log("Could not determine the ssl-cert-master. Missing edge case. "
"(current votes = {})".format(set_votes),
level=ERROR)
return False
def get_ssl_cert_master_votes():
"""Returns a list of unique votes."""
votes = []
# Gather election results from peers. These will need to be consistent.
for rid in relation_ids('cluster'):
for unit in related_units(rid):
m = relation_get(rid=rid, unit=unit,
attribute='ssl-cert-master')
if m is not None:
votes.append(m)
return list(set(votes))
def ensure_ssl_cert_master():
"""Ensure that an ssl cert master has been elected.
Normally the cluster leader will take control but we allow for this to be
ignored since this could be called before the cluster is ready.
"""
master_override = False
elect = is_elected_leader(CLUSTER_RES)
# If no peers we allow this unit to elect itsef as master and do
# sync immediately.
if not peer_units():
elect = True
master_override = True
if elect:
votes = get_ssl_cert_master_votes()
# We expect all peers to echo this setting
if not votes or 'unknown' in votes:
log("Notifying peers this unit is ssl-cert-master", level=INFO)
for rid in relation_ids('cluster'):
settings = {'ssl-cert-master': local_unit()}
relation_set(relation_id=rid, relation_settings=settings)
# Return now and wait for cluster-relation-changed (peer_echo) for
# sync.
return master_override
elif not is_ssl_cert_master(votes):
if not master_override:
log("Conscensus not reached - current master will need to "
"release", level=INFO)
return master_override
if not is_ssl_cert_master():
log("Not ssl cert master - skipping sync", level=INFO)
return False
return True
def stage_paths_for_sync(paths):
shutil.rmtree(SYNC_DIR)
ensure_ssl_dirs()
with tarfile.open(SSL_SYNC_ARCHIVE, 'w') as fd:
for path in paths:
if os.path.exists(path):
log("Adding path '%s' sync tarball" % (path), level=DEBUG)
fd.add(path)
else:
log("Path '%s' does not exist - not adding to sync "
"tarball" % (path), level=INFO)
ensure_permissions(SYNC_DIR, user=SSH_USER, group=KEYSTONE_USER,
perms=0o775, recurse=True)
def is_pki_enabled():
enable_pki = config('enable-pki')
if enable_pki and bool_from_string(enable_pki):
return True
return False
def ensure_pki_cert_paths():
certs = os.path.join(PKI_CERTS_DIR, 'certs')
privates = os.path.join(PKI_CERTS_DIR, 'privates')
not_exists = [p for p in [PKI_CERTS_DIR, certs, privates]
if not os.path.exists(p)]
if not_exists:
log("Configuring token signing cert paths", level=DEBUG)
perms = 0o775
for path in not_exists:
if not os.path.isdir(path):
mkdir(path=path, owner=SSH_USER, group=KEYSTONE_USER,
perms=perms)
else:
# Ensure accessible by ssh user and group (for sync).
ensure_permissions(path, user=SSH_USER, group=KEYSTONE_USER,
perms=perms)
def ensure_pki_dir_permissions():
# Ensure accessible by unison user and group (for sync).
ensure_permissions(PKI_CERTS_DIR, user=SSH_USER, group=KEYSTONE_USER,
perms=0o775, recurse=True)
def update_certs_if_available(f):
def _inner_update_certs_if_available(*args, **kwargs):
path = None
for rid in relation_ids('cluster'):
path = relation_get(attribute='ssl-cert-available-updates',
rid=rid, unit=local_unit())
if path and os.path.exists(path):
log("Updating certs from '%s'" % (path), level=DEBUG)
with tarfile.open(path) as fd:
files = ["/%s" % m.name for m in fd.getmembers()]
fd.extractall(path='/')
for syncfile in files:
ensure_permissions(syncfile, user=KEYSTONE_USER,
group=KEYSTONE_USER,
perms=0o744, recurse=True)
# Mark as complete
os.rename(path, "%s.complete" % (path))
else:
log("No cert updates available", level=DEBUG)
return f(*args, **kwargs)
return _inner_update_certs_if_available
def synchronize_ca(fatal=False):
"""Broadcast service credentials to peers.
By default a failure to sync is fatal and will result in a raised
exception.
This function uses a relation setting 'ssl-cert-master' to get some
leader stickiness while synchronisation is being carried out. This ensures
that the last host to create and broadcast cetificates has the option to
complete actions before electing the new leader as sync master.
Returns a dictionary of settings to be set on the cluster relation.
"""
paths_to_sync = []
peer_service_actions = {'restart': []}
peer_actions = []
if bool_from_string(config('https-service-endpoints')):
log("Syncing all endpoint certs since https-service-endpoints=True",
level=DEBUG)
paths_to_sync.append(SSL_DIR)
paths_to_sync.append(CA_CERT_PATH)
# We need to restart peer apache services to ensure they have picked up
# new ssl keys.
peer_service_actions['restart'].append('apache2')
peer_actions.append('update-ca-certificates')
if bool_from_string(config('use-https')):
log("Syncing keystone-endpoint certs since use-https=True",
level=DEBUG)
paths_to_sync.append(SSL_DIR)
paths_to_sync.append(APACHE_SSL_DIR)
paths_to_sync.append(CA_CERT_PATH)
# We need to restart peer apache services to ensure they have picked up
# new ssl keys.
peer_service_actions['restart'].append('apache2')
peer_actions.append('update-ca-certificates')
# NOTE: certs needed for token signing e.g. pki and revocation list query.
log("Syncing token certs", level=DEBUG)
# pike dropped support for PKI token; only run on releases <= pike
if CompareOpenStackReleases(os_release('keystone-common')) <= 'pike':
paths_to_sync.append(PKI_CERTS_DIR)
peer_actions.append('ensure-pki-permissions')
if not paths_to_sync:
log("Nothing to sync - skipping", level=DEBUG)
return {}
if not os.path.isdir(SYNC_FLAGS_DIR):
mkdir(SYNC_FLAGS_DIR, SSH_USER, KEYSTONE_USER, 0o775)
restart_trigger = None
for action, services in peer_service_actions.iteritems():
services = set(services)
if services:
restart_trigger = str(uuid.uuid4())
create_peer_service_actions(action, services)
create_peer_actions(peer_actions)
paths_to_sync = list(set(paths_to_sync))
stage_paths_for_sync(paths_to_sync)
hash1 = hashlib.sha256()
for path in paths_to_sync:
update_hash_from_path(hash1, path)
cluster_rel_settings = {'ssl-cert-available-updates': SSL_SYNC_ARCHIVE,
'sync-hash': hash1.hexdigest()}
synced_units = unison_sync([SSL_SYNC_ARCHIVE, SYNC_FLAGS_DIR])
if synced_units:
# Format here needs to match that used when peers request sync
synced_units = [u.replace('/', '-') for u in synced_units]
ssl_synced_units = \
json.dumps(synced_units)
# NOTE(hopem): we pull this onto the leader settings to avoid
# unnecessary cluster relation noise. This is possible because the
# setting is only needed by the cert master.
if 'ssl-synced-units' not in leader_get():
rid = relation_ids('cluster')[0]
relation_set_and_migrate_to_leader(relation_id=rid,
**{'ssl-synced-units':
ssl_synced_units})
else:
leader_set({'ssl-synced-units': ssl_synced_units})
if restart_trigger:
log("Sending restart-services-trigger=%s to all peers" %
(restart_trigger), level=DEBUG)
cluster_rel_settings['restart-services-trigger'] = restart_trigger
log("Sync complete", level=DEBUG)
return cluster_rel_settings
def clear_ssl_synced_units():
"""Clear the 'synced' units record on the cluster relation.
If new unit sync reauests are set this will ensure that a sync occurs when
the sync master receives the requests.
"""
log("Clearing ssl sync units", level=DEBUG)
for rid in relation_ids('cluster'):
if 'ssl-synced-units' not in leader_get():
relation_set_and_migrate_to_leader(relation_id=rid,
**{'ssl-synced-units': None})
else:
leader_set({'ssl-synced-units': None})
def update_hash_from_path(hash, path, recurse_depth=10):
"""Recurse through path and update the provided hash for every file found.
"""
if not recurse_depth:
log("Max recursion depth (%s) reached for update_hash_from_path() at "
"path='%s' - not going any deeper" % (recurse_depth, path),
level=WARNING)
return
for p in glob.glob("%s/*" % path):
if os.path.isdir(p):
update_hash_from_path(hash, p, recurse_depth=recurse_depth - 1)
else:
with open(p, 'r') as fd:
hash.update(fd.read())
def synchronize_ca_if_changed(force=False, fatal=False):
"""Decorator to perform ssl cert sync if decorated function modifies them
in any way.
If force is True a sync is done regardless.
"""
def inner_synchronize_ca_if_changed1(f):
def inner_synchronize_ca_if_changed2(*args, **kwargs):
# Only sync master can do sync. Ensure (a) we are not nested and
# (b) a master is elected and we are it.
acquired = SSL_SYNC_SEMAPHORE.acquire(blocking=0)
try:
if not acquired:
log("Nested sync - ignoring", level=DEBUG)
return f(*args, **kwargs)
if not ensure_ssl_cert_master():
log("Not ssl-cert-master - ignoring sync", level=DEBUG)
return f(*args, **kwargs)
peer_settings = {}
if not force:
hash1 = hashlib.sha256()
for path in SSL_DIRS:
update_hash_from_path(hash1, path)
ret = f(*args, **kwargs)
hash2 = hashlib.sha256()
for path in SSL_DIRS:
update_hash_from_path(hash2, path)
if hash1.hexdigest() != hash2.hexdigest():
log("SSL certs have changed - syncing peers",
level=DEBUG)
peer_settings = synchronize_ca(fatal=fatal)
else:
log("SSL certs have not changed - skipping sync",
level=DEBUG)
else:
ret = f(*args, **kwargs)
log("Doing forced ssl cert sync", level=DEBUG)
peer_settings = synchronize_ca(fatal=fatal)
# If we are the sync master but not leader, ensure we have
# relinquished master status.
cluster_rids = relation_ids('cluster')
if cluster_rids:
master = relation_get('ssl-cert-master',
rid=cluster_rids[0],
unit=local_unit())
if not is_leader() and master == local_unit():
log("Re-electing ssl cert master.", level=INFO)
peer_settings['ssl-cert-master'] = 'unknown'
if peer_settings:
relation_set(relation_id=cluster_rids[0],
relation_settings=peer_settings)
return ret
finally:
SSL_SYNC_SEMAPHORE.release()
return inner_synchronize_ca_if_changed2
return inner_synchronize_ca_if_changed1
@synchronize_ca_if_changed(force=True, fatal=True)
def force_ssl_sync():
"""Force SSL sync to all peers.
This is useful if we need to relinquish ssl-cert-master status while
making sure that the new master has up-to-date certs.
"""
return
def ensure_ssl_dir():
"""Ensure juju ssl dir exists and is unsion read/writable."""
# NOTE(thedac) Snap service restarts will override permissions
# in SNAP_LIB_DIR including SSL_DIR
perms = 0o775
if not os.path.isdir(SSL_DIR):
mkdir(SSL_DIR, SSH_USER, KEYSTONE_USER, perms)
else:
ensure_permissions(SSL_DIR, user=SSH_USER, group=KEYSTONE_USER,
perms=perms)
def get_ca(user=KEYSTONE_USER, group=KEYSTONE_USER):
"""Initialize a new CA object if one hasn't already been loaded.
This will create a new CA or load an existing one.
"""
if not ssl.CA_SINGLETON:
ensure_ssl_dir()
d_name = '_'.join(SSL_CA_NAME.lower().split(' '))
ca = ssl.JujuCA(name=SSL_CA_NAME, user=user, group=group,
ca_dir=os.path.join(SSL_DIR,
'%s_intermediate_ca' % d_name),
root_ca_dir=os.path.join(SSL_DIR,
'%s_root_ca' % d_name))
# Ensure a master is elected. This should cover the following cases:
# * single unit == 'oldest' unit is elected as master
# * multi unit + not clustered == 'oldest' unit is elcted as master
# * multi unit + clustered == cluster leader is elected as master
ensure_ssl_cert_master()
ssl.CA_SINGLETON.append(ca)
return ssl.CA_SINGLETON[0]
def relation_list(rid):
cmd = [
'relation-list',
@ -2017,8 +1389,6 @@ def add_service_to_keystone(relation_id=None, remote_unit=None):
relation_data["api_version"] = get_api_version()
relation_data["admin_domain_id"] = leader_get(
attribute='admin_domain_id')
# Get and pass CA bundle settings
relation_data.update(get_ssl_ca_settings())
# Allow the remote service to request creation of any additional
# roles. Currently used by Horizon
@ -2075,7 +1445,6 @@ def add_service_to_keystone(relation_id=None, remote_unit=None):
endpoints[ep][x] = v
services = []
https_cn = None
for ep in endpoints:
# weed out any unrelated relation stuff Juju might have added
# by ensuring each possible endpiont has appropriate fields
@ -2145,25 +1514,6 @@ def add_service_to_keystone(relation_id=None, remote_unit=None):
"admin_domain_id": leader_get(attribute='admin_domain_id'),
}
# generate or get a new cert/key for service if set to manage certs.
https_service_endpoints = config('https-service-endpoints')
if https_service_endpoints and bool_from_string(https_service_endpoints):
ca = get_ca(user=SSH_USER)
# NOTE(jamespage) may have multiple cns to deal with to iterate
https_cns = set(https_cns)
for https_cn in https_cns:
cert, key = ca.get_cert_and_key(common_name=https_cn)
relation_data['ssl_cert_{}'.format(https_cn)] = b64encode(cert)
relation_data['ssl_key_{}'.format(https_cn)] = b64encode(key)
# NOTE(jamespage) for backwards compatibility
cert, key = ca.get_cert_and_key(common_name=internal_cn)
relation_data['ssl_cert'] = b64encode(cert)
relation_data['ssl_key'] = b64encode(key)
# Get and pass CA bundle settings
relation_data.update(get_ssl_ca_settings())
peer_store_and_set(relation_id=relation_id, **relation_data)
# NOTE(dosaboy): '__null__' settings are for peer relation only so that
# settings can flushed so we filter them out for non-peer relation.
@ -2229,30 +1579,10 @@ def add_credentials_to_keystone(relation_id=None, remote_unit=None):
}
if domain:
relation_data['domain'] = domain
# Get and pass CA bundle settings
relation_data.update(get_ssl_ca_settings())
peer_store_and_set(relation_id=relation_id, **relation_data)
def get_ssl_ca_settings():
""" Get the Certificate Authority settings required to use the CA
:returns: Dictionary with https_keystone and ca_cert set
"""
ca_data = {}
https_service_endpoints = config('https-service-endpoints')
if (https_service_endpoints and
bool_from_string(https_service_endpoints)):
# Pass CA cert as client will need it to
# verify https connections
ca = get_ca(user=SSH_USER)
ca_bundle = ca.get_ca_bundle()
ca_data['https_keystone'] = 'True'
ca_data['ca_cert'] = b64encode(ca_bundle)
return ca_data
def get_protocol():
"""Determine the http protocol

View File

@ -37,68 +37,11 @@ class TestKeystoneContexts(CharmTestCase):
def setUp(self):
super(TestKeystoneContexts, self).setUp(context, TO_PATCH)
def test_is_cert_provided_in_config(self):
config = {'ssl_cert': 'somecert', 'ssl_key': 'greatkey'}
def fake_config(key):
return config.get(key)
self.config.side_effect = fake_config
self.assertTrue(context.is_cert_provided_in_config())
del config['ssl_cert']
self.assertFalse(context.is_cert_provided_in_config())
@patch.object(context, 'mkdir')
@patch('keystone_utils.get_ca')
@patch('keystone_utils.ensure_permissions')
@patch('keystone_utils.determine_ports', lambda: None)
@patch('keystone_utils.is_ssl_cert_master', lambda: False)
@patch.object(context, 'is_cert_provided_in_config', lambda: False)
@patch.object(context, 'log', lambda *args, **kwargs: None)
def test_apache_ssl_context_ssl_not_master(self, mock_ensure_permissions,
mock_get_ca, mock_mkdir):
context.ApacheSSLContext().configure_cert('foo')
context.ApacheSSLContext().configure_ca()
self.assertTrue(mock_mkdir.called)
self.assertTrue(mock_ensure_permissions.called)
self.assertFalse(mock_get_ca.called)
@patch('keystone_utils.ensure_permissions')
@patch.object(context, 'install_ca_cert')
@patch.object(context, 'b64decode')
@patch.object(context, 'mkdir', lambda *args: None)
@patch('keystone_utils.get_ca', lambda: None)
@patch('keystone_utils.determine_ports', lambda: None)
@patch('keystone_utils.is_ssl_cert_master', lambda: True)
@patch.object(context, 'log', lambda *args, **kwargs: None)
def test_apache_ssl_context_ssl_configure_ca(self, mock_b64decode,
mock_install_ca_cert,
mock_ensure_permissions):
config = {'ssl_cert': 'somecert', 'ssl_key': 'greatkey'}
def fake_config(key):
return config.get(key)
self.config.side_effect = fake_config
context.ApacheSSLContext().configure_ca()
self.assertFalse(mock_b64decode.called)
self.assertFalse(mock_install_ca_cert.called)
self.assertFalse(mock_ensure_permissions.called)
config['ssl_ca'] = 'foofoofalalala'
context.ApacheSSLContext().configure_ca()
self.assertTrue(mock_b64decode.called)
self.assertTrue(mock_install_ca_cert.called)
self.assertTrue(mock_ensure_permissions.called)
@patch('charmhelpers.contrib.hahelpers.cluster.relation_ids')
@patch('charmhelpers.contrib.openstack.ip.unit_get')
@patch('charmhelpers.contrib.openstack.ip.service_name')
@patch('charmhelpers.contrib.openstack.ip.config')
@patch('keystone_utils.determine_ports')
@patch('keystone_utils.is_ssl_cert_master')
@patch('charmhelpers.contrib.openstack.context.config')
@patch('charmhelpers.contrib.openstack.context.is_clustered')
@patch('charmhelpers.contrib.openstack.context.determine_apache_port')
@ -113,15 +56,12 @@ class TestKeystoneContexts(CharmTestCase):
mock_determine_apache_port,
mock_is_clustered,
mock_config,
mock_is_ssl_cert_master,
mock_determine_ports,
mock_ip_config,
mock_service_name,
mock_ip_unit_get,
mock_rel_ids,
):
mock_relation_ids.return_value = []
mock_is_ssl_cert_master.return_value = True
mock_https.return_value = True
mock_unit_get.return_value = '1.2.3.4'
mock_ip_unit_get.return_value = '1.2.3.4'

View File

@ -13,7 +13,6 @@
# limitations under the License.
import os
import uuid
import sys
from mock import call, patch, MagicMock
@ -43,8 +42,6 @@ with patch('charmhelpers.contrib.hardening.harden.harden') as mock_dec:
with patch('keystone_utils.run_in_apache') as mock_run_in_apache:
import keystone_hooks as hooks
from charmhelpers.contrib import unison
utils.register_configs = _reg
utils.restart_map = _map
@ -53,7 +50,6 @@ TO_PATCH = [
'Hooks',
'config',
'log',
'local_unit',
'filter_installed_packages',
'relation_ids',
'relation_set',
@ -89,17 +85,13 @@ TO_PATCH = [
'migrate_database',
'ensure_initial_admin',
'add_service_to_keystone',
'synchronize_ca_if_changed',
'update_nrpe_config',
'ensure_ssl_dirs',
'is_db_ready',
'create_or_show_domain',
'get_api_version',
# other
'check_call',
'execd_preinstall',
'mkdir',
'os',
# ip
'get_iface_for_address',
'get_netmask_for_address',
@ -123,10 +115,9 @@ class KeystoneRelationTests(CharmTestCase):
self.snap_install_requested.return_value = False
@patch.object(utils, 'os_release')
@patch.object(unison, 'ensure_user')
@patch.object(hooks, 'service_stop', lambda *args: None)
@patch.object(hooks, 'service_start', lambda *args: None)
def test_install_hook(self, ensure_user, os_release):
def test_install_hook(self, os_release):
os_release.return_value = 'havana'
self.run_in_apache.return_value = False
repo = 'cloud:precise-grizzly'
@ -134,33 +125,29 @@ class KeystoneRelationTests(CharmTestCase):
hooks.install()
self.assertTrue(self.execd_preinstall.called)
self.configure_installation_source.assert_called_with(repo)
ensure_user.assert_called_with(user=self.ssh_user, group='keystone')
self.assertTrue(self.apt_update.called)
self.apt_install.assert_called_with(
['apache2', 'haproxy', 'keystone', 'openssl', 'pwgen',
'python-keystoneclient', 'python-mysqldb', 'python-psycopg2',
'python-six', 'unison', 'uuid'], fatal=True)
'python-six', 'uuid'], fatal=True)
self.disable_unused_apache_sites.assert_not_called()
@patch.object(utils, 'os_release')
@patch.object(unison, 'ensure_user')
@patch.object(hooks, 'service_stop', lambda *args: None)
@patch.object(hooks, 'service_start', lambda *args: None)
def test_install_hook_apache2(self, ensure_user, os_release):
def test_install_hook_apache2(self, os_release):
os_release.return_value = 'havana'
self.run_in_apache.return_value = True
repo = 'cloud:xenial-newton'
self.test_config.set('openstack-origin', repo)
self.os.path.exists.return_value = True
hooks.install()
self.assertTrue(self.execd_preinstall.called)
self.configure_installation_source.assert_called_with(repo)
ensure_user.assert_called_with(user=self.ssh_user, group='keystone')
self.assertTrue(self.apt_update.called)
self.apt_install.assert_called_with(
['apache2', 'haproxy', 'keystone', 'openssl', 'pwgen',
'python-keystoneclient', 'python-mysqldb', 'python-psycopg2',
'python-six', 'unison', 'uuid'], fatal=True)
'python-six', 'uuid'], fatal=True)
self.disable_unused_apache_sites.assert_called_with()
mod_ch_openstack_utils = 'charmhelpers.contrib.openstack.utils'
@ -196,12 +183,9 @@ class KeystoneRelationTests(CharmTestCase):
hostname='192.168.20.1')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch.object(hooks, 'CONFIGS')
def test_db_changed_missing_relation_data(self, configs,
mock_ensure_ssl_cert_master,
mock_log):
mock_ensure_ssl_cert_master.return_value = False
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = []
hooks.db_changed()
@ -212,20 +196,15 @@ class KeystoneRelationTests(CharmTestCase):
@patch.object(hooks, 'update_all_identity_relation_units')
def _shared_db_test(self, configs, unit_name, mock_update_all):
self.relation_get.return_value = 'keystone/0 keystone/3'
self.local_unit.return_value = unit_name
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['shared-db']
configs.write = MagicMock()
hooks.db_changed()
@patch.object(hooks, 'leader_init_db_if_ready')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch.object(hooks, 'CONFIGS')
def test_db_changed(self, configs,
mock_ensure_ssl_cert_master,
leader_init):
def test_db_changed(self, configs, leader_init):
self.os_release.return_value = 'havana'
mock_ensure_ssl_cert_master.return_value = False
self._shared_db_test(configs, 'keystone/3')
self.assertEqual([call('/etc/keystone/keystone.conf')],
configs.write.call_args_list)
@ -236,37 +215,16 @@ class KeystoneRelationTests(CharmTestCase):
@patch.object(hooks, 'run_in_apache')
@patch.object(hooks, 'is_db_initialised')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch('keystone_utils.ensure_ssl_dirs')
@patch.object(hooks, 'ensure_permissions')
@patch.object(hooks, 'ensure_pki_cert_paths')
@patch.object(hooks, 'ensure_pki_dir_permissions')
@patch.object(hooks, 'ensure_ssl_dir')
@patch.object(hooks, 'is_ssl_cert_master')
@patch.object(hooks, 'send_ssl_sync_request')
@patch.object(hooks, 'peer_units')
@patch.object(hooks, 'admin_relation_changed')
@patch.object(hooks, 'cluster_joined')
@patch.object(unison, 'ensure_user')
@patch.object(unison, 'get_homedir')
@patch.object(hooks, 'CONFIGS')
@patch.object(hooks, 'identity_changed')
@patch.object(hooks, 'configure_https')
def test_config_changed_no_upgrade_leader(self, configure_https,
identity_changed,
configs, get_homedir,
ensure_user,
configs,
mock_cluster_joined,
admin_relation_changed,
mock_peer_units,
mock_send_ssl_sync_request,
mock_is_ssl_cert_master,
mock_ensure_ssl_dir,
mock_ensure_pki_cert_paths,
mock_ensure_permissions,
mock_ensure_pki_dir_permissions,
mock_ensure_ssl_dirs,
mock_ensure_ssl_cert_master,
mock_log,
mock_is_db_initialised,
mock_run_in_apache,
@ -282,19 +240,12 @@ class KeystoneRelationTests(CharmTestCase):
self.relation_ids.side_effect = fake_relation_ids
mock_run_in_apache.return_value = False
mock_is_ssl_cert_master.return_value = True
mock_is_db_initialised.return_value = True
self.is_db_ready.return_value = True
self.openstack_upgrade_available.return_value = False
self.is_elected_leader.return_value = True
# avoid having to mock syncer
mock_ensure_ssl_cert_master.return_value = False
mock_peer_units.return_value = []
self.related_units.return_value = ['unit/0']
hooks.config_changed()
ensure_user.assert_called_with(user=self.ssh_user, group='keystone')
get_homedir.assert_called_with(self.ssh_user)
self.save_script_rc.assert_called_with()
configure_https.assert_called_with()
@ -309,33 +260,14 @@ class KeystoneRelationTests(CharmTestCase):
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'run_in_apache')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch('keystone_utils.ensure_ssl_dirs')
@patch.object(hooks, 'ensure_permissions')
@patch.object(hooks, 'ensure_pki_cert_paths')
@patch.object(hooks, 'ensure_pki_dir_permissions')
@patch.object(hooks, 'ensure_ssl_dir')
@patch.object(hooks, 'peer_units')
@patch.object(hooks, 'is_ssl_cert_master')
@patch.object(hooks, 'cluster_joined')
@patch.object(unison, 'ensure_user')
@patch.object(unison, 'get_homedir')
@patch.object(hooks, 'CONFIGS')
@patch.object(hooks, 'identity_changed')
@patch.object(hooks, 'configure_https')
def test_config_changed_no_upgrade_not_leader(self, configure_https,
identity_changed,
configs, get_homedir,
ensure_user,
configs,
mock_cluster_joined,
mock_is_ssl_cert_master,
mock_peer_units,
mock_ensure_ssl_dir,
mock_ensure_permissions,
mock_ensure_pki_cert_paths,
mock_ensure_pki_permissions,
ensure_ssl_dirs,
mock_ensure_ssl_cert_master,
mock_log,
mock_run_in_apache, update,
mock_update_domains):
@ -349,15 +281,9 @@ class KeystoneRelationTests(CharmTestCase):
self.relation_ids.side_effect = fake_relation_ids
mock_run_in_apache.return_value = False
mock_is_ssl_cert_master.return_value = True
mock_peer_units.return_value = []
self.openstack_upgrade_available.return_value = False
self.is_elected_leader.return_value = False
mock_ensure_ssl_cert_master.return_value = False
hooks.config_changed()
ensure_user.assert_called_with(user=self.ssh_user, group='keystone')
get_homedir.assert_called_with(self.ssh_user)
self.assertFalse(mock_cluster_joined.called)
self.save_script_rc.assert_called_with()
@ -373,36 +299,16 @@ class KeystoneRelationTests(CharmTestCase):
@patch.object(hooks, 'run_in_apache')
@patch.object(hooks, 'is_db_initialised')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch('keystone_utils.ensure_ssl_dirs')
@patch.object(hooks, 'ensure_permissions')
@patch.object(hooks, 'ensure_pki_cert_paths')
@patch.object(hooks, 'ensure_pki_dir_permissions')
@patch.object(hooks, 'ensure_ssl_dir')
@patch.object(hooks, 'is_ssl_cert_master')
@patch.object(hooks, 'send_ssl_sync_request')
@patch.object(hooks, 'peer_units')
@patch.object(hooks, 'admin_relation_changed')
@patch.object(hooks, 'cluster_joined')
@patch.object(unison, 'ensure_user')
@patch.object(unison, 'get_homedir')
@patch.object(hooks, 'CONFIGS')
@patch.object(hooks, 'identity_changed')
@patch.object(hooks, 'configure_https')
def test_config_changed_with_openstack_upgrade(self, configure_https,
identity_changed,
configs, get_homedir,
ensure_user, cluster_joined,
configs,
cluster_joined,
admin_relation_changed,
mock_peer_units,
mock_send_ssl_sync_request,
mock_is_ssl_cert_master,
mock_ensure_ssl_dir,
mock_ensure_permissions,
mock_ensure_pki_cert_paths,
mock_ensure_pki_permissions,
mock_ensure_ssl_dirs,
mock_ensure_ssl_cert_master,
mock_log,
mock_is_db_initialised,
mock_run_in_apache,
@ -417,19 +323,12 @@ class KeystoneRelationTests(CharmTestCase):
self.relation_ids.side_effect = fake_relation_ids
mock_run_in_apache.return_value = False
mock_is_ssl_cert_master.return_value = True
self.is_db_ready.return_value = True
mock_is_db_initialised.return_value = True
self.openstack_upgrade_available.return_value = True
self.is_elected_leader.return_value = True
# avoid having to mock syncer
mock_ensure_ssl_cert_master.return_value = False
mock_peer_units.return_value = []
self.related_units.return_value = ['unit/0']
hooks.config_changed()
ensure_user.assert_called_with(user=self.ssh_user, group='keystone')
get_homedir.assert_called_with(self.ssh_user)
self.assertTrue(self.do_openstack_upgrade_reexec.called)
@ -442,31 +341,16 @@ class KeystoneRelationTests(CharmTestCase):
@patch.object(hooks, 'os_release')
@patch.object(hooks, 'run_in_apache')
@patch.object(hooks, 'initialise_pki')
@patch.object(hooks, 'is_db_initialised')
@patch.object(hooks, 'ensure_ssl_dir')
@patch.object(hooks, 'configure_https')
@patch.object(hooks, 'is_ssl_cert_master')
@patch.object(hooks, 'peer_units')
@patch.object(unison, 'get_homedir')
@patch.object(unison, 'ensure_user')
@patch('keystone_utils.ensure_ssl_cert_master')
def test_config_changed_with_openstack_upgrade_action(self,
ensure_ssl_cert,
ensure_user,
get_home,
peer_units, is_ssl,
config_https,
ensure_ssl_dir,
mock_db_init,
mock_initialise_pki,
mock_run_in_apache,
os_release):
os_release.return_value = 'ocata'
self.enable_memcache.return_value = False
mock_run_in_apache.return_value = False
ensure_ssl_cert.return_value = False
peer_units.return_value = []
self.openstack_upgrade_available.return_value = True
self.test_config.set('action-managed-upgrade', True)
@ -477,17 +361,18 @@ class KeystoneRelationTests(CharmTestCase):
@patch.object(hooks, 'is_db_initialised')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch.object(hooks, 'hashlib')
@patch.object(hooks, 'send_notifications')
def test_identity_changed_leader(self, mock_send_notifications,
mock_hashlib, mock_ensure_ssl_cert_master,
mock_log, mock_is_db_initialised):
self.expect_ha.return_value = False
mock_is_db_initialised.return_value = True
self.is_db_ready.return_value = True
self.is_service_present.return_value = True
mock_ensure_ssl_cert_master.return_value = False
self.relation_get.return_value = {
'public_url': 'http://dummy.local',
'admin_url': 'http://dummy.local',
'internal_url': 'http://dummy.local',
}
hooks.identity_changed(
relation_id='identity-service:0',
remote_unit='unit/0')
@ -500,31 +385,26 @@ class KeystoneRelationTests(CharmTestCase):
@patch.object(hooks, 'is_db_initialised')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch.object(hooks, 'hashlib')
@patch.object(hooks, 'send_notifications')
def test_identity_changed_leader_no_neutron(self, mock_send_notifications,
mock_hashlib,
mock_ensure_ssl_cert_master,
mock_log,
mock_is_db_initialised):
self.expect_ha.return_value = False
mock_is_db_initialised.return_value = True
self.is_db_ready.return_value = True
self.is_service_present.return_value = False
mock_ensure_ssl_cert_master.return_value = False
self.relation_get.return_value = {
'public_url': 'http://dummy.local',
'admin_url': 'http://dummy.local',
'internal_url': 'http://dummy.local',
}
hooks.identity_changed(
relation_id='identity-service:0',
remote_unit='unit/0')
self.assertFalse(self.delete_service_entry.called)
@patch.object(hooks, 'local_unit')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
def test_identity_changed_no_leader(self, mock_ensure_ssl_cert_master,
mock_log, mock_local_unit):
mock_ensure_ssl_cert_master.return_value = False
mock_local_unit.return_value = 'unit/0'
def test_identity_changed_no_leader(self, mock_log):
self.is_elected_leader.return_value = False
hooks.identity_changed(
relation_id='identity-service:0',
@ -533,60 +413,18 @@ class KeystoneRelationTests(CharmTestCase):
self.log.assert_called_with(
'Deferring identity_changed() to service leader.')
@patch.object(hooks, 'send_ssl_sync_request')
@patch.object(hooks, 'local_unit')
@patch.object(hooks, 'peer_units')
@patch.object(unison, 'ssh_authorized_peers')
def test_cluster_joined(self, ssh_authorized_peers, mock_peer_units,
mock_local_unit, mock_send_ssl_sync_request):
mock_local_unit.return_value = 'unit/0'
mock_peer_units.return_value = ['unit/0']
hooks.cluster_joined()
ssh_authorized_peers.assert_called_with(
user=self.ssh_user, group='juju_keystone',
peer_interface='cluster', ensure_local_user=True)
self.assertTrue(mock_send_ssl_sync_request.called)
mock_send_ssl_sync_request.reset_mock()
hooks.cluster_joined(rid='foo:1', ssl_sync_request=True)
self.assertTrue(mock_send_ssl_sync_request.called)
mock_send_ssl_sync_request.reset_mock()
hooks.cluster_joined(rid='foo:1', ssl_sync_request=False)
self.assertFalse(mock_send_ssl_sync_request.called)
@patch.object(hooks, 'relation_get_and_migrate')
@patch.object(hooks, 'initialise_pki')
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'get_ssl_sync_request_units')
@patch.object(hooks, 'is_ssl_cert_master')
@patch.object(hooks, 'peer_units')
@patch('keystone_utils.relation_ids')
@patch('keystone_utils.config')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch('keystone_utils.synchronize_ca')
@patch.object(hooks, 'check_peer_actions')
@patch.object(unison, 'ssh_authorized_peers')
@patch.object(hooks, 'CONFIGS')
def test_cluster_changed(self, configs, ssh_authorized_peers,
check_peer_actions, mock_synchronize_ca,
mock_ensure_ssl_cert_master,
def test_cluster_changed(self, configs,
mock_log, mock_config, mock_relation_ids,
mock_peer_units,
mock_is_ssl_cert_master,
mock_get_ssl_sync_request_units,
mock_update_all_identity_relation_units,
mock_initialise_pki,
mock_relation_get_and_migrate):
mock_update_all_identity_relation_units):
relation_settings = {'foo_passwd': '123',
'identity-service:16_foo': 'bar'}
mock_relation_get_and_migrate.return_value = None
mock_is_ssl_cert_master.return_value = False
mock_peer_units.return_value = ['unit/0']
mock_ensure_ssl_cert_master.return_value = False
mock_relation_ids.return_value = []
self.is_leader.return_value = False
@ -601,13 +439,8 @@ class KeystoneRelationTests(CharmTestCase):
mock_config.return_value = None
hooks.cluster_changed()
whitelist = ['_passwd', 'identity-service:', 'db-initialised',
'ssl-cert-available-updates', 'ssl-cert-master']
whitelist = ['_passwd', 'identity-service:', 'db-initialised']
self.peer_echo.assert_called_with(force=True, includes=whitelist)
ssh_authorized_peers.assert_called_with(
user=self.ssh_user, group='juju_keystone',
peer_interface='cluster', ensure_local_user=True)
self.assertFalse(mock_synchronize_ca.called)
self.assertTrue(configs.write_all.called)
@patch.object(hooks, 'update_all_identity_relation_units')
@ -786,54 +619,40 @@ class KeystoneRelationTests(CharmTestCase):
self.assertTrue(self.update_dns_ha_resource_params.called)
self.relation_set.assert_called_with(**args)
@patch.object(utils, 'peer_retrieve')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch('keystone_utils.synchronize_ca')
@patch.object(hooks, 'CONFIGS')
def test_ha_relation_changed_not_clustered_not_leader(self, configs,
mock_synchronize_ca,
mock_is_master,
mock_log):
mock_is_master.return_value = False
mock_log,
mock_peer_retrieve):
self.relation_get.return_value = False
self.is_elected_leader.return_value = False
hooks.ha_changed()
self.assertTrue(configs.write_all.called)
self.assertFalse(mock_synchronize_ca.called)
@patch.object(hooks, 'is_ssl_cert_master')
@patch.object(hooks, 'update_all_identity_relation_units_force_sync')
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'is_db_initialised')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch.object(hooks, 'identity_changed')
@patch.object(hooks, 'CONFIGS')
def test_ha_relation_changed_clustered_leader(self, configs,
identity_changed,
mock_ensure_ssl_cert_master,
mock_log,
mock_is_db_initialised,
update, cert_master):
update):
mock_is_db_initialised.return_value = True
self.is_db_ready.return_value = True
mock_ensure_ssl_cert_master.return_value = False
self.relation_get.return_value = True
self.is_elected_leader.return_value = True
self.relation_ids.return_value = ['identity-service:0']
self.related_units.return_value = ['unit/0']
cert_master.return_value = True
hooks.ha_changed()
self.assertTrue(configs.write_all.called)
self.assertTrue(update.called)
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch.object(hooks, 'CONFIGS')
def test_configure_https_enable(self, configs, mock_ensure_ssl_cert_master,
mock_log):
mock_ensure_ssl_cert_master.return_value = False
def test_configure_https_enable(self, configs, mock_log):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['https']
configs.write = MagicMock()
@ -844,12 +663,8 @@ class KeystoneRelationTests(CharmTestCase):
self.check_call.assert_called_with(cmd)
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch.object(hooks, 'CONFIGS')
def test_configure_https_disable(self, configs,
mock_ensure_ssl_cert_master,
mock_log):
mock_ensure_ssl_cert_master.return_value = False
def test_configure_https_disable(self, configs, mock_log):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['']
configs.write = MagicMock()
@ -865,16 +680,7 @@ class KeystoneRelationTests(CharmTestCase):
@patch.object(hooks, 'is_db_initialised')
@patch('keystone_utils.log')
@patch('keystone_utils.relation_ids')
@patch('keystone_utils.is_elected_leader')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch('keystone_utils.update_hash_from_path')
@patch('keystone_utils.synchronize_ca')
@patch.object(unison, 'ssh_authorized_peers')
def test_upgrade_charm_leader(self, ssh_authorized_peers,
mock_synchronize_ca,
mock_update_hash_from_path,
mock_ensure_ssl_cert_master,
mock_is_elected_leader,
def test_upgrade_charm_leader(self,
mock_relation_ids,
mock_log,
mock_is_db_initialised,
@ -884,21 +690,11 @@ class KeystoneRelationTests(CharmTestCase):
os_release.return_value = 'havana'
mock_is_db_initialised.return_value = True
mock_is_db_ready.return_value = True
mock_is_elected_leader.return_value = False
mock_relation_ids.return_value = []
mock_ensure_ssl_cert_master.return_value = True
# Ensure always returns diff
mock_update_hash_from_path.side_effect = \
lambda hash, *args, **kwargs: hash.update(str(uuid.uuid4()))
self.is_elected_leader.return_value = True
self.filter_installed_packages.return_value = []
hooks.upgrade_charm()
self.assertTrue(self.apt_install.called)
ssh_authorized_peers.assert_called_with(
user=self.ssh_user, group='juju_keystone',
peer_interface='cluster', ensure_local_user=True)
self.assertTrue(mock_synchronize_ca.called)
self.assertTrue(update.called)
@patch.object(hooks, 'update_all_identity_relation_units')
@ -1039,33 +835,22 @@ class KeystoneRelationTests(CharmTestCase):
# Still updates relations
self.assertTrue(self.relation_ids.called)
@patch.object(utils, 'peer_retrieve')
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(utils, 'os_release')
@patch('keystone_utils.log')
@patch('keystone_utils.relation_ids')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch('keystone_utils.update_hash_from_path')
@patch.object(unison, 'ssh_authorized_peers')
def test_upgrade_charm_not_leader(self, ssh_authorized_peers,
mock_update_hash_from_path,
mock_ensure_ssl_cert_master,
def test_upgrade_charm_not_leader(self,
mock_relation_ids,
mock_log,
os_release, update):
os_release, update, mock_peer_retrieve):
os_release.return_value = 'havana'
mock_relation_ids.return_value = []
mock_ensure_ssl_cert_master.return_value = False
# Ensure always returns diff
mock_update_hash_from_path.side_effect = \
lambda hash, *args, **kwargs: hash.update(str(uuid.uuid4()))
self.is_elected_leader.return_value = False
self.filter_installed_packages.return_value = []
mock_peer_retrieve.return_value = 'true'
self.is_elected_leader.return_value = False
hooks.upgrade_charm()
self.assertTrue(self.apt_install.called)
ssh_authorized_peers.assert_called_with(
user=self.ssh_user, group='juju_keystone',
peer_interface='cluster', ensure_local_user=True)
self.assertTrue(self.log.called)
self.assertFalse(update.called)

View File

@ -15,7 +15,6 @@
from mock import patch, call, MagicMock
from test_utils import CharmTestCase
import os
from base64 import b64encode
import subprocess
os.environ['JUJU_UNIT_NAME'] = 'keystone'
@ -30,7 +29,6 @@ TO_PATCH = [
'config',
'os_release',
'log',
'get_ca',
'create_role',
'create_service_entry',
'create_endpoint_template',
@ -41,12 +39,9 @@ TO_PATCH = [
'get_os_codename_install_source',
'grant_role',
'configure_installation_source',
'is_elected_leader',
'is_ssl_cert_master',
'https',
'lsb_release',
'peer_store_and_set',
'service_restart',
'service_stop',
'service_start',
'snap_install_requested',
@ -66,7 +61,6 @@ TO_PATCH = [
'time',
'pwgen',
'os_application_version_set',
'is_leader',
'reset_os_release',
]
@ -174,6 +168,7 @@ class TestKeystoneUtils(CharmTestCase):
ex = utils.BASE_PACKAGES_SNAP + ['memcached']
self.assertEqual(set(ex), set(result))
@patch.object(utils, 'is_elected_leader')
@patch.object(utils, 'disable_unused_apache_sites')
@patch('os.path.exists')
@patch.object(utils, 'run_in_apache')
@ -181,11 +176,11 @@ class TestKeystoneUtils(CharmTestCase):
@patch.object(utils, 'migrate_database')
def test_openstack_upgrade_leader(
self, migrate_database, determine_packages,
run_in_apache, os_path_exists, disable_unused_apache_sites):
run_in_apache, os_path_exists, disable_unused_apache_sites,
mock_is_elected_leader):
configs = MagicMock()
self.test_config.set('openstack-origin', 'cloud:xenial-newton')
determine_packages.return_value = []
self.is_elected_leader.return_value = True
os_path_exists.return_value = True
run_in_apache.return_value = True
@ -231,9 +226,8 @@ class TestKeystoneUtils(CharmTestCase):
@patch.object(utils, 'get_api_version')
@patch.object(utils, 'get_manager')
@patch.object(utils, 'resolve_address')
@patch.object(utils, 'b64encode')
def test_add_service_to_keystone_clustered_https_none_values(
self, b64encode, _resolve_address, _get_manager,
self, _resolve_address, _get_manager,
_get_api_version, _leader_get):
_get_api_version.return_value = 2
_leader_get.return_value = None
@ -241,11 +235,9 @@ class TestKeystoneUtils(CharmTestCase):
remote_unit = 'unit/0'
_resolve_address.return_value = '10.10.10.10'
self.https.return_value = True
self.test_config.set('https-service-endpoints', 'True')
self.test_config.set('vip', '10.10.10.10')
self.test_config.set('admin-port', 80)
self.test_config.set('service-port', 81)
b64encode.return_value = 'certificate'
self.get_requested_roles.return_value = ['role1', ]
self.relation_get.return_value = {'service': 'keystone',
@ -262,12 +254,10 @@ class TestKeystoneUtils(CharmTestCase):
relation_data = {'auth_host': '10.10.10.10',
'service_host': '10.10.10.10',
'auth_protocol': 'https',
'service_protocol': 'https',
'auth_port': 80,
'auth_protocol': 'https',
'service_port': 81,
'https_keystone': 'True',
'ca_cert': 'certificate',
'region': 'RegionOne',
'api_version': 2,
'admin_domain_id': None}
@ -649,12 +639,15 @@ class TestKeystoneUtils(CharmTestCase):
mock_relation_set.assert_called_once_with(relation_id=relation_id,
relation_settings=settings)
@patch.object(utils, 'is_elected_leader')
@patch.object(utils, 'peer_retrieve')
@patch.object(utils, 'peer_store')
def test_get_admin_passwd_pwd_set(self, mock_peer_store,
mock_peer_retrieve):
mock_peer_retrieve,
mock_is_elected_leader):
mock_peer_retrieve.return_value = None
self.test_config.set('admin-password', 'supersecret')
mock_is_elected_leader.return_value = True
self.assertEqual(utils.get_admin_passwd(), 'supersecret')
mock_peer_store.assert_called_once_with('admin_passwd', 'supersecret')
@ -702,96 +695,6 @@ class TestKeystoneUtils(CharmTestCase):
self.related_units.return_value = []
self.assertTrue(utils.is_db_ready())
@patch.object(utils, 'peer_units')
def test_ensure_ssl_cert_master_ssl_no_peers(self, mock_peer_units):
def mock_rel_get(unit=None, **kwargs):
return None
self.relation_get.side_effect = mock_rel_get
self.relation_ids.return_value = ['cluster:0']
self.local_unit.return_value = 'unit/0'
self.related_units.return_value = []
mock_peer_units.return_value = []
# This should get ignored since we are overriding
self.is_ssl_cert_master.return_value = False
self.is_elected_leader.return_value = False
self.assertTrue(utils.ensure_ssl_cert_master())
settings = {'ssl-cert-master': 'unit/0'}
self.relation_set.assert_called_with(relation_id='cluster:0',
relation_settings=settings)
@patch.object(utils, 'peer_units')
def test_ensure_ssl_cert_master_ssl_master_no_peers(self,
mock_peer_units):
def mock_rel_get(unit=None, **kwargs):
if unit == 'unit/0':
return 'unit/0'
return None
self.relation_get.side_effect = mock_rel_get
self.relation_ids.return_value = ['cluster:0']
self.local_unit.return_value = 'unit/0'
self.related_units.return_value = []
mock_peer_units.return_value = []
# This should get ignored since we are overriding
self.is_ssl_cert_master.return_value = False
self.is_elected_leader.return_value = False
self.assertTrue(utils.ensure_ssl_cert_master())
settings = {'ssl-cert-master': 'unit/0'}
self.relation_set.assert_called_with(relation_id='cluster:0',
relation_settings=settings)
@patch.object(utils, 'peer_units')
def test_ensure_ssl_cert_master_ssl_not_leader(self, mock_peer_units):
self.relation_ids.return_value = ['cluster:0']
self.local_unit.return_value = 'unit/0'
mock_peer_units.return_value = ['unit/1']
self.is_ssl_cert_master.return_value = False
self.is_elected_leader.return_value = False
self.assertFalse(utils.ensure_ssl_cert_master())
self.assertFalse(self.relation_set.called)
@patch.object(utils, 'peer_units')
def test_ensure_ssl_cert_master_is_leader_new_peer(self,
mock_peer_units):
def mock_rel_get(unit=None, **kwargs):
if unit == 'unit/0':
return 'unit/0'
return 'unknown'
self.relation_get.side_effect = mock_rel_get
self.relation_ids.return_value = ['cluster:0']
self.local_unit.return_value = 'unit/0'
mock_peer_units.return_value = ['unit/1']
self.related_units.return_value = ['unit/1']
self.is_ssl_cert_master.return_value = False
self.is_elected_leader.return_value = True
self.assertFalse(utils.ensure_ssl_cert_master())
settings = {'ssl-cert-master': 'unit/0'}
self.relation_set.assert_called_with(relation_id='cluster:0',
relation_settings=settings)
@patch.object(utils, 'peer_units')
def test_ensure_ssl_cert_master_is_leader_no_new_peer(self,
mock_peer_units):
def mock_rel_get(unit=None, **kwargs):
if unit == 'unit/0':
return 'unit/0'
return 'unit/0'
self.relation_get.side_effect = mock_rel_get
self.relation_ids.return_value = ['cluster:0']
self.local_unit.return_value = 'unit/0'
mock_peer_units.return_value = ['unit/1']
self.related_units.return_value = ['unit/1']
self.is_ssl_cert_master.return_value = False
self.is_elected_leader.return_value = True
self.assertFalse(utils.ensure_ssl_cert_master())
self.assertFalse(self.relation_set.called)
@patch.object(utils, 'leader_set')
@patch.object(utils, 'leader_get')
@patch('charmhelpers.contrib.openstack.ip.unit_get')
@ -826,30 +729,6 @@ class TestKeystoneUtils(CharmTestCase):
region='RegionOne',
)
@patch.object(utils, 'peer_units')
def test_ensure_ssl_cert_master_is_leader_bad_votes(self,
mock_peer_units):
counter = {0: 0}
def mock_rel_get(unit=None, **kwargs):
"""Returns a mix of votes."""
if unit == 'unit/0':
return 'unit/0'
ret = 'unit/%d' % (counter[0])
counter[0] += 1
return ret
self.relation_get.side_effect = mock_rel_get
self.relation_ids.return_value = ['cluster:0']
self.local_unit.return_value = 'unit/0'
mock_peer_units.return_value = ['unit/1']
self.related_units.return_value = ['unit/1']
self.is_ssl_cert_master.return_value = False
self.is_elected_leader.return_value = True
self.assertFalse(utils.ensure_ssl_cert_master())
self.assertFalse(self.relation_set.called)
@patch.object(utils, 'get_manager')
def test_is_service_present(self, KeystoneManager):
mock_keystone = MagicMock()
@ -1006,16 +885,6 @@ class TestKeystoneUtils(CharmTestCase):
protocol = utils.get_protocol()
self.assertEqual(protocol, 'https')
def test_get_ssl_ca_settings(self):
CA = MagicMock()
CA.get_ca_bundle.return_value = 'certstring'
self.test_config.set('https-service-endpoints', 'True')
self.get_ca.return_value = CA
expected_settings = {'https_keystone': 'True',
'ca_cert': b64encode('certstring')}
settings = utils.get_ssl_ca_settings()
self.assertEqual(settings, expected_settings)
@patch.object(utils, 'get_manager')
def test_add_credentials_keystone_not_ready(self, get_manager):
""" Verify add_credentials_to_keystone when the relation
@ -1201,67 +1070,6 @@ class TestKeystoneUtils(CharmTestCase):
self.peer_store_and_set.assert_called_with(relation_id=relation_id,
**relation_data)
@patch.object(utils, 'set_service_password')
@patch.object(utils, 'get_service_password')
@patch.object(utils, 'get_ssl_ca_settings')
@patch.object(utils, 'create_user_credentials')
@patch.object(utils, 'get_protocol')
@patch.object(utils, 'resolve_address')
@patch.object(utils, 'get_api_version')
@patch.object(utils, 'get_manager')
def test_add_credentials_keystone_ssl(self, get_manager,
get_api_version,
resolve_address,
get_protocol,
create_user_credentials,
get_ssl_ca_settings,
get_callback, set_callback):
""" Verify add_credentials with SSL """
manager = MagicMock()
manager.resolve_tenant_id.return_value = 'abcdef0123456789'
get_manager.return_value = manager
remote_unit = 'unit/0'
relation_id = 'identity-credentials:0'
get_api_version.return_value = 2
get_protocol.return_value = 'https'
resolve_address.return_value = '10.10.10.10'
create_user_credentials.return_value = 'password'
get_ssl_ca_settings.return_value = {'https_keystone': 'True',
'ca_cert': 'base64certstring'}
self.relation_get.return_value = {'username': 'requester'}
self.get_service_password.return_value = 'password'
self.get_requested_roles.return_value = []
self.test_config.set('admin-port', 80)
self.test_config.set('service-port', 81)
self.test_config.set('https-service-endpoints', 'True')
relation_data = {'auth_host': '10.10.10.10',
'credentials_host': '10.10.10.10',
'credentials_port': 81,
'auth_port': 80,
'auth_protocol': 'https',
'credentials_username': 'requester',
'credentials_protocol': 'https',
'credentials_password': 'password',
'credentials_project': 'services',
'credentials_project_id': 'abcdef0123456789',
'region': 'RegionOne',
'api_version': 2,
'https_keystone': 'True',
'ca_cert': 'base64certstring'}
utils.add_credentials_to_keystone(
relation_id=relation_id,
remote_unit=remote_unit)
create_user_credentials.assert_called_with('requester',
get_callback,
set_callback,
domain=None,
new_roles=[],
grants=['Admin'],
tenant='services')
self.peer_store_and_set.assert_called_with(relation_id=relation_id,
**relation_data)
@patch.object(utils.os, 'remove')
@patch.object(utils.os.path, 'exists')
def test_disable_unused_apache_sites(self, os_path_exists, os_remove):