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