From 37835947b2615a735aca6acde4a436c023267052 Mon Sep 17 00:00:00 2001 From: Michael Tremer Date: Tue, 30 Jun 2015 10:46:27 +0000 Subject: [PATCH] collecty: Import upstream changes The latency plugin was rewritten and uses liboping now. Signed-off-by: Michael Tremer --- collecty/collecty.nm | 3 +- ...ns-Automatically-replace-None-by-NaN.patch | 56 + .../0002-latency-Rewrite-latency-module.patch | 995 ++++++++++++++++++ 3 files changed, 1053 insertions(+), 1 deletion(-) create mode 100644 collecty/patches/0001-plugins-Automatically-replace-None-by-NaN.patch create mode 100644 collecty/patches/0002-latency-Rewrite-latency-module.patch diff --git a/collecty/collecty.nm b/collecty/collecty.nm index d1e03187d..ea14b1be7 100644 --- a/collecty/collecty.nm +++ b/collecty/collecty.nm @@ -5,7 +5,7 @@ name = collecty version = 003 -release = 2 +release = 3 maintainer = Michael Tremer 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 index 000000000..772c65d5b --- /dev/null +++ b/collecty/patches/0001-plugins-Automatically-replace-None-by-NaN.patch @@ -0,0 +1,56 @@ +From a9af411f0703eac939e0df5d5f75b46d35f531bc Mon Sep 17 00:00:00 2001 +From: Michael Tremer +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 +--- + 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 index 000000000..837502bdb --- /dev/null +++ b/collecty/patches/0002-latency-Rewrite-latency-module.patch @@ -0,0 +1,995 @@ +From 63f9f8beed445a80dcb492570b105c5b50e65a59 Mon Sep 17 00:00:00 2001 +From: Michael Tremer +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 +--- + 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 + #include + #include ++#include + #include + #include + #include + #include + #include ++#include + + #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 + -- 2.39.2