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