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