diff --git a/tobiko/openstack/keystone/_config_files.py b/tobiko/openstack/keystone/_config_files.py new file mode 100644 index 000000000..377324d85 --- /dev/null +++ b/tobiko/openstack/keystone/_config_files.py @@ -0,0 +1,44 @@ +# Copyright 2019 Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import absolute_import + +import os + +import appdirs + + +APPDIRS = appdirs.AppDirs('openstack', 'OpenStack', multipath='/etc') +CONFIG_HOME = APPDIRS.user_config_dir +CACHE_PATH = APPDIRS.user_cache_dir + +UNIX_CONFIG_HOME = os.path.join( + os.path.expanduser(os.path.join('~', '.config')), 'openstack') +UNIX_SITE_CONFIG_HOME = '/etc/openstack' + +SITE_CONFIG_HOME = APPDIRS.site_config_dir + +CONFIG_SEARCH_PATH = [ + os.getcwd(), + CONFIG_HOME, UNIX_CONFIG_HOME, + SITE_CONFIG_HOME, UNIX_SITE_CONFIG_HOME +] +YAML_SUFFIXES = ('.yaml', '.yml') +JSON_SUFFIXES = ('.json',) + + +def get_cloud_config_files(): + return [ + os.path.join(d, 'clouds' + s) + for d in CONFIG_SEARCH_PATH + for s in YAML_SUFFIXES + JSON_SUFFIXES] diff --git a/tobiko/openstack/keystone/_credentials.py b/tobiko/openstack/keystone/_credentials.py index 86e168fbc..fc1195d37 100644 --- a/tobiko/openstack/keystone/_credentials.py +++ b/tobiko/openstack/keystone/_credentials.py @@ -14,6 +14,7 @@ from __future__ import absolute_import import collections +import json import os import sys @@ -21,6 +22,7 @@ from oslo_log import log import yaml import tobiko +from tobiko.openstack.keystone import _config_files LOG = log.getLogger(__name__) @@ -223,6 +225,58 @@ class EnvironKeystoneCredentialsFixture(KeystoneCredentialsFixture): return value +class CloudsFileKeystoneCredentialsFixture(EnvironKeystoneCredentialsFixture): + + def __init__(self, credentials=None, environ=None, clouds_files=None): + super(CloudsFileKeystoneCredentialsFixture, self).__init__( + credentials=credentials, environ=environ) + self.clouds_files = ( + clouds_files or _config_files.get_cloud_config_files()) + + def _load_yaml_json_file(self, filelist): + for path in filelist: + if os.path.exists(path): + with open(path, 'r') as f: + if path.endswith('json'): + return path, json.load(f) + else: + return path, yaml.safe_load(f) + return None, {} + + def get_credentials(self): + cloud_name = self.get_env("OS_CLOUD") + if not cloud_name: + LOG.debug('No OS_CLOUD env variable') + return None + + file_name, clouds_config = self._load_yaml_json_file(self.clouds_files) + + clouds_config = clouds_config.get("clouds") + if not clouds_config: + LOG.debug('No clouds configs found in any of %s', + self.clouds_files) + return None + + config = clouds_config.get(cloud_name) + if not config: + LOG.debug("No %s cloud config found in cloud configs file %s", + cloud_name, file_name) + return None + + auth = config.get("auth", {}) + return keystone_credentials( + api_version=int(config.get("identity_api_version")), + auth_url=auth.get("auth_url"), + username=auth.get("username"), + password=auth.get("password"), + project_name=auth.get("project_name"), + domain_name=auth.get("domain_name"), + user_domain_name=auth.get("user_domain_name"), + project_domain_name=auth.get("project_domain_name"), + project_domain_id=auth.get("project_domain_id"), + trust_id=auth.get("trust_id")) + + class ConfigKeystoneCredentialsFixture(KeystoneCredentialsFixture): def get_credentials(self): @@ -258,6 +312,7 @@ class ConfigKeystoneCredentialsFixture(KeystoneCredentialsFixture): DEFAULT_KEYSTONE_CREDENTIALS_FIXTURES = [ + CloudsFileKeystoneCredentialsFixture, EnvironKeystoneCredentialsFixture, ConfigKeystoneCredentialsFixture] diff --git a/tobiko/tests/unit/openstack/keystone/test_credentials.py b/tobiko/tests/unit/openstack/keystone/test_credentials.py index 05dc7ecda..d86c65c2f 100644 --- a/tobiko/tests/unit/openstack/keystone/test_credentials.py +++ b/tobiko/tests/unit/openstack/keystone/test_credentials.py @@ -14,8 +14,12 @@ # under the License. from __future__ import absolute_import +import json import os +import mock +import yaml + import tobiko from tobiko import config from tobiko.openstack import keystone @@ -61,6 +65,17 @@ V3_ENVIRON = { 'OS_USER_DOMAIN_NAME': 'Default', 'OS_PROJECT_DOMAIN_NAME': 'Default'} +CLOUDS_CONFIG = { + 'clouds': { + 'test-cloud': { + 'auth': {'auth_url': V3_PARAMS['auth_url'], + 'password': V3_PARAMS['password'], + 'project_domain_name': V3_PARAMS['project_domain_name'], + 'project_name': V3_PARAMS['project_name'], + 'user_domain_name': V3_PARAMS['user_domain_name'], + 'username': V3_PARAMS['username']}, + 'identity_api_version': str(V3_PARAMS['api_version'])}}} + V3_ENVIRON_WITH_VERSION = dict(V3_ENVIRON, OS_IDENTITY_API_VERSION='3') @@ -163,6 +178,56 @@ class EnvironKeystoneCredentialsFixtureTest(openstack.OpenstackTest): self.assertEqual(V3_PARAMS, fixture.credentials.to_dict()) +class CloudsFileKeystoneCredentialsFixtureTestJson(openstack.OpenstackTest): + + clouds_file_name = "/tmp/test-cloud-file.json" + json_cloud_data = json.dumps(CLOUDS_CONFIG) + + def setUp(self): + super(CloudsFileKeystoneCredentialsFixtureTestJson, self).setUp() + self.patch(os, 'environ', {'OS_CLOUD': 'test-cloud'}) + self.patch(os.path, 'exists', return_value=True) + mocked_open = mock.mock_open(read_data=self.json_cloud_data) + self.patch(_credentials, "open", mocked_open) + + def test_setup_from_clouds_config_file(self): + fixture = _credentials.CloudsFileKeystoneCredentialsFixture( + clouds_files=[self.clouds_file_name]) + fixture.setUp() + fixture.credentials.validate() + self.assertEqual( + V3_PARAMS, fixture.credentials.to_dict()) + + def test_setup_from_file_no_os_cloud_env_set(self): + self.patch(os, 'environ', {}) + fixture = _credentials.CloudsFileKeystoneCredentialsFixture( + clouds_files=[self.clouds_file_name]) + fixture.setUp() + self.assertIsNone(fixture.credentials) + + def test_setup_from_file_no_clouds_config_in_file(self): + mocked_open = mock.mock_open(read_data=json.dumps({})) + self.patch(_credentials, "open", mocked_open) + fixture = _credentials.CloudsFileKeystoneCredentialsFixture( + clouds_files=[self.clouds_file_name]) + fixture.setUp() + self.assertIsNone(fixture.credentials) + + def test_setup_from_file_no_specified_cloud_config_in_file(self): + self.patch(os, 'environ', {'OS_CLOUD': 'some-other-cloud'}) + fixture = _credentials.CloudsFileKeystoneCredentialsFixture( + clouds_files=[self.clouds_file_name]) + fixture.setUp() + self.assertIsNone(fixture.credentials) + + +class CloudsFileKeystoneCredentialsFixtureTestYaml( + CloudsFileKeystoneCredentialsFixtureTestJson): + + clouds_file_name = "/tmp/test-cloud-file.yaml" + json_cloud_data = yaml.dump(CLOUDS_CONFIG) + + class ConfigKeystoneCredentialsFixtureTest(openstack.OpenstackTest): def patch_config(self, params, **kwargs):