diff options
-rw-r--r-- | os_client_config/cloud_config.py | 139 | ||||
-rw-r--r-- | os_client_config/tests/test_cloud_config.py | 135 | ||||
-rw-r--r-- | test-requirements.txt | 2 |
3 files changed, 276 insertions, 0 deletions
diff --git a/os_client_config/cloud_config.py b/os_client_config/cloud_config.py index 63ff8d2..6085a64 100644 --- a/os_client_config/cloud_config.py +++ b/os_client_config/cloud_config.py @@ -14,6 +14,11 @@ import warnings +from keystoneauth1 import plugin +from keystoneauth1 import session + +from os_client_config import exceptions + class CloudConfig(object): def __init__(self, name, region, config, @@ -25,6 +30,7 @@ class CloudConfig(object): self._force_ipv4 = force_ipv4 self._auth = auth_plugin self._openstack_config = openstack_config + self._keystone_session = None def __getattr__(self, key): """Return arbitrary attributes.""" @@ -119,6 +125,139 @@ class CloudConfig(object): """Return a keystoneauth plugin from the auth credentials.""" return self._auth + def get_session(self): + """Return a keystoneauth session based on the auth credentials.""" + if self._keystone_session is None: + if not self._auth: + raise exceptions.OpenStackConfigException( + "Problem with auth parameters") + (verify, cert) = self.get_requests_verify_args() + self._keystone_session = session.Session( + auth=self._auth, + verify=verify, + cert=cert, + timeout=self.config['api_timeout']) + return self._keystone_session + + def get_session_endpoint(self, service_key): + """Return the endpoint from config or the catalog. + + If a configuration lists an explicit endpoint for a service, + return that. Otherwise, fetch the service catalog from the + keystone session and return the appropriate endpoint. + + :param service_key: Generic key for service, such as 'compute' or + 'network' + + :returns: Endpoint for the service, or None if not found + """ + + override_endpoint = self.get_endpoint(service_key) + if override_endpoint: + return override_endpoint + # keystone is a special case in keystone, because what? + session = self.get_session() + if service_key == 'identity': + endpoint = session.get_endpoint(interface=plugin.AUTH_INTERFACE) + else: + endpoint = session.get_endpoint( + service_type=self.get_service_type(service_key), + service_name=self.get_service_name(service_key), + interface=self.get_interface(service_key), + region_name=self.region) + return endpoint + + def get_legacy_client( + self, service_key, client_class, interface_key=None, + pass_version_arg=True, **kwargs): + """Return a legacy OpenStack client object for the given config. + + Most of the OpenStack python-*client libraries have the same + interface for their client constructors, but there are several + parameters one wants to pass given a :class:`CloudConfig` object. + + In the future, OpenStack API consumption should be done through + the OpenStack SDK, but that's not ready yet. This is for getting + Client objects from python-*client only. + + :param service_key: Generic key for service, such as 'compute' or + 'network' + :param client_class: Class of the client to be instantiated. This + should be the unversioned version if there + is one, such as novaclient.client.Client, or + the versioned one, such as + neutronclient.v2_0.client.Client if there isn't + :param interface_key: (optional) Some clients, such as glanceclient + only accept the parameter 'interface' instead + of 'endpoint_type' - this is a get-out-of-jail + parameter for those until they can be aligned. + os-client-config understands this to be the + case if service_key is image, so this is really + only for use with other unknown broken clients. + :param pass_version_arg: (optional) If a versioned Client constructor + was passed to client_class, set this to + False, which will tell get_client to not + pass a version parameter. os-client-config + already understand that this is the + case for network, so it can be omitted in + that case. + :param kwargs: (optional) keyword args are passed through to the + Client constructor, so this is in case anything + additional needs to be passed in. + """ + # Because of course swift is different + if service_key == 'object-store': + return self._get_swift_client(client_class=client_class, **kwargs) + interface = self.get_interface(service_key) + # trigger exception on lack of service + endpoint = self.get_session_endpoint(service_key) + + if not interface_key: + if service_key == 'image': + interface_key = 'interface' + else: + interface_key = 'endpoint_type' + if service_key == 'network': + pass_version_arg = False + + constructor_args = dict( + session=self.get_session(), + service_name=self.get_service_name(service_key), + service_type=self.get_service_type(service_key), + region_name=self.region) + + if service_key == 'image': + # os-client-config does not depend on glanceclient, but if + # the user passed in glanceclient.client.Client, which they + # would need to do if they were requesting 'image' - then + # they necessarily have glanceclient installed + from glanceclient.common import utils as glance_utils + endpoint, version = glance_utils.strip_version(endpoint) + constructor_args['endpoint'] = endpoint + constructor_args.update(kwargs) + constructor_args[interface_key] = interface + if pass_version_arg: + version = self.get_api_version(service_key) + constructor_args['version'] = version + return client_class(**constructor_args) + + def _get_swift_client(self, client_class, **kwargs): + session = self.get_session() + token = session.get_token() + endpoint = self.get_session_endpoint(service_key='object-store') + if not endpoint: + return None + return client_class( + preauthurl=endpoint, + preauthtoken=token, + auth_version=self.get_api_version('identity'), + os_options=dict( + auth_token=token, + object_storage_url=endpoint, + region_name=self.get_region_name()), + timeout=self.api_timeout, + ) + def get_cache_expiration_time(self): if self._openstack_config: return self._openstack_config.get_cache_expiration_time() diff --git a/os_client_config/tests/test_cloud_config.py b/os_client_config/tests/test_cloud_config.py index c9317ad..f5ceac7 100644 --- a/os_client_config/tests/test_cloud_config.py +++ b/os_client_config/tests/test_cloud_config.py @@ -12,7 +12,13 @@ import copy +from keystoneauth1 import plugin as ksa_plugin +from keystoneauth1 import session as ksa_session +import mock + from os_client_config import cloud_config +from os_client_config import defaults +from os_client_config import exceptions from os_client_config.tests import base @@ -142,3 +148,132 @@ class TestCloudConfig(base.TestCase): cc.get_endpoint('image')) self.assertEqual(None, cc.get_service_name('compute')) self.assertEqual('locks', cc.get_service_name('identity')) + + def test_get_session_no_auth(self): + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig("test1", "region-al", config_dict) + self.assertRaises( + exceptions.OpenStackConfigException, + cc.get_session) + + @mock.patch.object(ksa_session, 'Session') + def test_get_session(self, mock_session): + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_session() + mock_session.assert_called_with( + auth=mock.ANY, + verify=True, cert=None, timeout=None) + + @mock.patch.object(ksa_session, 'Session') + def test_override_session_endpoint(self, mock_session): + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + self.assertEqual( + cc.get_session_endpoint('compute'), + fake_services_dict['compute_endpoint']) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_session_endpoint_identity(self, mock_get_session): + mock_session = mock.Mock() + mock_get_session.return_value = mock_session + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_session_endpoint('identity') + mock_session.get_endpoint.assert_called_with( + interface=ksa_plugin.AUTH_INTERFACE) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session') + def test_session_endpoint(self, mock_get_session): + mock_session = mock.Mock() + mock_get_session.return_value = mock_session + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_session_endpoint('orchestration') + mock_session.get_endpoint.assert_called_with( + interface='public', + service_name=None, + region_name='region-al', + service_type='orchestration') + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_object_store(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('object-store', mock_client) + mock_client.assert_called_with( + preauthtoken=mock.ANY, + os_options={ + 'auth_token': mock.ANY, + 'region_name': 'region-al', + 'object_storage_url': 'http://example.com/v2' + }, + preauthurl='http://example.com/v2', + auth_version='2.0', + timeout=None) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_image(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('image', mock_client) + mock_client.assert_called_with( + version='2', + service_name=None, + endpoint='http://example.com', + region_name='region-al', + interface='public', + session=mock.ANY, + # Not a typo - the config dict above overrides this + service_type='mage' + ) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_network(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('network', mock_client) + mock_client.assert_called_with( + endpoint_type='public', + region_name='region-al', + service_type='network', + session=mock.ANY, + service_name=None) + + @mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint') + def test_legacy_client_compute(self, mock_get_session_endpoint): + mock_client = mock.Mock() + mock_get_session_endpoint.return_value = 'http://example.com/v2' + config_dict = defaults.get_defaults() + config_dict.update(fake_services_dict) + cc = cloud_config.CloudConfig( + "test1", "region-al", config_dict, auth_plugin=mock.Mock()) + cc.get_legacy_client('compute', mock_client) + mock_client.assert_called_with( + version=2, + endpoint_type='public', + region_name='region-al', + service_type='compute', + session=mock.ANY, + service_name=None) diff --git a/test-requirements.txt b/test-requirements.txt index 9a02b04..7053051 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,6 +9,8 @@ extras fixtures>=0.3.14 discover jsonschema>=2.0.0,<3.0.0,!=2.5.0 +mock>=1.2 +python-glanceclient>=0.18.0 python-keystoneclient>=1.1.0 python-subunit>=0.0.18 sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 |