]> git.ipfire.org Git - people/amarx/ipfire-3.x.git/blame - collecty/patches/0002-latency-Rewrite-latency-module.patch
collecty: Import upstream changes
[people/amarx/ipfire-3.x.git] / collecty / patches / 0002-latency-Rewrite-latency-module.patch
CommitLineData
37835947
MT
1From 63f9f8beed445a80dcb492570b105c5b50e65a59 Mon Sep 17 00:00:00 2001
2From: Michael Tremer <michael.tremer@ipfire.org>
3Date: Mon, 29 Jun 2015 20:49:02 +0000
4Subject: [PATCH 2/2] latency: Rewrite latency module
5
6This patch replaces the builtin python implementation
7that pinged hosts by a Python C module that uses liboping.
8
9liboping is able to ping IPv6 hosts as well and should
10implement the ICMP protocol more appropriately.
11
12The graph has been extended so that hosts will have a
13line for latency over IPv6 and IPv4 if available and
14the packet loss is merged from both protocols, too.
15
16Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
17---
18 Makefile.am | 5 +-
19 configure.ac | 2 +
20 po/POTFILES.in | 1 -
21 src/_collectymodule.c | 338 ++++++++++++++++++++++++++++++++++++++++
22 src/collecty/ping.py | 324 --------------------------------------
23 src/collecty/plugins/latency.py | 151 +++++++++++-------
24 6 files changed, 437 insertions(+), 384 deletions(-)
25 delete mode 100644 src/collecty/ping.py
26
27diff --git a/Makefile.am b/Makefile.am
28index fe00da7..0b7e299 100644
29--- a/Makefile.am
30+++ b/Makefile.am
31@@ -79,8 +79,7 @@ collecty_PYTHON = \
32 src/collecty/daemon.py \
33 src/collecty/errors.py \
34 src/collecty/i18n.py \
35- src/collecty/logger.py \
36- src/collecty/ping.py
37+ src/collecty/logger.py
38
39 collectydir = $(pythondir)/collecty
40
41@@ -109,6 +108,7 @@ _collecty_la_SOURCES = \
42 _collecty_la_CFLAGS = \
43 $(AM_CFLAGS) \
44 $(LIBATASMART_CFLAGS) \
45+ $(OPING_CFLAGS) \
46 $(PYTHON_CFLAGS)
47
48 _collecty_la_LDFLAGS = \
49@@ -119,6 +119,7 @@ _collecty_la_LDFLAGS = \
50
51 _collecty_la_LIBADD = \
52 $(LIBATASMART_LIBS) \
53+ $(OPING_LIBS) \
54 $(PYTHON_LIBS) \
55 $(SENSORS_LIBS)
56
57diff --git a/configure.ac b/configure.ac
58index 59250ca..9b540fd 100644
59--- a/configure.ac
60+++ b/configure.ac
61@@ -61,6 +61,8 @@ AC_PROG_GCC_TRADITIONAL
62
63 AC_PATH_PROG([XSLTPROC], [xsltproc])
64
65+PKG_CHECK_MODULES([OPING], [liboping])
66+
67 # Python
68 AM_PATH_PYTHON([3.2])
69 PKG_CHECK_MODULES([PYTHON], [python-${PYTHON_VERSION}])
70diff --git a/po/POTFILES.in b/po/POTFILES.in
71index f6aebb3..a96f7b2 100644
72--- a/po/POTFILES.in
73+++ b/po/POTFILES.in
74@@ -5,7 +5,6 @@ src/collecty/daemon.py
75 src/collecty/errors.py
76 src/collecty/i18n.py
77 src/collecty/__init__.py
78-src/collecty/ping.py
79 src/collecty/plugins/base.py
80 src/collecty/plugins/conntrack.py
81 src/collecty/plugins/cpu.py
82diff --git a/src/_collectymodule.c b/src/_collectymodule.c
83index 422c27d..c13ca69 100644
84--- a/src/_collectymodule.c
85+++ b/src/_collectymodule.c
86@@ -22,15 +22,21 @@
87 #include <errno.h>
88 #include <fcntl.h>
89 #include <linux/hdreg.h>
90+#include <oping.h>
91 #include <sensors/error.h>
92 #include <sensors/sensors.h>
93 #include <stdbool.h>
94 #include <string.h>
95 #include <sys/ioctl.h>
96+#include <time.h>
97
98 #define MODEL_SIZE 40
99 #define SERIAL_SIZE 20
100
101+#define PING_HISTORY_SIZE 1024
102+#define PING_DEFAULT_COUNT 10
103+#define PING_DEFAULT_TIMEOUT 8
104+
105 typedef struct {
106 PyObject_HEAD
107 char* path;
108@@ -313,6 +319,324 @@ static PyTypeObject BlockDeviceType = {
109 BlockDevice_new, /* tp_new */
110 };
111
112+static PyObject* PyExc_PingError;
113+static PyObject* PyExc_PingAddHostError;
114+
115+typedef struct {
116+ PyObject_HEAD
117+ pingobj_t* ping;
118+ const char* host;
119+ struct {
120+ double history[PING_HISTORY_SIZE];
121+ size_t history_index;
122+ size_t history_size;
123+ size_t packets_sent;
124+ size_t packets_rcvd;
125+ double average;
126+ double stddev;
127+ double loss;
128+ } stats;
129+} PingObject;
130+
131+static void Ping_dealloc(PingObject* self) {
132+ if (self->ping)
133+ ping_destroy(self->ping);
134+
135+ Py_TYPE(self)->tp_free((PyObject*)self);
136+}
137+
138+static void Ping_init_stats(PingObject* self) {
139+ self->stats.history_index = 0;
140+ self->stats.history_size = 0;
141+ self->stats.packets_sent = 0;
142+ self->stats.packets_rcvd = 0;
143+
144+ self->stats.average = 0.0;
145+ self->stats.stddev = 0.0;
146+ self->stats.loss = 0.0;
147+}
148+
149+static PyObject* Ping_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
150+ PingObject* self = (PingObject*)type->tp_alloc(type, 0);
151+
152+ if (self) {
153+ self->ping = NULL;
154+ self->host = NULL;
155+
156+ Ping_init_stats(self);
157+ }
158+
159+ return (PyObject*)self;
160+}
161+
162+static int Ping_init(PingObject* self, PyObject* args, PyObject* kwds) {
163+ char* kwlist[] = {"host", "family", "timeout", "ttl", NULL};
164+ int family = PING_DEF_AF;
165+ double timeout = PING_DEFAULT_TIMEOUT;
166+ int ttl = PING_DEF_TTL;
167+
168+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|idi", kwlist, &self->host,
169+ &family, &timeout, &ttl))
170+ return -1;
171+
172+ if (family != AF_UNSPEC && family != AF_INET6 && family != AF_INET) {
173+ PyErr_Format(PyExc_ValueError, "Family must be AF_UNSPEC, AF_INET6, or AF_INET");
174+ return -1;
175+ }
176+
177+ if (timeout < 0) {
178+ PyErr_Format(PyExc_ValueError, "Timeout must be greater than zero");
179+ return -1;
180+ }
181+
182+ if (ttl < 1 || ttl > 255) {
183+ PyErr_Format(PyExc_ValueError, "TTL must be between 1 and 255");
184+ return -1;
185+ }
186+
187+ self->ping = ping_construct();
188+ if (!self->ping) {
189+ return -1;
190+ }
191+
192+ // Set options
193+ int r;
194+
195+ r = ping_setopt(self->ping, PING_OPT_AF, &family);
196+ if (r) {
197+ PyErr_Format(PyExc_RuntimeError, "Could not set address family: %s",
198+ ping_get_error(self->ping));
199+ return -1;
200+ }
201+
202+ if (timeout > 0) {
203+ r = ping_setopt(self->ping, PING_OPT_TIMEOUT, &timeout);
204+
205+ if (r) {
206+ PyErr_Format(PyExc_RuntimeError, "Could not set timeout: %s",
207+ ping_get_error(self->ping));
208+ return -1;
209+ }
210+ }
211+
212+ r = ping_setopt(self->ping, PING_OPT_TTL, &ttl);
213+ if (r) {
214+ PyErr_Format(PyExc_RuntimeError, "Could not set TTL: %s",
215+ ping_get_error(self->ping));
216+ return -1;
217+ }
218+
219+ return 0;
220+}
221+
222+static double Ping_compute_average(PingObject* self) {
223+ assert(self->stats.packets_rcvd > 0);
224+
225+ double total_latency = 0.0;
226+
227+ for (int i = 0; i < self->stats.history_size; i++) {
228+ if (self->stats.history[i] > 0)
229+ total_latency += self->stats.history[i];
230+ }
231+
232+ return total_latency / self->stats.packets_rcvd;
233+}
234+
235+static double Ping_compute_stddev(PingObject* self, double mean) {
236+ assert(self->stats.packets_rcvd > 0);
237+
238+ double deviation = 0.0;
239+
240+ for (int i = 0; i < self->stats.history_size; i++) {
241+ if (self->stats.history[i] > 0) {
242+ deviation += pow(self->stats.history[i] - mean, 2);
243+ }
244+ }
245+
246+ // Normalise
247+ deviation /= self->stats.packets_rcvd;
248+
249+ return sqrt(deviation);
250+}
251+
252+static void Ping_compute_stats(PingObject* self) {
253+ // Compute the average latency
254+ self->stats.average = Ping_compute_average(self);
255+
256+ // Compute the standard deviation
257+ self->stats.stddev = Ping_compute_stddev(self, self->stats.average);
258+
259+ // Compute lost packets
260+ self->stats.loss = 1.0;
261+ self->stats.loss -= (double)self->stats.packets_rcvd \
262+ / (double)self->stats.packets_sent;
263+}
264+
265+static double time_elapsed(struct timeval* t0) {
266+ struct timeval now;
267+ gettimeofday(&now, NULL);
268+
269+ double r = now.tv_sec - t0->tv_sec;
270+ r += ((double)now.tv_usec / 1000000) - ((double)t0->tv_usec / 1000000);
271+
272+ return r;
273+}
274+
275+static PyObject* Ping_ping(PingObject* self, PyObject* args, PyObject* kwds) {
276+ char* kwlist[] = {"count", "deadline", NULL};
277+ size_t count = PING_DEFAULT_COUNT;
278+ double deadline = 0;
279+
280+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Id", kwlist, &count, &deadline))
281+ return NULL;
282+
283+ int r = ping_host_add(self->ping, self->host);
284+ if (r) {
285+ PyErr_Format(PyExc_PingAddHostError, "Could not add host %s: %s",
286+ self->host, ping_get_error(self->ping));
287+ return NULL;
288+ }
289+
290+ // Reset all collected statistics in case ping() is called more than once.
291+ Ping_init_stats(self);
292+
293+ // Save start time
294+ struct timeval time_start;
295+ r = gettimeofday(&time_start, NULL);
296+ if (r) {
297+ PyErr_Format(PyExc_RuntimeError, "Could not determine start time");
298+ return NULL;
299+ }
300+
301+ // Do the pinging
302+ while (count--) {
303+ self->stats.packets_sent++;
304+
305+ Py_BEGIN_ALLOW_THREADS
306+ r = ping_send(self->ping);
307+ Py_END_ALLOW_THREADS
308+
309+ // Count recieved packets
310+ if (r >= 0) {
311+ self->stats.packets_rcvd += r;
312+
313+ // Raise any errors
314+ } else {
315+ PyErr_Format(PyExc_RuntimeError, "Error executing ping_send(): %s",
316+ ping_get_error(self->ping));
317+ return NULL;
318+ }
319+
320+ // Extract all data
321+ pingobj_iter_t* iter = ping_iterator_get(self->ping);
322+
323+ double* latency = &self->stats.history[self->stats.history_index];
324+ size_t buffer_size = sizeof(latency);
325+ ping_iterator_get_info(iter, PING_INFO_LATENCY, latency, &buffer_size);
326+
327+ // Increase the history pointer
328+ self->stats.history_index++;
329+ self->stats.history_index %= sizeof(self->stats.history);
330+
331+ // Increase the history size
332+ if (self->stats.history_size < sizeof(self->stats.history))
333+ self->stats.history_size++;
334+
335+ // Check if the deadline is due
336+ if (deadline > 0) {
337+ double elapsed_time = time_elapsed(&time_start);
338+
339+ // If we have run longer than the deadline is, we end the main loop
340+ if (elapsed_time >= deadline)
341+ break;
342+ }
343+ }
344+
345+ if (self->stats.packets_rcvd == 0) {
346+ PyErr_Format(PyExc_PingError, "No replies received");
347+ return NULL;
348+ }
349+
350+ Ping_compute_stats(self);
351+
352+ Py_RETURN_NONE;
353+}
354+
355+static PyObject* Ping_get_packets_sent(PingObject* self) {
356+ return PyLong_FromUnsignedLong(self->stats.packets_sent);
357+}
358+
359+static PyObject* Ping_get_packets_rcvd(PingObject* self) {
360+ return PyLong_FromUnsignedLong(self->stats.packets_rcvd);
361+}
362+
363+static PyObject* Ping_get_average(PingObject* self) {
364+ return PyFloat_FromDouble(self->stats.average);
365+}
366+
367+static PyObject* Ping_get_stddev(PingObject* self) {
368+ return PyFloat_FromDouble(self->stats.stddev);
369+}
370+
371+static PyObject* Ping_get_loss(PingObject* self) {
372+ return PyFloat_FromDouble(self->stats.loss);
373+}
374+
375+static PyGetSetDef Ping_getsetters[] = {
376+ {"average", (getter)Ping_get_average, NULL, NULL, NULL},
377+ {"loss", (getter)Ping_get_loss, NULL, NULL, NULL},
378+ {"stddev", (getter)Ping_get_stddev, NULL, NULL, NULL},
379+ {"packets_sent", (getter)Ping_get_packets_sent, NULL, NULL, NULL},
380+ {"packets_rcvd", (getter)Ping_get_packets_rcvd, NULL, NULL, NULL},
381+ {NULL}
382+};
383+
384+static PyMethodDef Ping_methods[] = {
385+ {"ping", (PyCFunction)Ping_ping, METH_VARARGS|METH_KEYWORDS, NULL},
386+ {NULL}
387+};
388+
389+static PyTypeObject PingType = {
390+ PyVarObject_HEAD_INIT(NULL, 0)
391+ "_collecty.Ping", /*tp_name*/
392+ sizeof(PingObject), /*tp_basicsize*/
393+ 0, /*tp_itemsize*/
394+ (destructor)Ping_dealloc, /*tp_dealloc*/
395+ 0, /*tp_print*/
396+ 0, /*tp_getattr*/
397+ 0, /*tp_setattr*/
398+ 0, /*tp_compare*/
399+ 0, /*tp_repr*/
400+ 0, /*tp_as_number*/
401+ 0, /*tp_as_sequence*/
402+ 0, /*tp_as_mapping*/
403+ 0, /*tp_hash */
404+ 0, /*tp_call*/
405+ 0, /*tp_str*/
406+ 0, /*tp_getattro*/
407+ 0, /*tp_setattro*/
408+ 0, /*tp_as_buffer*/
409+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/
410+ "Ping object", /* tp_doc */
411+ 0, /* tp_traverse */
412+ 0, /* tp_clear */
413+ 0, /* tp_richcompare */
414+ 0, /* tp_weaklistoffset */
415+ 0, /* tp_iter */
416+ 0, /* tp_iternext */
417+ Ping_methods, /* tp_methods */
418+ 0, /* tp_members */
419+ Ping_getsetters, /* tp_getset */
420+ 0, /* tp_base */
421+ 0, /* tp_dict */
422+ 0, /* tp_descr_get */
423+ 0, /* tp_descr_set */
424+ 0, /* tp_dictoffset */
425+ (initproc)Ping_init, /* tp_init */
426+ 0, /* tp_alloc */
427+ Ping_new, /* tp_new */
428+};
429+
430 typedef struct {
431 PyObject_HEAD
432 const sensors_chip_name* chip;
433@@ -743,6 +1067,9 @@ PyMODINIT_FUNC PyInit__collecty(void) {
434 if (PyType_Ready(&BlockDeviceType) < 0)
435 return NULL;
436
437+ if (PyType_Ready(&PingType) < 0)
438+ return NULL;
439+
440 if (PyType_Ready(&SensorType) < 0)
441 return NULL;
442
443@@ -751,6 +1078,17 @@ PyMODINIT_FUNC PyInit__collecty(void) {
444 Py_INCREF(&BlockDeviceType);
445 PyModule_AddObject(m, "BlockDevice", (PyObject*)&BlockDeviceType);
446
447+ Py_INCREF(&PingType);
448+ PyModule_AddObject(m, "Ping", (PyObject*)&PingType);
449+
450+ PyExc_PingError = PyErr_NewException("_collecty.PingError", NULL, NULL);
451+ Py_INCREF(PyExc_PingError);
452+ PyModule_AddObject(m, "PingError", PyExc_PingError);
453+
454+ PyExc_PingAddHostError = PyErr_NewException("_collecty.PingAddHostError", NULL, NULL);
455+ Py_INCREF(PyExc_PingAddHostError);
456+ PyModule_AddObject(m, "PingAddHostError", PyExc_PingAddHostError);
457+
458 Py_INCREF(&SensorType);
459 PyModule_AddObject(m, "Sensor", (PyObject*)&SensorType);
460
461diff --git a/src/collecty/ping.py b/src/collecty/ping.py
462deleted file mode 100644
463index e2d7970..0000000
464--- a/src/collecty/ping.py
465+++ /dev/null
466@@ -1,324 +0,0 @@
467-#!/usr/bin/python3
468-
469-import array
470-import math
471-import os
472-import random
473-import select
474-import socket
475-import struct
476-import sys
477-import time
478-
479-ICMP_TYPE_ECHO_REPLY = 0
480-ICMP_TYPE_ECHO_REQUEST = 8
481-ICMP_MAX_RECV = 2048
482-
483-MAX_SLEEP = 1000
484-
485-class PingError(Exception):
486- msg = None
487-
488-
489-class PingResolveError(PingError):
490- msg = "Could not resolve hostname"
491-
492-
493-class Ping(object):
494- def __init__(self, destination, timeout=1000, packet_size=56):
495- self.destination = self._resolve(destination)
496- self.timeout = timeout
497- self.packet_size = packet_size
498-
499- self.id = os.getpid() & 0xffff # XXX ? Is this a good idea?
500-
501- self.seq_number = 0
502-
503- # Number of sent packets.
504- self.send_count = 0
505-
506- # Save the delay of all responses.
507- self.times = []
508-
509- def run(self, count=None, deadline=None):
510- while True:
511- delay = self.do()
512-
513- self.seq_number += 1
514-
515- if count and self.seq_number >= count:
516- break
517-
518- if deadline and self.total_time >= deadline:
519- break
520-
521- if delay == None:
522- delay = 0
523-
524- if MAX_SLEEP > delay:
525- time.sleep((MAX_SLEEP - delay) / 1000)
526-
527- def do(self):
528- s = None
529- try:
530- # Open a socket for ICMP communication.
531- s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
532-
533- # Send one packet.
534- send_time = self.send_icmp_echo_request(s)
535-
536- # Increase number of sent packets (even if it could not be sent).
537- self.send_count += 1
538-
539- # If the packet could not be sent, we may stop here.
540- if send_time is None:
541- return
542-
543- # Wait for the reply.
544- receive_time, packet_size, ip, ip_header, icmp_header = self.receive_icmp_echo_reply(s)
545-
546- finally:
547- # Close the socket.
548- if s:
549- s.close()
550-
551- # If a packet has been received...
552- if receive_time:
553- delay = (receive_time - send_time) * 1000
554- self.times.append(delay)
555-
556- return delay
557-
558- def send_icmp_echo_request(self, s):
559- # Header is type (8), code (8), checksum (16), id (16), sequence (16)
560- checksum = 0
561-
562- # Create a header with checksum == 0.
563- header = struct.pack("!BBHHH", ICMP_TYPE_ECHO_REQUEST, 0,
564- checksum, self.id, self.seq_number)
565-
566- # Get some bytes for padding.
567- padding = os.urandom(self.packet_size)
568-
569- # Calculate the checksum for header + padding data.
570- checksum = self._calculate_checksum(header + padding)
571-
572- # Rebuild the header with the new checksum.
573- header = struct.pack("!BBHHH", ICMP_TYPE_ECHO_REQUEST, 0,
574- checksum, self.id, self.seq_number)
575-
576- # Build the packet.
577- packet = header + padding
578-
579- # Save the time when the packet has been sent.
580- send_time = time.time()
581-
582- # Send the packet.
583- try:
584- s.sendto(packet, (self.destination, 0))
585- except socket.error as e:
586- if e.errno == 1: # Operation not permitted
587- # The packet could not be sent, probably because of
588- # wrong firewall settings.
589- return
590-
591- return send_time
592-
593- def receive_icmp_echo_reply(self, s):
594- timeout = self.timeout / 1000.0
595-
596- # Wait until the reply packet arrived or until we hit timeout.
597- while True:
598- select_start = time.time()
599-
600- inputready, outputready, exceptready = select.select([s], [], [], timeout)
601- select_duration = (time.time() - select_start)
602-
603- if inputready == []: # Timeout
604- return None, 0, 0, 0, 0
605-
606- # Save the time when the packet has been received.
607- receive_time = time.time()
608-
609- # Read the packet from the socket.
610- packet_data, address = s.recvfrom(ICMP_MAX_RECV)
611-
612- # Parse the ICMP header.
613- icmp_header = self._header2dict(
614- ["type", "code", "checksum", "packet_id", "seq_number"],
615- "!BBHHH", packet_data[20:28]
616- )
617-
618- # This is the reply to our packet if the ID matches.
619- if icmp_header["packet_id"] == self.id:
620- # Parse the IP header.
621- ip_header = self._header2dict(
622- ["version", "type", "length", "id", "flags",
623- "ttl", "protocol", "checksum", "src_ip", "dst_ip"],
624- "!BBHHHBBHII", packet_data[:20]
625- )
626-
627- packet_size = len(packet_data) - 28
628- ip = socket.inet_ntoa(struct.pack("!I", ip_header["src_ip"]))
629-
630- return receive_time, packet_size, ip, ip_header, icmp_header
631-
632- # Check if the timeout has already been hit.
633- timeout = timeout - select_duration
634- if timeout <= 0:
635- return None, 0, 0, 0, 0
636-
637- def _header2dict(self, names, struct_format, data):
638- """
639- Unpack tghe raw received IP and ICMP header informations to a dict
640- """
641- unpacked_data = struct.unpack(struct_format, data)
642- return dict(list(zip(names, unpacked_data)))
643-
644- def _calculate_checksum(self, source_string):
645- if len(source_string) % 2:
646- source_string += "\x00"
647-
648- converted = array.array("H", source_string)
649- if sys.byteorder == "big":
650- converted.byteswap()
651-
652- val = sum(converted)
653-
654- # Truncate val to 32 bits (a variance from ping.c, which uses signed
655- # integers, but overflow is unlinkely in ping).
656- val &= 0xffffffff
657-
658- # Add high 16 bits to low 16 bits.
659- val = (val >> 16) + (val & 0xffff)
660-
661- # Add carry from above (if any).
662- val += (val >> 16)
663-
664- # Invert and truncate to 16 bits.
665- answer = ~val & 0xffff
666-
667- return socket.htons(answer)
668-
669- def _resolve(self, host):
670- """
671- Resolve host.
672- """
673- if self._is_valid_ipv4_address(host):
674- return host
675-
676- try:
677- return socket.gethostbyname(host)
678- except socket.gaierror as e:
679- if e.errno == -3:
680- raise PingResolveError
681-
682- raise
683-
684- def _is_valid_ipv4_address(self, addr):
685- """
686- Check addr to be a valid IPv4 address.
687- """
688- parts = addr.split(".")
689-
690- if not len(parts) == 4:
691- return False
692-
693- for part in parts:
694- try:
695- number = int(part)
696- except ValueError:
697- return False
698-
699- if number > 255:
700- return False
701-
702- return True
703-
704- @property
705- def receive_count(self):
706- """
707- The number of received packets.
708- """
709- return len(self.times)
710-
711- @property
712- def total_time(self):
713- """
714- The total time of all roundtrips.
715- """
716- try:
717- return sum(self.times)
718- except ValueError:
719- return
720-
721- @property
722- def min_time(self):
723- """
724- The smallest roundtrip time.
725- """
726- try:
727- return min(self.times)
728- except ValueError:
729- return
730-
731- @property
732- def max_time(self):
733- """
734- The biggest roundtrip time.
735- """
736- try:
737- return max(self.times)
738- except ValueError:
739- return
740-
741- @property
742- def avg_time(self):
743- """
744- Calculate the average response time.
745- """
746- try:
747- return self.total_time / self.receive_count
748- except ZeroDivisionError:
749- return
750-
751- @property
752- def variance(self):
753- """
754- Calculate the variance of all roundtrips.
755- """
756- if self.avg_time is None:
757- return
758-
759- var = 0
760-
761- for t in self.times:
762- var += (t - self.avg_time) ** 2
763-
764- var /= self.receive_count
765- return var
766-
767- @property
768- def stddev(self):
769- """
770- Standard deviation of all roundtrips.
771- """
772- return math.sqrt(self.variance)
773-
774- @property
775- def loss(self):
776- """
777- Outputs the percentage of dropped packets.
778- """
779- dropped = self.send_count - self.receive_count
780-
781- return dropped / self.send_count
782-
783-
784-if __name__ == "__main__":
785- p = Ping("ping.ipfire.org")
786- p.run(count=5)
787-
788- print("Min/Avg/Max/Stddev: %.2f/%.2f/%.2f/%.2f" % \
789- (p.min_time, p.avg_time, p.max_time, p.stddev))
790- print("Sent/Recv/Loss: %d/%d/%.2f" % (p.send_count, p.receive_count, p.loss))
791diff --git a/src/collecty/plugins/latency.py b/src/collecty/plugins/latency.py
792index df67102..a219240 100644
793--- a/src/collecty/plugins/latency.py
794+++ b/src/collecty/plugins/latency.py
795@@ -19,8 +19,9 @@
796 # #
797 ###############################################################################
798
799-import collecty.ping
800+import socket
801
802+import collecty._collecty
803 from . import base
804
805 from ..i18n import _
806@@ -37,86 +38,124 @@ class GraphTemplateLatency(base.GraphTemplate):
807 @property
808 def rrd_graph(self):
809 return [
810- "DEF:latency=%(file)s:latency:AVERAGE",
811- "DEF:latency_loss=%(file)s:latency_loss:AVERAGE",
812- "DEF:latency_stddev=%(file)s:latency_stddev:AVERAGE",
813-
814- # Compute loss in percentage.
815- "CDEF:latency_ploss=latency_loss,100,*",
816-
817- # Compute standard deviation.
818- "CDEF:stddev1=latency,latency_stddev,+",
819- "CDEF:stddev2=latency,latency_stddev,-",
820-
821- "CDEF:l005=latency_ploss,0,5,LIMIT,UN,UNKN,INF,IF",
822- "CDEF:l010=latency_ploss,5,10,LIMIT,UN,UNKN,INF,IF",
823- "CDEF:l025=latency_ploss,10,25,LIMIT,UN,UNKN,INF,IF",
824- "CDEF:l050=latency_ploss,25,50,LIMIT,UN,UNKN,INF,IF",
825- "CDEF:l100=latency_ploss,50,100,LIMIT,UN,UNKN,INF,IF",
826-
827+ "DEF:latency6=%(file)s:latency6:AVERAGE",
828+ "DEF:loss6=%(file)s:loss6:AVERAGE",
829+ "DEF:stddev6=%(file)s:stddev6:AVERAGE",
830+
831+ "DEF:latency4=%(file)s:latency4:AVERAGE",
832+ "DEF:loss4=%(file)s:loss4:AVERAGE",
833+ "DEF:stddev4=%(file)s:stddev4:AVERAGE",
834+
835+ # Compute the biggest loss and convert into percentage
836+ "CDEF:ploss=loss6,loss4,MAX,100,*",
837+
838+ # Compute standard deviation
839+ "CDEF:stddevarea6=stddev6,2,*",
840+ "CDEF:spacer6=latency6,stddev6,-",
841+ "CDEF:stddevarea4=stddev4,2,*",
842+ "CDEF:spacer4=latency4,stddev4,-",
843+
844+ "CDEF:l005=ploss,0,5,LIMIT,UN,UNKN,INF,IF",
845+ "CDEF:l010=ploss,5,10,LIMIT,UN,UNKN,INF,IF",
846+ "CDEF:l025=ploss,10,25,LIMIT,UN,UNKN,INF,IF",
847+ "CDEF:l050=ploss,25,50,LIMIT,UN,UNKN,INF,IF",
848+ "CDEF:l100=ploss,50,100,LIMIT,UN,UNKN,INF,IF",
849+
850+ "VDEF:latency6min=latency6,MINIMUM",
851+ "VDEF:latency6max=latency6,MAXIMUM",
852+ "VDEF:latency6avg=latency6,AVERAGE",
853+ "VDEF:latency4min=latency4,MINIMUM",
854+ "VDEF:latency4max=latency4,MAXIMUM",
855+ "VDEF:latency4avg=latency4,AVERAGE",
856+
857+ "LINE1:latency6avg#00ff0066:%s" % _("Average latency (IPv6)"),
858+ "LINE1:latency4avg#ff000066:%s\\r" % _("Average latency (IPv4)"),
859+
860+ "COMMENT:%s" % _("Packet Loss"),
861 "AREA:l005#ffffff:%s" % _("0-5%%"),
862- "AREA:l010#000000:%s" % _("5-10%%"),
863- "AREA:l025#ff0000:%s" % _("10-25%%"),
864- "AREA:l050#00ff00:%s" % _("25-50%%"),
865- "AREA:l100#0000ff:%s" % _("50-100%%") + "\\n",
866-
867- "LINE1:stddev1#00660088",
868- "LINE1:stddev2#00660088",
869-
870- "LINE3:latency#ff0000:%s" % _("Latency"),
871- "VDEF:latencymin=latency,MINIMUM",
872- "VDEF:latencymax=latency,MAXIMUM",
873- "VDEF:latencyavg=latency,AVERAGE",
874- "GPRINT:latencymax:%12s\:" % _("Maximum") + " %6.2lf",
875- "GPRINT:latencymin:%12s\:" % _("Minimum") + " %6.2lf",
876- "GPRINT:latencyavg:%12s\:" % _("Average") + " %6.2lf\\n",
877-
878- "LINE1:latencyavg#000000:%s" % _("Average latency"),
879+ "AREA:l010#cccccc:%s" % _("5-10%%"),
880+ "AREA:l025#999999:%s" % _("10-25%%"),
881+ "AREA:l050#666666:%s" % _("25-50%%"),
882+ "AREA:l100#333333:%s" % _("50-100%%") + "\\r",
883+
884+ "COMMENT: \\n", # empty line
885+
886+ "AREA:spacer4",
887+ "AREA:stddevarea4#ff000033:STACK",
888+ "LINE2:latency4#ff0000:%s" % _("Latency (IPv4)"),
889+ "GPRINT:latency4max:%12s\:" % _("Maximum") + " %6.2lf",
890+ "GPRINT:latency4min:%12s\:" % _("Minimum") + " %6.2lf",
891+ "GPRINT:latency4avg:%12s\:" % _("Average") + " %6.2lf\\n",
892+
893+ "AREA:spacer6",
894+ "AREA:stddevarea6#00ff0033:STACK",
895+ "LINE2:latency6#00ff00:%s" % _("Latency (IPv6)"),
896+ "GPRINT:latency6max:%12s\:" % _("Maximum") + " %6.2lf",
897+ "GPRINT:latency6min:%12s\:" % _("Minimum") + " %6.2lf",
898+ "GPRINT:latency6avg:%12s\:" % _("Average") + " %6.2lf\\n",
899 ]
900
901 @property
902 def graph_title(self):
903- return _("Latency to %(host)s")
904+ return _("Latency to %s") % self.object.hostname
905
906 @property
907 def graph_vertical_label(self):
908 return _("Milliseconds")
909
910+ @property
911+ def rrd_graph_args(self):
912+ return [
913+ "--legend-direction=bottomup",
914+ ]
915+
916
917 class LatencyObject(base.Object):
918 rrd_schema = [
919- "DS:latency:GAUGE:0:U",
920- "DS:latency_loss:GAUGE:0:100",
921- "DS:latency_stddev:GAUGE:0:U",
922+ "DS:latency6:GAUGE:0:U",
923+ "DS:stddev6:GAUGE:0:U",
924+ "DS:loss6:GAUGE:0:100",
925+ "DS:latency4:GAUGE:0:U",
926+ "DS:stddev4:GAUGE:0:U",
927+ "DS:loss4:GAUGE:0:100",
928 ]
929
930 def __repr__(self):
931 return "<%s %s>" % (self.__class__.__name__, self.hostname)
932
933- def init(self, hostname, deadline=None):
934+ def init(self, hostname):
935 self.hostname = hostname
936- self.deadline = deadline
937
938 @property
939 def id(self):
940 return self.hostname
941
942 def collect(self):
943- # Send up to five ICMP echo requests.
944- try:
945- ping = collecty.ping.Ping(destination=self.hostname, timeout=20000)
946- ping.run(count=5, deadline=self.deadline)
947+ result = []
948+
949+ for family in (socket.AF_INET6, socket.AF_INET):
950+ try:
951+ p = collecty._collecty.Ping(self.hostname, family=family)
952+ p.ping(count=5, deadline=10)
953+
954+ result += (p.average, p.stddev, p.loss)
955+
956+ except collecty._collecty.PingAddHostError as e:
957+ self.log.debug(_("Could not add host %(host)s for family %(family)s") \
958+ % { "host" : self.hostname, "family" : family })
959
960- except collecty.ping.PingError as e:
961- self.log.warning(_("Could not run latency check for %(host)s: %(msg)s") \
962- % { "host" : self.hostname, "msg" : e.msg })
963- return
964+ # No data available
965+ result += (None, None, None)
966+ continue
967
968- return (
969- "%.10f" % ping.avg_time,
970- "%.10f" % ping.loss,
971- "%.10f" % ping.stddev,
972- )
973+ except collecty._collecty.PingError as e:
974+ self.log.warning(_("Could not run latency check for %(host)s: %(msg)s") \
975+ % { "host" : self.hostname, "msg" : e })
976+
977+ # A hundred percent loss
978+ result += (None, None, 1)
979+
980+ return result
981
982
983 class LatencyPlugin(base.Plugin):
984@@ -127,7 +166,5 @@ class LatencyPlugin(base.Plugin):
985
986 @property
987 def objects(self):
988- deadline = self.interval / len(PING_HOSTS)
989-
990 for hostname in PING_HOSTS:
991- yield LatencyObject(self, hostname, deadline=deadline)
992+ yield LatencyObject(self, hostname)
993--
9941.8.1
995