diff --git a/.gitignore b/.gitignore index b2895cfa..bc87ebfa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ bin tags *.sw[nop] *.pyc +joined-string .unit-state.db trusty/** diff --git a/README.md b/README.md index 42d041aa..72ad4db3 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,19 @@ The following interfaces are provided: - identity-notifications: Used to broadcast messages to any services 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 -------- diff --git a/hooks/identity-credentials-relation-changed b/hooks/identity-credentials-relation-changed new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/identity-credentials-relation-changed @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/identity-credentials-relation-joined b/hooks/identity-credentials-relation-joined new file mode 120000 index 00000000..dd3b3eff --- /dev/null +++ b/hooks/identity-credentials-relation-joined @@ -0,0 +1 @@ +keystone_hooks.py \ No newline at end of file diff --git a/hooks/keystone_hooks.py b/hooks/keystone_hooks.py index 60607df5..42568fe5 100755 --- a/hooks/keystone_hooks.py +++ b/hooks/keystone_hooks.py @@ -54,6 +54,7 @@ from charmhelpers.contrib.openstack.utils import ( from keystone_utils import ( add_service_to_keystone, + add_credentials_to_keystone, determine_packages, do_openstack_upgrade_reexec, ensure_initial_admin, @@ -205,9 +206,6 @@ def config_changed_postupgrade(): 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) 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 unit in related_units(rid): 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) @@ -408,6 +413,33 @@ def identity_changed(relation_id=None, remote_unit=None): 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(): """Set sync request on cluster relation. @@ -511,9 +543,6 @@ def cluster_changed(): else: 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(): # Force and sync and trigger a sync master re-election since we are not # leader anymore. @@ -537,10 +566,7 @@ def leader_settings_changed(): # sure only the leader is running the cron job. CONFIGS.write(TOKEN_FLUSH_CRON_FILE) - log('Firing identity_changed hook for all related services.') - for rid in relation_ids('identity-service'): - for unit in related_units(rid): - identity_changed(relation_id=rid, remote_unit=unit) + update_all_identity_relation_units() @hooks.hook('ha-relation-joined') diff --git a/hooks/keystone_utils.py b/hooks/keystone_utils.py index baf4f14f..f820f9b5 100644 --- a/hooks/keystone_utils.py +++ b/hooks/keystone_utils.py @@ -1610,10 +1610,8 @@ def add_service_to_keystone(relation_id=None, remote_unit=None): 'internal_url']) https_cns = [] - if https(): - protocol = 'https' - else: - protocol = 'http' + protocol = get_protocol() + if single.issubset(settings): # other end of relation advertised only one endpoint 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["region"] = config('region') - 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() - relation_data['https_keystone'] = 'True' - relation_data['ca_cert'] = b64encode(ca_bundle) + # 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 @@ -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) relation_data['ssl_cert'] = b64encode(cert) relation_data['ssl_key'] = b64encode(key) - ca_bundle = ca.get_ca_bundle() - relation_data['ca_cert'] = b64encode(ca_bundle) - relation_data['https_keystone'] = 'True' + + # 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 @@ -1790,6 +1781,97 @@ def add_service_to_keystone(relation_id=None, remote_unit=None): 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): if service not in valid_services.keys(): log("Invalid service requested: '%s'" % service) @@ -1816,6 +1898,20 @@ def get_requested_roles(settings): 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(): """Check ipv6-mode validity and setup dependencies""" ubuntu_rel = lsb_release()['DISTRIB_CODENAME'].lower() @@ -1967,10 +2063,10 @@ def git_pre_install(): add_user_to_group('keystone', 'keystone') 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: - write_file(l, '', owner='keystone', group='keystone', perms=0600) + write_file(l, '', owner='keystone', group='keystone', perms=0o600) def git_post_install(projects_yaml): diff --git a/metadata.yaml b/metadata.yaml index e0ebe827..eef9e23d 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -23,6 +23,8 @@ provides: interface: keystone-notifications identity-admin: interface: keystone-admin + identity-credentials: + interface: keystone-credentials requires: shared-db: interface: mysql-shared diff --git a/unit_tests/test_keystone_hooks.py b/unit_tests/test_keystone_hooks.py index 03226585..a55fce6c 100644 --- a/unit_tests/test_keystone_hooks.py +++ b/unit_tests/test_keystone_hooks.py @@ -273,105 +273,31 @@ class KeystoneRelationTests(CharmTestCase): configs.write = MagicMock() hooks.pgsql_db_changed() - @patch('keystone_utils.relation_ids') - @patch('keystone_utils.peer_retrieve') - @patch('keystone_utils.peer_store') - @patch('keystone_utils.log') + @patch.object(hooks, 'leader_init_db_if_ready') @patch('keystone_utils.ensure_ssl_cert_master') @patch.object(hooks, 'CONFIGS') - @patch.object(hooks, 'identity_changed') - def test_db_changed_allowed(self, identity_changed, configs, - mock_ensure_ssl_cert_master, mock_log, - 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 + def test_db_changed(self, configs, + mock_ensure_ssl_cert_master, + leader_init): 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.assertEquals([call('/etc/keystone/keystone.conf')], configs.write.call_args_list) - self.migrate_database.assert_called_with() - self.assertTrue(self.ensure_initial_admin.called) - identity_changed.assert_called_with( - relation_id='identity-service:0', - remote_unit='unit/0') + self.assertTrue(leader_init.called) - @patch('keystone_utils.relation_ids') - @patch('keystone_utils.log') + @patch.object(hooks, 'leader_init_db_if_ready') @patch('keystone_utils.ensure_ssl_cert_master') @patch.object(hooks, 'CONFIGS') - @patch.object(hooks, 'identity_changed') - def test_db_changed_not_allowed(self, identity_changed, configs, - mock_ensure_ssl_cert_master, mock_log, - mock_relation_ids): - mock_relation_ids.return_value = [] - self.is_db_ready.return_value = False + def test_postgresql_db_changed(self, configs, + mock_ensure_ssl_cert_master, + leader_init): 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.assertEquals([call('/etc/keystone/keystone.conf')], configs.write.call_args_list) - self.migrate_database.assert_called_with() - self.assertTrue(self.ensure_initial_admin.called) - identity_changed.assert_called_with( - relation_id='identity-service:0', - remote_unit='unit/0') + self.assertTrue(leader_init.called) + @patch.object(hooks, 'update_all_identity_relation_units') @patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'is_db_initialised') @patch.object(hooks, 'git_install_requested') @@ -409,7 +335,8 @@ class KeystoneRelationTests(CharmTestCase): mock_ensure_ssl_cert_master, mock_log, git_requested, mock_is_db_initialised, - mock_run_in_apache): + mock_run_in_apache, + update): mock_run_in_apache.return_value = False git_requested.return_value = False mock_is_ssl_cert_master.return_value = True @@ -431,20 +358,14 @@ class KeystoneRelationTests(CharmTestCase): configure_https.assert_called_with() self.assertTrue(configs.write_all.called) - self.assertTrue(self.ensure_initial_admin.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') + self.assertTrue(update.called) + @patch.object(hooks, 'update_all_identity_relation_units') @patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'git_install_requested') @patch('keystone_utils.log') @patch('keystone_utils.ensure_ssl_cert_master') @patch('keystone_utils.ensure_ssl_dirs') - @patch.object(hooks, 'update_all_identity_relation_units') @patch.object(hooks, 'ensure_permissions') @patch.object(hooks, 'ensure_pki_cert_paths') @patch.object(hooks, 'ensure_pki_dir_permissions') @@ -467,11 +388,10 @@ class KeystoneRelationTests(CharmTestCase): mock_ensure_permissions, mock_ensure_pki_cert_paths, mock_ensure_pki_permissions, - mock_update_all_id_rel_units, ensure_ssl_dirs, mock_ensure_ssl_cert_master, mock_log, git_requested, - mock_run_in_apache): + mock_run_in_apache, update): mock_run_in_apache.return_value = False git_requested.return_value = False mock_is_ssl_cert_master.return_value = True @@ -489,9 +409,9 @@ class KeystoneRelationTests(CharmTestCase): self.assertTrue(configs.write_all.called) self.assertFalse(self.migrate_database.called) - self.assertFalse(self.ensure_initial_admin.called) - self.assertFalse(identity_changed.called) + self.assertTrue(update.called) + @patch.object(hooks, 'update_all_identity_relation_units') @patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'is_db_initialised') @patch.object(hooks, 'git_install_requested') @@ -528,7 +448,8 @@ class KeystoneRelationTests(CharmTestCase): mock_ensure_ssl_cert_master, mock_log, git_requested, mock_is_db_initialised, - mock_run_in_apache): + mock_run_in_apache, + update): mock_run_in_apache.return_value = False git_requested.return_value = False mock_is_ssl_cert_master.return_value = True @@ -552,14 +473,9 @@ class KeystoneRelationTests(CharmTestCase): configure_https.assert_called_with() self.assertTrue(configs.write_all.called) - self.assertTrue(self.ensure_initial_admin.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') + self.assertTrue(update.called) + @patch.object(hooks, 'update_all_identity_relation_units') @patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'initialise_pki') @patch.object(hooks, 'git_install_requested') @@ -591,7 +507,8 @@ class KeystoneRelationTests(CharmTestCase): mock_log, config_val_changed, git_requested, mock_initialise_pki, - mock_run_in_apache): + mock_run_in_apache, + update): mock_run_in_apache.return_value = False git_requested.return_value = True mock_ensure_ssl_cert_master.return_value = False @@ -620,6 +537,7 @@ class KeystoneRelationTests(CharmTestCase): self.git_install.assert_called_with(projects_yaml) self.assertFalse(self.openstack_upgrade_available.called) self.assertFalse(self.do_openstack_upgrade_reexec.called) + self.assertTrue(update.called) @patch.object(hooks, 'run_in_apache') @patch.object(hooks, 'initialise_pki') @@ -782,16 +700,14 @@ class KeystoneRelationTests(CharmTestCase): hooks.leader_elected() 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, 'identity_changed') - def test_leader_settings_changed(self, mock_identity_changed, - mock_write): + def test_leader_settings_changed(self, mock_write, update): self.relation_ids.return_value = ['identity:1'] self.related_units.return_value = ['keystone/1'] hooks.leader_settings_changed() mock_write.assert_has_calls([call(utils.TOKEN_FLUSH_CRON_FILE)]) - exp = [call(relation_id='identity:1', remote_unit='keystone/1')] - mock_identity_changed.assert_has_calls(exp) + self.assertTrue(update.called) def test_ha_joined(self): self.get_hacluster_config.return_value = { @@ -908,6 +824,7 @@ class KeystoneRelationTests(CharmTestCase): self.assertTrue(configs.write_all.called) self.assertFalse(mock_synchronize_ca.called) + @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') @@ -917,7 +834,8 @@ class KeystoneRelationTests(CharmTestCase): identity_changed, mock_ensure_ssl_cert_master, mock_log, - mock_is_db_initialised): + mock_is_db_initialised, + update): mock_is_db_initialised.return_value = True self.is_db_ready.return_value = True mock_ensure_ssl_cert_master.return_value = False @@ -928,11 +846,7 @@ class KeystoneRelationTests(CharmTestCase): hooks.ha_changed() self.assertTrue(configs.write_all.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') + self.assertTrue(update.called) @patch('keystone_utils.log') @patch('keystone_utils.ensure_ssl_cert_master') @@ -965,6 +879,7 @@ class KeystoneRelationTests(CharmTestCase): cmd = ['a2dissite', 'openstack_https_frontend'] self.check_call.assert_called_with(cmd) + @patch.object(hooks, 'update_all_identity_relation_units') @patch.object(utils, 'os_release') @patch.object(utils, 'git_install_requested') @patch.object(hooks, 'is_db_ready') @@ -986,7 +901,8 @@ class KeystoneRelationTests(CharmTestCase): mock_is_db_initialised, mock_is_db_ready, git_requested, - os_release): + os_release, + update): mock_is_db_initialised.return_value = True mock_is_db_ready.return_value = True mock_is_elected_leader.return_value = False @@ -1005,10 +921,138 @@ class KeystoneRelationTests(CharmTestCase): user=self.ssh_user, group='juju_keystone', peer_interface='cluster', ensure_local_user=True) self.assertTrue(mock_synchronize_ca.called) - self.log.assert_called_with( - 'Firing identity_changed hook for all related services.') - self.assertTrue(self.ensure_initial_admin.called) + self.assertTrue(update.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, 'git_install_requested') @patch('keystone_utils.log') @@ -1021,7 +1065,7 @@ class KeystoneRelationTests(CharmTestCase): mock_ensure_ssl_cert_master, mock_relation_ids, mock_log, git_requested, - os_release): + os_release, update): mock_relation_ids.return_value = [] mock_ensure_ssl_cert_master.return_value = False # Ensure always returns diff @@ -1037,4 +1081,4 @@ class KeystoneRelationTests(CharmTestCase): user=self.ssh_user, group='juju_keystone', peer_interface='cluster', ensure_local_user=True) self.assertTrue(self.log.called) - self.assertFalse(self.ensure_initial_admin.called) + self.assertFalse(update.called) diff --git a/unit_tests/test_keystone_utils.py b/unit_tests/test_keystone_utils.py index 3535935e..1e1f2199 100644 --- a/unit_tests/test_keystone_utils.py +++ b/unit_tests/test_keystone_utils.py @@ -1,6 +1,7 @@ from mock import patch, call, MagicMock, Mock from test_utils import CharmTestCase import os +from base64 import b64encode os.environ['JUJU_UNIT_NAME'] = 'keystone' with patch('charmhelpers.core.hookenv.config') as config: @@ -859,7 +860,9 @@ class TestKeystoneUtils(CharmTestCase): self.service_start.assert_called_once_with('apache2') 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] self.subprocess.call.side_effect = lambda x: call_returns.pop() utils.restart_pid_check('apache2', ptable_string='httpd') @@ -872,3 +875,258 @@ class TestKeystoneUtils(CharmTestCase): call(['pgrep', 'httpd']), ] 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)