Improve cinderv3 module and states

Add states and modules for volumes
and unify volume_type states and modules

Change-Id: I23cc715df696c828e7f7b53d5c4c85b146d93417
Closes-Issue: PROD-25036 (PROD:25036)
diff --git a/_modules/cinderv3/__init__.py b/_modules/cinderv3/__init__.py
index 650a6a2..9bb71f2 100644
--- a/_modules/cinderv3/__init__.py
+++ b/_modules/cinderv3/__init__.py
@@ -3,26 +3,41 @@
     REQUIREMENTS_MET = True
 except ImportError:
     REQUIREMENTS_MET = False
-import os
-import sys
 
-# i failed to load module witjout this
-# seems bugs in salt or it is only me
-sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
+from cinderv3 import lists
+from cinderv3 import volumes
+from cinderv3 import volume_types
+from cinderv3 import volume_actions
 
-import volume
+volume_list = lists.volume_list
+volume_list_get_details = lists.volume_list_get_details
+volume_create = volumes.volume_create
+volume_get_details = volumes.volume_get_details
+volume_update = volumes.volume_update
+volume_delete = volumes.volume_delete
+volume_metadata_create = volumes.volume_metadata_create
+volume_metadata_show = volumes.volume_metadata_show
+volume_metadata_update = volumes.volume_metadata_update
+volume_metadata_show_key = volumes.volume_metadata_show_key
+volume_metadata_delete = volumes.volume_metadata_delete
+volume_metadata_update_key = volumes.volume_metadata_update_key
+volume_type_list = lists.volume_type_list
+volume_type_get_details = volume_types.volume_type_get_details
+volume_type_create = volume_types.volume_type_create
+volume_type_delete = volume_types.volume_type_delete
+keys_volume_type_get = volume_types.keys_volume_type_get
+keys_volume_type_set = volume_types.keys_volume_type_set
+image_upload_volume = volume_actions.image_upload_volume
 
-volume_list = volume.volume_list
-volume_type_list = volume.volume_type_list
-volume_type_get = volume.volume_type_get
-volume_type_create = volume.volume_type_create
-volume_type_delete = volume.volume_type_delete
-keys_volume_type_get = volume.keys_volume_type_get
-keys_volume_type_set = volume.keys_volume_type_set
-
-__all__ = ('volume_list', 'volume_type_list', 'volume_type_get',
+__all__ = ('volume_list', 'volume_create', 'volume_delete',
+           'volume_get_details', 'volume_list_get_details', 'volume_update',
+           'volume_metadata_create', 'volume_metadata_delete',
+           'volume_metadata_show', 'volume_metadata_show_key',
+           'volume_metadata_update', 'volume_metadata_update_key',
+           'volume_type_list', 'volume_type_get_details',
            'volume_type_create', 'keys_volume_type_get',
-           'keys_volume_type_set', 'volume_type_delete')
+           'keys_volume_type_set', 'volume_type_delete',
+           'image_upload_volume',)
 
 
 def __virtual__():
diff --git a/_modules/cinderv3/arg_converter.py b/_modules/cinderv3/arg_converter.py
new file mode 100644
index 0000000..3506c6d
--- /dev/null
+++ b/_modules/cinderv3/arg_converter.py
@@ -0,0 +1,89 @@
+import uuid
+from cinderv3 import common
+from cinderv3 import lists
+
+
+class CheckId(object):
+    def check_id(self, val):
+        try:
+            return str(uuid.UUID(val)) == val
+        except (TypeError, ValueError, AttributeError):
+            return False
+
+
+def named_checker(resource, ref, cloud_name):
+    resp_key = response_keys[resource]
+    resp = resource_lists[resource](
+        name=ref, cloud_name=cloud_name)
+    try:
+        resp = resp[resp_key]
+    except KeyError:
+        raise common.ResourceNotFound(resp_key, ref)
+    if len(resp) == 0:
+        raise common.ResourceNotFound(resp_key, ref)
+    elif len(resp) > 1:
+        raise common.MultipleResourcesFound(resp_key, ref)
+    return resp[0]['id']
+
+
+def nameless_checker(resource, ref, cloud_name):
+    item_id = None
+    resp_key = response_keys[resource]
+    resp = resource_lists[resource](cloud_name=cloud_name)
+    try:
+        resp = resp[resp_key]
+    except KeyError:
+        raise common.ResourceNotFound(resp_key, ref)
+    for item in resp:
+        if item["name"] == ref:
+            if item_id is not None:
+                raise common.MultipleResourcesFound(resp_key, ref)
+            item_id = item["id"]
+    if not item_id:
+        raise common.ResourceNotFound(resp_key, ref)
+    return item_id
+
+
+
+resource_lists = {
+    'volume': lists.volume_list,
+    'volume_type': lists.volume_type_list
+}
+
+
+response_keys = {
+    'volume': 'volumes',
+    'volume_type': 'volume_types',
+}
+
+
+name_checkers = {
+    'volume': named_checker,
+    'volume_type': nameless_checker,
+}
+
+
+def get_by_name_or_uuid_multiple(resource_arg_name_pairs):
+    def wrap(func):
+        def wrapped_f(*args, **kwargs):
+            results = []
+            args_start = 0
+            for index, (resource, arg_name) in enumerate(
+                    resource_arg_name_pairs):
+                if arg_name in kwargs:
+                    ref = kwargs.pop(arg_name, None)
+                else:
+                    ref = args[index]
+                    args_start += 1
+                cloud_name = kwargs['cloud_name']
+                checker = CheckId()
+                if checker.check_id(ref):
+                    results.append(ref)
+                else:
+                    # Then we have name not uuid
+                    res = name_checkers[resource](resource, ref, cloud_name)
+                    results.append(res)
+                results.extend(args[args_start:])
+            return func(*results, **kwargs)
+        return wrapped_f
+    return wrap
\ No newline at end of file
diff --git a/_modules/cinderv3/common.py b/_modules/cinderv3/common.py
index 08447fb..4edd604 100644
--- a/_modules/cinderv3/common.py
+++ b/_modules/cinderv3/common.py
@@ -1,64 +1,100 @@
-import six
-import logging
-import uuid
 import time
-
+import logging
 import os_client_config
-from salt import exceptions
-
 
 log = logging.getLogger(__name__)
 
-SERVICE_KEY = 'volumev3'
+
+class CinderException(Exception):
+
+    _msg = "Cinder module exception occured."
+
+    def __init__(self, message=None, **kwargs):
+        super(CinderException, self).__init__(message or self._msg)
 
 
-def get_raw_client(cloud_name):
+class NoCinderEndpoint(CinderException):
+    _msg = "Cinder endpoint not found in keystone catalog."
+
+
+class NoAuthPluginConfigured(CinderException):
+    _msg = ("You are using keystoneauth auth plugin that does not support "
+            "fetching endpoint list from token (noauth or admin_token).")
+
+
+class NoCredentials(CinderException):
+    _msg = "Please provide cloud name present in clouds.yaml."
+
+
+class ResourceNotFound(CinderException):
+    _msg = "Uniq resource: {resource} with name: {name} not found."
+
+    def __init__(self, resource, name, **kwargs):
+        super(CinderException, self).__init__(
+            self._msg.format(resource=resource, name=name))
+
+
+class MultipleResourcesFound(CinderException):
+    _msg = "Multiple resource: {resource} with name: {name} found."
+
+    def __init__(self, resource, name, **kwargs):
+        super(CinderException, self).__init__(
+            self._msg.format(resource=resource, name=name))
+
+
+def _get_raw_client(cloud_name):
+    service_type = 'volumev3'
     config = os_client_config.OpenStackConfig()
     cloud = config.get_one_cloud(cloud_name)
-    adapter = cloud.get_session_client(SERVICE_KEY)
+    adapter = cloud.get_session_client(service_type)
     try:
         access_info = adapter.session.auth.get_access(adapter.session)
         endpoints = access_info.service_catalog.get_endpoints()
-    except (AttributeError, ValueError) as exc:
-        six.raise_from(exc, exceptions.SaltInvocationError(
-            "Cannot load keystoneauth plugin. Please check your environment "
-            "configuration."))
-    if SERVICE_KEY not in endpoints:
-        raise exceptions.SaltInvocationError("Cannot find cinder endpoint in "
-                                             "environment endpoint list.")
+    except (AttributeError, ValueError):
+        e = NoAuthPluginConfigured()
+        log.exception('%s' % e)
+        raise e
+    if service_type not in endpoints:
+        if not service_type:
+            e = NoCinderEndpoint()
+            log.error('%s' % e)
+            raise e
     return adapter
 
 
 def send(method):
     def wrap(func):
-        @six.wraps(func)
         def wrapped_f(*args, **kwargs):
-            cloud_name = kwargs.pop('cloud_name', None)
             connect_retries = 30
             connect_retry_delay = 1
+            cloud_name = kwargs.pop('cloud_name')
             if not cloud_name:
-                raise exceptions.SaltInvocationError(
-                    "No cloud_name specified. Please provide cloud_name "
-                    "parameter")
-            adapter = get_raw_client(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, request_kwargs = func(*args, **kwargs)
+            url, json = func(*args, **kwargs)
             response = None
             for i in range(connect_retries):
                 try:
-                  response = getattr(adapter, method.lower())(
-                      url, connect_retries=connect_retries,
-                      **request_kwargs)
+                    if json:
+                        response = getattr(adapter, method)(
+                            url, json=json, connect_retries=connect_retries)
+                    else:
+                        response = getattr(adapter, method)(url)
                 except Exception as e:
                     if hasattr(e, 'http_status') and (e.http_status >= 500
-                        or e.http_status == 0):
+                                                      or e.http_status == 0):
                         msg = ("Got retriable exception when contacting "
                                "Cinder API. Sleeping for %ss. Attepmpts "
                                "%s of %s")
-                        log.error(msg % (connect_retry_delay, i, connect_retries))
+                        log.error(
+                            msg % (connect_retry_delay, i, connect_retries))
                         time.sleep(connect_retry_delay)
                         continue
                 break
@@ -66,55 +102,8 @@
                 return {}
             try:
                 resp = response.json()
-            except ValueError:
+            except:
                 resp = response.content
             return resp
         return wrapped_f
     return wrap
-
-
-def _check_uuid(val):
-    try:
-        return str(uuid.UUID(val)) == val
-    except (TypeError, ValueError, AttributeError):
-        return False
-
-
-def get_by_name_or_uuid(resource_list, resp_key):
-    def wrap(func):
-        @six.wraps(func)
-        def wrapped_f(*args, **kwargs):
-            if 'name' in kwargs:
-                ref = kwargs.pop('name', None)
-                start_arg = 0
-            else:
-                start_arg = 1
-                ref = args[0]
-            item_id = None
-            if _check_uuid(ref):
-                item_id = ref
-            else:
-                cloud_name = kwargs['cloud_name']
-                # seems no filtering on volume type name in cinder
-                resp = resource_list(cloud_name=cloud_name)[resp_key]
-                # so need to search in list directly
-                for item in resp:
-                    if item["name"] == ref:
-                        if item_id is not None:
-                            msg = ("Multiple resource: {resource} "
-                                   "with name: {name} found ").format(
-                                    resource=resp_key, name=ref)
-                            return {"result": False,
-                                    "body": msg,
-                                    "status_code": 400}
-                        item_id = item["id"]
-                if not item_id:
-                    msg = ("Uniq {resource} resource "
-                           "with name={name} not found.").format(
-                            resource=resp_key, name=ref)
-                    return {"result": False,
-                            "body": msg,
-                            "status_code": 404}
-            return func(item_id, *args[start_arg:], **kwargs)
-        return wrapped_f
-    return wrap
diff --git a/_modules/cinderv3/lists.py b/_modules/cinderv3/lists.py
new file mode 100644
index 0000000..d0bf4e9
--- /dev/null
+++ b/_modules/cinderv3/lists.py
@@ -0,0 +1,29 @@
+from cinderv3.common import send
+
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+@send("get")
+def volume_list(**kwargs):
+    """
+    Return list of cinder volumes.
+    """
+    url = '/volumes?{}'.format(urlencode(kwargs))
+    return url, None
+
+
+@send("get")
+def volume_list_get_details(**kwargs):
+    url = '/volumes/detail?{}'.format(urlencode(kwargs))
+    return url, None
+
+
+@send("get")
+def volume_type_list(**kwargs):
+    """
+    Return list of volume types
+    """
+    url = '/types?{}'.format(urlencode(kwargs))
+    return url, None
diff --git a/_modules/cinderv3/volume_actions.py b/_modules/cinderv3/volume_actions.py
new file mode 100644
index 0000000..35697c6
--- /dev/null
+++ b/_modules/cinderv3/volume_actions.py
@@ -0,0 +1,20 @@
+from cinderv3.common import send
+from cinderv3.arg_converter import get_by_name_or_uuid_multiple
+
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+
+@get_by_name_or_uuid_multiple([('volume', 'volume_id')])
+@send('post')
+def image_upload_volume(volume_id, image_name, **kwargs):
+    url = '/volumes/{}/action'.format(volume_id)
+    json = {
+        'os-volume_upload_image': {
+            'image_name': image_name,
+        }
+    }
+    json['os-volume_upload_image'].update(kwargs)
+    return url, json
diff --git a/_modules/cinderv3/volume.py b/_modules/cinderv3/volume_types.py
similarity index 62%
rename from _modules/cinderv3/volume.py
rename to _modules/cinderv3/volume_types.py
index 876416e..8e6ebd1 100644
--- a/_modules/cinderv3/volume.py
+++ b/_modules/cinderv3/volume_types.py
@@ -1,80 +1,60 @@
-from __future__ import absolute_import
-
-import common
-
+from cinderv3.common import send
+from cinderv3.arg_converter import get_by_name_or_uuid_multiple
 try:
     from urllib.parse import urlencode
 except ImportError:
     from urllib import urlencode
 
 
-@common.send("get")
-def volume_list(**kwargs):
-    """
-    Return list of cinder volumes.
-    """
-    url = '/volumes?{}'.format(urlencode(kwargs))
-    return url, {}
-
-
-@common.send("get")
-def volume_type_list(**kwargs):
-    """
-    Return list of volume types
-    """
-    url = '/types?{}'.format(urlencode(kwargs))
-    return url, {}
-
-
-@common.get_by_name_or_uuid(volume_type_list, 'volume_types')
-@common.send("get")
-def volume_type_get(volume_type_id, **kwargs):
+@get_by_name_or_uuid_multiple([('volume_type', 'volume_type_id')])
+@send("get")
+def volume_type_get_details(volume_type_id, **kwargs):
     """
     Returns id of the specified volume type name
     """
     url = "/types/{volume_type_id}".format(volume_type_id=volume_type_id)
-    return url, {}
+    return url, None
 
 
-@common.get_by_name_or_uuid(volume_type_list, 'volume_types')
-@common.send("delete")
+@get_by_name_or_uuid_multiple([('volume_type', 'volume_type_id')])
+@send("delete")
 def volume_type_delete(volume_type_id, **kwargs):
     """
     delete the specified volume type
     """
     url = "/types/{volume_type_id}".format(volume_type_id=volume_type_id)
-    return url, {}
+    return url, None
 
 
-@common.send("post")
+@send("post")
 def volume_type_create(name, **kwargs):
     """
     Create cinder volume type
     """
     url = "/types"
     req = {"volume_type": {"name": name}}
-    return url, {'json': req}
+    return url, req
 
 
-@common.get_by_name_or_uuid(volume_type_list, 'volume_types')
-@common.send("get")
+@get_by_name_or_uuid_multiple([('volume_type', 'volume_type_id')])
+@send("get")
 def keys_volume_type_get(volume_type_id, **kwargs):
     """
     Return extra specs of the specified volume type.
     """
     url = "/types/{volume_type_id}/extra_specs".format(
         volume_type_id=volume_type_id)
-    return url, {}
+    return url, None
 
 
-@common.send("put")
+@send("put")
 def _key_volume_type_set(type_id, key, value, **kwargs):
     url = "/types/{volume_type_id}/extra_specs/{key}".format(
         volume_type_id=type_id, key=key)
-    return url, {'json': {str(key): str(value)}}
+    return url, {str(key): str(value)}
 
 
-@common.get_by_name_or_uuid(volume_type_list, 'volume_types')
+@get_by_name_or_uuid_multiple([('volume_type', 'volume_type_id')])
 def keys_volume_type_set(volume_type_id, keys=None, **kwargs):
     """
     Set extra specs of the specified volume type.
diff --git a/_modules/cinderv3/volumes.py b/_modules/cinderv3/volumes.py
new file mode 100644
index 0000000..6b8353a
--- /dev/null
+++ b/_modules/cinderv3/volumes.py
@@ -0,0 +1,90 @@
+from cinderv3.common import send
+from cinderv3.arg_converter import get_by_name_or_uuid_multiple
+
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+
+@send("post")
+def volume_create(size, **kwargs):
+    url = '/volumes'
+    json = {'volume': {
+        'size': size,
+    }}
+    json['volume'].update(kwargs)
+    return url, json
+
+
+@get_by_name_or_uuid_multiple([('volume', 'volume_id')])
+@send("get")
+def volume_get_details(volume_id, **kwargs):
+    url = '/volumes/{}'.format(volume_id)
+    return url, None
+
+
+@get_by_name_or_uuid_multiple([('volume', 'volume_id')])
+@send("put")
+def volume_update(volume_id, **kwargs):
+    url = '/volumes/{}'.format(volume_id)
+    json = {'volume': kwargs}
+    return url, json
+
+
+@get_by_name_or_uuid_multiple([('volume', 'volume_id')])
+@send("delete")
+def volume_delete(volume_id, **kwargs):
+    url = '/volumes/{}'.format(volume_id)
+    return url, None
+
+
+@get_by_name_or_uuid_multiple([('volume', 'volume_id')])
+@send("post")
+def volume_metadata_create(volume_id, metadata, **kwargs):
+    url = '/volumes/{}/metadata'.format(volume_id)
+    json = {
+        'metadata': metadata,
+    }
+    return url, json
+
+
+@get_by_name_or_uuid_multiple([('volume', 'volume_id')])
+@send("get")
+def volume_metadata_show(volume_id, **kwargs):
+    url = '/volumes/{}/metadata'.format(volume_id)
+    return url, None
+
+
+@get_by_name_or_uuid_multiple([('volume', 'volume_id')])
+@send("put")
+def volume_metadata_update(volume_id, metadata, **kwargs):
+    url = '/volumes/{}/metadata'.format(volume_id)
+    json = {
+        'metadata': metadata,
+    }
+    return url, json
+
+
+@get_by_name_or_uuid_multiple([('volume', 'volume_id')])
+@send("get")
+def volume_metadata_show_key(volume_id, key, **kwargs):
+    url = '/volumes/{}/metadata/{}'.format(volume_id, key)
+    return url, None
+
+
+@get_by_name_or_uuid_multiple([('volume', 'volume_id')])
+@send("delete")
+def volume_metadata_delete(volume_id, key):
+    url = '/volumes/{}/metadata/{}'.format(volume_id, key)
+    return url, None
+
+
+@get_by_name_or_uuid_multiple([('volume', 'volume_id')])
+@send("put")
+def volume_metadata_update_key(volume_id, key, meta, **kwargs):
+    url = '/volumes/{}/metadata/{}'.format(volume_id, key)
+    json = {
+        'meta': meta,
+    }
+    return url, json