diff --git a/sahara/context.py b/sahara/context.py index 65cfc5fd..202383e3 100644 --- a/sahara/context.py +++ b/sahara/context.py @@ -47,6 +47,7 @@ class Context(context.RequestContext): resource_uuid=None, current_instance_info=None, request_id=None, + auth_plugin=None, overwrite=True, **kwargs): if kwargs: @@ -64,6 +65,7 @@ class Context(context.RequestContext): self.tenant_name = tenant_name self.remote_semaphore = remote_semaphore or semaphore.Semaphore( CONF.cluster_remote_threshold) + self.auth_plugin = auth_plugin self.roles = roles if overwrite or not hasattr(context._request_store, 'context'): self.update_store() @@ -87,6 +89,7 @@ class Context(context.RequestContext): self.resource_uuid, self.current_instance_info, self.request_id, + self.auth_plugin, overwrite=False) def to_dict(self): diff --git a/sahara/service/sessions.py b/sahara/service/sessions.py new file mode 100644 index 00000000..57eb42e1 --- /dev/null +++ b/sahara/service/sessions.py @@ -0,0 +1,106 @@ +# Copyright (c) 2015 Red Hat, Inc. +# +# 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 keystoneclient import session as keystone +from oslo_config import cfg +from oslo_log import log as logging + +from sahara import exceptions as ex +from sahara.i18n import _ +from sahara.i18n import _LE + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + +_SESSION_CACHE = None + +SESSION_TYPE_GENERIC = 'generic' +SESSION_TYPE_KEYSTONE = 'keystone' + + +def cache(): + global _SESSION_CACHE + if not _SESSION_CACHE: + _SESSION_CACHE = SessionCache() + return _SESSION_CACHE + + +class SessionCache(object): + '''A cache of keystone Session objects + + When a requested Session is not currently cached, it will be + acquired from specific information in this module. Sessions should + be referenced by their OpenStack project name and not the service + name, this is to allow for multiple service implementations + while retaining the ability to generate Session objects. In all + cases, the constant values in this module should be used to + communicate the session type. + + ''' + def __init__(self): + '''create a new SessionCache''' + self._sessions = {} + self._session_funcs = { + SESSION_TYPE_GENERIC: self.get_generic_session, + SESSION_TYPE_KEYSTONE: self.get_keystone_session, + } + + def _set_session(self, session_type, session): + '''Set the session for a given type. + + :param session_type: the type of session to set. + + :param session: the session to associate with the type + ''' + self._sessions[session_type] = session + + def get_session(self, session_type=SESSION_TYPE_GENERIC): + '''Return a Session for the requested type + + :param session_type: the type of Session to get, if None a generic + session will be returned. + + :raises SaharaException: if the requested session type is not + found. + ''' + session_function = self._session_funcs.get(session_type) + if session_function: + return session_function() + else: + LOG.error( + _LE('Requesting an unknown session type (type: {type})'). + format(type=session_type)) + raise ex.SaharaException( + _('Session type {type} not recognized'). + format(type=session_type)) + + def get_generic_session(self): + session = self._sessions.get(SESSION_TYPE_GENERIC) + if not session: + session = keystone.Session() + self._set_session(SESSION_TYPE_GENERIC, session) + return session + + def get_keystone_session(self): + session = self._sessions.get(SESSION_TYPE_KEYSTONE) + if not session: + if CONF.keystone.ca_file: + session = keystone.Session(cert=CONF.keystone.ca_file, + verify=CONF.keystone.api_insecure) + else: + session = self.get_generic_session() + self._set_session(SESSION_TYPE_KEYSTONE, session) + return session diff --git a/sahara/service/trusts.py b/sahara/service/trusts.py index 5a911dc8..3231cfd5 100644 --- a/sahara/service/trusts.py +++ b/sahara/service/trusts.py @@ -39,36 +39,42 @@ def _get_expiry(): hours=CONF.cluster_operation_trust_expiration_hours) -def create_trust(trustor, - trustee, - role_names, - impersonation=True, - project_id=None, - expires=True): +def create_trust(trustor, trustee, role_names, impersonation=True, + project_id=None, expires=True): '''Create a trust and return it's identifier - :param trustor: The Keystone client delegating the trust. - :param trustee: The Keystone client consuming the trust. + :param trustor: The user delegating the trust, this is an auth plugin. + + :param trustee: The user consuming the trust, this is an auth plugin. + :param role_names: A list of role names to be assigned. + :param impersonation: Should the trustee impersonate trustor, default is True. + :param project_id: The project that the trust will be scoped into, default is the trustor's project id. + :param expires: The trust will expire if this is set to True. + :returns: A valid trust id. + :raises CreationFailed: If the trust cannot be created. ''' if project_id is None: - project_id = trustor.tenant_id + project_id = keystone.project_id_from_auth(trustor) try: expires_at = _get_expiry() if expires else None - trust = trustor.trusts.create(trustor_user=trustor.user_id, - trustee_user=trustee.user_id, - impersonation=impersonation, - role_names=role_names, - project=project_id, - expires_at=expires_at) + trustor_user_id = keystone.user_id_from_auth(trustor) + trustee_user_id = keystone.user_id_from_auth(trustee) + client = keystone.client_from_auth(trustor) + trust = client.trusts.create(trustor_user=trustor_user_id, + trustee_user=trustee_user_id, + impersonation=impersonation, + role_names=role_names, + project=project_id, + expires_at=expires_at) LOG.debug('Created trust {trust_id}'.format( trust_id=six.text_type(trust.id))) return trust.id @@ -87,9 +93,10 @@ def create_trust_for_cluster(cluster, expires=True): :param expires: The trust will expire if this is set to True. ''' - trustor = keystone.client() ctx = context.current() - trustee = keystone.client_for_admin() + trustor = keystone.auth() + trustee = keystone.auth_for_admin( + project_name=CONF.keystone_authtoken.admin_tenant_name) trust_id = create_trust(trustor=trustor, trustee=trustee, @@ -104,13 +111,16 @@ def create_trust_for_cluster(cluster, expires=True): def delete_trust(trustee, trust_id): '''Delete a trust from a trustee - :param trustee: The Keystone client to delete the trust from. + :param trustee: The user to delete the trust from, this is an auth plugin. + :param trust_id: The identifier of the trust to delete. + :raises DeletionFailed: If the trust cannot be deleted. ''' try: - trustee.trusts.delete(trust_id) + client = keystone.client_from_auth(trustee) + client.trusts.delete(trust_id) LOG.debug('Deleted trust {trust_id}'.format( trust_id=six.text_type(trust_id))) except Exception as e: @@ -130,9 +140,8 @@ def delete_trust_from_cluster(cluster): ''' if cluster.trust_id: - keystone_client = keystone.client_for_admin_from_trust( - cluster.trust_id) - delete_trust(keystone_client, cluster.trust_id) + keystone_auth = keystone.auth_for_admin(trust_id=cluster.trust_id) + delete_trust(keystone_auth, cluster.trust_id) ctx = context.current() conductor.cluster_update(ctx, cluster, @@ -154,6 +163,8 @@ def use_os_admin_auth_token(cluster): ctx = context.current() ctx.username = CONF.keystone_authtoken.admin_user ctx.tenant_id = cluster.tenant_id - client = keystone.client_for_admin_from_trust(cluster.trust_id) - ctx.auth_token = client.auth_token - ctx.service_catalog = json.dumps(client.service_catalog.get_data()) + ctx.auth_plugin = keystone.auth_for_admin( + trust_id=cluster.trust_id) + ctx.auth_token = keystone.token_from_auth(ctx.auth_plugin) + ctx.service_catalog = json.dumps( + keystone.service_catalog_from_auth(ctx.auth_plugin)) diff --git a/sahara/tests/unit/service/test_sessions.py b/sahara/tests/unit/service/test_sessions.py new file mode 100644 index 00000000..4bde92d8 --- /dev/null +++ b/sahara/tests/unit/service/test_sessions.py @@ -0,0 +1,33 @@ +# Copyright (c) 2015 Red Hat, Inc. +# +# 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 keystoneclient import session as keystone + +from sahara import exceptions as ex +from sahara.service import sessions +from sahara.tests.unit import base + + +class TestSessionCache(base.SaharaTestCase): + + def test_get_session(self): + sc = sessions.cache() + + session = sc.get_session() + self.assertTrue(isinstance(session, keystone.Session)) + + self.assertRaises(ex.SaharaException, + sc.get_session, + session_type='bad service') diff --git a/sahara/tests/unit/service/test_trusts.py b/sahara/tests/unit/service/test_trusts.py index 7e26fab5..79fd4cf7 100644 --- a/sahara/tests/unit/service/test_trusts.py +++ b/sahara/tests/unit/service/test_trusts.py @@ -27,20 +27,27 @@ class FakeTrust(object): class TestTrusts(base.SaharaTestCase): - def _trustor(self): + def _client(self): create = mock.Mock() create.return_value = FakeTrust("trust_id") - trustor_trusts = mock.Mock(create=create) - trustor = mock.Mock(user_id="trustor_id", tenant_id="tenant_id", - trusts=trustor_trusts) - return trustor + client_trusts = mock.Mock(create=create) + client = mock.Mock(trusts=client_trusts) + return client - def test_create_trust(self): - trustor = self._trustor() - trustee = mock.Mock(user_id="trustee_id") + @mock.patch('sahara.utils.openstack.keystone.client_from_auth') + @mock.patch('sahara.utils.openstack.keystone.project_id_from_auth') + @mock.patch('sahara.utils.openstack.keystone.user_id_from_auth') + def test_create_trust(self, user_id_from_auth, project_id_from_auth, + client_from_auth): + project_id_from_auth.return_value = 'tenant_id' + user_id_from_auth.side_effect = ['trustor_id', 'trustee_id'] + trustor = 'trustor_id' + trustee = 'trustee_id' + client = self._client() + client_from_auth.return_value = client trust_id = trusts.create_trust(trustor, trustee, "role_names", expires=True) - trustor.trusts.create.assert_called_with( + client.trusts.create.assert_called_with( trustor_user="trustor_id", trustee_user="trustee_id", impersonation=True, @@ -50,29 +57,42 @@ class TestTrusts(base.SaharaTestCase): ) self.assertEqual("trust_id", trust_id) - @mock.patch('sahara.utils.openstack.keystone.client_for_admin') - @mock.patch('sahara.utils.openstack.keystone.client') - @mock.patch('sahara.conductor.API.cluster_update') - @mock.patch('sahara.context.current') - def test_create_trust_for_cluster(self, m_current, m_cluster_update, - m_client, m_client_for_admin): - ctx = mock.Mock(roles="role_names") - trustee = mock.Mock(user_id="trustee_id") - trustor = self._trustor() + user_id_from_auth.side_effect = ['trustor_id', 'trustee_id'] + client = self._client() + client_from_auth.return_value = client + trust_id = trusts.create_trust(trustor, trustee, "role_names", + project_id='injected_project', + expires=False) + client.trusts.create.assert_called_with(trustor_user="trustor_id", + trustee_user="trustee_id", + impersonation=True, + role_names="role_names", + project="injected_project", + expires_at=None) + self.assertEqual("trust_id", trust_id) - m_current.return_value = ctx - m_client_for_admin.return_value = trustee - m_client.return_value = trustor + @mock.patch('sahara.conductor.API.cluster_update') + @mock.patch('sahara.service.trusts.create_trust') + @mock.patch('sahara.utils.openstack.keystone.auth_for_admin') + @mock.patch('sahara.context.current') + def test_create_trust_for_cluster(self, context_current, auth_for_admin, + create_trust, cluster_update): + self.override_config('admin_tenant_name', 'admin_project', + group='keystone_authtoken') + trustor_auth = mock.Mock() + ctx = mock.Mock(roles="role_names", auth_plugin=trustor_auth) + context_current.return_value = ctx + trustee_auth = mock.Mock() + auth_for_admin.return_value = trustee_auth + create_trust.return_value = 'trust_id' trusts.create_trust_for_cluster("cluster") - trustor.trusts.create.assert_called_with( - trustor_user="trustor_id", - trustee_user="trustee_id", - impersonation=True, - role_names="role_names", - project="tenant_id", - expires_at=mock.ANY - ) - m_cluster_update.assert_called_with(ctx, "cluster", - {"trust_id": "trust_id"}) + auth_for_admin.assert_called_with(project_name='admin_project') + create_trust.assert_called_with(trustor=trustor_auth, + trustee=trustee_auth, + role_names='role_names', + expires=True) + + cluster_update.assert_called_with(ctx, "cluster", + {"trust_id": "trust_id"}) diff --git a/sahara/utils/api.py b/sahara/utils/api.py index f40dc188..6a1632b5 100644 --- a/sahara/utils/api.py +++ b/sahara/utils/api.py @@ -78,6 +78,7 @@ class Rest(flask.Blueprint): kwargs.pop("tenant_id") req_id = flask.request.environ.get(oslo_req_id.ENV_REQUEST_ID) + auth_plugin = flask.request.environ.get('keystone.token_auth') ctx = context.Context( flask.request.headers['X-User-Id'], flask.request.headers['X-Tenant-Id'], @@ -86,6 +87,7 @@ class Rest(flask.Blueprint): flask.request.headers['X-User-Name'], flask.request.headers['X-Tenant-Name'], flask.request.headers['X-Roles'].split(','), + auth_plugin=auth_plugin, request_id=req_id) context.set_ctx(ctx) if flask.request.method in ['POST', 'PUT', 'PATCH']: diff --git a/sahara/utils/openstack/keystone.py b/sahara/utils/openstack/keystone.py index 96e3d628..372f97b0 100644 --- a/sahara/utils/openstack/keystone.py +++ b/sahara/utils/openstack/keystone.py @@ -20,6 +20,7 @@ from keystoneclient.v3 import client as keystone_client_v3 from oslo_config import cfg from sahara import context +from sahara.service import sessions from sahara.utils.openstack import base @@ -73,14 +74,142 @@ CONF.register_opts(opts) CONF.register_opts(ssl_opts, group=keystone_group) +def auth(): + '''Return a token auth plugin for the current context.''' + ctx = context.current() + return ctx.auth_plugin or _token_auth(ctx.auth_token, ctx.tenant_id) + + +def auth_for_admin(project_name=None, trust_id=None): + '''Return an auth plugin for the admin. + + :param project_name: a project to scope the auth with (optional). + + :param trust_id: a trust to scope the auth with (optional). + + :returns: an auth plugin object for the admin. + ''' + # TODO(elmiko) revisit the project_domain_name if we start getting + # into federated authentication. it will need to match the domain that + # the project_name exists in. + auth = _password_auth( + username=CONF.keystone_authtoken.admin_user, + password=CONF.keystone_authtoken.admin_password, + project_name=project_name, + user_domain_name=CONF.admin_user_domain_name, + project_domain_name=CONF.admin_project_domain_name, + trust_id=trust_id) + return auth + + +def auth_for_proxy(username, password, trust_id=None): + '''Return an auth plugin for the proxy user. + + :param username: the name of the proxy user. + + :param password: the proxy user's password. + + :param trust_id: a trust to scope the auth with (optional). + + :returns: an auth plugin object for the proxy user. + ''' + auth = _password_auth( + username=username, + password=password, + user_domain_name=CONF.proxy_user_domain_name, + trust_id=trust_id) + return auth + + def client(): '''Return the current context client.''' - ctx = context.current() - - return _client(username=ctx.username, token=ctx.auth_token, - tenant_id=ctx.tenant_id) + return client_from_auth(auth()) +def client_for_admin(): + '''Return the Sahara admin user client.''' + auth = auth_for_admin( + project_name=CONF.keystone_authtoken.admin_tenant_name) + return client_from_auth(auth) + + +def client_from_auth(auth): + '''Return a session based client from the auth plugin provided. + + A session is obtained from the global session cache. + + :param auth: the auth plugin object to use in client creation. + + :returns: a keystone client + ''' + session = sessions.cache().get_session(sessions.SESSION_TYPE_KEYSTONE) + if CONF.use_identity_api_v3: + client_class = keystone_client_v3.Client + else: + client_class = keystone_client.Client + return client_class(session=session, auth=auth) + + +def project_id_from_auth(auth): + '''Return the project id associated with an auth plugin. + + :param auth: the auth plugin to inspect. + + :returns: the project id associated with the auth plugin. + ''' + return auth.get_project_id( + sessions.cache().get_session(sessions.SESSION_TYPE_KEYSTONE)) + + +def service_catalog_from_auth(auth): + '''Return the service catalog associated with an auth plugin. + + :param auth: the auth plugin to inspect. + + :returns: a list containing the service catalog. + ''' + if CONF.use_identity_api_v3: + return auth.get_access( + sessions.cache().get_session()).get('catalog', []) + else: + return auth.get_access( + sessions.cache().get_session()).get('serviceCatalog', []) + + +# TODO(elmiko) factor this out when redoing the barbicanclient +def session_for_admin(): + '''Return a Keystone session for the admin user.''' + auth = _password_auth( + username=CONF.keystone_authtoken.admin_user, + password=CONF.keystone_authtoken.admin_password, + project_name=CONF.keystone_authtoken.admin_tenant_name, + user_domain_name=CONF.admin_user_domain_name, + project_domain_name=CONF.admin_project_domain_name) + return keystone_session.Session(auth=auth) + + +def token_from_auth(auth): + '''Return an authentication token from an auth plugin. + + :param auth: the auth plugin to acquire a token from. + + :returns: an auth token in string format. + ''' + return keystone_session.Session(auth=auth).get_token() + + +def user_id_from_auth(auth): + '''Return a user id associated with an auth plugin. + + :param auth: the auth plugin to inspect. + + :returns: a token associated with the auth. + ''' + return auth.get_user_id(sessions.cache().get_session( + sessions.SESSION_TYPE_KEYSTONE)) + + +# TODO(elmiko) deprecate this when all client have been migrated to sessions def _client(username, password=None, token=None, tenant_name=None, tenant_id=None, trust_id=None, domain_name=None): @@ -111,64 +240,68 @@ def _client(username, password=None, token=None, tenant_name=None, return keystone -def _admin_client(project_name=None, trust_id=None): - username = CONF.keystone_authtoken.admin_user - password = CONF.keystone_authtoken.admin_password - keystone = _client(username=username, - password=password, - tenant_name=project_name, - trust_id=trust_id) - return keystone +def _password_auth(username, password, project_name, user_domain_name=None, + project_domain_name=None, trust_id=None): + '''Return a password auth plugin object. + :param username: the user to authenticate as. -def client_for_admin(): - '''Return the Sahara admin user client.''' - return _admin_client( - project_name=CONF.keystone_authtoken.admin_tenant_name) + :param password: the user's password. + :param project_name: the project(ex. tenant) name to scope the auth. -def client_for_admin_from_trust(trust_id): - '''Return the Sahara admin user client scoped to a trust.''' - return _admin_client(trust_id=trust_id) + :param user_domain_name: the domain the user belongs to. + :param project_domain_name: the domain the project belongs to. -def client_for_proxy_user(username, password, trust_id=None): - '''Return a client for the proxy user specified.''' - return _client(username=username, - password=password, - domain_name=CONF.proxy_user_domain_name, - trust_id=trust_id) + :param trust_id: a trust id to scope the auth. - -def _session(username, password, project_name, user_domain_name=None, - project_domain_name=None): + :returns: a password auth plugin object. + ''' passwd_kwargs = dict( auth_url=base.retrieve_auth_url(), - username=CONF.keystone_authtoken.admin_user, - password=CONF.keystone_authtoken.admin_password + username=username, + password=password ) - if CONF.use_identity_api_v3: passwd_kwargs.update(dict( project_name=project_name, user_domain_name=user_domain_name, - project_domain_name=project_domain_name + project_domain_name=project_domain_name, + trust_id=trust_id )) auth = keystone_identity.v3.Password(**passwd_kwargs) else: passwd_kwargs.update(dict( - tenant_name=project_name + tenant_name=project_name, + trust_id=trust_id )) auth = keystone_identity.v2.Password(**passwd_kwargs) - - return keystone_session.Session(auth=auth) + return auth -def session_for_admin(): - '''Return a Keystone session for the admin user.''' - return _session( - username=CONF.keystone_authtoken.admin_user, - password=CONF.keystone_authtoken.admin_password, - project_name=CONF.keystone_authtoken.admin_tenant_name, - user_domain_name=CONF.admin_user_domain_name, - project_domain_name=CONF.admin_project_domain_name) +def _token_auth(token, project_id, project_domain_name='Default'): + '''Return a token auth plugin object. + + :param token: the token to use for authentication. + + :param project_id: the project(ex. tenant) id to scope the auth. + + :returns: a token auth plugin object. + ''' + token_kwargs = dict( + auth_url=base.retrieve_auth_url(), + token=token + ) + if CONF.use_identity_api_v3: + token_kwargs.update(dict( + project_id=project_id, + project_domain_name=project_domain_name, + )) + auth = keystone_identity.v3.Token(**token_kwargs) + else: + token_kwargs.update(dict( + tenant_id=project_id + )) + auth = keystone_identity.v2.Token(**token_kwargs) + return auth diff --git a/sahara/utils/openstack/swift.py b/sahara/utils/openstack/swift.py index b1e60536..5d820bc0 100644 --- a/sahara/utils/openstack/swift.py +++ b/sahara/utils/openstack/swift.py @@ -55,8 +55,8 @@ def client(username, password, trust_id=None): ''' if trust_id: - proxyclient = k.client_for_proxy_user(username, password, trust_id) - return client_from_token(proxyclient.auth_token) + proxyauth = k.auth_for_proxy_user(username, password, trust_id) + return client_from_token(k.token_from_auth(proxyauth)) else: return swiftclient.Connection( auth_version='2.0', diff --git a/sahara/utils/proxy.py b/sahara/utils/proxy.py index 8f771003..4bbd9ae9 100644 --- a/sahara/utils/proxy.py +++ b/sahara/utils/proxy.py @@ -61,8 +61,8 @@ def create_proxy_user_for_job_execution(job_execution): ''' username = 'job_{0}'.format(job_execution.id) password = proxy_user_create(username) - current_user = k.client() - proxy_user = k.client_for_proxy_user(username, password) + current_user = k.auth() + proxy_user = k.auth_for_proxy(username, password) trust_id = t.create_trust(trustor=current_user, trustee=proxy_user, role_names=CONF.proxy_user_role_names) @@ -87,9 +87,9 @@ def delete_proxy_user_for_job_execution(job_execution): proxy_username = proxy_configs.get('proxy_username') proxy_password = proxy_configs.get('proxy_password') proxy_trust_id = proxy_configs.get('proxy_trust_id') - proxy_user = k.client_for_proxy_user(proxy_username, - proxy_password, - proxy_trust_id) + proxy_user = k.auth_for_proxy(proxy_username, + proxy_password, + proxy_trust_id) t.delete_trust(proxy_user, proxy_trust_id) proxy_user_delete(proxy_username) update = job_execution.job_configs.to_dict() @@ -108,8 +108,8 @@ def create_proxy_user_for_cluster(cluster): return cluster username = 'cluster_{0}'.format(cluster.id) password = proxy_user_create(username) - current_user = k.client() - proxy_user = k.client_for_proxy_user(username, password) + current_user = k.auth() + proxy_user = k.auth_for_proxy(username, password) trust_id = t.create_trust(trustor=current_user, trustee=proxy_user, role_names=CONF.proxy_user_role_names) @@ -133,9 +133,9 @@ def delete_proxy_user_for_cluster(cluster): proxy_username = proxy_configs.get('proxy_username') proxy_password = proxy_configs.get('proxy_password') proxy_trust_id = proxy_configs.get('proxy_trust_id') - proxy_user = k.client_for_proxy_user(proxy_username, - proxy_password, - proxy_trust_id) + proxy_user = k.auth_for_proxy(proxy_username, + proxy_password, + proxy_trust_id) t.delete_trust(proxy_user, proxy_trust_id) proxy_user_delete(proxy_username) update = {'cluster_configs': cluster.cluster_configs.to_dict()}