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/_states/cinderv3.py b/_states/cinderv3.py
index c45e970..15cff40 100644
--- a/_states/cinderv3.py
+++ b/_states/cinderv3.py
@@ -5,73 +5,96 @@
 import ast
 import logging
 
-LOG = logging.getLogger(__name__)
+log = logging.getLogger(__name__)
 
 
 def __virtual__():
-    return 'cinderv3'
+    return 'cinderv3' if 'cinderv3.volume_list' in __salt__ else False  # noqa
 
 
 def _cinder_call(fname, *args, **kwargs):
     return __salt__['cinderv3.{}'.format(fname)](*args, **kwargs)
 
 
-def volume_type_present(name=None, cloud_name=None):
-    """
-    Ensures that the specified volume type is present.
-    """
-    ret = {
-        'name': name,
-        'changes': {},
-        'result': True,
-        'comment': 'Volume type "{0}" already exists'.format(name)
-    }
-    type_req = _cinder_call('volume_type_get', name=name, cloud_name=cloud_name)
-    if type_req.get("result"):
-        return ret
-    else:
-        create_req = _cinder_call('volume_type_create', name=name,
-                                  cloud_name=cloud_name)
-        if create_req.get("result") is False:
-            ret = {
-                'name': name,
-                'changes': {},
-                'result': False,
-                'comment': 'Volume type "{0}" failed to create'.format(name)
-            }
+def _resource_present(resource, name, cloud_name, **kwargs):
+    try:
+        method_name = '{}_get_details'.format(resource)
+        exact_resource = _cinder_call(
+            method_name, name, cloud_name=cloud_name
+        )[resource]
+    except Exception as e:
+        if 'ResourceNotFound' in repr(e):
+            try:
+                method_name = '{}_create'.format(resource)
+                resp = _cinder_call(
+                    method_name, name=name, cloud_name=cloud_name, **kwargs
+                )
+            except Exception as e:
+                log.exception('Cinder {0} create failed with {1}'.
+                    format(resource, e))
+                return _failed('create', name, resource)
+            return _succeeded('create', name, resource, resp)
+        elif 'MultipleResourcesFound' in repr(e):
+            return _failed('find', name, resource)
         else:
-            ret['comment'] = 'Volume type {0} has been created'.format(name)
-            ret['changes']['Volume type'] = 'Created'
-        return ret
+            raise
+
+    to_update = {}
+    if to_update:
+        for key in kwargs:
+            if key not in exact_resource or kwargs[key] != exact_resource[key]:
+                to_update[key] = kwargs[key]
+        try:
+            method_name = '{}_update'.format(resource)
+            resp = _cinder_call(
+                method_name, name, cloud_name=cloud_name, **to_update
+            )
+        except Exception as e:
+            log.exception('Cinder {0} update failed with {1}'.format(resource, e))
+            return _failed('update', name, resource)
+        return _succeeded('update', name, resource, resp)
+    return _succeeded('no_changes', name, resource)
 
 
-def volume_type_absent(name=None, cloud_name=None):
-    """
-    Ensures that the specified volume type is absent.
-    """
-    ret = {
-        'name': name,
-        'changes': {},
-        'result': True,
-        'comment': 'Volume type "{0}" not found'.format(name)
-    }
-    type_req = _cinder_call('volume_type_get', name=name, cloud_name=cloud_name)
-    if not type_req.get("result"):
-        return ret
-    else:
-        delete_req = _cinder_call('volume_type_delete', name=name,
-                                  cloud_name=cloud_name)
-        if delete_req.get("result") is False:
-            ret = {
-                'name': name,
-                'changes': {},
-                'result': False,
-                'comment': 'Volume type "{0}" failed to delete'.format(name)
-            }
-        else:
-            ret['comment'] = 'Volume type {0} has been deleted'.format(name)
-            ret['changes']['Volume type'] = 'Deleted'
-        return ret
+def _resource_absent(resource, name, cloud_name):
+    try:
+        method_name = '{}_get_details'.format(resource)
+        _cinder_call(
+            method_name, name, cloud_name=cloud_name
+        )[resource]
+    except Exception as e:
+        if 'ResourceNotFound' in repr(e):
+            return _succeeded('absent', name, resource)
+        if 'MultipleResourcesFound' in repr(e):
+            return _failed('find', name, resource)
+    try:
+        method_name = '{}_delete'.format(resource)
+        _cinder_call(
+            method_name, name, cloud_name=cloud_name
+        )
+    except Exception as e:
+        log.error('Cinder delete {0} failed with {1}'.format(resource, e))
+        return _failed('delete', name, resource)
+    return _succeeded('delete', name, resource)
+
+
+def volume_type_present(name, cloud_name, **kwargs):
+    return _resource_present('volume_type', name, cloud_name, **kwargs)
+
+
+def volume_type_absent(name, cloud_name):
+    return _resource_absent('volume_type', name, cloud_name)
+
+
+def volume_present(name, cloud_name, size, **kwargs):
+    kwargs.update({
+        'size': size,
+    })
+    return _resource_present('volume', name, cloud_name, **kwargs)
+
+
+def volume_absent(name, cloud_name):
+    return _resource_absent('volume', name, cloud_name)
 
 
 def volume_type_key_present(name=None, key=None, value=None, cloud_name=None):
@@ -80,8 +103,8 @@
     """
     keys = "{u'" + key + "': u'" + value + "'}"
     keys = ast.literal_eval(keys)
-    signal_create = _cinder_call('keys_volume_type_set',
-                                 name=name, keys=keys, cloud_name=cloud_name)
+    signal_create = _cinder_call('keys_volume_type_set', name, keys=keys,
+                                 cloud_name=cloud_name)
     if signal_create["result"] is True:
         ret = {
             'name': name,
@@ -97,3 +120,37 @@
             'comment': signal_create.get("comment")
         }
     return ret
+
+
+def _succeeded(op, name, resource, changes=None):
+    msg_map = {
+        'create': '{0} {1} created',
+        'delete': '{0} {1} removed',
+        'update': '{0} {1} updated',
+        'no_changes': '{0} {1} is in desired state',
+        'absent': '{0} {1} not present',
+        'resources_moved': '{1} resources were moved from {0}',
+    }
+    changes_dict = {
+        'name': name,
+        'result': True,
+        'comment': msg_map[op].format(resource, name),
+        'changes': changes or {},
+    }
+    return changes_dict
+
+def _failed(op, name, resource):
+    msg_map = {
+        'create': '{0} {1} failed to create',
+        'delete': '{0} {1} failed to delete',
+        'update': '{0} {1} failed to update',
+        'find': '{0} {1} found multiple {0}',
+        'resources_moved': 'failed to move {1} from {0}',
+    }
+    changes_dict = {
+        'name': name,
+        'result': False,
+        'comment': msg_map[op].format(resource, name),
+        'changes': {},
+    }
+    return changes_dict
\ No newline at end of file