Merge "Provide complete set of Nova metrics"
diff --git a/.kitchen.yml b/.kitchen.yml
new file mode 100644
index 0000000..e96d842
--- /dev/null
+++ b/.kitchen.yml
@@ -0,0 +1,47 @@
+---
+driver:
+  name: docker
+  hostname: collectd.ci.local
+  use_sudo: false
+
+provisioner:
+  name: salt_solo
+  salt_install: bootstrap
+  salt_bootstrap_url: https://bootstrap.saltstack.com
+  salt_version: latest
+  require_chef: false
+  log_level: error
+  formula: collectd
+  grains:
+    noservices: True
+  state_top:
+    base:
+      "*":
+        - collectd
+  pillars:
+    top.sls:
+      base:
+        "*":
+          - collectd
+  dependencies:
+    - name: linux
+      repo: git
+      source: https://github.com/salt-formulas/salt-formula-linux
+
+verifier:
+  name: inspec
+  sudo: true
+
+platforms:
+  - name: <%=ENV['PLATFORM'] || 'ubuntu-xenial'%>
+    driver_config:
+      image: <%=ENV['PLATFORM'] || 'trevorj/salty-whales:xenial'%>
+      platform: ubuntu
+
+suites:
+
+  - name: client
+    provisioner:
+      pillars-from-files:
+        collectd.sls: tests/pillar/client.sls
+# vim: ft=yaml sw=2 ts=2 sts=2 tw=125
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..4f34af2
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,40 @@
+sudo: required
+services:
+  - docker
+
+install:
+  - pip install PyYAML
+  - pip install virtualenv
+  - |
+    test -e Gemfile || cat <<EOF > Gemfile
+    source 'https://rubygems.org'
+    gem 'rake'
+    gem 'test-kitchen'
+    gem 'kitchen-docker'
+    gem 'kitchen-inspec'
+    gem 'inspec'
+    gem 'kitchen-salt', :git => 'https://github.com/salt-formulas/kitchen-salt.git'
+  - bundle install
+
+env:
+    - PLATFORM=trevorj/salty-whales:trusty
+    - PLATFORM=trevorj/salty-whales:xenial
+
+
+before_script:
+  - set -o pipefail
+  - make test | tail
+
+script:
+  - test ! -e .kitchen.yml || bundle exec kitchen test -t tests/integration
+
+notifications:
+  webhooks:
+    urls:
+      - https://webhooks.gitter.im/e/6123573504759330786b
+    on_success: change  # options: [always|never|change] default: always
+    on_failure: never  # options: [always|never|change] default: always
+    on_start: never     # options: [always|never|change] default: always
+    on_cancel: never    # options: [always|never|change] default: always
+    on_error: never    # options: [always|never|change] default: always
+  email: false
diff --git a/collectd/client.sls b/collectd/client.sls
index eca7071..e0fbf5d 100644
--- a/collectd/client.sls
+++ b/collectd/client.sls
@@ -1,4 +1,4 @@
-{%- from "collectd/map.jinja" import client with context %}
+{%- from "collectd/map.jinja" import client, service_grains with context %}
 {%- if client.enabled %}
 
 include:
@@ -12,43 +12,6 @@
   - require:
     - pkg: collectd_client_packages
 
-{%- set service_grains = {'collectd': {'remote_plugin': {}, 'local_plugin': {}}} %}
-
-{%- for service_name, service in pillar.items() %}
-{%- if service.get('_support', {}).get('collectd', {}).get('enabled', False) %}
-
-{%- set grains_fragment_file = service_name+'/meta/collectd.yml' %}
-{%- macro load_grains_file() %}{% include grains_fragment_file ignore missing %}{% endmacro %}
-{%- set grains_yaml = load_grains_file()|load_yaml %}
-
-{%- if grains_yaml is mapping %}
-{%- set service_grains = salt['grains.filter_by']({'default': service_grains}, merge={'collectd': grains_yaml}) %}
-{%- endif %}
-
-{%- endif %}
-{%- endfor %}
-
-collectd_client_grain:
-  file.managed:
-  - name: /etc/salt/grains.d/collectd
-  - source: salt://collectd/files/collectd.grain
-  - template: jinja
-  - user: root
-  - mode: 600
-  - defaults:
-    service_grains: {{ service_grains|yaml }}
-  - require:
-    - pkg: collectd_client_packages
-    - file: collectd_client_grains_dir
-
-collectd_client_grain_validity_check:
-  cmd.wait:
-  - name: python -c "import yaml; stream = file('/etc/salt/grains.d/collectd', 'r'); yaml.load(stream); stream.close()"
-  - require:
-    - pkg: collectd_client_packages
-  - watch:
-    - file: collectd_client_grain
-
 {%- set plugins = service_grains.collectd.local_plugin %}
 {%- include "collectd/_service.sls" %}
 
diff --git a/collectd/files/collectd.grain b/collectd/files/collectd.grain
deleted file mode 100644
index 3e3b373..0000000
--- a/collectd/files/collectd.grain
+++ /dev/null
@@ -1 +0,0 @@
-{{ service_grains|yaml(False) }}
diff --git a/collectd/files/plugin/collectd_base.py b/collectd/files/plugin/collectd_base.py
index 36c0060..4a61168 100644
--- a/collectd/files/plugin/collectd_base.py
+++ b/collectd/files/plugin/collectd_base.py
@@ -22,6 +22,8 @@
 import traceback
 
 
+TIMEOUT_BIN = '/usr/bin/timeout'
+
 INTERVAL = 10
 
 
@@ -167,7 +169,8 @@
         )
         v.dispatch()
 
-    def execute(self, cmd, shell=True, cwd=None, log_error=True):
+    def execute(self, cmd, shell=True, cwd=None, log_error=True,
+                signal='TERM'):
         """Executes a program with arguments.
 
         Args:
@@ -179,6 +182,8 @@
             (default=None).
             log_error: whether to log an error when the command returned a
             non-zero status code (default=True).
+            signal: the signal used to kill the command if the timeout occurs
+            (default TERM).
 
         Returns:
             A tuple containing the return code, the standard output and the
@@ -189,9 +194,12 @@
             (-1, None, None) if the program couldn't be executed at all.
         """
         start_time = time.time()
+        full_cmd = [TIMEOUT_BIN, '-k', '1', '-s', signal, str(self.timeout)]
+        full_cmd.extend(cmd)
+
         try:
             proc = subprocess.Popen(
-                cmd,
+                full_cmd,
                 cwd=cwd,
                 shell=shell,
                 stdout=subprocess.PIPE,
@@ -201,18 +209,30 @@
             stdout = stdout.rstrip('\n')
         except Exception as e:
             self.logger.error("Cannot execute command '%s': %s : %s" %
-                              (cmd, str(e), traceback.format_exc()))
+                              (full_cmd, str(e), traceback.format_exc()))
             return (-1, None, None)
 
         returncode = proc.returncode
 
-        if returncode != 0 and log_error:
-            self.logger.error("Command '%s' failed (return code %d): %s" %
-                              (cmd, returncode, stderr))
+        if returncode != 0:
+            # timeout command returns usually 124 (TERM) or 137 (KILL) when the
+            # timeout occurs.
+            # But for some reason, python subprocess rewrites the return
+            # code with the (negative) signal sent when the the signal is not
+            # catched by the process.
+            if returncode == 124 or returncode < 0:
+                stderr = 'timeout {}s'.format(self.timeout)
+                msg = "Command '{}' timeout {}s".format(cmd, self.timeout)
+            else:
+                msg = "Command '{}' failed (return code {}): {}".format(
+                    cmd, returncode, stderr)
+
+            if log_error:
+                self.logger.error(msg)
         if self.debug:
             elapsedtime = time.time() - start_time
             self.logger.info("Command '%s' returned %s in %0.3fs" %
-                             (cmd, returncode, elapsedtime))
+                             (full_cmd, returncode, elapsedtime))
 
         return (returncode, stdout, stderr)
 
@@ -273,6 +293,7 @@
     def shutdown_callback(self):
         pass
 
+
 class CephBase(Base):
 
     def __init__(self, *args, **kwargs):
diff --git a/collectd/files/plugin/collectd_docker_info.py b/collectd/files/plugin/collectd_docker_info.py
index e0589f1..08b8ca4 100644
--- a/collectd/files/plugin/collectd_docker_info.py
+++ b/collectd/files/plugin/collectd_docker_info.py
@@ -31,6 +31,7 @@
     def __init__(self, *args, **kwargs):
         super(DockerInfoPlugin, self).__init__(*args, **kwargs)
         self.plugin = NAME
+        self.timeout = 3
 
     def itermetrics(self):
         cmd = [DOCKER_BINARY, 'info', '-f', "{{ json .}}"]
diff --git a/collectd/files/plugin/collectd_elasticsearch_node.py b/collectd/files/plugin/collectd_elasticsearch_node.py
index 1ce23fa..2cfc7af 100644
--- a/collectd/files/plugin/collectd_elasticsearch_node.py
+++ b/collectd/files/plugin/collectd_elasticsearch_node.py
@@ -27,21 +27,97 @@
     def __init__(self, *args, **kwargs):
         super(ElasticsearchNodePlugin, self).__init__(*args, **kwargs)
         self.plugin = NAME
+        self._previous = {}
+
+    @staticmethod
+    def _metric(name, values, meta=None):
+        return {'type_instance': name, 'values': values, 'meta': meta or {}}
+
+    def _get_latency(self, name, count, time):
+        cname = '{}_count'.format(name)
+        tname = '{}_time'.format(name)
+        prev_count = self._previous.get(cname)
+        prev_time = self._previous.get(tname)
+        self._previous[cname] = count
+        self._previous[tname] = time
+        if prev_count and prev_time:
+            diff_count = count - prev_count
+            diff_time = time - prev_time
+            return diff_time / diff_count if diff_count > 0 else 0
 
     def itermetrics(self):
         stats = self.query_api('_nodes/_local/stats').get(
             'nodes', {}).values()[0]
-        yield {
-            'type_instance': 'documents',
-            'values': stats['indices']['docs']['count']
-        }
-        yield {
-            'type_instance': 'documents_deleted',
-            'values': stats['indices']['docs']['deleted']
-        }
-        # TODO: collectd more metrics
-        # See https://www.elastic.co/guide/en/elasticsearch/guide/current/
-        # _monitoring_individual_nodes.html
+        indices = stats['indices']
+        yield self._metric('documents', indices['docs']['count'])
+        yield self._metric('documents_deleted', indices['docs']['deleted'])
+        yield self._metric(
+            'indexing_current', indices['indexing']['index_current'])
+        yield self._metric(
+            'indexing_failed', indices['indexing']['index_failed'])
+        indexing_latency = self._get_latency(
+            'indexing', indices['indexing']['index_total'],
+            indices['indexing']['index_time_in_millis'])
+        if indexing_latency:
+            yield self._metric('indexing_latency', indexing_latency)
+        yield self._metric('store_size', indices['store']['size_in_bytes'])
+        fd_open = 0
+        if stats['process']['max_file_descriptors'] > 0:
+            fd_open = 100.0 * stats['process']['open_file_descriptors'] \
+                / stats['process']['max_file_descriptors']
+        yield self._metric('fd_open_percent', fd_open)
+
+        thread_pools = stats['thread_pool']
+        for pool in ('bulk', 'flush', 'search', 'index', 'get'):
+            yield self._metric('thread_pool_queue',
+                               thread_pools[pool]['queue'], {'pool': pool})
+            yield self._metric('thread_pool_rejected',
+                               thread_pools[pool]['rejected'], {'pool': pool})
+            yield self._metric('thread_pool_completed',
+                               thread_pools[pool]['completed'], {'pool': pool})
+        mem = stats['jvm']['mem']
+        yield self._metric('jvm_heap_max', mem['heap_max_in_bytes'])
+        yield self._metric('jvm_heap_used_percent', mem['heap_used_percent'])
+        yield self._metric('jvm_heap_used', mem['heap_used_in_bytes'])
+        for pool, stat in mem['pools'].items():
+            yield self._metric(
+                'jvm_heap_pool', stat['used_in_bytes'], {'pool': pool})
+        gc = stats['jvm']['gc']
+        for pool, stat in gc['collectors'].items():
+            yield self._metric('jvm_gc_count', stat['collection_count'],
+                               {'pool': pool})
+            yield self._metric('jvm_gc_time',
+                               stat['collection_time_in_millis'],
+                               {'pool': pool})
+
+        search = indices['search']
+        for phase in ('query', 'fetch'):
+            yield self._metric('{}_current'.format(phase),
+                               search['{}_current'.format(phase)])
+            latency = self._get_latency(
+                phase,
+                search['{}_total'.format(phase)],
+                search['{}_time_in_millis'.format(phase)])
+            if latency is not None:
+                yield self._metric('{}_latency'.format(phase), latency)
+        yield self._metric('query_count', search['query_total'])
+
+        query = indices['query_cache']
+        yield self._metric('query_cache_size', query['memory_size_in_bytes'])
+        yield self._metric('query_cache_evictions', query['evictions'])
+
+        fielddata = indices['fielddata']
+        yield self._metric('fielddata_size', fielddata['memory_size_in_bytes'])
+        yield self._metric('fielddata_evictions', fielddata['evictions'])
+
+        for operation in ('merges', 'flush', 'refresh'):
+            yield self._metric(operation, indices[operation]['total'])
+            latency = self._get_latency(
+                operation,
+                indices[operation]['total'],
+                indices[operation]['total_time_in_millis'])
+            if latency is not None:
+                yield self._metric('{}_latency'.format(operation), latency)
 
 
 plugin = ElasticsearchNodePlugin(collectd)
diff --git a/collectd/files/plugin/collectd_k8s_get.py b/collectd/files/plugin/collectd_k8s_get.py
new file mode 100644
index 0000000..2d8742a
--- /dev/null
+++ b/collectd/files/plugin/collectd_k8s_get.py
@@ -0,0 +1,192 @@
+#!/usr/bin/python
+# Copyright 2016 Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+if __name__ == '__main__':
+    import collectd_fake as collectd
+else:
+    import collectd
+
+import collectd_base as base
+import requests
+
+NAME = 'k8s'
+INTERVAL = 30
+
+
+class K8sGetPlugin(base.Base):
+    def __init__(self, *args, **kwargs):
+        super(K8sGetPlugin, self).__init__(*args, **kwargs)
+        self._threads = {}
+        self.session = None
+        self.plugin = NAME
+        self.endpoint = None
+        self.verify = False
+        self.client_key = None
+        self.client_certs = None
+
+        self.interval = INTERVAL
+        self.polling_interval = INTERVAL
+
+        self.timeout = 3
+        self.max_retries = 2
+
+    def shutdown_callback(self):
+        for tid, t in self._threads.items():
+            if t.is_alive():
+                self.logger.info('Waiting for {} thread to finish'.format(tid))
+                t.stop()
+                t.join()
+
+    def config_callback(self, config):
+        super(K8sGetPlugin, self).config_callback(config)
+        for node in config.children:
+            if node.key == "Endpoint":
+                self.endpoint = node.values[0]
+            elif node.key == 'Verify':
+                if node.values[0].lower() == 'false':
+                    self.verify = False
+            elif node.key == 'ClientCert':
+                self.client_cert = node.values[0]
+            elif node.key == 'ClientKey':
+                self.client_key = node.values[0]
+
+        session = requests.Session()
+        if self.endpoint.startswith('https'):
+            session.mount(
+                'https://',
+                requests.adapters.HTTPAdapter(max_retries=self.max_retries)
+            )
+        else:
+            session.mount(
+                'http://',
+                requests.adapters.HTTPAdapter(max_retries=self.max_retries)
+            )
+
+        session.verify = self.verify
+        if self.client_cert and self.client_key:
+            session.cert = (self.client_cert, self.client_key)
+        elif self.client_cert:
+            session.cert = self.client_cert
+
+        self.session = session
+
+    def get(self, url):
+
+        def get():
+            try:
+                r = self.session.get(url, timeout=self.timeout)
+                data = r.json()
+            except Exception as e:
+                self.logger.warning("Got exception for '{}': {}".format(
+                    url, e)
+                )
+                raise base.CheckException('Fail to get {}'.self(url))
+
+            else:
+
+                if r.status_code != 200:
+                    msg = ("{} responded with code {} "
+                           "while 200 is expected").format(url, r.status_code)
+                    self.logger.warning(msg)
+                    raise base.CheckException(msg)
+            return data.get('items', [])
+
+        if url not in self._threads:
+            t = base.AsyncPoller(self.collectd,
+                                 get,
+                                 self.polling_interval,
+                                 url)
+            t.start()
+            self._threads[url] = t
+
+        t = self._threads[url]
+        if not t.is_alive():
+            self.logger.warning("Unexpected end of the thread {}".format(
+                t.name))
+            del self._threads[url]
+            return []
+
+        return t.results
+
+    @staticmethod
+    def _check_conditions(conditions, _type):
+        return all(
+            [cnd.get('status') == 'True' for cnd in conditions
+             if cnd.get('type') == _type]
+        )
+
+    def itermetrics(self):
+        nodes = self.get('{}/api/v1/nodes'.format(self.endpoint))
+        total, total_ready = (0, 0)
+        for node in nodes:
+            self.logger.debug(node.get('metadata', {}).get('name'))
+            conditions = node.get(
+                'status', {}).get('conditions', [])
+            if self._check_conditions(conditions, _type='Ready'):
+                total_ready += 1
+            total += 1
+        if total > 0:
+            yield {'values': (100.0 * (total - total_ready)) / total,
+                   'plugin_instance': 'nodes_percent',
+                   'meta': {'status': 'not_ready',
+                            'discard_hostname': True},
+                   }
+
+        yield {'values': total_ready,
+               'plugin_instance': 'nodes',
+               'meta': {'status': 'ready', 'discard_hostname': True},
+               }
+        yield {'values': total - total_ready,
+               'plugin_instance': 'nodes',
+               'meta': {'status': 'not_ready', 'discard_hostname': True},
+               }
+        yield {'values': total,
+               'plugin_instance': 'nodes_total',
+               'meta': {'discard_hostname': True}
+               }
+
+
+plugin = K8sGetPlugin(collectd, disable_check_metric=True)
+
+
+def config_callback(conf):
+    plugin.config_callback(conf)
+
+
+def notification_callback(notification):
+    plugin.notification_callback(notification)
+
+
+def read_callback():
+    plugin.conditional_read_callback()
+
+
+if __name__ == '__main__':
+    plugin.endpoint = 'https://172.16.10.253:443'
+    plugin.verify = False
+    plugin.client_key = '/etc/kubernetes/ssl/kubelet-client.key'
+    plugin.client_cert = '/etc/kubernetes/ssl/kubelet-client.crt'
+
+    collectd.load_configuration(plugin)
+    plugin.read_callback()
+    import time
+    time.sleep(base.INTERVAL)
+    plugin.read_callback()
+    plugin.shutdown_callback()
+else:
+    collectd.register_config(config_callback)
+    collectd.register_notification(notification_callback)
+    collectd.register_read(read_callback, base.INTERVAL)
+
diff --git a/collectd/files/plugin/collectd_k8s_kubectl_get.py b/collectd/files/plugin/collectd_k8s_kubectl_get.py
deleted file mode 100644
index 1333120..0000000
--- a/collectd/files/plugin/collectd_k8s_kubectl_get.py
+++ /dev/null
@@ -1,149 +0,0 @@
-#!/usr/bin/python
-# Copyright 2017 Mirantis, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-if __name__ == '__main__':
-    import collectd_fake as collectd
-else:
-    import collectd
-import json
-import time
-
-import collectd_base as base
-
-NAME = 'k8s'
-KUBECTL_BINARY = '/usr/bin/kubectl'
-INTERVAL = 30
-
-
-class K8sPlugin(base.Base):
-
-    def __init__(self, *args, **kwargs):
-        super(K8sPlugin, self).__init__(*args, **kwargs)
-        self.plugin = NAME
-        self._threads = {}
-        self.polling_interval = INTERVAL
-        self.resources = []
-        self._get_nodes = False
-
-    def shutdown_callback(self):
-        for tid, t in self._threads.items():
-            if t.is_alive():
-                self.logger.info('Waiting for {} thread to finish'.format(tid))
-                t.stop()
-                t.join()
-
-    def config_callback(self, config):
-        super(K8sPlugin, self).config_callback(config)
-        for node in config.children:
-            if node.key == 'PollingInterval':
-                self.polling_interval = int(node.values[0])
-            elif node.key == 'GetNodes':
-                if node.values[0].lower() == 'true':
-                    self._get_nodes = True
-
-    def kubectl_get(self, resource):
-
-        def kubectl_poller():
-            cmd = [KUBECTL_BINARY, 'get', '-o', 'json', resource]
-            data = self.execute_to_json(cmd, shell=False, log_error=True)
-            return data.get('items', [])
-
-        if resource not in self._threads:
-            t = base.AsyncPoller(self.collectd,
-                                 kubectl_poller,
-                                 self.polling_interval,
-                                 resource)
-            t.start()
-            self._threads[resource] = t
-
-        t = self._threads[resource]
-        if not t.is_alive():
-            self.logger.warning("Unexpected end of the thread {}".format(
-                t.name))
-            del self._threads[resource]
-            return []
-
-        return t.results
-
-    @staticmethod
-    def _check_conditions(conditions, _type):
-        return all(
-            [cnd.get('status') == 'True' for cnd in conditions
-             if cnd.get('type') == _type]
-        )
-
-    def _iter_node_metrics(self, nodes):
-        if nodes:
-            total, total_ready = (0, 0)
-            for node in nodes:
-                self.logger.debug(node.get('metadata', {}).get('name'))
-                conditions = node.get(
-                    'status', {}).get('conditions', [])
-                if self._check_conditions(conditions, _type='Ready'):
-                    total_ready += 1
-                total += 1
-            if total > 0:
-                yield {'values': (100.0 * (total - total_ready)) / total,
-                       'plugin_instance': 'nodes_percent',
-                       'meta': {'status': 'not_ready'},
-                       }
-
-            yield {'values': total_ready,
-                   'plugin_instance': 'nodes',
-                   'meta': {'status': 'ready'},
-                   }
-            yield {'values': total - total_ready,
-                   'plugin_instance': 'nodes',
-                   'meta': {'status': 'not_ready'},
-                   }
-            yield {'values': total,
-                   'plugin_instance': 'nodes_total'
-                   }
-
-    def itermetrics(self):
-        if self._get_nodes:
-            items = self.kubectl_get('nodes')
-            return self._iter_node_metrics(items)
-
-
-plugin = K8sPlugin(collectd, disable_check_metric=True)
-
-
-def init_callback():
-    plugin.restore_sigchld()
-
-
-def config_callback(conf):
-    plugin.config_callback(conf)
-
-
-def read_callback():
-    plugin.read_callback()
-
-if __name__ == '__main__':
-    collectd.load_configuration(plugin)
-    plugin._get_nodes = True
-    plugin.read_callback()
-    collectd.info('Sleeping for {}s'.format(INTERVAL))
-    time.sleep(INTERVAL)
-    plugin.read_callback()
-    plugin.shutdown_callback()
-else:
-    collectd.register_init(init_callback)
-    collectd.register_config(config_callback)
-    collectd.register_read(read_callback, INTERVAL)
-
-
-
diff --git a/collectd/files/plugin/hypervisor_stats.py b/collectd/files/plugin/hypervisor_stats.py
index 5fc3bdb..7d1696f 100644
--- a/collectd/files/plugin/hypervisor_stats.py
+++ b/collectd/files/plugin/hypervisor_stats.py
@@ -140,6 +140,7 @@
                     'meta': {
                         'aggregate': agg,
                         'aggregate_id': agg_id,
+                        'meta': {'discard_hostname': True}
                     }
                 }
         # Dispatch the global metrics
@@ -147,6 +148,7 @@
             yield {
                 'type_instance': 'total_{}'.format(k),
                 'values': v,
+                'meta': {'discard_hostname': True}
             }
 
 plugin = HypervisorStatsPlugin(collectd, PLUGIN_NAME,
diff --git a/collectd/files/plugin/openstack_cinder.py b/collectd/files/plugin/openstack_cinder.py
index fe667a6..9f8e0e5 100644
--- a/collectd/files/plugin/openstack_cinder.py
+++ b/collectd/files/plugin/openstack_cinder.py
@@ -23,6 +23,13 @@
 
 PLUGIN_NAME = 'openstack_cinder'
 INTERVAL = openstack.INTERVAL
+volumes_statuses = ('creating', 'available', 'attaching',
+                    'in-use', 'deleting', 'error',
+                    'error-deleting', 'backing-up',
+                    'restoring-backup', 'error_restoring',
+                    'error_extending')
+snapshots_statuses = ('creating', 'available', 'deleting',
+                      'error', 'error_deleting')
 
 
 class CinderStatsPlugin(openstack.CollectdPlugin):
@@ -38,11 +45,18 @@
         self.interval = INTERVAL
         self.pagination_limit = 500
 
-    def itermetrics(self):
+    @staticmethod
+    def gen_metric(name, nb, state):
+        return {
+            'plugin_instance': name,
+            'values': nb,
+            'meta': {
+                'state': state,
+                'discard_hostname': True,
+            }
+        }
 
-        volumes_details = self.get_objects('cinderv2', 'volumes',
-                                           params={'all_tenants': 1},
-                                           detail=True)
+    def itermetrics(self):
 
         def groupby(d):
             return d.get('status', 'unknown').lower()
@@ -50,45 +64,44 @@
         def count_size_bytes(d):
             return d.get('size', 0) * 10 ** 9
 
-        status = self.count_objects_group_by(volumes_details,
-                                             group_by_func=groupby)
-        for s, nb in status.iteritems():
-            yield {
-                'plugin_instance': 'volumes',
-                'values': nb,
-                'meta': {'state': s, 'discard_hostname': True}
-            }
+        vols_details = self.get_objects('cinderv2', 'volumes',
+                                        params={'all_tenants': 1},
+                                        detail=True)
+        vols_status = self.count_objects_group_by(vols_details,
+                                                  group_by_func=groupby)
+        for status in volumes_statuses:
+            nb = vols_status.get(status, 0)
+            yield CinderStatsPlugin.gen_metric('volumes',
+                                               nb,
+                                               status)
 
-        sizes = self.count_objects_group_by(volumes_details,
-                                            group_by_func=groupby,
-                                            count_func=count_size_bytes)
-        for s, size in sizes.iteritems():
-            yield {
-                'plugin_instance': 'volumes_size',
-                'values': size,
-                'meta': {'state': s, 'discard_hostname': True}
-            }
+        vols_sizes = self.count_objects_group_by(vols_details,
+                                                 group_by_func=groupby,
+                                                 count_func=count_size_bytes)
+        for status in volumes_statuses:
+            nb = vols_sizes.get(status, 0)
+            yield CinderStatsPlugin.gen_metric('volumes_size',
+                                               nb,
+                                               status)
 
         snaps_details = self.get_objects('cinderv2', 'snapshots',
                                          params={'all_tenants': 1})
-        status_snaps = self.count_objects_group_by(snaps_details,
+        snaps_status = self.count_objects_group_by(snaps_details,
                                                    group_by_func=groupby)
-        for s, nb in status_snaps.iteritems():
-            yield {
-                'plugin_instance': 'snapshots',
-                'values': nb,
-                'meta': {'state': s, 'discard_hostname': True}
-            }
+        for status in snapshots_statuses:
+            nb = snaps_status.get(status, 0)
+            yield CinderStatsPlugin.gen_metric('snapshots',
+                                               nb,
+                                               status)
 
-        sizes = self.count_objects_group_by(snaps_details,
-                                            group_by_func=groupby,
-                                            count_func=count_size_bytes)
-        for n, size in sizes.iteritems():
-            yield {
-                'plugin_instance': 'snapshots_size',
-                'values': size,
-                'meta': {'state': s, 'discard_hostname': True}
-            }
+        snaps_sizes = self.count_objects_group_by(snaps_details,
+                                                  group_by_func=groupby,
+                                                  count_func=count_size_bytes)
+        for status in snapshots_statuses:
+            nb = snaps_sizes.get(status, 0)
+            yield CinderStatsPlugin.gen_metric('snapshots_size',
+                                               nb,
+                                               status)
 
 
 plugin = CinderStatsPlugin(collectd, PLUGIN_NAME, disable_check_metric=True)
@@ -105,6 +118,7 @@
 def read_callback():
     plugin.conditional_read_callback()
 
+
 if __name__ == '__main__':
     import time
     collectd.load_configuration(plugin)
diff --git a/collectd/files/plugin/openstack_glance.py b/collectd/files/plugin/openstack_glance.py
index efb94be..efd4f72 100644
--- a/collectd/files/plugin/openstack_glance.py
+++ b/collectd/files/plugin/openstack_glance.py
@@ -23,6 +23,10 @@
 
 PLUGIN_NAME = 'openstack_glance'
 INTERVAL = openstack.INTERVAL
+image_types = ('snapshots', 'images')
+visibilities = ('public', 'private', 'community', 'shared')
+statuses = ('active', 'queued', 'saving', 'killed', 'deleted',
+            'deactivated', 'pending_delete')
 
 
 class GlanceStatsPlugin(openstack.CollectdPlugin):
@@ -38,20 +42,19 @@
         self.interval = INTERVAL
         self.pagination_limit = 25
 
-    def itermetrics(self):
+    @staticmethod
+    def gen_metric(name, nb, visibility, state):
+        return {
+            'plugin_instance': name,
+            'values': nb,
+            'meta': {
+                'visibility': visibility,
+                'state': state,
+                'discard_hostname': True,
+            }
+        }
 
-        def default_metrics(suffix=''):
-            ret = {}
-            for name in ('snapshots', 'images'):
-                for visibility in ('public', 'private',
-                                   'community', 'shared'):
-                    for status in ('active', 'queued', 'saving',
-                                   'killed', 'deleted', 'deactivated',
-                                   'pending_delete'):
-                        key = '%s%s.%s.%s' % (name, suffix,
-                                              visibility, status)
-                        ret[key] = 0
-            return ret
+    def itermetrics(self):
 
         def is_snap(d):
             return d.get('image_type') == 'snapshot'
@@ -67,18 +70,19 @@
                                           api_version='v2',
                                           params={},
                                           detail=False)
-        status = self.count_objects_group_by(images_details,
-                                             group_by_func=groupby)
-        if len(status) == 0:
-            status = default_metrics()
-        for s, nb in status.iteritems():
-            (name, visibility, state) = s.split('.')
-            yield {
-                'plugin_instance': name,
-                'values': nb,
-                'meta': {'visibility': visibility, 'state': state,
-                         'discard_hostname': True}
-            }
+        img_status = self.count_objects_group_by(images_details,
+                                                 group_by_func=groupby)
+        for name in image_types:
+            for visibility in visibilities:
+                for status in statuses:
+                    nb = img_status.get('{}.{}.{}'.format(name,
+                                                          visibility,
+                                                          status),
+                                        0)
+                    yield GlanceStatsPlugin.gen_metric(name,
+                                                       nb,
+                                                       visibility,
+                                                       status)
 
         # sizes
         def count_size_bytes(d):
@@ -91,19 +95,21 @@
                 return 'snapshots_size.%s.%s' % (p, status)
             return 'images_size.%s.%s' % (p, status)
 
-        sizes = self.count_objects_group_by(images_details,
-                                            group_by_func=groupby_size,
-                                            count_func=count_size_bytes)
-        if len(sizes) == 0:
-            sizes = default_metrics('_size')
-        for s, nb in sizes.iteritems():
-            (name, visibility, state) = s.split('.')
-            yield {
-                'plugin_instance': name,
-                'values': nb,
-                'meta': {'visibility': visibility, 'state': state,
-                         'discard_hostname': True},
-            }
+        img_sizes = self.count_objects_group_by(images_details,
+                                                group_by_func=groupby_size,
+                                                count_func=count_size_bytes)
+        for name in image_types:
+            for visibility in visibilities:
+                for status in statuses:
+                    nb = img_sizes.get('{}_size.{}.{}'.format(name,
+                                                              visibility,
+                                                              status),
+                                       0)
+                    yield GlanceStatsPlugin.gen_metric('{}_size'.format(name),
+                                                       nb,
+                                                       visibility,
+                                                       status)
+
 
 plugin = GlanceStatsPlugin(collectd, PLUGIN_NAME, disable_check_metric=True)
 
@@ -119,6 +125,7 @@
 def read_callback():
     plugin.conditional_read_callback()
 
+
 if __name__ == '__main__':
     import time
     collectd.load_configuration(plugin)
diff --git a/collectd/files/plugin/openstack_neutron.py b/collectd/files/plugin/openstack_neutron.py
index 1d147c7..a297b94 100644
--- a/collectd/files/plugin/openstack_neutron.py
+++ b/collectd/files/plugin/openstack_neutron.py
@@ -75,7 +75,7 @@
         yield {
             'plugin_instance': 'networks',
             'type_instance': 'total',
-            'values': len(status),
+            'values': len(networks),
             'meta': {'discard_hostname': True},
         }
 
diff --git a/collectd/map.jinja b/collectd/map.jinja
index 1077513..650c640 100644
--- a/collectd/map.jinja
+++ b/collectd/map.jinja
@@ -51,3 +51,15 @@
         'automatic_starting': True,
     }
 }, merge=salt['pillar.get']('collectd:remote_client')) %}
+
+{%- set service_grains = {'collectd': {'remote_plugin': {}, 'local_plugin': {}}} %}
+{%- for service_name, service in pillar.items() %}
+  {%- if service.get('_support', {}).get('collectd', {}).get('enabled', False) %}
+    {%- set grains_fragment_file = service_name+'/meta/collectd.yml' %}
+    {%- macro load_grains_file() %}{% include grains_fragment_file ignore missing %}{% endmacro %}
+    {%- set grains_yaml = load_grains_file()|load_yaml %}
+    {%- if grains_yaml is mapping %}
+      {%- set service_grains = salt['grains.filter_by']({'default': service_grains}, merge={'collectd': grains_yaml}) %}
+    {%- endif %}
+  {%- endif %}
+{%- endfor %}
diff --git a/collectd/meta/salt.yml b/collectd/meta/salt.yml
new file mode 100644
index 0000000..30f0f1f
--- /dev/null
+++ b/collectd/meta/salt.yml
@@ -0,0 +1,6 @@
+grain:
+  {%- if pillar.collectd.client is defined %}
+  {%- from "collectd/map.jinja" import service_grains with context -%}
+  collectd:
+    {{ service_grains|yaml(False)|indent(4) }}
+  {%- endif %}
diff --git a/tests/pillar/client.sls b/tests/pillar/client.sls
index b970e72..93bd7ad 100644
--- a/tests/pillar/client.sls
+++ b/tests/pillar/client.sls
@@ -1,4 +1,3 @@
-
 collectd:
   client:
     enabled: true
@@ -8,4 +7,7 @@
         engine: carbon
         host: 127.0.0.1
         port: 2023
-
+linux:
+  system:
+    name: hostname
+    domain: domain