--- /dev/null
+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
+