diff --git a/zaza/openstack/charm_tests/keystone/__init__.py b/zaza/openstack/charm_tests/keystone/__init__.py index e244e16..432eac2 100644 --- a/zaza/openstack/charm_tests/keystone/__init__.py +++ b/zaza/openstack/charm_tests/keystone/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. """Collection of code for setting up and testing keystone.""" +import contextlib import zaza import zaza.openstack.charm_tests.test_utils as test_utils import zaza.openstack.utilities.openstack as openstack_utils @@ -59,3 +60,12 @@ class BaseKeystoneTest(test_utils.OpenStackBaseTest): openstack_utils.get_keystone_session_client( cls.admin_keystone_session, client_api_version=cls.default_api_version)) + + @contextlib.contextmanager + def v3_keystone_preferred(self): + """Set the preferred keystone api to v3 within called context.""" + with self.config_change( + {'preferred-api-version': self.default_api_version}, + {'preferred-api-version': '3'}, + application_name="keystone"): + yield diff --git a/zaza/openstack/charm_tests/keystone/tests.py b/zaza/openstack/charm_tests/keystone/tests.py index 66727cc..eda5441 100644 --- a/zaza/openstack/charm_tests/keystone/tests.py +++ b/zaza/openstack/charm_tests/keystone/tests.py @@ -17,14 +17,13 @@ import collections import json import logging import pprint - import keystoneauth1 import zaza.model import zaza.openstack.utilities.exceptions as zaza_exceptions import zaza.openstack.utilities.juju as juju_utils import zaza.openstack.utilities.openstack as openstack_utils - +import zaza.charm_lifecycle.utils as lifecycle_utils import zaza.openstack.charm_tests.test_utils as test_utils from zaza.openstack.charm_tests.keystone import ( BaseKeystoneTest, @@ -189,10 +188,7 @@ class AuthenticationAuthorizationTest(BaseKeystoneTest): openstack_utils.get_os_release('trusty_mitaka')): logging.info('skipping test < trusty_mitaka') return - with self.config_change( - {'preferred-api-version': self.default_api_version}, - {'preferred-api-version': '3'}, - application_name="keystone"): + with self.v3_keystone_preferred(): for ip in self.keystone_ips: try: logging.info('keystone IP {}'.format(ip)) @@ -212,7 +208,7 @@ class AuthenticationAuthorizationTest(BaseKeystoneTest): def test_end_user_domain_admin_access(self): """Verify that end-user domain admin does not have elevated privileges. - In additon to validating that the `policy.json` is written and the + In addition to validating that the `policy.json` is written and the service is restarted on config-changed, the test validates that our `policy.json` is correct. @@ -222,10 +218,7 @@ class AuthenticationAuthorizationTest(BaseKeystoneTest): openstack_utils.get_os_release('xenial_ocata')): logging.info('skipping test < xenial_ocata') return - with self.config_change( - {'preferred-api-version': self.default_api_version}, - {'preferred-api-version': '3'}, - application_name="keystone"): + with self.v3_keystone_preferred(): for ip in self.keystone_ips: openrc = { 'API_VERSION': 3, @@ -257,7 +250,7 @@ class AuthenticationAuthorizationTest(BaseKeystoneTest): 'allowed when it should not be.') logging.info('OK') - def test_end_user_acccess_and_token(self): + def test_end_user_access_and_token(self): """Verify regular end-user access resources and validate token data. In effect this also validates user creation, presence of standard @@ -371,3 +364,81 @@ class SecurityTests(BaseKeystoneTest): expected_passes, expected_failures, expected_to_pass=False) + + +class LdapTests(BaseKeystoneTest): + """Keystone ldap tests tests.""" + + @classmethod + def setUpClass(cls): + """Run class setup for running Keystone ldap-tests.""" + super(LdapTests, cls).setUpClass() + + def _get_ldap_config(self): + """Generate ldap config for current model. + + :return: tuple of whether ldap-server is running and if so, config + for the keystone-ldap application. + :rtype: Tuple[bool, Dict[str,str]] + """ + ldap_ips = zaza.model.get_app_ips("ldap-server") + self.assertTrue(ldap_ips, "Should be at least one ldap server") + return { + 'ldap-server': "ldap://{}".format(ldap_ips[0]), + 'ldap-user': 'cn=admin,dc=test,dc=com', + 'ldap-password': 'crapper', + 'ldap-suffix': 'dc=test,dc=com', + 'domain-name': 'userdomain', + } + + def _find_keystone_v3_user(self, username, domain): + """Find a user within a specified keystone v3 domain. + + :param str username: Username to search for in keystone + :param str domain: username selected from which domain + :return: return username if found + :rtype: Optional[str] + """ + for ip in self.keystone_ips: + logging.info('Keystone IP {}'.format(ip)) + session = openstack_utils.get_keystone_session( + openstack_utils.get_overcloud_auth(address=ip)) + client = openstack_utils.get_keystone_session_client(session) + + domain_users = client.users.list( + domain=client.domains.find(name=domain).id + ) + + usernames = [u.name.lower() for u in domain_users] + if username.lower() in usernames: + return username + + logging.debug( + "User {} was not found. Returning None.".format(username) + ) + return None + + def test_100_keystone_ldap_users(self): + """Validate basic functionality of keystone API with ldap.""" + application_name = 'keystone-ldap' + config = self._get_ldap_config() + + with self.config_change( + self.config_current(application_name, config.keys()), + config, + application_name=application_name): + logging.info( + 'Waiting for users to become available in keystone...' + ) + test_config = lifecycle_utils.get_charm_config(fatal=False) + zaza.model.wait_for_application_states( + states=test_config.get("target_deploy_status", {}) + ) + + with self.v3_keystone_preferred(): + # NOTE(jamespage): Test fixture should have johndoe and janedoe + # accounts + johndoe = self._find_keystone_v3_user('john doe', 'userdomain') + self.assertIsNotNone(johndoe, "user 'john doe' was unknown") + janedoe = self._find_keystone_v3_user('jane doe', 'userdomain') + self.assertIsNotNone(janedoe, "user 'jane doe' was unknown") diff --git a/zaza/openstack/charm_tests/test_utils.py b/zaza/openstack/charm_tests/test_utils.py index 82df994..5903119 100644 --- a/zaza/openstack/charm_tests/test_utils.py +++ b/zaza/openstack/charm_tests/test_utils.py @@ -11,12 +11,11 @@ # 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. -"""Module containg base class for implementing charm tests.""" +"""Module containing base class for implementing charm tests.""" import contextlib import logging import subprocess import unittest -import zaza.model import zaza.model as model import zaza.charm_lifecycle.utils as lifecycle_utils @@ -28,7 +27,7 @@ def skipIfNotHA(service_name): """Run decorator to skip tests if application not in HA configuration.""" def _skipIfNotHA_inner_1(f): def _skipIfNotHA_inner_2(*args, **kwargs): - ips = zaza.model.get_app_ips( + ips = model.get_app_ips( service_name) if len(ips) > 1: return f(*args, **kwargs) @@ -70,7 +69,7 @@ def audit_assertions(action, :param expected_passes: List of test names that are expected to pass :type expected_passes: List[str] :param expected_failures: List of test names that are expected to fail - :type expexted_failures: List[str] + :type expected_failures: List[str] :raises: AssertionError if the assertion fails. """ if expected_failures is None: @@ -115,7 +114,7 @@ class OpenStackBaseTest(unittest.TestCase): @classmethod def setUpClass(cls, application_name=None, model_alias=None): - """Run setup for test class to create common resourcea.""" + """Run setup for test class to create common resources.""" cls.model_aliases = model.get_juju_model_aliases() if model_alias: cls.model_name = cls.model_aliases[model_alias] @@ -133,6 +132,50 @@ class OpenStackBaseTest(unittest.TestCase): model_name=cls.model_name) logging.debug('Leader unit is {}'.format(cls.lead_unit)) + def config_current(self, application_name=None, keys=None): + """Get Current Config of an application normalized into key-values. + + :param application_name: String application name for use when called + by a charm under test other than the object's + application. + :type application_name: Optional[str] + :param keys: iterable of strs to index into the current config. If + None, return all keys from the config + :type keys: Optional[Iterable[str]] + :return: Dictionary of requested config from application + :rtype: Dict[str, Any] + """ + if not application_name: + application_name = self.application_name + + _app_config = model.get_application_config(application_name) + + keys = keys or _app_config.keys() + return { + k: _app_config.get(k, {}).get('value') + for k in keys + } + + @staticmethod + def _stringed_value_config(config): + """Stringify values in a dict. + + Workaround: + libjuju refuses to accept data with types other than strings + through the zuzu.model.set_application_config + + :param config: Config dictionary with any typed values + :type config: Dict[str,Any] + :return: Config Dictionary with string-ly typed values + :rtype: Dict[str,str] + """ + # if v is None, stringify to '' + # otherwise use a strict cast with str(...) + return { + k: '' if v is None else str(v) + for k, v in config.items() + } + @contextlib.contextmanager def config_change(self, default_config, alternate_config, application_name=None): @@ -158,17 +201,14 @@ class OpenStackBaseTest(unittest.TestCase): """ if not application_name: application_name = self.application_name + # we need to compare config values to what is already applied before # attempting to set them. otherwise the model will behave differently # than we would expect while waiting for completion of the change - _app_config = model.get_application_config(application_name) - app_config = {} - # convert the more elaborate config structure from libjuju to something - # we can compare to what the caller supplies to this function - for k in alternate_config.keys(): - # note that conversion to string for all values is due to - # attempting to set any config with other types lead to Traceback - app_config[k] = str(_app_config.get(k, {}).get('value', '')) + app_config = self.config_current( + application_name, keys=alternate_config.keys() + ) + if all(item in app_config.items() for item in alternate_config.items()): logging.debug('alternate_config equals what is already applied ' @@ -185,7 +225,7 @@ class OpenStackBaseTest(unittest.TestCase): .format(alternate_config)) model.set_application_config( application_name, - alternate_config, + self._stringed_value_config(alternate_config), model_name=self.model_name) logging.debug( @@ -205,7 +245,7 @@ class OpenStackBaseTest(unittest.TestCase): logging.debug('Restoring charm setting to {}'.format(default_config)) model.set_application_config( application_name, - default_config, + self._stringed_value_config(default_config), model_name=self.model_name) logging.debug(