Introduce separate module,state to work with v3 only
This patch introduce new keystonev3 module and state that uses
os_client_config library for authenticaion and raw client to send
requests directly to API.
Since v3 resource structure/resource relation are completely different
we introduce new pillar keystone:client:resources:v3 which will contain
all resources we manage via v3 client.
The module,state implements basic functionality to manage:
* users
* projects
* services
* endpoints
* roles
Other resources will be added in separate patches when needed.
Bootstrap of keystone is done via bootstrap script in server.sls in
Queens as admin token is removed.
Related-Prod: PROD-19148
Change-Id: I10a7cf720955437e3757a1c9699e4a60e1327ba3
diff --git a/_modules/keystonev3/__init__.py b/_modules/keystonev3/__init__.py
new file mode 100644
index 0000000..9581cb1
--- /dev/null
+++ b/_modules/keystonev3/__init__.py
@@ -0,0 +1,88 @@
+try:
+ import os_client_config # noqa
+ from keystoneauth1 import exceptions as ka_exceptions # noqa
+ REQUIREMENTS_MET = True
+except ImportError:
+ REQUIREMENTS_MET = False
+
+from keystonev3 import endpoints
+from keystonev3 import roles
+from keystonev3 import services
+from keystonev3 import projects
+from keystonev3 import users
+
+endpoint_get_details = endpoints.endpoint_get_details
+endpoint_update = endpoints.endpoint_update
+endpoint_delete = endpoints.endpoint_delete
+endpoint_list = endpoints.endpoint_list
+endpoint_create = endpoints.endpoint_create
+
+role_assignment_list = roles.role_assignment_list
+role_assignment_check = roles.role_assignment_check
+role_add = roles.role_add
+role_delete = roles.role_delete
+role_get_details = roles.role_get_details
+role_update = roles.role_update
+role_delete = roles.role_delete
+role_list = roles.role_list
+role_create = roles.role_create
+
+service_get_details = services.service_get_details
+service_update = services.service_update
+service_delete = services.service_delete
+service_list = services.service_list
+service_create = services.service_create
+
+project_get_details = projects.project_get_details
+project_update = projects.project_update
+project_delete = projects.project_delete
+project_list = projects.project_list
+project_create = projects.project_create
+
+user_get_details = users.user_get_details
+user_update = users.user_update
+user_delete = users.user_delete
+user_list = users.user_list
+user_create = users.user_create
+
+
+__all__ = (
+ 'endpoint_get_details',
+ 'endpoint_update',
+ 'endpoint_delete',
+ 'endpoint_list',
+ 'endpoint_create',
+ 'role_assignment_list',
+ 'role_assignment_check',
+ 'role_add',
+ 'role_delete',
+ 'role_get_details',
+ 'role_update',
+ 'role_delete',
+ 'role_list',
+ 'role_create',
+ 'service_get_details',
+ 'service_update',
+ 'service_delete',
+ 'service_list',
+ 'service_create',
+ 'project_get_details',
+ 'project_update',
+ 'project_delete',
+ 'project_list',
+ 'project_create',
+ 'user_get_details',
+ 'user_update',
+ 'user_delete',
+ 'user_list',
+ 'user_create'
+)
+
+
+def __virtual__():
+ """Only load keystonev3 if requirements are available."""
+ if REQUIREMENTS_MET:
+ return 'keystonev3'
+ else:
+ return False, ("The keystonev3 execution module cannot be loaded: "
+ "os_client_config or keystoneauth are unavailable.")
diff --git a/_modules/keystonev3/common.py b/_modules/keystonev3/common.py
new file mode 100644
index 0000000..52b7914
--- /dev/null
+++ b/_modules/keystonev3/common.py
@@ -0,0 +1,131 @@
+import logging
+import os_client_config
+import uuid
+
+log = logging.getLogger(__name__)
+
+
+class KeystoneException(Exception):
+
+ _msg = "Keystone module exception occured."
+
+ def __init__(self, message=None, **kwargs):
+ super(KeystoneException, self).__init__(message or self._msg)
+
+
+class NoKeystoneEndpoint(KeystoneException):
+ _msg = "Keystone endpoint not found in keystone catalog."
+
+
+class NoAuthPluginConfigured(KeystoneException):
+ _msg = ("You are using keystoneauth auth plugin that does not support "
+ "fetching endpoint list from token (noauth or admin_token).")
+
+
+class NoCredentials(KeystoneException):
+ _msg = "Please provide cloud name present in clouds.yaml."
+
+
+class ResourceNotFound(KeystoneException):
+ _msg = "Uniq resource: {resource} with name: {name} not found."
+
+ def __init__(self, resource, name, **kwargs):
+ super(KeystoneException, self).__init__(
+ self._msg.format(resource=resource, name=name))
+
+
+class MultipleResourcesFound(KeystoneException):
+ _msg = "Multiple resource: {resource} with name: {name} found."
+
+ def __init__(self, resource, name, **kwargs):
+ super(KeystoneException, self).__init__(
+ self._msg.format(resource=resource, name=name))
+
+
+def _get_raw_client(cloud_name):
+ service_type = 'identity'
+ config = os_client_config.OpenStackConfig()
+ cloud = config.get_one_cloud(cloud_name)
+ adapter = cloud.get_session_client(service_type)
+ adapter.version = '3'
+ try:
+ access_info = adapter.session.auth.get_access(adapter.session)
+ endpoints = access_info.service_catalog.get_endpoints()
+ except (AttributeError, ValueError):
+ e = NoAuthPluginConfigured()
+ log.exception('%s' % e)
+ raise e
+ if service_type not in endpoints:
+ if not service_type:
+ e = NoKeystoneEndpoint()
+ log.error('%s' % e)
+ raise e
+ return adapter
+
+
+def send(method, microversion_header=None):
+ def wrap(func):
+ def wrapped_f(*args, **kwargs):
+ headers = kwargs.pop('headers', {})
+ if kwargs.get('microversion'):
+ headers.setdefault(microversion_header,
+ kwargs.get('microversion'))
+ cloud_name = kwargs.pop('cloud_name')
+ if not cloud_name:
+ e = NoCredentials()
+ log.error('%s' % e)
+ raise e
+ adapter = _get_raw_client(cloud_name)
+ # Remove salt internal kwargs
+ kwarg_keys = list(kwargs.keys())
+ for k in kwarg_keys:
+ if k.startswith('__'):
+ kwargs.pop(k)
+ url, json = func(*args, **kwargs)
+ if json:
+ response = getattr(adapter, method)(url, headers=headers,
+ json=json)
+ else:
+ response = getattr(adapter, method)(url, headers=headers)
+ if not response.content:
+ return {}
+ try:
+ resp = response.json()
+ except:
+ resp = response.content
+ return resp
+ return wrapped_f
+ return wrap
+
+
+def _check_uuid(val):
+ try:
+ return str(uuid.UUID(val)).replace('-', '') == val
+ except (TypeError, ValueError, AttributeError):
+ return False
+
+
+def get_by_name_or_uuid(resource_list, resp_key, arg_name):
+ def wrap(func):
+ def wrapped_f(*args, **kwargs):
+ if arg_name in kwargs:
+ ref = kwargs.pop(arg_name, None)
+ start_arg = 0
+ else:
+ start_arg = 1
+ ref = args[0]
+ cloud_name = kwargs['cloud_name']
+ if _check_uuid(ref):
+ uuid = ref
+ else:
+ # Then we have name not uuid
+ resp = resource_list(
+ name=ref, cloud_name=cloud_name)[resp_key]
+ if len(resp) == 0:
+ raise ResourceNotFound(resp_key, ref)
+ elif len(resp) > 1:
+ raise MultipleResourcesFound(resp_key, ref)
+ uuid = resp[0]['id']
+ return func(uuid, *args[start_arg:], **kwargs)
+ return wrapped_f
+ return wrap
diff --git a/_modules/keystonev3/endpoints.py b/_modules/keystonev3/endpoints.py
new file mode 100644
index 0000000..4230ad3
--- /dev/null
+++ b/_modules/keystonev3/endpoints.py
@@ -0,0 +1,42 @@
+from keystonev3.common import send
+
+try:
+ from urllib.parse import urlencode
+except ImportError:
+ from urllib import urlencode
+
+
+@send('get')
+def endpoint_get_details(endpoint_id, **kwargs):
+ url = '/endpoints/{}?{}'.format(endpoint_id, urlencode(kwargs))
+ return url, None
+
+
+@send('patch')
+def endpoint_update(endpoint_id, **kwargs):
+ url = '/endpoints/{}'.format(endpoint_id)
+ json = {
+ 'endpoint': kwargs,
+ }
+ return url, json
+
+
+@send('delete')
+def endpoint_delete(endpoint_id, **kwargs):
+ url = '/endpoints/{}'.format(endpoint_id)
+ return url, None
+
+
+@send('get')
+def endpoint_list(**kwargs):
+ url = '/endpoints?{}'.format(urlencode(kwargs))
+ return url, None
+
+
+@send('post')
+def endpoint_create(**kwargs):
+ url = '/endpoints'
+ json = {
+ 'endpoint': kwargs,
+ }
+ return url, json
diff --git a/_modules/keystonev3/projects.py b/_modules/keystonev3/projects.py
new file mode 100644
index 0000000..26e44fe
--- /dev/null
+++ b/_modules/keystonev3/projects.py
@@ -0,0 +1,44 @@
+from keystonev3.common import get_by_name_or_uuid, send
+try:
+ from urllib.parse import urlencode
+except ImportError:
+ from urllib import urlencode
+
+
+@send('get')
+def project_list(**kwargs):
+ url = '/projects?{}'.format(urlencode(kwargs))
+ return url, None
+
+
+@get_by_name_or_uuid(project_list, 'projects', 'project_id')
+@send('get')
+def project_get_details(project_id, **kwargs):
+ url = '/projects/{}?{}'.format(project_id, urlencode(kwargs))
+ return url, None
+
+
+@get_by_name_or_uuid(project_list, 'projects', 'project_id')
+@send('patch')
+def project_update(project_id, **kwargs):
+ url = '/projects/{}'.format(project_id)
+ json = {
+ 'project': kwargs,
+ }
+ return url, json
+
+
+@get_by_name_or_uuid(project_list, 'projects', 'project_id')
+@send('delete')
+def project_delete(project_id, **kwargs):
+ url = '/projects/{}'.format(project_id)
+ return url, None
+
+
+@send('post')
+def project_create(**kwargs):
+ url = '/projects'
+ json = {
+ 'project': kwargs,
+ }
+ return url, json
diff --git a/_modules/keystonev3/roles.py b/_modules/keystonev3/roles.py
new file mode 100644
index 0000000..bd85c16
--- /dev/null
+++ b/_modules/keystonev3/roles.py
@@ -0,0 +1,95 @@
+from keystonev3.common import get_by_name_or_uuid, send
+from keystonev3.common import KeystoneException
+
+try:
+ from urllib.parse import urlencode
+except ImportError:
+ from urllib import urlencode
+
+
+@send('get')
+def role_list(**kwargs):
+ url = '/roles?{}'.format(urlencode(kwargs))
+ return url, None
+
+
+@send('get')
+def role_assignment_list(**kwargs):
+ url = '/role_assignments?{}'.format(urlencode(kwargs))
+ return url, None
+
+
+@send('put')
+def role_add(user_id, role_id, project_id=None, domain_id=None, **kwargs):
+ if (project_id and domain_id) or (not project_id and not domain_id):
+ raise KeystoneException('Role can be assigned either to project '
+ 'or domain.')
+ if project_id:
+ url = '/projects/{}/users/{}/roles/{}'.format(project_id, user_id,
+ role_id)
+ elif domain_id:
+ url = '/domains/{}/users/{}/roles/{}'.format(domain_id, user_id,
+ role_id)
+ return url, None
+
+
+@send('delete')
+def role_delete(user_id, role_id, project_id=None, domain_id=None, **kwargs):
+ if (project_id and domain_id) or (not project_id and not domain_id):
+ raise KeystoneException('Role can be unassigned either from project '
+ 'or domain.')
+ if project_id:
+ url = '/projects/{}/users/{}/roles/{}'.format(project_id, user_id,
+ role_id)
+ elif domain_id:
+ url = '/domains/{}/users/{}/roles/{}'.format(domain_id, user_id,
+ role_id)
+ return url, None
+
+
+@send('head')
+def role_assignment_check(user_id, role_id, project_id=None,
+ domain_id=None, **kwargs):
+ if (project_id and domain_id) or (not project_id and not domain_id):
+ raise KeystoneException('Role can be assigned either to project '
+ 'or domain.')
+ if project_id:
+ url = '/projects/{}/users/{}/roles/{}'.format(project_id, user_id,
+ role_id)
+ elif domain_id:
+ url = '/domains/{}/users/{}/roles/{}'.format(domain_id, user_id,
+ role_id)
+ return url, None
+
+
+@get_by_name_or_uuid(role_list, 'roles', 'role_id')
+@send('get')
+def role_get_details(role_id, **kwargs):
+ url = '/roles/{}?{}'.format(role_id, urlencode(kwargs))
+ return url, None
+
+
+@get_by_name_or_uuid(role_list, 'roles', 'role_id')
+@send('patch')
+def role_update(role_id, **kwargs):
+ url = '/roles/{}'.format(role_id)
+ json = {
+ 'role': kwargs,
+ }
+ return url, json
+
+
+@get_by_name_or_uuid(role_list, 'roles', 'role_id')
+@send('delete')
+def role_remove(role_id, **kwargs):
+ url = '/roles/{}'.format(role_id)
+ return url, None
+
+
+@send('post')
+def role_create(**kwargs):
+ url = '/roles'
+ json = {
+ 'role': kwargs,
+ }
+ return url, json
diff --git a/_modules/keystonev3/services.py b/_modules/keystonev3/services.py
new file mode 100644
index 0000000..f917cbf
--- /dev/null
+++ b/_modules/keystonev3/services.py
@@ -0,0 +1,45 @@
+from keystonev3.common import get_by_name_or_uuid, send
+
+try:
+ from urllib.parse import urlencode
+except ImportError:
+ from urllib import urlencode
+
+
+@send('get')
+def service_list(**kwargs):
+ url = '/services?{}'.format(urlencode(kwargs))
+ return url, None
+
+
+@get_by_name_or_uuid(service_list, 'services', 'service_id')
+@send('get')
+def service_get_details(service_id, **kwargs):
+ url = '/services/{}?{}'.format(service_id, urlencode(kwargs))
+ return url, None
+
+
+@get_by_name_or_uuid(service_list, 'services', 'service_id')
+@send('patch')
+def service_update(service_id, **kwargs):
+ url = '/services/{}'.format(service_id)
+ json = {
+ 'service': kwargs,
+ }
+ return url, json
+
+
+@get_by_name_or_uuid(service_list, 'services', 'service_id')
+@send('delete')
+def service_delete(service_id, **kwargs):
+ url = '/services/{}'.format(service_id)
+ return url, None
+
+
+@send('post')
+def service_create(**kwargs):
+ url = '/services'
+ json = {
+ 'service': kwargs,
+ }
+ return url, json
diff --git a/_modules/keystonev3/users.py b/_modules/keystonev3/users.py
new file mode 100644
index 0000000..9582eef
--- /dev/null
+++ b/_modules/keystonev3/users.py
@@ -0,0 +1,44 @@
+from keystonev3.common import get_by_name_or_uuid, send
+try:
+ from urllib.parse import urlencode
+except ImportError:
+ from urllib import urlencode
+
+
+@send('get')
+def user_list(**kwargs):
+ url = '/users?{}'.format(urlencode(kwargs))
+ return url, None
+
+
+@get_by_name_or_uuid(user_list, 'users', 'user_id')
+@send('get')
+def user_get_details(user_id, **kwargs):
+ url = '/users/{}?{}'.format(user_id, urlencode(kwargs))
+ return url, None
+
+
+@get_by_name_or_uuid(user_list, 'users', 'user_id')
+@send('patch')
+def user_update(user_id, **kwargs):
+ url = '/users/{}'.format(user_id)
+ json = {
+ 'user': kwargs,
+ }
+ return url, json
+
+
+@get_by_name_or_uuid(user_list, 'users', 'user_id')
+@send('delete')
+def user_delete(user_id, **kwargs):
+ url = '/users/{}'.format(user_id)
+ return url, None
+
+
+@send('post')
+def user_create(**kwargs):
+ url = '/users'
+ json = {
+ 'user': kwargs,
+ }
+ return url, json
diff --git a/_states/keystonev3.py b/_states/keystonev3.py
new file mode 100644
index 0000000..4a63d60
--- /dev/null
+++ b/_states/keystonev3.py
@@ -0,0 +1,421 @@
+import logging
+
+
+def __virtual__():
+ return 'keystonev3' if 'keystonev3.endpoint_list' in __salt__ else False # noqa
+
+
+log = logging.getLogger(__name__)
+
+
+def _keystonev3_call(fname, *args, **kwargs):
+ return __salt__['keystonev3.{}'.format(fname)](*args, **kwargs) # noqa
+
+
+def endpoint_present(name, url, interface, service_id, cloud_name, **kwargs):
+
+ service_id = _keystonev3_call(
+ 'service_get_details', service_id,
+ cloud_name=cloud_name)['service']['id']
+
+ endpoints = _keystonev3_call(
+ 'endpoint_list', name=name, service_id=service_id, interface=interface,
+ cloud_name=cloud_name)['endpoints']
+
+ if not endpoints:
+ try:
+ resp = _keystonev3_call(
+ 'endpoint_create', url=url, interface=interface,
+ service_id=service_id, cloud_name=cloud_name, **kwargs
+ )
+ except Exception as e:
+ log.error('Keystone endpoint create failed with {}'.format(e))
+ return _create_failed(name, 'endpoint')
+ return _created(name, 'endpoint', resp)
+ elif len(endpoints) == 1:
+ exact_endpoint = endpoints[0]
+ endpoint_id = exact_endpoint['id']
+ changable = (
+ 'url', 'region', 'interface', 'service_id'
+ )
+ to_update = {}
+ to_check = {'url': url}
+ to_check.update(kwargs)
+
+ for key in to_check:
+ if (key in changable and (key not in exact_endpoint or
+ to_check[key] != exact_endpoint[key])):
+ to_update[key] = to_check[key]
+ if to_update:
+ try:
+ resp = _keystonev3_call(
+ 'endpoint_update', endpoint_id=endpoint_id,
+ cloud_name=cloud_name, **to_update
+ )
+ except Exception as e:
+ log.error('Keystone endpoint update failed with {}'.format(e))
+ return _update_failed(name, 'endpoint')
+ return _updated(name, 'endpoint', resp)
+ else:
+ return _no_changes(name, 'endpoint')
+ else:
+ return _find_failed(name, 'endpoint')
+
+
+def endpoint_absent(name, cloud_name):
+ endpoints = _keystonev3_call(
+ 'endpoint_list', name=name, cloud_name=cloud_name
+ )['endpoints']
+ if not endpoints:
+ return _absent(name, 'endpoint')
+ elif len(endpoints) == 1:
+ try:
+ _keystonev3_call(
+ 'endpoint_delete', endpoints[0]['id'], cloud_name=cloud_name
+ )
+ except Exception as e:
+ log.error('Keystone delete endpoint failed with {}'.format(e))
+ return _delete_failed(name, 'endpoint')
+ return _deleted(name, 'endpoint')
+ else:
+ return _find_failed(name, 'endpoint')
+
+
+def service_present(name, type, cloud_name, **kwargs):
+
+ service_id = ''
+
+ try:
+ exact_service = _keystonev3_call(
+ 'service_get_details', name,
+ cloud_name=cloud_name)['service']
+ service_id = exact_service['id']
+ except Exception as e:
+ if 'ResourceNotFound' in repr(e):
+ pass
+ else:
+ log.error('Failed to get service {}'.format(e))
+ return _create_failed(name, 'service')
+
+ if not service_id:
+ try:
+ resp = _keystonev3_call(
+ 'service_create', name=name, type=type,
+ cloud_name=cloud_name, **kwargs
+ )
+ except Exception as e:
+ log.error('Keystone service create failed with {}'.format(e))
+ return _create_failed(name, 'service')
+ return _created(name, 'service', resp)
+
+ else:
+ changable = ('type', 'enabled', 'description')
+ to_update = {}
+ to_check = {'type': type}
+ to_check.update(kwargs)
+
+ for key in to_check:
+ if (key in changable and (key not in exact_service or
+ to_check[key] != exact_service[key])):
+ to_update[key] = to_check[key]
+ if to_update:
+ try:
+ resp = _keystonev3_call(
+ 'service_update', service_id=service_id,
+ cloud_name=cloud_name, **to_update
+ )
+ except Exception as e:
+ log.error('Keystone service update failed with {}'.format(e))
+ return _update_failed(name, 'service')
+ return _updated(name, 'service', resp)
+ else:
+ return _no_changes(name, 'service')
+ return _find_failed(name, 'service')
+
+
+def project_present(name, cloud_name, **kwargs):
+
+ projects = _keystonev3_call(
+ 'project_list', name=name, cloud_name=cloud_name
+ )['projects']
+
+ if not projects:
+ try:
+ resp = _keystonev3_call(
+ 'project_create', name=name, cloud_name=cloud_name, **kwargs
+ )
+ except Exception as e:
+ log.error('Keystone project create failed with {}'.format(e))
+ return _create_failed(name, 'project')
+ return _created(name, 'project', resp)
+ elif len(projects) == 1:
+ exact_project = projects[0]
+ project_id = exact_project['id']
+ changable = (
+ 'is_domain', 'description', 'domain_id', 'enabled',
+ 'parent_id', 'tags'
+ )
+ to_update = {}
+
+ for key in kwargs:
+ if (key in changable and (key not in exact_project or
+ kwargs[key] != exact_project[key])):
+ to_update[key] = kwargs[key]
+
+ if to_update:
+ try:
+ resp = _keystonev3_call(
+ 'project_update', project_id=project_id,
+ cloud_name=cloud_name, **to_update
+ )
+ except Exception as e:
+ log.error('Keystone project update failed with {}'.format(e))
+ return _update_failed(name, 'project')
+ return _updated(name, 'project', resp)
+ else:
+ return _no_changes(name, 'project')
+ else:
+ return _find_failed(name, 'project')
+
+
+def user_present(name, cloud_name, password_reset=False, **kwargs):
+
+ users = _keystonev3_call(
+ 'user_list', name=name, cloud_name=cloud_name
+ )['users']
+
+ if 'default_project_id' in kwargs:
+ kwargs['default_project_id'] = _keystonev3_call(
+ 'project_get_details', kwargs['default_project_id'],
+ cloud_name=cloud_name)['project']['id']
+
+ if not users:
+ try:
+ resp = _keystonev3_call(
+ 'user_create', name=name, cloud_name=cloud_name, **kwargs
+ )
+ except Exception as e:
+ log.error('Keystone user create failed with {}'.format(e))
+ return _create_failed(name, 'user')
+ return _created(name, 'user', resp)
+
+ elif len(users) == 1:
+ exact_user = users[0]
+ user_id = exact_user['id']
+ changable = (
+ 'default_project_id', 'domain_id', 'enabled', 'email'
+ )
+ if password_reset:
+ changable += ('password',)
+ to_update = {}
+
+ for key in kwargs:
+ if (key in changable and (key not in exact_user or
+ kwargs[key] != exact_user[key])):
+ to_update[key] = kwargs[key]
+
+ if to_update:
+ log.info('Updating keystone user {} with: {}'.format(user_id,
+ to_update))
+ try:
+ resp = _keystonev3_call(
+ 'user_update', user_id=user_id,
+ cloud_name=cloud_name, **to_update
+ )
+ except Exception as e:
+ log.error('Keystone user update failed with {}'.format(e))
+ return _update_failed(name, 'user')
+ return _updated(name, 'user', resp)
+ else:
+ return _no_changes(name, 'user')
+ else:
+ return _find_failed(name, 'user')
+
+
+def user_role_assigned(name, role_id, cloud_name, project_id=None,
+ domain_id=None, role_domain_id=None, **kwargs):
+
+ user_id = _keystonev3_call(
+ 'user_get_details', name,
+ cloud_name=cloud_name)['user']['id']
+
+ if project_id:
+ project_id = _keystonev3_call(
+ 'project_get_details', project_id,
+ cloud_name=cloud_name)['project']['id']
+
+# TODO: Add when domain support is added.
+# if domain_id and not uuidutils.is_uuid_like(domain_id):
+# domain_id = _keystonev3_call(
+# 'domain_get_details', domain_id,
+# cloud_name=cloud_name)['domain']['id']
+
+# if role_domain_id and not uuidutils.is_uuid_like(role_domain_id):
+# role_domain_id = _keystonev3_call(
+# 'domain_get_details', role_domain_id,
+# cloud_name=cloud_name)['domain']['id']
+
+ if role_id:
+ role_id = _keystonev3_call(
+ 'role_get_details', role_id, domain_id=role_domain_id,
+ cloud_name=cloud_name)['role']['id']
+
+ req_kwargs = {'role.id': role_id, 'user.id': user_id,
+ 'cloud_name': cloud_name}
+ if domain_id:
+ req_kwargs['domain_id'] = domain_id
+ if project_id:
+ req_kwargs['project_id'] = project_id
+
+ role_assignments = _keystonev3_call(
+ 'role_assignment_list', **req_kwargs)['role_assignments']
+
+ req_kwargs = {'cloud_name': cloud_name, 'user_id': user_id,
+ 'role_id': role_id}
+ if domain_id:
+ req_kwargs['domain_id'] = domain_id
+ if project_id:
+ req_kwargs['project_id'] = project_id
+
+ if not role_assignments:
+ try:
+ resp = _keystonev3_call('role_add', **req_kwargs)
+ except Exception as e:
+ log.error('Keystone user role assignment with {}'.format(e))
+ return _create_failed(name, 'user_role_assignment')
+ # We check for exact assignment when did role_assignment_list
+ # on this stage we already just assigned role if it was missed.
+ return _created(name, 'user_role_assignment', resp)
+ return _no_changes(name, 'user_role_assignment')
+
+
+def role_present(name, cloud_name, **kwargs):
+
+ roles = _keystonev3_call(
+ 'role_list', name=name, cloud_name=cloud_name
+ )['roles']
+
+ if 'domain_id' in kwargs:
+ kwargs['domain_id'] = _keystonev3_call(
+ 'domain_get_details', kwargs['domain_id'],
+ cloud_name=cloud_name)['domains']
+
+ if not roles:
+ try:
+ resp = _keystonev3_call(
+ 'role_create', name=name, cloud_name=cloud_name, **kwargs
+ )
+ except Exception as e:
+ log.error('Keystone role create failed with {}'.format(e))
+ return _create_failed(name, 'role')
+ return _created(name, 'role', resp)
+ elif len(roles) == 1:
+ exact_role = roles[0]
+ role_id = exact_role['id']
+ changable = ('domain_id')
+ to_update = {}
+
+ for key in kwargs:
+ if (key in changable and (key not in exact_role or
+ kwargs[key] != exact_role[key])):
+ to_update[key] = kwargs[key]
+
+ if to_update:
+ try:
+ resp = _keystonev3_call(
+ 'role_update', role_id=role_id,
+ cloud_name=cloud_name, **to_update
+ )
+ except Exception as e:
+ log.error('Keystone role update failed with {}'.format(e))
+ return _update_failed(name, 'role')
+ return _updated(name, 'role', resp)
+ else:
+ return _no_changes(name, 'role')
+ else:
+ return _find_failed(name, 'role')
+
+
+def _created(name, resource, resource_definition):
+ changes_dict = {
+ 'name': name,
+ 'changes': resource_definition,
+ 'result': True,
+ 'comment': '{}{} created'.format(resource, name)
+ }
+ return changes_dict
+
+
+def _updated(name, resource, resource_definition):
+ changes_dict = {
+ 'name': name,
+ 'changes': resource_definition,
+ 'result': True,
+ 'comment': '{}{} updated'.format(resource, name)
+ }
+ return changes_dict
+
+
+def _no_changes(name, resource):
+ changes_dict = {
+ 'name': name,
+ 'changes': {},
+ 'result': True,
+ 'comment': '{}{} is in desired state'.format(resource, name)
+ }
+ return changes_dict
+
+
+def _deleted(name, resource):
+ changes_dict = {
+ 'name': name,
+ 'changes': {},
+ 'result': True,
+ 'comment': '{}{} removed'.format(resource, name)
+ }
+ return changes_dict
+
+
+def _absent(name, resource):
+ changes_dict = {'name': name,
+ 'changes': {},
+ 'comment': '{0} {1} not present'.format(resource, name),
+ 'result': True}
+ return changes_dict
+
+
+def _delete_failed(name, resource):
+ changes_dict = {'name': name,
+ 'changes': {},
+ 'comment': '{0} {1} failed to delete'.format(resource,
+ name),
+ 'result': False}
+ return changes_dict
+
+
+def _create_failed(name, resource):
+ changes_dict = {'name': name,
+ 'changes': {},
+ 'comment': '{0} {1} failed to create'.format(resource,
+ name),
+ 'result': False}
+ return changes_dict
+
+
+def _update_failed(name, resource):
+ changes_dict = {'name': name,
+ 'changes': {},
+ 'comment': '{0} {1} failed to update'.format(resource,
+ name),
+ 'result': False}
+ return changes_dict
+
+
+def _find_failed(name, resource):
+ changes_dict = {
+ 'name': name,
+ 'changes': {},
+ 'comment': '{0} {1} found multiple {0}'.format(resource, name),
+ 'result': False,
+ }
+ return changes_dict
diff --git a/keystone/client/init.sls b/keystone/client/init.sls
index c4fc48c..a779542 100644
--- a/keystone/client/init.sls
+++ b/keystone/client/init.sls
@@ -1,6 +1,6 @@
-
include:
- keystone.client.service
- keystone.client.project
- keystone.client.server
- keystone.client.os_client_config
+- keystone.client.resources
diff --git a/keystone/client/project.sls b/keystone/client/project.sls
index a9d6ff0..240c99f 100644
--- a/keystone/client/project.sls
+++ b/keystone/client/project.sls
@@ -1,5 +1,7 @@
{%- from "keystone/map.jinja" import client with context %}
-{%- if client.enabled %}
+{# this legacy client is deprecated and will be removed when pike is EOL #}
+{# it is not recommended to use it for v3 API #}
+{%- if client.enabled and not client.get('resources', {}).get('v3', {}).get('enabled', False) %}
{%- if client.tenant is defined %}
diff --git a/keystone/client/resources/init.sls b/keystone/client/resources/init.sls
new file mode 100644
index 0000000..3b91daa
--- /dev/null
+++ b/keystone/client/resources/init.sls
@@ -0,0 +1,2 @@
+include:
+- keystone.client.resources.v3
diff --git a/keystone/client/resources/v3.sls b/keystone/client/resources/v3.sls
new file mode 100644
index 0000000..015fdcf
--- /dev/null
+++ b/keystone/client/resources/v3.sls
@@ -0,0 +1,132 @@
+{%- from "keystone/map.jinja" import client with context %}
+{%- set resources = client.get('resources', {}).get('v3', {}) %}
+
+{%- if resources.get('enabled', False) %}
+
+{% for role_name,role in resources.get('roles', {}).iteritems() %}
+
+{%- if role.enabled %}
+keystone_role_{{ role_name }}:
+ keystonev3.role_present:
+ - cloud_name: {{ role.get('cloud_name', resources.cloud_name) }}
+ {#- The role name is not uniq among domains, use name here to have ability create #}
+ {#- roles with the same name in different domains #}
+ - name: {{ role.name }}
+ {%- if role.domain_id is defined %}
+ - domain_id: {{ role.domain_id }}
+ {%- endif %}
+{%- else %}
+keystone_role_{{ role_name }}:
+ keystonev3.role_absent:
+ - cloud_name: {{ role.get('cloud_name', resources.cloud_name) }}
+ - name: {{ role_name }}
+{%- endif %}
+
+{%- endfor %}
+
+{% for service_name,service in resources.get('services', {}).iteritems() %}
+keystone_service_{{ service_name }}_{{ service.type }}:
+ keystonev3.service_present:
+ - cloud_name: {{ service.get('cloud_name', resources.cloud_name) }}
+ - name: {{ service_name }}
+ - type: {{ service.type }}
+ {%- if service.description is defined %}
+ - description: {{ service.description }}
+ {%- endif %}
+ {%- if service.enabled is defined %}
+ - enabled: {{ service.enabled }}
+ {%- endif %}
+
+ {% for endpoint_name, endpoint in service.get('endpoints', {}).iteritems() %}
+
+keystone_endpoint_{{ endpoint_name }}_{{ endpoint.interface }}_{{ endpoint.region }}:
+ keystonev3.endpoint_present:
+ - name: {{ endpoint_name }}
+ - cloud_name: {{ endpoint.get('cloud_name', resources.cloud_name) }}
+ - url: {{ endpoint.url }}
+ - interface: {{ endpoint.interface }}
+ - service_id: {{ service_name }}
+ - region: {{ endpoint.region }}
+ - require:
+ - keystone_service_{{ service_name }}_{{ service.type }}
+
+ {%- endfor %}
+{% endfor %}
+
+{% for domain_name, domain in resources.get('domains', {}).iteritems() %}
+
+{#- TODO: Add domain support #}
+ {%- for project_name, project in domain.get('projects', {}).iteritems() %}
+keystone_project_{{ project_name }}:
+ keystonev3.project_present:
+ - cloud_name: {{ project.get('cloud_name', resources.cloud_name) }}
+ - name: {{ project_name }}
+ {%- if project.is_domain is defined %}
+ - is_domain: {{ project.is_domain }}
+ {%- endif %}
+ {%- if project.description is defined %}
+ - description: {{ project.description }}
+ {%- endif %}
+{# TODO unkomment when domain support is added. #}
+{# {- if project.domain_id is defined %} #}
+{# - domain_id: {{ project.domain_id }} #}
+{# {%- endif %} #}
+ {%- if project.enabled is defined %}
+ - enabled: {{ project.enabled }}
+ {%- endif %}
+ {%- if project.parent_id is defined %}
+ - parent_id: {{ project.parent_id }}
+ {%- endif %}
+ {%- if project.tags is defined %}
+ - tags: {{ project.tags }}
+ {%- endif %}
+
+ {%- endfor %}
+
+{%- endfor %}
+
+{%- for user_name, user in resources.get('users', {}).iteritems() %}
+
+keystone_user_{{ user_name }}:
+ keystonev3.user_present:
+ - cloud_name: {{ user.get('cloud_name', resources.cloud_name) }}
+ - name: {{ user_name }}
+ {%- if user.default_project_id is defined %}
+ - default_project_id: {{ user.default_project_id }}
+ {%- endif %}
+ {%- if user.domain_id is defined %}
+ - domain_id: {{ user.domain_id }}
+ {%- endif %}
+ {%- if user.enabled is defined %}
+ - enabled: {{ user.enabled }}
+ {%- endif %}
+ {%- if user.password is defined %}
+ - password: {{ user.password }}
+ {%- endif %}
+ {%- if user.email is defined %}
+ - email: {{ user.email }}
+ {%- endif %}
+ {%- if user.password_reset is defined %}
+ - password_reset: {{ user.password_reset }}
+ {%- endif %}
+
+ {%- for role_name,role in user.get('roles', {}).iteritems() %}
+keystone_user_{{ user_name }}_role_{{ role.name }}_assigned:
+ keystonev3.user_role_assigned:
+ - name: {{ user_name }}
+ - role_id: {{ role.name }}
+ - cloud_name: {{ user.get('cloud_name', resources.cloud_name) }}
+ {%- if role.domain_id is defined %}
+ - domain_id: {{ role.domain_id }}
+ {%- endif %}
+ {%- if role.project_id is defined %}
+ - project_id: {{ role.project_id }}
+ {%- endif %}
+ {%- if role.role_domain_id is defined %}
+ - role_domain_id: {{ role.role_domain_id }}
+ {%- endif %}
+ {%- endfor %}
+
+{%- endfor %}
+
+{%- endif %}
diff --git a/keystone/client/server.sls b/keystone/client/server.sls
index ef4a5be..d66052e 100644
--- a/keystone/client/server.sls
+++ b/keystone/client/server.sls
@@ -1,5 +1,7 @@
{%- from "keystone/map.jinja" import client with context %}
-{%- if client.enabled %}
+{# this legacy client is deprecated and will be removed when pike is EOL #}
+{# it is not recommended to use it for v3 API #}
+{%- if client.enabled and not client.get('resources', {}).get('v3', {}).get('enabled', False) %}
{%- for server_name, server in client.get('server', {}).items() %}
diff --git a/keystone/server.sls b/keystone/server.sls
index 904c296..1095fd2 100644
--- a/keystone/server.sls
+++ b/keystone/server.sls
@@ -354,6 +354,23 @@
{%- endif %}
{%- endif %}
+{%- if server.version not in ['mitaka', 'newton', 'ocata', 'pike'] %}
+{%- if not grains.get('noservices', False) %}
+keystone_identity_bootstrap_setup:
+ cmd.run:
+ - name: keystone-manage bootstrap
+ --bootstrap-password {{ server.admin_password }}
+ --bootstrap-username {{ server.admin_name }}
+ --bootstrap-project-name admin
+ --bootstrap-role-name admin
+ --bootstrap-service-name keystone
+ --bootstrap-region-id {{ server.get('admin_region', 'RegionOne') }}
+ --bootstrap-internal-url {{ server.bind.get('protocol', 'http') }}://{{ server.bind.address }}:{{ server.bind.get('port', 5000) }}
+ - unless:
+ . /root/keystonercv3; openstack endpoint list --service identity --interface internal -f value -c URL |grep {{ server.bind.get('port', 5000) }}
+{%- endif %}
+{%- endif %}
+
{%- if not grains.get('noservices', False) %}
{%- if not salt['pillar.get']('linux:system:repo:mirantis_openstack', False) %}