collecty: Update to version 004
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
This commit is contained in:
parent
65c47a0107
commit
5fc517c0e5
|
@ -4,8 +4,8 @@
|
|||
###############################################################################
|
||||
|
||||
name = collecty
|
||||
version = 003
|
||||
release = 4
|
||||
version = 004
|
||||
release = 1
|
||||
|
||||
maintainer = Michael Tremer <michael.tremer@ipfire.org>
|
||||
groups = System/Monitoring
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
From a9af411f0703eac939e0df5d5f75b46d35f531bc Mon Sep 17 00:00:00 2001
|
||||
From: Michael Tremer <michael.tremer@ipfire.org>
|
||||
Date: Mon, 29 Jun 2015 20:44:18 +0000
|
||||
Subject: [PATCH 1/2] plugins: Automatically replace None by NaN
|
||||
|
||||
rrdtool uses NaN to represent no value. Python uses None.
|
||||
This patch automatically translates from None to NaN.
|
||||
|
||||
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
|
||||
---
|
||||
src/collecty/plugins/base.py | 22 ++++++++++++++++++++--
|
||||
1 file changed, 20 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/src/collecty/plugins/base.py b/src/collecty/plugins/base.py
|
||||
index bed461f..cf9c3b4 100644
|
||||
--- a/src/collecty/plugins/base.py
|
||||
+++ b/src/collecty/plugins/base.py
|
||||
@@ -147,8 +147,7 @@ class Plugin(object, metaclass=PluginRegistration):
|
||||
try:
|
||||
result = o.collect()
|
||||
|
||||
- if isinstance(result, tuple) or isinstance(result, list):
|
||||
- result = ":".join(("%s" % e for e in result))
|
||||
+ result = self._format_result(result)
|
||||
except:
|
||||
self.log.warning(_("Unhandled exception in %s.collect()") % o, exc_info=True)
|
||||
continue
|
||||
@@ -170,6 +169,25 @@ class Plugin(object, metaclass=PluginRegistration):
|
||||
if delay >= 60:
|
||||
self.log.warning(_("A worker thread was stalled for %.4fs") % delay)
|
||||
|
||||
+ @staticmethod
|
||||
+ def _format_result(result):
|
||||
+ if not isinstance(result, tuple) and not isinstance(result, list):
|
||||
+ return result
|
||||
+
|
||||
+ # Replace all Nones by NaN
|
||||
+ s = []
|
||||
+
|
||||
+ for e in result:
|
||||
+ if e is None:
|
||||
+ e = "NaN"
|
||||
+
|
||||
+ # Format as string
|
||||
+ e = "%s" % e
|
||||
+
|
||||
+ s.append(e)
|
||||
+
|
||||
+ return ":".join(s)
|
||||
+
|
||||
def get_object(self, id):
|
||||
for object in self.objects:
|
||||
if not object.id == id:
|
||||
--
|
||||
1.8.1
|
||||
|
|
@ -1,995 +0,0 @@
|
|||
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
|
||||
|
Loading…
Reference in New Issue