Create identity-credentials relation

Charms use this relation to obtain keystone credentials without
creating a service catalog entry. Set 'username' only on the relation
and keystone will set defaults and return authentication details.

Possible relation settings:
username: Username to be created.
project: Project (tenant) name to be created. Defaults to services
         project.
requested_roles: Comma delimited list of roles to be created
requested_grants: Comma delimited list of roles to be granted.
                  Defaults to Admin role.
domain: Keystone v3 domain the user will be created in.
        Defaults to the Default domain.

Change-Id: I465d2273560d86752d1bfc7497a9139a9604f814
This commit is contained in:
David Ames 2016-04-12 15:52:58 -07:00
parent 6370c98e55
commit 30a5fe0999
9 changed files with 595 additions and 153 deletions

1
.gitignore vendored
View File

@ -5,5 +5,6 @@ bin
tags tags
*.sw[nop] *.sw[nop]
*.pyc *.pyc
joined-string
.unit-state.db .unit-state.db
trusty/** trusty/**

View File

@ -37,6 +37,19 @@ The following interfaces are provided:
- identity-notifications: Used to broadcast messages to any services - identity-notifications: Used to broadcast messages to any services
listening on the interface. listening on the interface.
- identity-credentials: Charms use this relation to obtain keystone
credentials without creating a service catalog entry. Set 'username'
only on the relation and keystone will set defaults and return
authentication details. Possible relation settings:
username: Username to be created.
project: Project (tenant) name to be created. Defaults to services
project.
requested_roles: Comma delimited list of roles to be created
requested_grants: Comma delimited list of roles to be granted.
Defaults to Admin role.
domain: Keystone v3 domain the user will be created in. Defaults
to the Default domain.
Database Database
-------- --------

View File

@ -0,0 +1 @@
keystone_hooks.py

View File

@ -0,0 +1 @@
keystone_hooks.py

View File

@ -54,6 +54,7 @@ from charmhelpers.contrib.openstack.utils import (
from keystone_utils import ( from keystone_utils import (
add_service_to_keystone, add_service_to_keystone,
add_credentials_to_keystone,
determine_packages, determine_packages,
do_openstack_upgrade_reexec, do_openstack_upgrade_reexec,
ensure_initial_admin, ensure_initial_admin,
@ -205,9 +206,6 @@ def config_changed_postupgrade():
update_all_identity_relation_units() update_all_identity_relation_units()
for rid in relation_ids('identity-admin'):
admin_relation_changed(rid)
# Ensure sync request is sent out (needed for any/all ssl change) # Ensure sync request is sent out (needed for any/all ssl change)
send_ssl_sync_request() send_ssl_sync_request()
@ -298,6 +296,13 @@ def update_all_identity_relation_units(check_db_ready=True):
for rid in relation_ids('identity-service'): for rid in relation_ids('identity-service'):
for unit in related_units(rid): for unit in related_units(rid):
identity_changed(relation_id=rid, remote_unit=unit) identity_changed(relation_id=rid, remote_unit=unit)
log('Firing admin_relation_changed hook for all related services.')
for rid in relation_ids('identity-admin'):
admin_relation_changed(rid)
log('Firing identity_credentials_changed hook for all related services.')
for rid in relation_ids('identity-credentials'):
for unit in related_units(rid):
identity_credentials_changed(relation_id=rid, remote_unit=unit)
@synchronize_ca_if_changed(force=True) @synchronize_ca_if_changed(force=True)
@ -408,6 +413,33 @@ def identity_changed(relation_id=None, remote_unit=None):
send_notifications(notifications) send_notifications(notifications)
@hooks.hook('identity-credentials-relation-joined',
'identity-credentials-relation-changed')
def identity_credentials_changed(relation_id=None, remote_unit=None):
"""Update the identity credentials relation on change
Calls add_credentials_to_keystone
:param relation_id: Relation id of the relation
:param remote_unit: Related unit on the relation
"""
if is_elected_leader(CLUSTER_RES):
if not is_db_ready():
log("identity-credentials-relation-changed hook fired before db "
"ready - deferring until db ready", level=WARNING)
return
if not is_db_initialised():
log("Database not yet initialised - deferring "
"identity-credentials-relation updates", level=INFO)
return
# Create the tenant user
add_credentials_to_keystone(relation_id, remote_unit)
else:
log('Deferring identity_credentials_changed() to service leader.')
def send_ssl_sync_request(): def send_ssl_sync_request():
"""Set sync request on cluster relation. """Set sync request on cluster relation.
@ -511,9 +543,6 @@ def cluster_changed():
else: else:
update_all_identity_relation_units() update_all_identity_relation_units()
for rid in relation_ids('identity-admin'):
admin_relation_changed(rid)
if not is_elected_leader(CLUSTER_RES) and is_ssl_cert_master(): if not is_elected_leader(CLUSTER_RES) and is_ssl_cert_master():
# Force and sync and trigger a sync master re-election since we are not # Force and sync and trigger a sync master re-election since we are not
# leader anymore. # leader anymore.
@ -537,10 +566,7 @@ def leader_settings_changed():
# sure only the leader is running the cron job. # sure only the leader is running the cron job.
CONFIGS.write(TOKEN_FLUSH_CRON_FILE) CONFIGS.write(TOKEN_FLUSH_CRON_FILE)
log('Firing identity_changed hook for all related services.') update_all_identity_relation_units()
for rid in relation_ids('identity-service'):
for unit in related_units(rid):
identity_changed(relation_id=rid, remote_unit=unit)
@hooks.hook('ha-relation-joined') @hooks.hook('ha-relation-joined')

View File

@ -1610,10 +1610,8 @@ def add_service_to_keystone(relation_id=None, remote_unit=None):
'internal_url']) 'internal_url'])
https_cns = [] https_cns = []
if https(): protocol = get_protocol()
protocol = 'https'
else:
protocol = 'http'
if single.issubset(settings): if single.issubset(settings):
# other end of relation advertised only one endpoint # other end of relation advertised only one endpoint
if 'None' in settings.itervalues(): if 'None' in settings.itervalues():
@ -1630,15 +1628,8 @@ def add_service_to_keystone(relation_id=None, remote_unit=None):
relation_data["service_port"] = config('service-port') relation_data["service_port"] = config('service-port')
relation_data["region"] = config('region') relation_data["region"] = config('region')
https_service_endpoints = config('https-service-endpoints') # Get and pass CA bundle settings
if (https_service_endpoints and relation_data.update(get_ssl_ca_settings())
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()
relation_data['https_keystone'] = 'True'
relation_data['ca_cert'] = b64encode(ca_bundle)
# Allow the remote service to request creation of any additional # Allow the remote service to request creation of any additional
# roles. Currently used by Horizon # roles. Currently used by Horizon
@ -1779,9 +1770,9 @@ def add_service_to_keystone(relation_id=None, remote_unit=None):
cert, key = ca.get_cert_and_key(common_name=internal_cn) cert, key = ca.get_cert_and_key(common_name=internal_cn)
relation_data['ssl_cert'] = b64encode(cert) relation_data['ssl_cert'] = b64encode(cert)
relation_data['ssl_key'] = b64encode(key) relation_data['ssl_key'] = b64encode(key)
ca_bundle = ca.get_ca_bundle()
relation_data['ca_cert'] = b64encode(ca_bundle) # Get and pass CA bundle settings
relation_data['https_keystone'] = 'True' relation_data.update(get_ssl_ca_settings())
peer_store_and_set(relation_id=relation_id, **relation_data) peer_store_and_set(relation_id=relation_id, **relation_data)
# NOTE(dosaboy): '__null__' settings are for peer relation only so that # NOTE(dosaboy): '__null__' settings are for peer relation only so that
@ -1790,6 +1781,97 @@ def add_service_to_keystone(relation_id=None, remote_unit=None):
relation_set(relation_id=relation_id, **filtered) relation_set(relation_id=relation_id, **filtered)
def add_credentials_to_keystone(relation_id=None, remote_unit=None):
"""Add authentication credentials without a service endpoint
Creates credentials and then peer stores and relation sets them
:param relation_id: Relation id of the relation
:param remote_unit: Related unit on the relation
"""
manager = get_manager()
settings = relation_get(rid=relation_id, unit=remote_unit)
credentials_username = settings.get('username')
if not credentials_username:
log("identity-credentials peer has not yet set username")
return
if get_api_version() == 2:
domain = None
else:
domain = settings.get('domain') or DEFAULT_DOMAIN
# Use passed project or the service project
credentials_project = settings.get('project') or config('service-tenant')
create_tenant(credentials_project)
# Use passed grants or default to granting the Admin role
credentials_grants = (get_requested_grants(settings) or
[config('admin-role')])
# Create the user
credentials_password = create_user_credentials(
credentials_username,
get_service_password(credentials_username),
project=credentials_project,
new_roles=get_requested_roles(settings),
grants=credentials_grants,
domain=domain)
protocol = get_protocol()
relation_data = {
"auth_host": resolve_address(ADMIN),
"credentials_host": resolve_address(PUBLIC),
"credentials_port": config("service-port"),
"auth_port": config("admin-port"),
"credentials_username": credentials_username,
"credentials_password": credentials_password,
"credentials_project": credentials_project,
"credentials_project_id":
manager.resolve_tenant_id(credentials_project),
"auth_protocol": protocol,
"credentials_protocol": protocol,
"api_version": get_api_version(),
"region": config('region')
}
# 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
:returns: http or https
"""
if https():
protocol = 'https'
else:
protocol = 'http'
return protocol
def ensure_valid_service(service): def ensure_valid_service(service):
if service not in valid_services.keys(): if service not in valid_services.keys():
log("Invalid service requested: '%s'" % service) log("Invalid service requested: '%s'" % service)
@ -1816,6 +1898,20 @@ def get_requested_roles(settings):
return [] return []
def get_requested_grants(settings):
"""Retrieve any valid requested_grants from dict settings
:param settings: dictionary which may contain key, requested_grants,
with comma delimited list of roles to grant.
:returns: list of roles to grant
"""
if ('requested_grants' in settings and
settings['requested_grants'] not in ['None', None]):
return settings['requested_grants'].split(',')
else:
return []
def setup_ipv6(): def setup_ipv6():
"""Check ipv6-mode validity and setup dependencies""" """Check ipv6-mode validity and setup dependencies"""
ubuntu_rel = lsb_release()['DISTRIB_CODENAME'].lower() ubuntu_rel = lsb_release()['DISTRIB_CODENAME'].lower()
@ -1967,10 +2063,10 @@ def git_pre_install():
add_user_to_group('keystone', 'keystone') add_user_to_group('keystone', 'keystone')
for d in dirs: for d in dirs:
mkdir(d, owner='keystone', group='keystone', perms=0755, force=False) mkdir(d, owner='keystone', group='keystone', perms=0o755, force=False)
for l in logs: for l in logs:
write_file(l, '', owner='keystone', group='keystone', perms=0600) write_file(l, '', owner='keystone', group='keystone', perms=0o600)
def git_post_install(projects_yaml): def git_post_install(projects_yaml):

View File

@ -23,6 +23,8 @@ provides:
interface: keystone-notifications interface: keystone-notifications
identity-admin: identity-admin:
interface: keystone-admin interface: keystone-admin
identity-credentials:
interface: keystone-credentials
requires: requires:
shared-db: shared-db:
interface: mysql-shared interface: mysql-shared

View File

@ -273,105 +273,31 @@ class KeystoneRelationTests(CharmTestCase):
configs.write = MagicMock() configs.write = MagicMock()
hooks.pgsql_db_changed() hooks.pgsql_db_changed()
@patch('keystone_utils.relation_ids') @patch.object(hooks, 'leader_init_db_if_ready')
@patch('keystone_utils.peer_retrieve')
@patch('keystone_utils.peer_store')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master') @patch('keystone_utils.ensure_ssl_cert_master')
@patch.object(hooks, 'CONFIGS') @patch.object(hooks, 'CONFIGS')
@patch.object(hooks, 'identity_changed') def test_db_changed(self, configs,
def test_db_changed_allowed(self, identity_changed, configs, mock_ensure_ssl_cert_master,
mock_ensure_ssl_cert_master, mock_log, leader_init):
mock_peer_store,
mock_peer_retrieve, mock_relation_ids):
mock_relation_ids.return_value = ['peer/0']
peer_settings = {}
def fake_peer_store(key, val):
peer_settings[key] = val
def fake_migrate():
fake_peer_store('db-initialised', 'True')
self.migrate_database.side_effect = fake_migrate
mock_peer_store.side_effect = fake_peer_store
mock_peer_retrieve.side_effect = lambda key: peer_settings.get(key)
self.is_db_ready.return_value = True
mock_ensure_ssl_cert_master.return_value = False mock_ensure_ssl_cert_master.return_value = False
self.relation_ids.return_value = ['identity-service:0']
self.related_units.return_value = ['unit/0']
self._shared_db_test(configs, 'keystone/3') self._shared_db_test(configs, 'keystone/3')
self.assertEquals([call('/etc/keystone/keystone.conf')], self.assertEquals([call('/etc/keystone/keystone.conf')],
configs.write.call_args_list) configs.write.call_args_list)
self.migrate_database.assert_called_with() self.assertTrue(leader_init.called)
self.assertTrue(self.ensure_initial_admin.called)
identity_changed.assert_called_with(
relation_id='identity-service:0',
remote_unit='unit/0')
@patch('keystone_utils.relation_ids') @patch.object(hooks, 'leader_init_db_if_ready')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master') @patch('keystone_utils.ensure_ssl_cert_master')
@patch.object(hooks, 'CONFIGS') @patch.object(hooks, 'CONFIGS')
@patch.object(hooks, 'identity_changed') def test_postgresql_db_changed(self, configs,
def test_db_changed_not_allowed(self, identity_changed, configs, mock_ensure_ssl_cert_master,
mock_ensure_ssl_cert_master, mock_log, leader_init):
mock_relation_ids):
mock_relation_ids.return_value = []
self.is_db_ready.return_value = False
mock_ensure_ssl_cert_master.return_value = False mock_ensure_ssl_cert_master.return_value = False
self.relation_ids.return_value = ['identity-service:0']
self.related_units.return_value = ['unit/0']
self._shared_db_test(configs, 'keystone/2')
self.assertEquals([call('/etc/keystone/keystone.conf')],
configs.write.call_args_list)
self.assertFalse(self.migrate_database.called)
self.assertFalse(self.ensure_initial_admin.called)
self.assertFalse(identity_changed.called)
@patch('keystone_utils.relation_ids')
@patch('keystone_utils.peer_retrieve')
@patch('keystone_utils.peer_store')
@patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master')
@patch.object(hooks, 'CONFIGS')
@patch.object(hooks, 'identity_changed')
def test_postgresql_db_changed(self, identity_changed, configs,
mock_ensure_ssl_cert_master, mock_log,
mock_peer_store, mock_peer_retrieve,
mock_relation_ids):
self.os_release.return_value = 'kilo'
mock_relation_ids.return_value = ['peer/0']
peer_settings = {}
def fake_peer_store(key, val):
peer_settings[key] = val
def fake_migrate():
fake_peer_store('db-initialised', 'True')
self.migrate_database.side_effect = fake_migrate
mock_peer_store.side_effect = fake_peer_store
mock_peer_retrieve.side_effect = lambda key: peer_settings.get(key)
self.is_db_ready.return_value = True
mock_ensure_ssl_cert_master.return_value = False
self.relation_ids.return_value = ['identity-service:0']
self.related_units.return_value = ['unit/0']
self._postgresql_db_test(configs) self._postgresql_db_test(configs)
self.assertEquals([call('/etc/keystone/keystone.conf')], self.assertEquals([call('/etc/keystone/keystone.conf')],
configs.write.call_args_list) configs.write.call_args_list)
self.migrate_database.assert_called_with() self.assertTrue(leader_init.called)
self.assertTrue(self.ensure_initial_admin.called)
identity_changed.assert_called_with(
relation_id='identity-service:0',
remote_unit='unit/0')
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'run_in_apache')
@patch.object(hooks, 'is_db_initialised') @patch.object(hooks, 'is_db_initialised')
@patch.object(hooks, 'git_install_requested') @patch.object(hooks, 'git_install_requested')
@ -409,7 +335,8 @@ class KeystoneRelationTests(CharmTestCase):
mock_ensure_ssl_cert_master, mock_ensure_ssl_cert_master,
mock_log, git_requested, mock_log, git_requested,
mock_is_db_initialised, mock_is_db_initialised,
mock_run_in_apache): mock_run_in_apache,
update):
mock_run_in_apache.return_value = False mock_run_in_apache.return_value = False
git_requested.return_value = False git_requested.return_value = False
mock_is_ssl_cert_master.return_value = True mock_is_ssl_cert_master.return_value = True
@ -431,20 +358,14 @@ class KeystoneRelationTests(CharmTestCase):
configure_https.assert_called_with() configure_https.assert_called_with()
self.assertTrue(configs.write_all.called) self.assertTrue(configs.write_all.called)
self.assertTrue(self.ensure_initial_admin.called) self.assertTrue(update.called)
self.log.assert_called_with(
'Firing identity_changed hook for all related services.')
identity_changed.assert_called_with(
relation_id='identity-service:0',
remote_unit='unit/0')
admin_relation_changed.assert_called_with('identity-service:0')
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'run_in_apache')
@patch.object(hooks, 'git_install_requested') @patch.object(hooks, 'git_install_requested')
@patch('keystone_utils.log') @patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master') @patch('keystone_utils.ensure_ssl_cert_master')
@patch('keystone_utils.ensure_ssl_dirs') @patch('keystone_utils.ensure_ssl_dirs')
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'ensure_permissions') @patch.object(hooks, 'ensure_permissions')
@patch.object(hooks, 'ensure_pki_cert_paths') @patch.object(hooks, 'ensure_pki_cert_paths')
@patch.object(hooks, 'ensure_pki_dir_permissions') @patch.object(hooks, 'ensure_pki_dir_permissions')
@ -467,11 +388,10 @@ class KeystoneRelationTests(CharmTestCase):
mock_ensure_permissions, mock_ensure_permissions,
mock_ensure_pki_cert_paths, mock_ensure_pki_cert_paths,
mock_ensure_pki_permissions, mock_ensure_pki_permissions,
mock_update_all_id_rel_units,
ensure_ssl_dirs, ensure_ssl_dirs,
mock_ensure_ssl_cert_master, mock_ensure_ssl_cert_master,
mock_log, git_requested, mock_log, git_requested,
mock_run_in_apache): mock_run_in_apache, update):
mock_run_in_apache.return_value = False mock_run_in_apache.return_value = False
git_requested.return_value = False git_requested.return_value = False
mock_is_ssl_cert_master.return_value = True mock_is_ssl_cert_master.return_value = True
@ -489,9 +409,9 @@ class KeystoneRelationTests(CharmTestCase):
self.assertTrue(configs.write_all.called) self.assertTrue(configs.write_all.called)
self.assertFalse(self.migrate_database.called) self.assertFalse(self.migrate_database.called)
self.assertFalse(self.ensure_initial_admin.called) self.assertTrue(update.called)
self.assertFalse(identity_changed.called)
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'run_in_apache')
@patch.object(hooks, 'is_db_initialised') @patch.object(hooks, 'is_db_initialised')
@patch.object(hooks, 'git_install_requested') @patch.object(hooks, 'git_install_requested')
@ -528,7 +448,8 @@ class KeystoneRelationTests(CharmTestCase):
mock_ensure_ssl_cert_master, mock_ensure_ssl_cert_master,
mock_log, git_requested, mock_log, git_requested,
mock_is_db_initialised, mock_is_db_initialised,
mock_run_in_apache): mock_run_in_apache,
update):
mock_run_in_apache.return_value = False mock_run_in_apache.return_value = False
git_requested.return_value = False git_requested.return_value = False
mock_is_ssl_cert_master.return_value = True mock_is_ssl_cert_master.return_value = True
@ -552,14 +473,9 @@ class KeystoneRelationTests(CharmTestCase):
configure_https.assert_called_with() configure_https.assert_called_with()
self.assertTrue(configs.write_all.called) self.assertTrue(configs.write_all.called)
self.assertTrue(self.ensure_initial_admin.called) self.assertTrue(update.called)
self.log.assert_called_with(
'Firing identity_changed hook for all related services.')
identity_changed.assert_called_with(
relation_id='identity-service:0',
remote_unit='unit/0')
admin_relation_changed.assert_called_with('identity-service:0')
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'run_in_apache')
@patch.object(hooks, 'initialise_pki') @patch.object(hooks, 'initialise_pki')
@patch.object(hooks, 'git_install_requested') @patch.object(hooks, 'git_install_requested')
@ -591,7 +507,8 @@ class KeystoneRelationTests(CharmTestCase):
mock_log, config_val_changed, mock_log, config_val_changed,
git_requested, git_requested,
mock_initialise_pki, mock_initialise_pki,
mock_run_in_apache): mock_run_in_apache,
update):
mock_run_in_apache.return_value = False mock_run_in_apache.return_value = False
git_requested.return_value = True git_requested.return_value = True
mock_ensure_ssl_cert_master.return_value = False mock_ensure_ssl_cert_master.return_value = False
@ -620,6 +537,7 @@ class KeystoneRelationTests(CharmTestCase):
self.git_install.assert_called_with(projects_yaml) self.git_install.assert_called_with(projects_yaml)
self.assertFalse(self.openstack_upgrade_available.called) self.assertFalse(self.openstack_upgrade_available.called)
self.assertFalse(self.do_openstack_upgrade_reexec.called) self.assertFalse(self.do_openstack_upgrade_reexec.called)
self.assertTrue(update.called)
@patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'run_in_apache')
@patch.object(hooks, 'initialise_pki') @patch.object(hooks, 'initialise_pki')
@ -782,16 +700,14 @@ class KeystoneRelationTests(CharmTestCase):
hooks.leader_elected() hooks.leader_elected()
mock_write.assert_has_calls([call(utils.TOKEN_FLUSH_CRON_FILE)]) mock_write.assert_has_calls([call(utils.TOKEN_FLUSH_CRON_FILE)])
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks.CONFIGS, 'write') @patch.object(hooks.CONFIGS, 'write')
@patch.object(hooks, 'identity_changed') def test_leader_settings_changed(self, mock_write, update):
def test_leader_settings_changed(self, mock_identity_changed,
mock_write):
self.relation_ids.return_value = ['identity:1'] self.relation_ids.return_value = ['identity:1']
self.related_units.return_value = ['keystone/1'] self.related_units.return_value = ['keystone/1']
hooks.leader_settings_changed() hooks.leader_settings_changed()
mock_write.assert_has_calls([call(utils.TOKEN_FLUSH_CRON_FILE)]) mock_write.assert_has_calls([call(utils.TOKEN_FLUSH_CRON_FILE)])
exp = [call(relation_id='identity:1', remote_unit='keystone/1')] self.assertTrue(update.called)
mock_identity_changed.assert_has_calls(exp)
def test_ha_joined(self): def test_ha_joined(self):
self.get_hacluster_config.return_value = { self.get_hacluster_config.return_value = {
@ -908,6 +824,7 @@ class KeystoneRelationTests(CharmTestCase):
self.assertTrue(configs.write_all.called) self.assertTrue(configs.write_all.called)
self.assertFalse(mock_synchronize_ca.called) self.assertFalse(mock_synchronize_ca.called)
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'is_db_initialised') @patch.object(hooks, 'is_db_initialised')
@patch('keystone_utils.log') @patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master') @patch('keystone_utils.ensure_ssl_cert_master')
@ -917,7 +834,8 @@ class KeystoneRelationTests(CharmTestCase):
identity_changed, identity_changed,
mock_ensure_ssl_cert_master, mock_ensure_ssl_cert_master,
mock_log, mock_log,
mock_is_db_initialised): mock_is_db_initialised,
update):
mock_is_db_initialised.return_value = True mock_is_db_initialised.return_value = True
self.is_db_ready.return_value = True self.is_db_ready.return_value = True
mock_ensure_ssl_cert_master.return_value = False mock_ensure_ssl_cert_master.return_value = False
@ -928,11 +846,7 @@ class KeystoneRelationTests(CharmTestCase):
hooks.ha_changed() hooks.ha_changed()
self.assertTrue(configs.write_all.called) self.assertTrue(configs.write_all.called)
self.log.assert_called_with( self.assertTrue(update.called)
'Firing identity_changed hook for all related services.')
identity_changed.assert_called_with(
relation_id='identity-service:0',
remote_unit='unit/0')
@patch('keystone_utils.log') @patch('keystone_utils.log')
@patch('keystone_utils.ensure_ssl_cert_master') @patch('keystone_utils.ensure_ssl_cert_master')
@ -965,6 +879,7 @@ class KeystoneRelationTests(CharmTestCase):
cmd = ['a2dissite', 'openstack_https_frontend'] cmd = ['a2dissite', 'openstack_https_frontend']
self.check_call.assert_called_with(cmd) self.check_call.assert_called_with(cmd)
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(utils, 'os_release') @patch.object(utils, 'os_release')
@patch.object(utils, 'git_install_requested') @patch.object(utils, 'git_install_requested')
@patch.object(hooks, 'is_db_ready') @patch.object(hooks, 'is_db_ready')
@ -986,7 +901,8 @@ class KeystoneRelationTests(CharmTestCase):
mock_is_db_initialised, mock_is_db_initialised,
mock_is_db_ready, mock_is_db_ready,
git_requested, git_requested,
os_release): os_release,
update):
mock_is_db_initialised.return_value = True mock_is_db_initialised.return_value = True
mock_is_db_ready.return_value = True mock_is_db_ready.return_value = True
mock_is_elected_leader.return_value = False mock_is_elected_leader.return_value = False
@ -1005,10 +921,138 @@ class KeystoneRelationTests(CharmTestCase):
user=self.ssh_user, group='juju_keystone', user=self.ssh_user, group='juju_keystone',
peer_interface='cluster', ensure_local_user=True) peer_interface='cluster', ensure_local_user=True)
self.assertTrue(mock_synchronize_ca.called) self.assertTrue(mock_synchronize_ca.called)
self.log.assert_called_with( self.assertTrue(update.called)
'Firing identity_changed hook for all related services.')
self.assertTrue(self.ensure_initial_admin.called)
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'is_db_initialised')
def test_leader_init_db_if_ready(self, is_db_initialized,
update):
""" Verify leader initilaizes db """
self.is_elected_leader.return_value = True
is_db_initialized.return_value = False
self.is_db_ready.return_value = True
hooks.leader_init_db_if_ready()
self.is_db_ready.assert_called_with(use_current_context=False)
self.migrate_database.assert_called_with()
update.assert_called_with(check_db_ready=False)
@patch.object(hooks, 'update_all_identity_relation_units')
def test_leader_init_db_not_leader(self, update):
""" Verify non-leader does not initilaize db """
self.is_elected_leader.return_value = False
hooks.leader_init_db_if_ready()
self.is_elected_leader.assert_called_with('grp_ks_vips')
self.log.assert_called_with("Not leader - skipping db init",
level='DEBUG')
self.assertFalse(self.migrate_database.called)
self.assertFalse(update.called)
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'is_db_initialised')
def test_leader_init_db_not_initilaized(self, is_db_initialized, update):
""" Verify leader does not initilaize db when already initialized """
self.is_elected_leader.return_value = True
is_db_initialized.return_value = True
hooks.leader_init_db_if_ready()
self.log.assert_called_with('Database already initialised - skipping '
'db init', level='DEBUG')
self.assertFalse(self.migrate_database.called)
self.assertFalse(update.called)
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(hooks, 'is_db_initialised')
def test_leader_init_db_not_ready(self, is_db_initialized, update):
""" Verify leader does not initilaize db when db not ready """
self.is_elected_leader.return_value = True
is_db_initialized.return_value = False
self.is_db_ready.return_value = False
hooks.leader_init_db_if_ready()
self.is_db_ready.assert_called_with(use_current_context=False)
self.log.assert_called_with('Allowed_units list provided and this '
'unit not present', level='INFO')
self.assertFalse(self.migrate_database.called)
self.assertFalse(update.called)
@patch.object(hooks, 'admin_relation_changed')
@patch.object(hooks, 'identity_credentials_changed')
@patch.object(hooks, 'identity_changed')
@patch.object(hooks, 'is_db_initialised')
@patch.object(hooks, 'CONFIGS')
def test_update_all_identity_relation_units(self, configs,
is_db_initialized,
identity_changed,
identity_credentials_changed,
admin_relation_changed):
""" Verify all identity relations are updated """
is_db_initialized.return_value = True
self.relation_ids.return_value = ['identity-relation:0']
self.related_units.return_value = ['unit/0']
log_calls = [call('Firing identity_changed hook for all related '
'services.'),
call('Firing admin_relation_changed hook for all related '
'services.'),
call('Firing identity_credentials_changed hook for all '
'related services.')]
hooks.update_all_identity_relation_units(check_db_ready=False)
self.assertTrue(configs.write_all.called)
identity_changed.assert_called_with(
relation_id='identity-relation:0',
remote_unit='unit/0')
identity_credentials_changed.assert_called_with(
relation_id='identity-relation:0',
remote_unit='unit/0')
admin_relation_changed.assert_called_with('identity-relation:0')
self.log.assert_has_calls(log_calls, any_order=True)
@patch.object(hooks, 'CONFIGS')
def test_update_all_db_not_ready(self, configs):
""" Verify update identity relations when DB is not ready """
self.is_db_ready.return_value = False
hooks.update_all_identity_relation_units(check_db_ready=True)
self.assertTrue(configs.write_all.called)
self.assertTrue(self.is_db_ready.called)
self.log.assert_called_with('Allowed_units list provided and this '
'unit not present', level='INFO')
self.assertFalse(self.relation_ids.called)
@patch.object(hooks, 'is_db_initialised')
@patch.object(hooks, 'CONFIGS')
def test_update_all_db_not_initializd(self, configs, is_db_initialized):
""" Verify update identity relations when DB is not initialized """
is_db_initialized.return_value = False
hooks.update_all_identity_relation_units(check_db_ready=False)
self.assertTrue(configs.write_all.called)
self.assertFalse(self.is_db_ready.called)
self.log.assert_called_with('Database not yet initialised - '
'deferring identity-relation updates',
level='INFO')
self.assertFalse(self.relation_ids.called)
@patch.object(hooks, 'is_db_initialised')
@patch.object(hooks, 'CONFIGS')
def test_update_all_leader(self, configs, is_db_initialized):
""" Verify update identity relations when the leader"""
self.is_elected_leader.return_value = True
is_db_initialized.return_value = True
hooks.update_all_identity_relation_units(check_db_ready=False)
self.assertTrue(configs.write_all.called)
self.assertTrue(self.ensure_initial_admin.called)
# Still updates relations
self.assertTrue(self.relation_ids.called)
@patch.object(hooks, 'is_db_initialised')
@patch.object(hooks, 'CONFIGS')
def test_update_all_not_leader(self, configs, is_db_initialized):
""" Verify update identity relations when not the leader"""
self.is_elected_leader.return_value = False
is_db_initialized.return_value = True
hooks.update_all_identity_relation_units(check_db_ready=False)
self.assertTrue(configs.write_all.called)
self.assertFalse(self.ensure_initial_admin.called)
# Still updates relations
self.assertTrue(self.relation_ids.called)
@patch.object(hooks, 'update_all_identity_relation_units')
@patch.object(utils, 'os_release') @patch.object(utils, 'os_release')
@patch.object(utils, 'git_install_requested') @patch.object(utils, 'git_install_requested')
@patch('keystone_utils.log') @patch('keystone_utils.log')
@ -1021,7 +1065,7 @@ class KeystoneRelationTests(CharmTestCase):
mock_ensure_ssl_cert_master, mock_ensure_ssl_cert_master,
mock_relation_ids, mock_relation_ids,
mock_log, git_requested, mock_log, git_requested,
os_release): os_release, update):
mock_relation_ids.return_value = [] mock_relation_ids.return_value = []
mock_ensure_ssl_cert_master.return_value = False mock_ensure_ssl_cert_master.return_value = False
# Ensure always returns diff # Ensure always returns diff
@ -1037,4 +1081,4 @@ class KeystoneRelationTests(CharmTestCase):
user=self.ssh_user, group='juju_keystone', user=self.ssh_user, group='juju_keystone',
peer_interface='cluster', ensure_local_user=True) peer_interface='cluster', ensure_local_user=True)
self.assertTrue(self.log.called) self.assertTrue(self.log.called)
self.assertFalse(self.ensure_initial_admin.called) self.assertFalse(update.called)

View File

@ -1,6 +1,7 @@
from mock import patch, call, MagicMock, Mock from mock import patch, call, MagicMock, Mock
from test_utils import CharmTestCase from test_utils import CharmTestCase
import os import os
from base64 import b64encode
os.environ['JUJU_UNIT_NAME'] = 'keystone' os.environ['JUJU_UNIT_NAME'] = 'keystone'
with patch('charmhelpers.core.hookenv.config') as config: with patch('charmhelpers.core.hookenv.config') as config:
@ -859,7 +860,9 @@ class TestKeystoneUtils(CharmTestCase):
self.service_start.assert_called_once_with('apache2') self.service_start.assert_called_once_with('apache2')
self.subprocess.call.assert_called_once_with(['pgrep', 'httpd']) self.subprocess.call.assert_called_once_with(['pgrep', 'httpd'])
def test_restart_pid_check_ptable_string_retry(self): # Do not sleep() to speed up manual runs.
@patch('charmhelpers.core.decorators.time')
def test_restart_pid_check_ptable_string_retry(self, mock_time):
call_returns = [1, 0, 0] call_returns = [1, 0, 0]
self.subprocess.call.side_effect = lambda x: call_returns.pop() self.subprocess.call.side_effect = lambda x: call_returns.pop()
utils.restart_pid_check('apache2', ptable_string='httpd') utils.restart_pid_check('apache2', ptable_string='httpd')
@ -872,3 +875,258 @@ class TestKeystoneUtils(CharmTestCase):
call(['pgrep', 'httpd']), call(['pgrep', 'httpd']),
] ]
self.assertEquals(self.subprocess.call.call_args_list, expected) self.assertEquals(self.subprocess.call.call_args_list, expected)
def test_get_requested_grants(self):
settings = {'requested_grants': 'Admin,Member'}
expected_results = ['Admin', 'Member']
self.assertEqual(utils.get_requested_grants(settings),
expected_results)
settings = {'not_requsted_grants': 'something else'}
expected_results = []
self.assertEqual(utils.get_requested_grants(settings),
expected_results)
@patch.object(utils, 'https')
def test_get_protocol(self, https):
# http
https.return_value = False
protocol = utils.get_protocol()
self.assertEqual(protocol, 'http')
# https
https.return_value = True
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
data is incomplete """
relation_id = 'identity-credentials:0'
remote_unit = 'unit/0'
self.relation_get.return_value = {}
utils.add_credentials_to_keystone(
relation_id=relation_id,
remote_unit=remote_unit)
self.log.assert_called_with('identity-credentials peer has not yet '
'set username')
@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_username_only(self, get_manager,
get_api_version,
resolve_address,
get_protocol,
create_user_credentials):
""" Verify add_credentials with only username """
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 = 'http'
resolve_address.return_value = '10.10.10.10'
create_user_credentials.return_value = 'password'
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('service-tenant', 'services')
relation_data = {'auth_host': '10.10.10.10',
'credentials_host': '10.10.10.10',
'credentials_port': 81,
'auth_port': 80,
'auth_protocol': 'http',
'credentials_username': 'requester',
'credentials_protocol': 'http',
'credentials_password': 'password',
'credentials_project': 'services',
'credentials_project_id': 'abcdef0123456789',
'region': 'RegionOne',
'api_version': 2}
utils.add_credentials_to_keystone(
relation_id=relation_id,
remote_unit=remote_unit)
create_user_credentials.assert_called_with('requester', 'password',
domain=None,
new_roles=[],
grants=['Admin'],
project='services')
self.peer_store_and_set.assert_called_with(relation_id=relation_id,
**relation_data)
@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_kv3(self, get_manager,
get_api_version,
resolve_address,
get_protocol,
create_user_credentials):
""" Verify add_credentials with Keystone V3 """
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 = 3
get_protocol.return_value = 'http'
resolve_address.return_value = '10.10.10.10'
create_user_credentials.return_value = 'password'
self.relation_get.return_value = {'username': 'requester',
'domain': 'Non-Default'}
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)
relation_data = {'auth_host': '10.10.10.10',
'credentials_host': '10.10.10.10',
'credentials_port': 81,
'auth_port': 80,
'auth_protocol': 'http',
'credentials_username': 'requester',
'credentials_protocol': 'http',
'credentials_password': 'password',
'credentials_project': 'services',
'credentials_project_id': 'abcdef0123456789',
'region': 'RegionOne',
'api_version': 3}
utils.add_credentials_to_keystone(
relation_id=relation_id,
remote_unit=remote_unit)
create_user_credentials.assert_called_with('requester', 'password',
domain='Non-Default',
new_roles=[],
grants=['Admin'],
project='services')
self.peer_store_and_set.assert_called_with(relation_id=relation_id,
**relation_data)
@patch.object(utils, 'create_tenant')
@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_roles_grants(self, get_manager,
get_api_version,
resolve_address,
get_protocol,
create_user_credentials,
create_tenant):
""" Verify add_credentials with all relation settings """
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 = 'http'
resolve_address.return_value = '10.10.10.10'
create_user_credentials.return_value = 'password'
self.relation_get.return_value = {'username': 'requester',
'project': 'myproject',
'requested_roles': 'New,Member',
'requested_grants': 'New,Member'}
self.get_service_password.return_value = 'password'
self.get_requested_roles.return_value = ['New', 'Member']
self.test_config.set('admin-port', 80)
self.test_config.set('service-port', 81)
relation_data = {'auth_host': '10.10.10.10',
'credentials_host': '10.10.10.10',
'credentials_port': 81,
'auth_port': 80,
'auth_protocol': 'http',
'credentials_username': 'requester',
'credentials_protocol': 'http',
'credentials_password': 'password',
'credentials_project': 'myproject',
'credentials_project_id': 'abcdef0123456789',
'region': 'RegionOne',
'api_version': 2}
utils.add_credentials_to_keystone(
relation_id=relation_id,
remote_unit=remote_unit)
create_tenant.assert_called_with('myproject')
create_user_credentials.assert_called_with('requester', 'password',
domain=None,
new_roles=['New', 'Member'],
grants=['New', 'Member'],
project='myproject')
self.peer_store_and_set.assert_called_with(relation_id=relation_id,
**relation_data)
@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):
""" 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', 'password',
domain=None,
new_roles=[],
grants=['Admin'],
project='services')
self.peer_store_and_set.assert_called_with(relation_id=relation_id,
**relation_data)