]> git.ipfire.org Git - collecty.git/blob - src/_collecty/ping.c
latency: Silence "no replies received" when no IPv6 connectivity
[collecty.git] / src / _collecty / ping.c
1 /*
2 * collecty
3 * Copyright (C) 2015 IPFire Team (www.ipfire.org)
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <Python.h>
20
21 #include <errno.h>
22 #include <oping.h>
23 #include <time.h>
24
25 #include "_collectymodule.h"
26
27 static PyGetSetDef Ping_getsetters[] = {
28 {"average", (getter)Ping_get_average, NULL, NULL, NULL},
29 {"loss", (getter)Ping_get_loss, NULL, NULL, NULL},
30 {"stddev", (getter)Ping_get_stddev, NULL, NULL, NULL},
31 {"packets_sent", (getter)Ping_get_packets_sent, NULL, NULL, NULL},
32 {"packets_rcvd", (getter)Ping_get_packets_rcvd, NULL, NULL, NULL},
33 {NULL}
34 };
35
36 static PyMethodDef Ping_methods[] = {
37 {"ping", (PyCFunction)Ping_ping, METH_VARARGS|METH_KEYWORDS, NULL},
38 {NULL}
39 };
40
41 PyTypeObject PingType = {
42 PyVarObject_HEAD_INIT(NULL, 0)
43 "_collecty.Ping", /*tp_name*/
44 sizeof(PingObject), /*tp_basicsize*/
45 0, /*tp_itemsize*/
46 (destructor)Ping_dealloc, /*tp_dealloc*/
47 0, /*tp_print*/
48 0, /*tp_getattr*/
49 0, /*tp_setattr*/
50 0, /*tp_compare*/
51 0, /*tp_repr*/
52 0, /*tp_as_number*/
53 0, /*tp_as_sequence*/
54 0, /*tp_as_mapping*/
55 0, /*tp_hash */
56 0, /*tp_call*/
57 0, /*tp_str*/
58 0, /*tp_getattro*/
59 0, /*tp_setattro*/
60 0, /*tp_as_buffer*/
61 Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags*/
62 "Ping object", /* tp_doc */
63 0, /* tp_traverse */
64 0, /* tp_clear */
65 0, /* tp_richcompare */
66 0, /* tp_weaklistoffset */
67 0, /* tp_iter */
68 0, /* tp_iternext */
69 Ping_methods, /* tp_methods */
70 0, /* tp_members */
71 Ping_getsetters, /* tp_getset */
72 0, /* tp_base */
73 0, /* tp_dict */
74 0, /* tp_descr_get */
75 0, /* tp_descr_set */
76 0, /* tp_dictoffset */
77 (initproc)Ping_init, /* tp_init */
78 0, /* tp_alloc */
79 Ping_new, /* tp_new */
80 };
81
82 void Ping_dealloc(PingObject* self) {
83 if (self->ping)
84 ping_destroy(self->ping);
85
86 Py_TYPE(self)->tp_free((PyObject*)self);
87 }
88
89 void Ping_init_stats(PingObject* self) {
90 self->stats.history_index = 0;
91 self->stats.history_size = 0;
92 self->stats.packets_sent = 0;
93 self->stats.packets_rcvd = 0;
94
95 self->stats.average = 0.0;
96 self->stats.stddev = 0.0;
97 self->stats.loss = 0.0;
98 }
99
100 PyObject* Ping_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
101 PingObject* self = (PingObject*)type->tp_alloc(type, 0);
102
103 if (self) {
104 self->ping = NULL;
105 self->host = NULL;
106
107 Ping_init_stats(self);
108 }
109
110 return (PyObject*)self;
111 }
112
113 int Ping_init(PingObject* self, PyObject* args, PyObject* kwds) {
114 char* kwlist[] = {"host", "family", "timeout", "ttl", NULL};
115 int family = PING_DEF_AF;
116 double timeout = PING_DEFAULT_TIMEOUT;
117 int ttl = PING_DEF_TTL;
118
119 if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|idi", kwlist, &self->host,
120 &family, &timeout, &ttl))
121 return -1;
122
123 if (family != AF_UNSPEC && family != AF_INET6 && family != AF_INET) {
124 PyErr_Format(PyExc_ValueError, "Family must be AF_UNSPEC, AF_INET6, or AF_INET");
125 return -1;
126 }
127
128 if (timeout < 0) {
129 PyErr_Format(PyExc_ValueError, "Timeout must be greater than zero");
130 return -1;
131 }
132
133 if (ttl < 1 || ttl > 255) {
134 PyErr_Format(PyExc_ValueError, "TTL must be between 1 and 255");
135 return -1;
136 }
137
138 self->ping = ping_construct();
139 if (!self->ping) {
140 return -1;
141 }
142
143 // Set options
144 int r;
145
146 r = ping_setopt(self->ping, PING_OPT_AF, &family);
147 if (r) {
148 PyErr_Format(PyExc_RuntimeError, "Could not set address family: %s",
149 ping_get_error(self->ping));
150 return -1;
151 }
152
153 if (timeout > 0) {
154 r = ping_setopt(self->ping, PING_OPT_TIMEOUT, &timeout);
155
156 if (r) {
157 PyErr_Format(PyExc_RuntimeError, "Could not set timeout: %s",
158 ping_get_error(self->ping));
159 return -1;
160 }
161 }
162
163 r = ping_setopt(self->ping, PING_OPT_TTL, &ttl);
164 if (r) {
165 PyErr_Format(PyExc_RuntimeError, "Could not set TTL: %s",
166 ping_get_error(self->ping));
167 return -1;
168 }
169
170 return 0;
171 }
172
173 double Ping_compute_average(PingObject* self) {
174 assert(self->stats.packets_rcvd > 0);
175
176 double total_latency = 0.0;
177
178 for (int i = 0; i < self->stats.history_size; i++) {
179 if (self->stats.history[i] > 0)
180 total_latency += self->stats.history[i];
181 }
182
183 return total_latency / self->stats.packets_rcvd;
184 }
185
186 double Ping_compute_stddev(PingObject* self, double mean) {
187 assert(self->stats.packets_rcvd > 0);
188
189 double deviation = 0.0;
190
191 for (int i = 0; i < self->stats.history_size; i++) {
192 if (self->stats.history[i] > 0) {
193 deviation += pow(self->stats.history[i] - mean, 2);
194 }
195 }
196
197 // Normalise
198 deviation /= self->stats.packets_rcvd;
199
200 return sqrt(deviation);
201 }
202
203 static void Ping_compute_stats(PingObject* self) {
204 // Compute the average latency
205 self->stats.average = Ping_compute_average(self);
206
207 // Compute the standard deviation
208 self->stats.stddev = Ping_compute_stddev(self, self->stats.average);
209
210 // Compute lost packets
211 self->stats.loss = 1.0;
212 self->stats.loss -= (double)self->stats.packets_rcvd \
213 / (double)self->stats.packets_sent;
214 }
215
216 static double time_elapsed(struct timeval* t0) {
217 struct timeval now;
218 gettimeofday(&now, NULL);
219
220 double r = now.tv_sec - t0->tv_sec;
221 r += ((double)now.tv_usec / 1000000) - ((double)t0->tv_usec / 1000000);
222
223 return r;
224 }
225
226 PyObject* Ping_ping(PingObject* self, PyObject* args, PyObject* kwds) {
227 char* kwlist[] = {"count", "deadline", NULL};
228 size_t count = PING_DEFAULT_COUNT;
229 double deadline = 0;
230
231 if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Id", kwlist, &count, &deadline))
232 return NULL;
233
234 int r = ping_host_add(self->ping, self->host);
235 if (r) {
236 PyErr_Format(PyExc_PingAddHostError, "Could not add host %s: %s",
237 self->host, ping_get_error(self->ping));
238 return NULL;
239 }
240
241 // Reset all collected statistics in case ping() is called more than once.
242 Ping_init_stats(self);
243
244 // Save start time
245 struct timeval time_start;
246 r = gettimeofday(&time_start, NULL);
247 if (r) {
248 PyErr_Format(PyExc_RuntimeError, "Could not determine start time");
249 return NULL;
250 }
251
252 // Do the pinging
253 while (count--) {
254 self->stats.packets_sent++;
255
256 Py_BEGIN_ALLOW_THREADS
257 r = ping_send(self->ping);
258 Py_END_ALLOW_THREADS
259
260 // Count recieved packets
261 if (r >= 0) {
262 self->stats.packets_rcvd += r;
263
264 // Raise any errors
265 } else {
266 PyErr_Format(PyExc_RuntimeError, "Error executing ping_send(): %s",
267 ping_get_error(self->ping));
268 return NULL;
269 }
270
271 // Extract all data
272 pingobj_iter_t* iter = ping_iterator_get(self->ping);
273
274 double* latency = &self->stats.history[self->stats.history_index];
275 size_t buffer_size = sizeof(latency);
276 ping_iterator_get_info(iter, PING_INFO_LATENCY, latency, &buffer_size);
277
278 // Increase the history pointer
279 self->stats.history_index++;
280 self->stats.history_index %= sizeof(self->stats.history);
281
282 // Increase the history size
283 if (self->stats.history_size < sizeof(self->stats.history))
284 self->stats.history_size++;
285
286 // Check if the deadline is due
287 if (deadline > 0) {
288 double elapsed_time = time_elapsed(&time_start);
289
290 // If we have run longer than the deadline is, we end the main loop
291 if (elapsed_time >= deadline)
292 break;
293 }
294 }
295
296 if (self->stats.packets_rcvd == 0) {
297 PyErr_Format(PyExc_PingNoReplyError, "No replies received from %s", self->host);
298 return NULL;
299 }
300
301 Ping_compute_stats(self);
302
303 Py_RETURN_NONE;
304 }
305
306 PyObject* Ping_get_packets_sent(PingObject* self) {
307 return PyLong_FromUnsignedLong(self->stats.packets_sent);
308 }
309
310 PyObject* Ping_get_packets_rcvd(PingObject* self) {
311 return PyLong_FromUnsignedLong(self->stats.packets_rcvd);
312 }
313
314 PyObject* Ping_get_average(PingObject* self) {
315 return PyFloat_FromDouble(self->stats.average);
316 }
317
318 PyObject* Ping_get_stddev(PingObject* self) {
319 return PyFloat_FromDouble(self->stats.stddev);
320 }
321
322 PyObject* Ping_get_loss(PingObject* self) {
323 return PyFloat_FromDouble(self->stats.loss);
324 }