]> git.ipfire.org Git - people/amarx/ipfire-3.x.git/commitdiff
collecty: Import upstream changes
authorMichael Tremer <michael.tremer@ipfire.org>
Tue, 30 Jun 2015 10:46:27 +0000 (10:46 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Tue, 30 Jun 2015 10:46:27 +0000 (10:46 +0000)
The latency plugin was rewritten and uses liboping now.

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
collecty/collecty.nm
collecty/patches/0001-plugins-Automatically-replace-None-by-NaN.patch [new file with mode: 0644]
collecty/patches/0002-latency-Rewrite-latency-module.patch [new file with mode: 0644]

index d1e03187dc56f8af45cf54240ca70d3bf119a811..ea14b1be7d64759f09fb6dd63e3619597ad7ce59 100644 (file)
@@ -5,7 +5,7 @@
 
 name       = collecty
 version    = 003
-release    = 2
+release    = 3
 
 maintainer = Michael Tremer <michael.tremer@ipfire.org>
 groups     = System/Monitoring
@@ -31,6 +31,7 @@ build
                intltool
                libatasmart-devel
                libtool-devel
+               liboping-devel
                libxslt
                lm-sensors-devel
                python3-devel
diff --git a/collecty/patches/0001-plugins-Automatically-replace-None-by-NaN.patch b/collecty/patches/0001-plugins-Automatically-replace-None-by-NaN.patch
new file mode 100644 (file)
index 0000000..772c65d
--- /dev/null
@@ -0,0 +1,56 @@
+From a9af411f0703eac939e0df5d5f75b46d35f531bc Mon Sep 17 00:00:00 2001
+From: Michael Tremer <michael.tremer@ipfire.org>
+Date: Mon, 29 Jun 2015 20:44:18 +0000
+Subject: [PATCH 1/2] plugins: Automatically replace None by NaN
+
+rrdtool uses NaN to represent no value. Python uses None.
+This patch automatically translates from None to NaN.
+
+Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
+---
+ src/collecty/plugins/base.py | 22 ++++++++++++++++++++--
+ 1 file changed, 20 insertions(+), 2 deletions(-)
+
+diff --git a/src/collecty/plugins/base.py b/src/collecty/plugins/base.py
+index bed461f..cf9c3b4 100644
+--- a/src/collecty/plugins/base.py
++++ b/src/collecty/plugins/base.py
+@@ -147,8 +147,7 @@ class Plugin(object, metaclass=PluginRegistration):
+                       try:
+                               result = o.collect()
+-                              if isinstance(result, tuple) or isinstance(result, list):
+-                                      result = ":".join(("%s" % e for e in result))
++                              result = self._format_result(result)
+                       except:
+                               self.log.warning(_("Unhandled exception in %s.collect()") % o, exc_info=True)
+                               continue
+@@ -170,6 +169,25 @@ class Plugin(object, metaclass=PluginRegistration):
+               if delay >= 60:
+                       self.log.warning(_("A worker thread was stalled for %.4fs") % delay)
++      @staticmethod
++      def _format_result(result):
++              if not isinstance(result, tuple) and not isinstance(result, list):
++                      return result
++
++              # Replace all Nones by NaN
++              s = []
++
++              for e in result:
++                      if e is None:
++                              e = "NaN"
++
++                      # Format as string
++                      e = "%s" % e
++
++                      s.append(e)
++
++              return ":".join(s)
++
+       def get_object(self, id):
+               for object in self.objects:
+                       if not object.id == id:
+-- 
+1.8.1
+
diff --git a/collecty/patches/0002-latency-Rewrite-latency-module.patch b/collecty/patches/0002-latency-Rewrite-latency-module.patch
new file mode 100644 (file)
index 0000000..837502b
--- /dev/null
@@ -0,0 +1,995 @@
+From 63f9f8beed445a80dcb492570b105c5b50e65a59 Mon Sep 17 00:00:00 2001
+From: Michael Tremer <michael.tremer@ipfire.org>
+Date: Mon, 29 Jun 2015 20:49:02 +0000
+Subject: [PATCH 2/2] latency: Rewrite latency module
+
+This patch replaces the builtin python implementation
+that pinged hosts by a Python C module that uses liboping.
+
+liboping is able to ping IPv6 hosts as well and should
+implement the ICMP protocol more appropriately.
+
+The graph has been extended so that hosts will have a
+line for latency over IPv6 and IPv4 if available and
+the packet loss is merged from both protocols, too.
+
+Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
+---
+ Makefile.am                     |   5 +-
+ configure.ac                    |   2 +
+ po/POTFILES.in                  |   1 -
+ src/_collectymodule.c           | 338 ++++++++++++++++++++++++++++++++++++++++
+ src/collecty/ping.py            | 324 --------------------------------------
+ src/collecty/plugins/latency.py | 151 +++++++++++-------
+ 6 files changed, 437 insertions(+), 384 deletions(-)
+ delete mode 100644 src/collecty/ping.py
+
+diff --git a/Makefile.am b/Makefile.am
+index fe00da7..0b7e299 100644
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -79,8 +79,7 @@ collecty_PYTHON = \
+       src/collecty/daemon.py \
+       src/collecty/errors.py \
+       src/collecty/i18n.py \
+-      src/collecty/logger.py \
+-      src/collecty/ping.py
++      src/collecty/logger.py
+ collectydir = $(pythondir)/collecty
+@@ -109,6 +108,7 @@ _collecty_la_SOURCES = \
+ _collecty_la_CFLAGS = \
+       $(AM_CFLAGS) \
+       $(LIBATASMART_CFLAGS) \
++      $(OPING_CFLAGS) \
+       $(PYTHON_CFLAGS)
+ _collecty_la_LDFLAGS = \
+@@ -119,6 +119,7 @@ _collecty_la_LDFLAGS = \
+ _collecty_la_LIBADD = \
+       $(LIBATASMART_LIBS) \
++      $(OPING_LIBS) \
+       $(PYTHON_LIBS) \
+       $(SENSORS_LIBS)
+diff --git a/configure.ac b/configure.ac
+index 59250ca..9b540fd 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -61,6 +61,8 @@ AC_PROG_GCC_TRADITIONAL
+ AC_PATH_PROG([XSLTPROC], [xsltproc])
++PKG_CHECK_MODULES([OPING], [liboping])
++
+ # Python
+ AM_PATH_PYTHON([3.2])
+ PKG_CHECK_MODULES([PYTHON], [python-${PYTHON_VERSION}])
+diff --git a/po/POTFILES.in b/po/POTFILES.in
+index f6aebb3..a96f7b2 100644
+--- a/po/POTFILES.in
++++ b/po/POTFILES.in
+@@ -5,7 +5,6 @@ src/collecty/daemon.py
+ src/collecty/errors.py
+ src/collecty/i18n.py
+ src/collecty/__init__.py
+-src/collecty/ping.py
+ src/collecty/plugins/base.py
+ src/collecty/plugins/conntrack.py
+ src/collecty/plugins/cpu.py
+diff --git a/src/_collectymodule.c b/src/_collectymodule.c
+index 422c27d..c13ca69 100644
+--- a/src/_collectymodule.c
++++ b/src/_collectymodule.c
+@@ -22,15 +22,21 @@
+ #include <errno.h>
+ #include <fcntl.h>
+ #include <linux/hdreg.h>
++#include <oping.h>
+ #include <sensors/error.h>
+ #include <sensors/sensors.h>
+ #include <stdbool.h>
+ #include <string.h>
+ #include <sys/ioctl.h>
++#include <time.h>
+ #define MODEL_SIZE  40
+ #define SERIAL_SIZE 20
++#define PING_HISTORY_SIZE 1024
++#define PING_DEFAULT_COUNT 10
++#define PING_DEFAULT_TIMEOUT 8
++
+ typedef struct {
+       PyObject_HEAD
+       char* path;
+@@ -313,6 +319,324 @@ static PyTypeObject BlockDeviceType = {
+       BlockDevice_new,                    /* tp_new */
+ };
++static PyObject* PyExc_PingError;
++static PyObject* PyExc_PingAddHostError;
++
++typedef struct {
++      PyObject_HEAD
++      pingobj_t* ping;
++      const char* host;
++      struct {
++              double history[PING_HISTORY_SIZE];
++              size_t history_index;
++              size_t history_size;
++              size_t packets_sent;
++              size_t packets_rcvd;
++              double average;
++              double stddev;
++              double loss;
++      } stats;
++} PingObject;
++
++static void Ping_dealloc(PingObject* self) {
++      if (self->ping)
++              ping_destroy(self->ping);
++
++      Py_TYPE(self)->tp_free((PyObject*)self);
++}
++
++static void Ping_init_stats(PingObject* self) {
++      self->stats.history_index = 0;
++      self->stats.history_size = 0;
++      self->stats.packets_sent = 0;
++      self->stats.packets_rcvd = 0;
++
++      self->stats.average = 0.0;
++      self->stats.stddev = 0.0;
++      self->stats.loss = 0.0;
++}
++
++static PyObject* Ping_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
++      PingObject* self = (PingObject*)type->tp_alloc(type, 0);
++
++      if (self) {
++              self->ping = NULL;
++              self->host = NULL;
++
++              Ping_init_stats(self);
++      }
++
++      return (PyObject*)self;
++}
++
++static int Ping_init(PingObject* self, PyObject* args, PyObject* kwds) {
++      char* kwlist[] = {"host", "family", "timeout", "ttl", NULL};
++      int family = PING_DEF_AF;
++      double timeout = PING_DEFAULT_TIMEOUT;
++      int ttl = PING_DEF_TTL;
++
++      if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|idi", kwlist, &self->host,
++                      &family, &timeout, &ttl))
++              return -1;
++
++      if (family != AF_UNSPEC && family != AF_INET6 && family != AF_INET) {
++              PyErr_Format(PyExc_ValueError, "Family must be AF_UNSPEC, AF_INET6, or AF_INET");
++              return -1;
++      }
++
++      if (timeout < 0) {
++              PyErr_Format(PyExc_ValueError, "Timeout must be greater than zero");
++              return -1;
++      }
++
++      if (ttl < 1 || ttl > 255) {
++              PyErr_Format(PyExc_ValueError, "TTL must be between 1 and 255");
++              return -1;
++      }
++
++      self->ping = ping_construct();
++      if (!self->ping) {
++              return -1;
++      }
++
++      // Set options
++      int r;
++
++      r = ping_setopt(self->ping, PING_OPT_AF, &family);
++      if (r) {
++              PyErr_Format(PyExc_RuntimeError, "Could not set address family: %s",
++                      ping_get_error(self->ping));
++              return -1;
++      }
++
++      if (timeout > 0) {
++              r = ping_setopt(self->ping, PING_OPT_TIMEOUT, &timeout);
++
++              if (r) {
++                      PyErr_Format(PyExc_RuntimeError, "Could not set timeout: %s",
++                              ping_get_error(self->ping));
++                      return -1;
++              }
++      }
++
++      r = ping_setopt(self->ping, PING_OPT_TTL, &ttl);
++      if (r) {
++              PyErr_Format(PyExc_RuntimeError, "Could not set TTL: %s",
++                      ping_get_error(self->ping));
++              return -1;
++      }
++
++      return 0;
++}
++
++static double Ping_compute_average(PingObject* self) {
++      assert(self->stats.packets_rcvd > 0);
++
++      double total_latency = 0.0;
++
++      for (int i = 0; i < self->stats.history_size; i++) {
++              if (self->stats.history[i] > 0)
++                      total_latency += self->stats.history[i];
++      }
++
++      return total_latency / self->stats.packets_rcvd;
++}
++
++static double Ping_compute_stddev(PingObject* self, double mean) {
++      assert(self->stats.packets_rcvd > 0);
++
++      double deviation = 0.0;
++
++      for (int i = 0; i < self->stats.history_size; i++) {
++              if (self->stats.history[i] > 0) {
++                      deviation += pow(self->stats.history[i] - mean, 2);
++              }
++      }
++
++      // Normalise
++      deviation /= self->stats.packets_rcvd;
++
++      return sqrt(deviation);
++}
++
++static void Ping_compute_stats(PingObject* self) {
++      // Compute the average latency
++      self->stats.average = Ping_compute_average(self);
++
++      // Compute the standard deviation
++      self->stats.stddev = Ping_compute_stddev(self, self->stats.average);
++
++      // Compute lost packets
++      self->stats.loss = 1.0;
++      self->stats.loss -= (double)self->stats.packets_rcvd \
++              / (double)self->stats.packets_sent;
++}
++
++static double time_elapsed(struct timeval* t0) {
++      struct timeval now;
++      gettimeofday(&now, NULL);
++
++      double r = now.tv_sec - t0->tv_sec;
++      r += ((double)now.tv_usec / 1000000) - ((double)t0->tv_usec / 1000000);
++
++      return r;
++}
++
++static PyObject* Ping_ping(PingObject* self, PyObject* args, PyObject* kwds) {
++      char* kwlist[] = {"count", "deadline", NULL};
++      size_t count = PING_DEFAULT_COUNT;
++      double deadline = 0;
++
++      if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Id", kwlist, &count, &deadline))
++              return NULL;
++
++      int r = ping_host_add(self->ping, self->host);
++      if (r) {
++              PyErr_Format(PyExc_PingAddHostError, "Could not add host %s: %s",
++                      self->host, ping_get_error(self->ping));
++              return NULL;
++      }
++
++      // Reset all collected statistics in case ping() is called more than once.
++      Ping_init_stats(self);
++
++      // Save start time
++      struct timeval time_start;
++      r = gettimeofday(&time_start, NULL);
++      if (r) {
++              PyErr_Format(PyExc_RuntimeError, "Could not determine start time");
++              return NULL;
++      }
++
++      // Do the pinging
++      while (count--) {
++              self->stats.packets_sent++;
++
++              Py_BEGIN_ALLOW_THREADS
++              r = ping_send(self->ping);
++              Py_END_ALLOW_THREADS
++
++              // Count recieved packets
++              if (r >= 0) {
++                      self->stats.packets_rcvd += r;
++
++              // Raise any errors
++              } else {
++                      PyErr_Format(PyExc_RuntimeError, "Error executing ping_send(): %s",
++                              ping_get_error(self->ping));
++                      return NULL;
++              }
++
++              // Extract all data
++              pingobj_iter_t* iter = ping_iterator_get(self->ping);
++
++              double* latency = &self->stats.history[self->stats.history_index];
++              size_t buffer_size = sizeof(latency);
++              ping_iterator_get_info(iter, PING_INFO_LATENCY, latency, &buffer_size);
++
++              // Increase the history pointer
++              self->stats.history_index++;
++              self->stats.history_index %= sizeof(self->stats.history);
++
++              // Increase the history size
++              if (self->stats.history_size < sizeof(self->stats.history))
++                      self->stats.history_size++;
++
++              // Check if the deadline is due
++              if (deadline > 0) {
++                      double elapsed_time = time_elapsed(&time_start);
++
++                      // If we have run longer than the deadline is, we end the main loop
++                      if (elapsed_time >= deadline)
++                              break;
++              }
++      }
++
++      if (self->stats.packets_rcvd == 0) {
++              PyErr_Format(PyExc_PingError, "No replies received");
++              return NULL;
++      }
++
++      Ping_compute_stats(self);
++
++      Py_RETURN_NONE;
++}
++
++static PyObject* Ping_get_packets_sent(PingObject* self) {
++      return PyLong_FromUnsignedLong(self->stats.packets_sent);
++}
++
++static PyObject* Ping_get_packets_rcvd(PingObject* self) {
++      return PyLong_FromUnsignedLong(self->stats.packets_rcvd);
++}
++
++static PyObject* Ping_get_average(PingObject* self) {
++      return PyFloat_FromDouble(self->stats.average);
++}
++
++static PyObject* Ping_get_stddev(PingObject* self) {
++      return PyFloat_FromDouble(self->stats.stddev);
++}
++
++static PyObject* Ping_get_loss(PingObject* self) {
++      return PyFloat_FromDouble(self->stats.loss);
++}
++
++static PyGetSetDef Ping_getsetters[] = {
++      {"average", (getter)Ping_get_average, NULL, NULL, NULL},
++      {"loss", (getter)Ping_get_loss, NULL, NULL, NULL},
++      {"stddev", (getter)Ping_get_stddev, NULL, NULL, NULL},
++      {"packets_sent", (getter)Ping_get_packets_sent, NULL, NULL, NULL},
++      {"packets_rcvd", (getter)Ping_get_packets_rcvd, NULL, NULL, NULL},
++      {NULL}
++};
++
++static PyMethodDef Ping_methods[] = {
++      {"ping", (PyCFunction)Ping_ping, METH_VARARGS|METH_KEYWORDS, NULL},
++      {NULL}
++};
++
++static PyTypeObject PingType = {
++      PyVarObject_HEAD_INIT(NULL, 0)
++      "_collecty.Ping",                   /*tp_name*/
++      sizeof(PingObject),                 /*tp_basicsize*/
++      0,                                  /*tp_itemsize*/
++      (destructor)Ping_dealloc,           /*tp_dealloc*/
++      0,                                  /*tp_print*/
++      0,                                  /*tp_getattr*/
++      0,                                  /*tp_setattr*/
++      0,                                  /*tp_compare*/
++      0,                                  /*tp_repr*/
++      0,                                  /*tp_as_number*/
++      0,                                  /*tp_as_sequence*/
++      0,                                  /*tp_as_mapping*/
++      0,                                  /*tp_hash */
++      0,                                  /*tp_call*/
++      0,                                  /*tp_str*/
++      0,                                  /*tp_getattro*/
++      0,                                  /*tp_setattro*/
++      0,                                  /*tp_as_buffer*/
++      Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/
++      "Ping object",                      /* tp_doc */
++      0,                                          /* tp_traverse */
++      0,                                          /* tp_clear */
++      0,                                          /* tp_richcompare */
++      0,                                          /* tp_weaklistoffset */
++      0,                                          /* tp_iter */
++      0,                                          /* tp_iternext */
++      Ping_methods,                       /* tp_methods */
++      0,                                  /* tp_members */
++      Ping_getsetters,                    /* tp_getset */
++      0,                                  /* tp_base */
++      0,                                  /* tp_dict */
++      0,                                  /* tp_descr_get */
++      0,                                  /* tp_descr_set */
++      0,                                  /* tp_dictoffset */
++      (initproc)Ping_init,                /* tp_init */
++      0,                                  /* tp_alloc */
++      Ping_new,                           /* tp_new */
++};
++
+ typedef struct {
+       PyObject_HEAD
+       const sensors_chip_name* chip;
+@@ -743,6 +1067,9 @@ PyMODINIT_FUNC PyInit__collecty(void) {
+       if (PyType_Ready(&BlockDeviceType) < 0)
+               return NULL;
++      if (PyType_Ready(&PingType) < 0)
++              return NULL;
++
+       if (PyType_Ready(&SensorType) < 0)
+               return NULL;
+@@ -751,6 +1078,17 @@ PyMODINIT_FUNC PyInit__collecty(void) {
+       Py_INCREF(&BlockDeviceType);
+       PyModule_AddObject(m, "BlockDevice", (PyObject*)&BlockDeviceType);
++      Py_INCREF(&PingType);
++      PyModule_AddObject(m, "Ping", (PyObject*)&PingType);
++
++      PyExc_PingError = PyErr_NewException("_collecty.PingError", NULL, NULL);
++      Py_INCREF(PyExc_PingError);
++      PyModule_AddObject(m, "PingError", PyExc_PingError);
++
++      PyExc_PingAddHostError = PyErr_NewException("_collecty.PingAddHostError", NULL, NULL);
++      Py_INCREF(PyExc_PingAddHostError);
++      PyModule_AddObject(m, "PingAddHostError", PyExc_PingAddHostError);
++
+       Py_INCREF(&SensorType);
+       PyModule_AddObject(m, "Sensor", (PyObject*)&SensorType);
+diff --git a/src/collecty/ping.py b/src/collecty/ping.py
+deleted file mode 100644
+index e2d7970..0000000
+--- a/src/collecty/ping.py
++++ /dev/null
+@@ -1,324 +0,0 @@
+-#!/usr/bin/python3
+-
+-import array
+-import math
+-import os
+-import random
+-import select
+-import socket
+-import struct
+-import sys
+-import time
+-
+-ICMP_TYPE_ECHO_REPLY = 0
+-ICMP_TYPE_ECHO_REQUEST = 8
+-ICMP_MAX_RECV = 2048
+-
+-MAX_SLEEP = 1000
+-
+-class PingError(Exception):
+-      msg = None
+-
+-
+-class PingResolveError(PingError):
+-      msg = "Could not resolve hostname"
+-
+-
+-class Ping(object):
+-      def __init__(self, destination, timeout=1000, packet_size=56):
+-              self.destination = self._resolve(destination)
+-              self.timeout = timeout
+-              self.packet_size = packet_size
+-
+-              self.id = os.getpid() & 0xffff # XXX ? Is this a good idea?
+-
+-              self.seq_number = 0
+-
+-              # Number of sent packets.
+-              self.send_count = 0
+-
+-              # Save the delay of all responses.
+-              self.times = []
+-
+-      def run(self, count=None, deadline=None):
+-              while True:
+-                      delay = self.do()
+-
+-                      self.seq_number += 1
+-
+-                      if count and self.seq_number >= count:
+-                              break
+-
+-                      if deadline and self.total_time >= deadline:
+-                              break
+-
+-                      if delay == None:
+-                              delay = 0
+-
+-                      if MAX_SLEEP > delay:
+-                              time.sleep((MAX_SLEEP - delay) / 1000)
+-
+-      def do(self):
+-              s = None
+-              try:
+-                      # Open a socket for ICMP communication.
+-                      s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
+-
+-                      # Send one packet.
+-                      send_time = self.send_icmp_echo_request(s)
+-
+-                      # Increase number of sent packets (even if it could not be sent).
+-                      self.send_count += 1
+-
+-                      # If the packet could not be sent, we may stop here.
+-                      if send_time is None:
+-                              return
+-
+-                      # Wait for the reply.
+-                      receive_time, packet_size, ip, ip_header, icmp_header = self.receive_icmp_echo_reply(s)
+-
+-              finally:
+-                      # Close the socket.
+-                      if s:
+-                              s.close()
+-
+-              # If a packet has been received...
+-              if receive_time:
+-                      delay = (receive_time - send_time) * 1000
+-                      self.times.append(delay)
+-
+-                      return delay
+-
+-      def send_icmp_echo_request(self, s):
+-              # Header is type (8), code (8), checksum (16), id (16), sequence (16)
+-              checksum = 0
+-
+-              # Create a header with checksum == 0.
+-              header = struct.pack("!BBHHH", ICMP_TYPE_ECHO_REQUEST, 0,
+-                      checksum, self.id, self.seq_number)
+-
+-              # Get some bytes for padding.
+-              padding = os.urandom(self.packet_size)
+-
+-              # Calculate the checksum for header + padding data.
+-              checksum = self._calculate_checksum(header + padding)
+-
+-              # Rebuild the header with the new checksum.
+-              header = struct.pack("!BBHHH", ICMP_TYPE_ECHO_REQUEST, 0,
+-                      checksum, self.id, self.seq_number)
+-
+-              # Build the packet.
+-              packet = header + padding
+-
+-              # Save the time when the packet has been sent.
+-              send_time = time.time()
+-
+-              # Send the packet.
+-              try:
+-                      s.sendto(packet, (self.destination, 0))
+-              except socket.error as e:
+-                      if e.errno == 1: # Operation not permitted
+-                              # The packet could not be sent, probably because of
+-                              # wrong firewall settings.
+-                              return
+-
+-              return send_time
+-
+-      def receive_icmp_echo_reply(self, s):
+-              timeout = self.timeout / 1000.0
+-
+-              # Wait until the reply packet arrived or until we hit timeout.
+-              while True:
+-                      select_start = time.time()
+-
+-                      inputready, outputready, exceptready = select.select([s], [], [], timeout)
+-                      select_duration = (time.time() - select_start)
+-
+-                      if inputready == []: # Timeout
+-                              return None, 0, 0, 0, 0
+-
+-                      # Save the time when the packet has been received.
+-                      receive_time = time.time()
+-
+-                      # Read the packet from the socket.
+-                      packet_data, address = s.recvfrom(ICMP_MAX_RECV)
+-
+-                      # Parse the ICMP header.
+-                      icmp_header = self._header2dict(
+-                              ["type", "code", "checksum", "packet_id", "seq_number"],
+-                              "!BBHHH", packet_data[20:28]
+-                      )
+-
+-                      # This is the reply to our packet if the ID matches.
+-                      if icmp_header["packet_id"] == self.id:
+-                              # Parse the IP header.
+-                              ip_header = self._header2dict(
+-                                      ["version", "type", "length", "id", "flags",
+-                                      "ttl", "protocol", "checksum", "src_ip", "dst_ip"],
+-                                      "!BBHHHBBHII", packet_data[:20]
+-                              )
+-
+-                              packet_size = len(packet_data) - 28
+-                              ip = socket.inet_ntoa(struct.pack("!I", ip_header["src_ip"]))
+-
+-                              return receive_time, packet_size, ip, ip_header, icmp_header
+-
+-                      # Check if the timeout has already been hit.
+-                      timeout = timeout - select_duration
+-                      if timeout <= 0:
+-                              return None, 0, 0, 0, 0
+-
+-      def _header2dict(self, names, struct_format, data):
+-              """
+-                      Unpack tghe raw received IP and ICMP header informations to a dict
+-              """
+-              unpacked_data = struct.unpack(struct_format, data)
+-              return dict(list(zip(names, unpacked_data)))
+-
+-      def _calculate_checksum(self, source_string):
+-              if len(source_string) % 2:
+-                      source_string += "\x00"
+-
+-              converted = array.array("H", source_string)
+-              if sys.byteorder == "big":
+-                      converted.byteswap()
+-
+-              val = sum(converted)
+-
+-              # Truncate val to 32 bits (a variance from ping.c, which uses signed
+-              # integers, but overflow is unlinkely in ping).
+-              val &= 0xffffffff
+-
+-              # Add high 16 bits to low 16 bits.
+-              val = (val >> 16) + (val & 0xffff)
+-
+-              # Add carry from above (if any).
+-              val += (val >> 16)
+-
+-              # Invert and truncate to 16 bits.
+-              answer = ~val & 0xffff
+-
+-              return socket.htons(answer)
+-
+-      def _resolve(self, host):
+-              """
+-                      Resolve host.
+-              """
+-              if self._is_valid_ipv4_address(host):
+-                      return host
+-
+-              try:
+-                      return socket.gethostbyname(host)
+-              except socket.gaierror as e:
+-                      if e.errno == -3:
+-                              raise PingResolveError
+-
+-                      raise
+-
+-      def _is_valid_ipv4_address(self, addr):
+-              """
+-                      Check addr to be a valid IPv4 address.
+-              """
+-              parts = addr.split(".")
+-
+-              if not len(parts) == 4:
+-                      return False
+-
+-              for part in parts:
+-                      try:
+-                              number = int(part)
+-                      except ValueError:
+-                              return False
+-
+-                      if number > 255:
+-                              return False
+-
+-              return True
+-
+-      @property
+-      def receive_count(self):
+-              """
+-                      The number of received packets.
+-              """
+-              return len(self.times)
+-
+-      @property
+-      def total_time(self):
+-              """
+-                      The total time of all roundtrips.
+-              """
+-              try:
+-                      return sum(self.times)
+-              except ValueError:
+-                      return
+-
+-      @property
+-      def min_time(self):
+-              """
+-                      The smallest roundtrip time.
+-              """
+-              try:
+-                      return min(self.times)
+-              except ValueError:
+-                      return
+-
+-      @property
+-      def max_time(self):
+-              """
+-                      The biggest roundtrip time.
+-              """
+-              try:
+-                      return max(self.times)
+-              except ValueError:
+-                      return
+-
+-      @property
+-      def avg_time(self):
+-              """
+-                      Calculate the average response time.
+-              """
+-              try:
+-                      return self.total_time / self.receive_count
+-              except ZeroDivisionError:
+-                      return
+-
+-      @property
+-      def variance(self):
+-              """
+-                      Calculate the variance of all roundtrips.
+-              """
+-              if self.avg_time is None:
+-                      return
+-
+-              var = 0
+-
+-              for t in self.times:
+-                      var += (t - self.avg_time) ** 2
+-
+-              var /= self.receive_count
+-              return var
+-
+-      @property
+-      def stddev(self):
+-              """
+-                      Standard deviation of all roundtrips.
+-              """
+-              return math.sqrt(self.variance)
+-
+-      @property
+-      def loss(self):
+-              """
+-                      Outputs the percentage of dropped packets.
+-              """
+-              dropped = self.send_count - self.receive_count
+-
+-              return dropped / self.send_count
+-
+-
+-if __name__ == "__main__":
+-      p = Ping("ping.ipfire.org")
+-      p.run(count=5)
+-
+-      print("Min/Avg/Max/Stddev: %.2f/%.2f/%.2f/%.2f" % \
+-              (p.min_time, p.avg_time, p.max_time, p.stddev))
+-      print("Sent/Recv/Loss: %d/%d/%.2f" % (p.send_count, p.receive_count, p.loss))
+diff --git a/src/collecty/plugins/latency.py b/src/collecty/plugins/latency.py
+index df67102..a219240 100644
+--- a/src/collecty/plugins/latency.py
++++ b/src/collecty/plugins/latency.py
+@@ -19,8 +19,9 @@
+ #                                                                             #
+ ###############################################################################
+-import collecty.ping
++import socket
++import collecty._collecty
+ from . import base
+ from ..i18n import _
+@@ -37,86 +38,124 @@ class GraphTemplateLatency(base.GraphTemplate):
+       @property
+       def rrd_graph(self):
+               return [
+-                      "DEF:latency=%(file)s:latency:AVERAGE",
+-                      "DEF:latency_loss=%(file)s:latency_loss:AVERAGE",
+-                      "DEF:latency_stddev=%(file)s:latency_stddev:AVERAGE",
+-
+-                      # Compute loss in percentage.
+-                      "CDEF:latency_ploss=latency_loss,100,*",
+-
+-                      # Compute standard deviation.
+-                      "CDEF:stddev1=latency,latency_stddev,+",
+-                      "CDEF:stddev2=latency,latency_stddev,-",
+-
+-                      "CDEF:l005=latency_ploss,0,5,LIMIT,UN,UNKN,INF,IF",
+-                      "CDEF:l010=latency_ploss,5,10,LIMIT,UN,UNKN,INF,IF",
+-                      "CDEF:l025=latency_ploss,10,25,LIMIT,UN,UNKN,INF,IF",
+-                      "CDEF:l050=latency_ploss,25,50,LIMIT,UN,UNKN,INF,IF",
+-                      "CDEF:l100=latency_ploss,50,100,LIMIT,UN,UNKN,INF,IF",
+-
++                      "DEF:latency6=%(file)s:latency6:AVERAGE",
++                      "DEF:loss6=%(file)s:loss6:AVERAGE",
++                      "DEF:stddev6=%(file)s:stddev6:AVERAGE",
++
++                      "DEF:latency4=%(file)s:latency4:AVERAGE",
++                      "DEF:loss4=%(file)s:loss4:AVERAGE",
++                      "DEF:stddev4=%(file)s:stddev4:AVERAGE",
++
++                      # Compute the biggest loss and convert into percentage
++                      "CDEF:ploss=loss6,loss4,MAX,100,*",
++
++                      # Compute standard deviation
++                      "CDEF:stddevarea6=stddev6,2,*",
++                      "CDEF:spacer6=latency6,stddev6,-",
++                      "CDEF:stddevarea4=stddev4,2,*",
++                      "CDEF:spacer4=latency4,stddev4,-",
++
++                      "CDEF:l005=ploss,0,5,LIMIT,UN,UNKN,INF,IF",
++                      "CDEF:l010=ploss,5,10,LIMIT,UN,UNKN,INF,IF",
++                      "CDEF:l025=ploss,10,25,LIMIT,UN,UNKN,INF,IF",
++                      "CDEF:l050=ploss,25,50,LIMIT,UN,UNKN,INF,IF",
++                      "CDEF:l100=ploss,50,100,LIMIT,UN,UNKN,INF,IF",
++
++                      "VDEF:latency6min=latency6,MINIMUM",
++                      "VDEF:latency6max=latency6,MAXIMUM",
++                      "VDEF:latency6avg=latency6,AVERAGE",
++                      "VDEF:latency4min=latency4,MINIMUM",
++                      "VDEF:latency4max=latency4,MAXIMUM",
++                      "VDEF:latency4avg=latency4,AVERAGE",
++
++                      "LINE1:latency6avg#00ff0066:%s" % _("Average latency (IPv6)"),
++                      "LINE1:latency4avg#ff000066:%s\\r" % _("Average latency (IPv4)"),
++
++                      "COMMENT:%s" % _("Packet Loss"),
+                       "AREA:l005#ffffff:%s" % _("0-5%%"),
+-                      "AREA:l010#000000:%s" % _("5-10%%"),
+-                      "AREA:l025#ff0000:%s" % _("10-25%%"),
+-                      "AREA:l050#00ff00:%s" % _("25-50%%"),
+-                      "AREA:l100#0000ff:%s" % _("50-100%%") + "\\n",
+-
+-                      "LINE1:stddev1#00660088",
+-                      "LINE1:stddev2#00660088",
+-
+-                      "LINE3:latency#ff0000:%s" % _("Latency"),
+-                      "VDEF:latencymin=latency,MINIMUM",
+-                      "VDEF:latencymax=latency,MAXIMUM",
+-                      "VDEF:latencyavg=latency,AVERAGE",
+-                      "GPRINT:latencymax:%12s\:" % _("Maximum") + " %6.2lf",
+-                      "GPRINT:latencymin:%12s\:" % _("Minimum") + " %6.2lf",
+-                      "GPRINT:latencyavg:%12s\:" % _("Average") + " %6.2lf\\n",
+-
+-                      "LINE1:latencyavg#000000:%s" % _("Average latency"),
++                      "AREA:l010#cccccc:%s" % _("5-10%%"),
++                      "AREA:l025#999999:%s" % _("10-25%%"),
++                      "AREA:l050#666666:%s" % _("25-50%%"),
++                      "AREA:l100#333333:%s" % _("50-100%%") + "\\r",
++
++                      "COMMENT: \\n", # empty line
++
++                      "AREA:spacer4",
++                      "AREA:stddevarea4#ff000033:STACK",
++                      "LINE2:latency4#ff0000:%s" % _("Latency (IPv4)"),
++                      "GPRINT:latency4max:%12s\:" % _("Maximum") + " %6.2lf",
++                      "GPRINT:latency4min:%12s\:" % _("Minimum") + " %6.2lf",
++                      "GPRINT:latency4avg:%12s\:" % _("Average") + " %6.2lf\\n",
++
++                      "AREA:spacer6",
++                      "AREA:stddevarea6#00ff0033:STACK",
++                      "LINE2:latency6#00ff00:%s" % _("Latency (IPv6)"),
++                      "GPRINT:latency6max:%12s\:" % _("Maximum") + " %6.2lf",
++                      "GPRINT:latency6min:%12s\:" % _("Minimum") + " %6.2lf",
++                      "GPRINT:latency6avg:%12s\:" % _("Average") + " %6.2lf\\n",
+               ]
+       @property
+       def graph_title(self):
+-              return _("Latency to %(host)s")
++              return _("Latency to %s") % self.object.hostname
+       @property
+       def graph_vertical_label(self):
+               return _("Milliseconds")
++      @property
++      def rrd_graph_args(self):
++              return [
++                      "--legend-direction=bottomup",
++              ]
++
+ class LatencyObject(base.Object):
+       rrd_schema = [
+-              "DS:latency:GAUGE:0:U",
+-              "DS:latency_loss:GAUGE:0:100",
+-              "DS:latency_stddev:GAUGE:0:U",
++              "DS:latency6:GAUGE:0:U",
++              "DS:stddev6:GAUGE:0:U",
++              "DS:loss6:GAUGE:0:100",
++              "DS:latency4:GAUGE:0:U",
++              "DS:stddev4:GAUGE:0:U",
++              "DS:loss4:GAUGE:0:100",
+       ]
+       def __repr__(self):
+               return "<%s %s>" % (self.__class__.__name__, self.hostname)
+-      def init(self, hostname, deadline=None):
++      def init(self, hostname):
+               self.hostname = hostname
+-              self.deadline = deadline
+       @property
+       def id(self):
+               return self.hostname
+       def collect(self):
+-              # Send up to five ICMP echo requests.
+-              try:
+-                      ping = collecty.ping.Ping(destination=self.hostname, timeout=20000)
+-                      ping.run(count=5, deadline=self.deadline)
++              result = []
++
++              for family in (socket.AF_INET6, socket.AF_INET):
++                      try:
++                              p = collecty._collecty.Ping(self.hostname, family=family)
++                              p.ping(count=5, deadline=10)
++
++                              result += (p.average, p.stddev, p.loss)
++
++                      except collecty._collecty.PingAddHostError as e:
++                              self.log.debug(_("Could not add host %(host)s for family %(family)s") \
++                                      % { "host" : self.hostname, "family" : family })
+-              except collecty.ping.PingError as e:
+-                      self.log.warning(_("Could not run latency check for %(host)s: %(msg)s") \
+-                              % { "host" : self.hostname, "msg" : e.msg })
+-                      return
++                              # No data available
++                              result += (None, None, None)
++                              continue
+-              return (
+-                      "%.10f" % ping.avg_time,
+-                      "%.10f" % ping.loss,
+-                      "%.10f" % ping.stddev,
+-              )
++                      except collecty._collecty.PingError as e:
++                              self.log.warning(_("Could not run latency check for %(host)s: %(msg)s") \
++                                      % { "host" : self.hostname, "msg" : e })
++
++                              # A hundred percent loss
++                              result += (None, None, 1)
++
++              return result
+ class LatencyPlugin(base.Plugin):
+@@ -127,7 +166,5 @@ class LatencyPlugin(base.Plugin):
+       @property
+       def objects(self):
+-              deadline = self.interval / len(PING_HOSTS)
+-
+               for hostname in PING_HOSTS:
+-                      yield LatencyObject(self, hostname, deadline=deadline)
++                      yield LatencyObject(self, hostname)
+-- 
+1.8.1
+