Merge "Add plugin Calico Bird"
diff --git a/collectd/files/collectd_http_check.conf b/collectd/files/collectd_http_check.conf
index c9e418e..bb19265 100644
--- a/collectd/files/collectd_http_check.conf
+++ b/collectd/files/collectd_http_check.conf
@@ -11,6 +11,12 @@
   {%- if params.get('expected_content') %}
   ExpectedContent "{{ name }}" "{{ params.expected_content|replace('"','\\"') }}"
   {%- endif %}
+  {%- if params.get('metric_name') %}
+  MetricName "{{ name }}" "{{ params.metric_name }}"
+  {%- endif %}
+  {%- if params.get('discard_hostname') %}
+  DiscardHostname "{{ name }}" "{{ params.discard_hostname }}"
+  {%- endif %}
   {%- if params.verify is defined %}
   Verify "{{ name }}" "{{ params.verify }}"
   {%- endif %}
diff --git a/collectd/files/plugin/check_local_endpoint.py b/collectd/files/plugin/check_local_endpoint.py
index e01715f..dc77f42 100644
--- a/collectd/files/plugin/check_local_endpoint.py
+++ b/collectd/files/plugin/check_local_endpoint.py
@@ -15,10 +15,10 @@
 
 import collectd
 import collectd_base as base
-import http_check
+import collectd_http_check as http_check
 
 
-NAME = 'check_local_endpoint'
+NAME = 'openstack_check_local_api'
 
 
 class CheckLocalEndpoint(http_check.HTTPCheckPlugin):
@@ -27,7 +27,7 @@
         super(CheckLocalEndpoint, self).__init__(*args, **kwargs)
         self.plugin = NAME
 
-plugin = CheckLocalEndpoint(collectd)
+plugin = CheckLocalEndpoint(collectd, disable_check_metric=True)
 
 
 def config_callback(conf):
diff --git a/collectd/files/plugin/collectd_base.py b/collectd/files/plugin/collectd_base.py
index 6643693..36c0060 100644
--- a/collectd/files/plugin/collectd_base.py
+++ b/collectd/files/plugin/collectd_base.py
@@ -122,9 +122,10 @@
         """Iterate over the collected metrics
 
         This class must be implemented by the subclass and should yield dict
-        objects that represent the collected values. Each dict has 6 keys:
+        objects that represent the collected values. Each dict has 7 keys:
             - 'values', a scalar number or a list of numbers if the type
             defines several datasources.
+            - 'plugin' (optional)
             - 'type_instance' (optional)
             - 'plugin_instance' (optional)
             - 'type' (optional, default='gauge')
@@ -153,9 +154,9 @@
                 (self.plugin, type_instance[:24], len(type_instance),
                  self.MAX_IDENTIFIER_LENGTH))
 
-        plugin_instance = metric.get('plugin_instance', self.plugin_instance)
+        plugin_instance = metric.get('plugin_instance') or self.plugin_instance
         v = self.collectd.Values(
-            plugin=self.plugin,
+            plugin=metric.get('plugin') or self.plugin,
             host=metric.get('hostname', ''),
             type=metric.get('type', 'gauge'),
             plugin_instance=plugin_instance,
diff --git a/collectd/files/plugin/collectd_calico_felix.py b/collectd/files/plugin/collectd_calico_felix.py
new file mode 100644
index 0000000..f571796
--- /dev/null
+++ b/collectd/files/plugin/collectd_calico_felix.py
@@ -0,0 +1,157 @@
+#!/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 re
+import requests
+
+import collectd_base as base
+
+
+NAME = 'calico_felix'
+# Default sampling interval
+INTERVAL = 60
+
+
+class CalicoFelixPlugin(base.Base):
+
+    def __init__(self, *args, **kwargs):
+        super(CalicoFelixPlugin, self).__init__(*args, **kwargs)
+        self.plugin = NAME
+        self.session = requests.Session()
+        self.session.mount(
+            'http://',
+            requests.adapters.HTTPAdapter(max_retries=self.max_retries)
+        )
+        self.session.mount(
+            'https://',
+            requests.adapters.HTTPAdapter(max_retries=self.max_retries)
+        )
+        self.url = None
+
+    def config_callback(self, config):
+        super(CalicoFelixPlugin, self).config_callback(config)
+        for node in config.children:
+            self.logger.debug("Got config request for '{}': {}".format(
+                node.key.lower(), node.values[0])
+            )
+            if node.key.lower() == "url":
+                self.url = node.values[0]
+
+    def itermetrics(self):
+        if self.url:
+            self.logger.debug("Requesting URL {}".format(
+                self.url)
+            )
+            try:
+                r = self.session.get(self.url, timeout=self.timeout)
+            except Exception as e:
+                msg = "Got exception for '{}': {}".format(self.url, e)
+                raise base.CheckException(msg)
+
+            if r.status_code != 200:
+                self.logger.error(
+                    ("{} responded with code {} "
+                     "").format(self.url,
+                                r.status_code))
+                raise base.CheckException(
+                    "Failed to gather Calico Felix metrics ({})".format(
+                        r.status_code
+                    )
+                )
+            self.logger.debug(
+                "Got response from {}: '{}'"
+                "".format(self.url, r.text))
+            # Example payload:
+            # # HELP felix_active_local_endpoints Number
+            # # of active endpoints on this host.
+            # # TYPE felix_active_local_endpoints gauge
+            # felix_active_local_endpoints 1
+            # # HELP felix_iptables_chains Number of active iptables chains.
+            # # TYPE felix_iptables_chains gauge
+            # felix_iptables_chains{ip_version="4",table="filter"} 14
+            # felix_iptables_chains{ip_version="4",table="nat"} 6
+            # felix_iptables_chains{ip_version="4",table="raw"} 6
+            # felix_iptables_chains{ip_version="6",table="filter"} 14
+            # felix_iptables_chains{ip_version="6",table="nat"} 6
+            # felix_iptables_chains{ip_version="6",table="raw"} 6
+            # # HELP go_goroutines Number of goroutines that currently exist.
+            # # TYPE go_goroutineqs gauge
+            # go_goroutines 39
+            for l in r.text.split('\n'):
+                # Line is empty or is a comment
+                if not l or l.startswith('#'):
+                    continue
+
+                (name, rval) = l.split()
+                self.logger.debug(
+                    "Got val for {}: '{}'".format(name, rval))
+                # For some metrics, remove the existing felix prefix
+                # to ensure homogeneity
+                if name.startswith('felix_'):
+                    name = name.replace('felix_', '', 1)
+                # Initialization of returned metric
+                ret_metric = {
+                    'values': rval
+                }
+                # Metric can have implicit dimensions. For example:
+                # felix_iptables_rules{ip_version="4",table="filter"}
+                m = re.search(
+                    '^(?P<name>[^{]+)(?:{(?P<dimensions>.[^}]+)})?$',
+                    name)
+                if not m:
+                    self.logger.error(
+                        "Error parsing metric name {}".format(
+                            name))
+                    continue
+
+                if m.group('dimensions'):
+                    name = m.group('name')
+                    meta = {}
+                    for d in m.group('dimensions').split(','):
+                        (k, v) = d.split('=')
+                        meta[k] = v.strip('"')
+                    if len(meta) > 0:
+                        ret_metric['meta'] = meta
+                ret_metric['type_instance'] = m.group('name')
+                yield ret_metric
+
+
+plugin = CalicoFelixPlugin(collectd)
+
+
+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__':
+    collectd.load_configuration(plugin)
+    plugin.read_callback()
+else:
+    collectd.register_config(config_callback)
+    collectd.register_notification(notification_callback)
+    collectd.register_read(read_callback, INTERVAL)
diff --git a/collectd/files/plugin/collectd_http_check.py b/collectd/files/plugin/collectd_http_check.py
index 1ad78c5..897bae7 100644
--- a/collectd/files/plugin/collectd_http_check.py
+++ b/collectd/files/plugin/collectd_http_check.py
@@ -38,6 +38,8 @@
 
         self.timeout = 3
         self.max_retries = 2
+        self.metric_names = {}
+        self.discard_hostnames = {}
 
         self.interval = base.INTERVAL
         self.polling_interval = base.INTERVAL
@@ -57,6 +59,11 @@
         for node in config.children:
             if node.key == "Url":
                 self.urls[node.values[0]] = node.values[1]
+            elif node.key == 'MetricName':
+                self.metric_names[node.values[0]] = node.values[1]
+            elif node.key == 'DiscardHostname':
+                if node.values[1].lower() == 'true':
+                    self.discard_hostnames[node.values[0]] = True
             elif node.key == 'ExpectedCode':
                 self.expected_codes[node.values[0]] = int(node.values[1])
             elif node.key == 'ExpectedContent':
@@ -146,7 +153,14 @@
         for name, url in self.urls.items():
             r = self.check_url(name, url)
             if r:
-                yield {'type_instance': name, 'values': r}
+                meta = {'service': name}
+                if self.discard_hostnames.get(name):
+                    meta['discard_hostname'] = True
+
+                yield {'meta': meta,
+                       'values': r,
+                       'plugin': self.metric_names.get(name),
+                       }
 
 
 plugin = HTTPCheckPlugin(collectd, disable_check_metric=True)
diff --git a/collectd/files/plugin/hypervisor_stats.py b/collectd/files/plugin/hypervisor_stats.py
index 84f735e..7c0d26d 100644
--- a/collectd/files/plugin/hypervisor_stats.py
+++ b/collectd/files/plugin/hypervisor_stats.py
@@ -60,7 +60,7 @@
             for agg in aggregates_list:
                 nova_aggregates[agg['name']] = {
                     'id': agg['id'],
-                    'hosts': agg['hosts'],
+                    'hosts': [h.split('.')[0] for h in agg['hosts']],
                     'metrics': {'free_vcpus': 0},
                 }
                 nova_aggregates[agg['name']]['metrics'].update(
@@ -88,7 +88,7 @@
                 total_stats[v] += m_val
                 for agg in nova_aggregates.keys():
                     agg_hosts = nova_aggregates[agg]['hosts']
-                    if stats['hypervisor_hostname'] in agg_hosts:
+                    if host in agg_hosts:
                         nova_aggregates[agg]['metrics'][v] += m_val
             if 'cpu_ratio' in self.extra_config:
                 m_vcpus = stats.get('vcpus', 0)
@@ -103,7 +103,7 @@
                 total_stats['free_vcpus'] += free
                 for agg in nova_aggregates.keys():
                     agg_hosts = nova_aggregates[agg]['hosts']
-                    if stats['hypervisor_hostname'] in agg_hosts:
+                    if host in agg_hosts:
                         free = ((int(self.extra_config['cpu_ratio'] *
                                      m_vcpus)) -
                                 m_vcpus_used)