]> git.ipfire.org Git - collecty.git/commitdiff
latency: Rewrite latency module
authorMichael Tremer <michael.tremer@ipfire.org>
Mon, 29 Jun 2015 20:49:02 +0000 (20:49 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Mon, 29 Jun 2015 20:56:04 +0000 (20:56 +0000)
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
configure.ac
po/POTFILES.in
src/_collectymodule.c
src/collecty/ping.py [deleted file]
src/collecty/plugins/latency.py

index fe00da70d244df941f55b00b03d210f703098ba8..0b7e29910a305f29bb78caaf1bb0825e73835daa 100644 (file)
@@ -79,8 +79,7 @@ collecty_PYTHON = \
        src/collecty/daemon.py \
        src/collecty/errors.py \
        src/collecty/i18n.py \
        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
 
 
 collectydir = $(pythondir)/collecty
 
@@ -109,6 +108,7 @@ _collecty_la_SOURCES = \
 _collecty_la_CFLAGS = \
        $(AM_CFLAGS) \
        $(LIBATASMART_CFLAGS) \
 _collecty_la_CFLAGS = \
        $(AM_CFLAGS) \
        $(LIBATASMART_CFLAGS) \
+       $(OPING_CFLAGS) \
        $(PYTHON_CFLAGS)
 
 _collecty_la_LDFLAGS = \
        $(PYTHON_CFLAGS)
 
 _collecty_la_LDFLAGS = \
@@ -119,6 +119,7 @@ _collecty_la_LDFLAGS = \
 
 _collecty_la_LIBADD = \
        $(LIBATASMART_LIBS) \
 
 _collecty_la_LIBADD = \
        $(LIBATASMART_LIBS) \
+       $(OPING_LIBS) \
        $(PYTHON_LIBS) \
        $(SENSORS_LIBS)
 
        $(PYTHON_LIBS) \
        $(SENSORS_LIBS)
 
index 59250cad8d8aad7be1a41ba43c8f6ca3c369b82a..9b540fd6f15f6f1657afe0c564a8c91eaeedc255 100644 (file)
@@ -61,6 +61,8 @@ AC_PROG_GCC_TRADITIONAL
 
 AC_PATH_PROG([XSLTPROC], [xsltproc])
 
 
 AC_PATH_PROG([XSLTPROC], [xsltproc])
 
+PKG_CHECK_MODULES([OPING], [liboping])
+
 # Python
 AM_PATH_PYTHON([3.2])
 PKG_CHECK_MODULES([PYTHON], [python-${PYTHON_VERSION}])
 # Python
 AM_PATH_PYTHON([3.2])
 PKG_CHECK_MODULES([PYTHON], [python-${PYTHON_VERSION}])
index f6aebb371a988722dac88eb0a0e43c8911e30ca5..a96f7b21868ed70b5c3c3c3c1f8a61331b513c0f 100644 (file)
@@ -5,7 +5,6 @@ src/collecty/daemon.py
 src/collecty/errors.py
 src/collecty/i18n.py
 src/collecty/__init__.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
 src/collecty/plugins/base.py
 src/collecty/plugins/conntrack.py
 src/collecty/plugins/cpu.py
index 422c27dd538e60a86394ed41f37515922aba101c..c13ca69d05595216135ef16eae89f868b0fffa97 100644 (file)
 #include <errno.h>
 #include <fcntl.h>
 #include <linux/hdreg.h>
 #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 <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 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;
 typedef struct {
        PyObject_HEAD
        char* path;
@@ -313,6 +319,324 @@ static PyTypeObject BlockDeviceType = {
        BlockDevice_new,                    /* tp_new */
 };
 
        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;
 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(&BlockDeviceType) < 0)
                return NULL;
 
+       if (PyType_Ready(&PingType) < 0)
+               return NULL;
+
        if (PyType_Ready(&SensorType) < 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(&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);
 
        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 (file)
index e2d7970..0000000
+++ /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))
index df6710216181145252772b0cf4e86c24554e1cc1..a219240fbfd3eb4a955867244d1f7559096ef1e8 100644 (file)
@@ -19,8 +19,9 @@
 #                                                                             #
 ###############################################################################
 
 #                                                                             #
 ###############################################################################
 
-import collecty.ping
+import socket
 
 
+import collecty._collecty
 from . import base
 
 from ..i18n import _
 from . import base
 
 from ..i18n import _
@@ -37,86 +38,124 @@ class GraphTemplateLatency(base.GraphTemplate):
        @property
        def rrd_graph(self):
                return [
        @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: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):
                ]
 
        @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 graph_vertical_label(self):
                return _("Milliseconds")
 
+       @property
+       def rrd_graph_args(self):
+               return [
+                       "--legend-direction=bottomup",
+               ]
+
 
 class LatencyObject(base.Object):
        rrd_schema = [
 
 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 __repr__(self):
                return "<%s %s>" % (self.__class__.__name__, self.hostname)
 
-       def init(self, hostname, deadline=None):
+       def init(self, hostname):
                self.hostname = hostname
                self.hostname = hostname
-               self.deadline = deadline
 
        @property
        def id(self):
                return self.hostname
 
        def collect(self):
 
        @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):
 
 
 class LatencyPlugin(base.Plugin):
@@ -127,7 +166,5 @@ class LatencyPlugin(base.Plugin):
 
        @property
        def objects(self):
 
        @property
        def objects(self):
-               deadline = self.interval / len(PING_HOSTS)
-
                for hostname in PING_HOSTS:
                for hostname in PING_HOSTS:
-                       yield LatencyObject(self, hostname, deadline=deadline)
+                       yield LatencyObject(self, hostname)