Add module and states for gnocchi

Initial version of the module allows to create/delete/update/list
archive policies and rules.
Also added client states supporting rules and policies creation

Change-Id: I7341dfb26a39275e1a9b55f7a49fd2ace9584612
Related-Prod: https://mirantis.jira.com/browse/PROD-20719
diff --git a/README.rst b/README.rst
index 984d971..0edefad 100644
--- a/README.rst
+++ b/README.rst
@@ -232,6 +232,38 @@
           address: 127.0.0.1
           port: 8125
 
+Gnocchi archive policy definition example:
+
+.. code-block:: yaml
+
+    gnocchi:
+      client:
+        enabled: True
+        resources:
+          v1:
+            enabled: true
+            cloud_name: admin_identity
+            archive_policies:
+              test_policy:
+                definition:
+                  - granularity: '1h'
+                    points: 10
+                    timespan: '10h'
+                  - granularity: '2h'
+                    points: 10
+                    timespan: '20h'
+                aggregation_methods:
+                  - mean
+                  - max
+                back_window: 2
+                rules:
+                  test_policy_rule1:
+                    metric_pattern: 'fo.*'
+                  test_policy_rule2:
+                    metric_pattern: 'foo2.*'
+
+
+
 More Information
 ================
 
diff --git a/_modules/gnocchiv1/__init__.py b/_modules/gnocchiv1/__init__.py
new file mode 100644
index 0000000..16edda5
--- /dev/null
+++ b/_modules/gnocchiv1/__init__.py
@@ -0,0 +1,33 @@
+try:
+    import os_client_config
+    from keystoneauth1 import exceptions as ka_exceptions
+    REQUIREMENTS_MET = True
+except ImportError:
+    REQUIREMENTS_MET = False
+
+from gnocchiv1 import archive_policy
+
+archive_policy_create = archive_policy.archive_policy_create
+archive_policy_delete = archive_policy.archive_policy_delete
+archive_policy_list = archive_policy.archive_policy_list
+archive_policy_update = archive_policy.archive_policy_update
+archive_policy_read = archive_policy.archive_policy_read
+
+archive_policy_rule_create = archive_policy.archive_policy_rule_create
+archive_policy_rule_delete = archive_policy.archive_policy_rule_delete
+archive_policy_rule_list = archive_policy.archive_policy_rule_list
+archive_policy_rule_read = archive_policy.archive_policy_rule_read
+
+__all__ = (
+    'archive_policy_update', 'archive_policy_create', 'archive_policy_list', 'archive_policy_delete',
+    'archive_policy_read', 'archive_policy_rule_create', 'archive_policy_rule_delete', 'archive_policy_rule_list',
+    'archive_policy_rule_read'
+)
+
+def __virtual__():
+    """Only load gnocchiv1 if requirements are available."""
+    if REQUIREMENTS_MET:
+        return 'gnocchiv1'
+    else:
+        return False, ("The gnocchiv1 execution module cannot be loaded: "
+                       "os_client_config or keystoneauth are unavailable.")
diff --git a/_modules/gnocchiv1/archive_policy.py b/_modules/gnocchiv1/archive_policy.py
new file mode 100644
index 0000000..b5d78a2
--- /dev/null
+++ b/_modules/gnocchiv1/archive_policy.py
@@ -0,0 +1,55 @@
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+import hashlib
+
+from gnocchiv1.common import send, get_raw_client
+
+@send('get')
+def archive_policy_list(**kwargs):
+    url = '/archive_policy?{}'.format(urlencode(kwargs))
+    return url, {}
+
+
+@send('post')
+def archive_policy_create(**kwargs):
+     url = '/archive_policy'
+     return url, {'json': kwargs}
+
+@send('get')
+def archive_policy_read(policy_name, **kwargs):
+    url = '/archive_policy/{}'.format(policy_name)
+    return url, {}
+
+
+@send('patch')
+def archive_policy_update(policy_name, **kwargs):
+    url = '/archive_policy/{}'.format(policy_name)
+    return url, {'json': kwargs}
+
+
+@send('delete')
+def archive_policy_delete(policy_name, **kwargs):
+     url = '/archive_policy/{}'.format(policy_name)
+     return url, {}
+
+@send('get')
+def archive_policy_rule_list(**kwargs):
+    url = '/archive_policy_rule?{}'.format(urlencode(kwargs))
+    return url, {}
+
+@send('post')
+def archive_policy_rule_create(**kwargs):
+     url = '/archive_policy_rule'
+     return url, {'json': kwargs}
+
+@send('get')
+def archive_policy_rule_read(rule_name, **kwargs):
+    url = '/archive_policy_rule/{}'.format(rule_name)
+    return url, {}
+
+@send('delete')
+def archive_policy_rule_delete(rule_name, **kwargs):
+     url = '/archive_policy_rule/{}'.format(rule_name)
+     return url, {}
diff --git a/_modules/gnocchiv1/common.py b/_modules/gnocchiv1/common.py
new file mode 100644
index 0000000..7364c2c
--- /dev/null
+++ b/_modules/gnocchiv1/common.py
@@ -0,0 +1,120 @@
+import logging
+import os_client_config
+from uuid import UUID
+
+log = logging.getLogger(__name__)
+
+
+class GnocchiException(Exception):
+
+    _msg = "Gnocchi module exception occured."
+
+    def __init__(self, message=None, **kwargs):
+        super(GnocchiException, self).__init__(message or self._msg)
+
+
+class NoGnocchiEndpoint(GnocchiException):
+    _msg = "Gnocchi endpoint not found in keystone catalog."
+
+
+class NoAuthPluginConfigured(GnocchiException):
+    _msg = ("You are using keystoneauth auth plugin that does not support "
+            "fetching endpoint list from token (noauth or admin_token).")
+
+
+class NoCredentials(GnocchiException):
+    _msg = "Please provide cloud name present in clouds.yaml."
+
+
+class ResourceNotFound(GnocchiException):
+    _msg = "Uniq resource: {resource} with name: {name} not found."
+
+    def __init__(self, resource, name, **kwargs):
+        super(GnocchiException, self).__init__(
+            self._msg.format(resource=resource, name=name))
+
+
+class MultipleResourcesFound(GnocchiException):
+    _msg = "Multiple resource: {resource} with name: {name} found."
+
+    def __init__(self, resource, name, **kwargs):
+        super(GnocchiException, self).__init__(
+            self._msg.format(resource=resource, name=name))
+
+
+def get_raw_client(cloud_name):
+    service_type = 'metric'
+    config = os_client_config.OpenStackConfig()
+    cloud = config.get_one_cloud(cloud_name)
+    adapter = cloud.get_session_client(service_type)
+    adapter.version = '1'
+    try:
+        access_info = adapter.session.auth.get_access(adapter.session)
+        endpoints = access_info.service_catalog.get_endpoints()
+    except (AttributeError, ValueError) as exc:
+        log.exception('%s' % exc)
+        e = NoAuthPluginConfigured()
+        log.exception('%s' % e)
+        raise e
+    if service_type not in endpoints:
+        if not service_type:
+            e = NoGnocchiEndpoint()
+            log.error('%s' % e)
+            raise e
+    return adapter
+
+
+def send(method):
+    def wrap(func):
+        def wrapped_f(*args, **kwargs):
+            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, request_kwargs = func(*args, **kwargs)
+            response = getattr(adapter, method)(url, **request_kwargs)
+            if not response.content:
+                return {}
+            return response.json()
+        return wrapped_f
+    return wrap
+
+
+def _check_uuid(val):
+    try:
+        return str(UUID(val)).replace('-', '') == val.replace('-', '')
+    except (TypeError, ValueError, AttributeError):
+        return False
+
+
+def get_by_name_or_uuid(resource_list, resp_key):
+    def wrap(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]
+            if _check_uuid(ref):
+                uuid = ref
+            else:
+                # Then we have name not uuid
+                cloud_name = kwargs['cloud_name']
+                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/_states/gnocchiv1.py b/_states/gnocchiv1.py
new file mode 100644
index 0000000..5cadd65
--- /dev/null
+++ b/_states/gnocchiv1.py
@@ -0,0 +1,238 @@
+# -*- coding: utf-8 -*-
+'''
+Managing entities in Gnocchi
+===================================
+'''
+# Import python libs
+import logging
+
+# Import Gnocchi libs
+def __virtual__():
+    return 'gnocchiv1' if 'gnocchiv1.archive_policy_list' in __salt__ else False
+
+
+log = logging.getLogger(__name__)
+
+
+def _gnocchiv1_call(fname, *args, **kwargs):
+    return __salt__['gnocchiv1.{}'.format(fname)](*args, **kwargs)
+
+
+def archive_policy_present(name, cloud_name, definition, **kwargs):
+    """
+    Creates an archive policy
+
+    This state checks if an archive policy is present and, if not, creates an
+    archive policy with specified name, definition, aggregation methods and
+    back_window.
+
+    :param name: name of the archive policy
+    :param cloud_name: name of the cloud in cloud.yaml
+    :param definition: List of dictionaries. Every dictionary may contain:
+            :param granularity: String indicating policy's granularity e.g. '0:00:01'
+            :param points: Integer indicating policy's points e.g. 10
+            :param: timespan parameters of archive policy. e.g. '0:00:10'
+    :param aggregation_methods: List of aggregation methods used in archive policy
+    :param back_window: Integer specifies the number of coarsest periods to keep
+    """
+    try:
+        archive_policy = _gnocchiv1_call(
+            'archive_policy_read', name, cloud_name=cloud_name
+        )
+    except Exception as e:
+        if 'NotFound' in repr(e):
+            try:
+                resp = _gnocchiv1_call(
+                    'archive_policy_create', name=name, cloud_name=cloud_name,
+                    definition=definition, **kwargs
+                )
+            except Exception as e:
+                log.error('Gnocchi archive policy create failed with {}'.format(e))
+                return _create_failed(name, 'archive_policy')
+            return _created(name, 'archive_policy', resp)
+        else:
+            raise
+    if archive_policy:
+        # TODO: Implement and design archive policy update procedure
+        return _no_changes(name, 'archive_policy')
+    else:
+        return _find_failed(name, 'archive_policy')
+
+
+def archive_policy_absent(name, cloud_name):
+    try:
+        archive_policy = _gnocchiv1_call(
+            'archive_policy_read', name, cloud_name=cloud_name
+        )
+    except Exception as e:
+        if 'NotFound' in repr(e):
+            return _absent(name, 'archive_policy')
+    if archive_policy:
+        try:
+            resp = _gnocchiv1_call(
+                    'archive_policy_delete', name, cloud_name=cloud_name
+                )
+        except Exception as e:
+            log.error('Archive policy delete failed with {}'.format(e))
+            return _delete_failed(name, 'archive_policy')
+        return _deleted(name, 'archive_policy')
+    else:
+        return _find_failed(name, 'archive_policy')
+
+
+def archive_policy_rule_present(name, cloud_name, archive_policy_name, metric_pattern):
+    """
+    Creates an archive policy rule
+
+    This state checks if an archive policy rule is present and, if not, creates an
+    archive policy rule with specified name, archive policy name, and metric_pattern.
+
+    :param name: name of the archive policy rule
+    :param cloud_name: name of the cloud in cloud.yaml
+    :param archive_policy_name: name of archive policy for specified rule
+    :param metric_pattern: pattern for metrics classified by the rule
+    """
+    try:
+        archive_policy_rule = _gnocchiv1_call(
+            'archive_policy_rule_read', name, cloud_name=cloud_name
+        )
+    except Exception as e:
+        if 'NotFound' in repr(e):
+            try:
+                resp = _gnocchiv1_call(
+                    'archive_policy_rule_create', name=name, cloud_name=cloud_name,
+                    archive_policy_name=archive_policy_name, metric_pattern=metric_pattern,
+                )
+            except Exception as e:
+                log.error('Gnocchi archive policy rule create failed with {}'.format(e))
+                return _create_failed(name, 'archive_policy_rule')
+            return _created(name, 'archive_policy_rule', resp)
+        else:
+            raise
+    if archive_policy_rule:
+        # Currently Gnocchi API doesn't allow to update properties in rules,
+        # but they can be recreated.
+        if ((archive_policy_rule['archive_policy_name'] != archive_policy_name) or
+            (archive_policy_rule['metric_pattern'] != metric_pattern)):
+            try:
+                resp = _gnocchiv1_call(
+                    'archive_policy_rule_delete', name, cloud_name=cloud_name
+                )
+                resp = _gnocchiv1_call(
+                    'archive_policy_rule_create', name=name, cloud_name=cloud_name,
+                    archive_policy_name=archive_policy_name, metric_pattern=metric_pattern
+                )
+            except Exception as e:
+                log.error('Archive policy rule update failed with {}'.format(e))
+                return _update_failed(name, 'archive_policy_rule')
+            return _updated(name, 'archive_policy_rule', resp)
+        else:
+            return _no_changes(name, 'archive_policy_rule')
+    else:
+        return _find_failed(name, 'archive_policy_rule')
+
+def archive_policy_rule_absent(name, cloud_name):
+    try:
+        archive_policy_rule = _gnocchiv1_call(
+            'archive_policy_rule_read', name, cloud_name=cloud_name
+        )
+    except Exception as e:
+        if 'NotFound' in repr(e):
+            return _absent(name, 'archive_policy_rule')
+    if archive_policy_rule:
+        try:
+            resp = _gnocchiv1_call(
+                    'archive_policy_rule_delete', name, cloud_name=cloud_name
+                )
+        except Exception as e:
+            log.error('Archive policy rule delete failed with {}'.format(e))
+            return _delete_failed(name, 'archive_policy_rule')
+        return _deleted(name, 'archive_policy_rule')
+    else:
+        return _find_failed(name, 'archive_policy_rule')
+
+
+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/gnocchi/client/init.sls b/gnocchi/client/init.sls
new file mode 100644
index 0000000..3424f2e
--- /dev/null
+++ b/gnocchi/client/init.sls
@@ -0,0 +1,3 @@
+include:
+- gnocchi.client.service
+- gnocchi.client.resources
diff --git a/gnocchi/client/resources/init.sls b/gnocchi/client/resources/init.sls
new file mode 100644
index 0000000..1c3e447
--- /dev/null
+++ b/gnocchi/client/resources/init.sls
@@ -0,0 +1,2 @@
+include:
+- gnocchi.client.resources.v1
diff --git a/gnocchi/client/resources/v1.sls b/gnocchi/client/resources/v1.sls
new file mode 100644
index 0000000..bc9ab19
--- /dev/null
+++ b/gnocchi/client/resources/v1.sls
@@ -0,0 +1,37 @@
+{%- from "gnocchi/map.jinja" import client with context %}
+
+{%- set resources = client.get('resources', {}).get('v1', {}) %}
+
+{%- if resources.get('enabled', False) %}
+
+{%- for policy_name, policy in resources.get('archive_policies', {}).iteritems() %}
+
+gnocchi_archive_policy_{{ policy_name }}:
+  gnocchiv1.archive_policy_present:
+  - cloud_name: {{ policy.get('cloud_name', resources.cloud_name) }}
+  - name: {{ policy_name }}
+  - definition:
+    {{ policy.definition|yaml(false)|indent(4) }}
+{%- if policy.aggregation_methods is defined %}
+  - aggregation_methods:
+    {{ policy.aggregation_methods|yaml(false)|indent(4) }}
+{%- endif %}
+{%- if policy.back_window is defined %}
+  - back_window: {{ policy.back_window }}
+{%- endif %}
+
+{%- for rule_name, rule in policy.get('rules', {}).iteritems() %}
+
+gnocchi_archive_policy_rule_{{ rule_name }}:
+  gnocchiv1.archive_policy_rule_present:
+  - cloud_name: {{ policy.get('cloud_name', resources.cloud_name) }}
+  - name: {{ rule_name }}
+  - archive_policy_name: {{ policy_name }}
+  - metric_pattern: {{ rule.metric_pattern }}
+  - require:
+    - gnocchi_archive_policy_{{ policy_name }}
+{%- endfor %}
+
+{%- endfor %}
+
+{%- endif %}
diff --git a/gnocchi/client.sls b/gnocchi/client/service.sls
similarity index 100%
rename from gnocchi/client.sls
rename to gnocchi/client/service.sls