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) %}