1 /*
2  *   This program is is free software; you can redistribute it and/or modify
3  *   it under the terms of the GNU General Public License as published by
4  *   the Free Software Foundation; either version 2 of the License, or (at
5  *   your option) any later version.
6  *
7  *   This program is distributed in the hope that it will be useful,
8  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
9  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  *   GNU General Public License for more details.
11  *
12  *   You should have received a copy of the GNU General Public License
13  *   along with this program; if not, write to the Free Software
14  *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15  */
16 
17 /**
18  * $Id: 77f0db091549300b963b199d3cb3aafc2b663d55 $
19  * @file collectd.c
20  * @brief Helper functions to enabled radsniff to talk to collectd
21  *
22  * @copyright 2013 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
23  */
24 #include <assert.h>
25 #include <ctype.h>
26 
27 #ifdef HAVE_COLLECTDC_H
28 #include <collectd/client.h>
29 #include <freeradius-devel/radsniff.h>
30 
31 /** Copy a 64bit unsigned integer into a double
32  *
33  */
34 /*
35 static void _copy_uint64_to_double(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl)
36 {
37 	assert(tmpl->src);
38 	assert(tmpl->dst);
39 
40 	*((double *) tmpl->dst) = *((uint64_t *) tmpl->src);
41 }
42 */
43 
44 /*
45 static void _copy_uint64_to_uint64(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl)
46 {
47 	assert(tmpl->src);
48 	assert(tmpl->dst);
49 
50 	*((uint64_t *) tmpl->dst) = *((uint64_t *) tmpl->src);
51 }
52 */
53 
_copy_double_to_double(UNUSED rs_t * conf,rs_stats_value_tmpl_t * tmpl)54 static void _copy_double_to_double(UNUSED rs_t *conf, rs_stats_value_tmpl_t *tmpl)
55 {
56 	assert(tmpl->src);
57 	assert(tmpl->dst);
58 
59 	*((double *) tmpl->dst) = *((double*) tmpl->src);
60 }
61 
62 
63 /** Allocates a stats template which describes a single guage/counter
64  *
65  * This is just intended to simplify allocating a fairly complex memory structure
66  * src and dst pointers must be set
67  *
68  * @param ctx Context to allocate collectd struct in.
69  * @param conf Radsniff configuration.
70  * @param plugin_instance usually the type of packet (in our case).
71  * @param type string, the name of a collection of stats e.g. exchange
72  * @param type_instance the name of the counter/guage within the collection e.g. latency.
73  * @param stats structure to derive statistics from.
74  * @param values Value templates used to populate lcc_value_list.
75  * @return a new rs_stats_tmpl_t on success or NULL on failure.
76  */
rs_stats_collectd_init(TALLOC_CTX * ctx,rs_t * conf,char const * plugin_instance,char const * type,char const * type_instance,void * stats,rs_stats_value_tmpl_t const * values)77 static rs_stats_tmpl_t *rs_stats_collectd_init(TALLOC_CTX *ctx, rs_t *conf,
78 					       char const *plugin_instance,
79 					       char const *type, char const *type_instance,
80 					       void *stats,
81 					       rs_stats_value_tmpl_t const *values)
82 {
83 	static char hostname[255];
84 	static char fqdn[LCC_NAME_LEN];
85 
86 	size_t len;
87 	int i;
88 	char *p;
89 
90 	rs_stats_tmpl_t *tmpl;
91 	lcc_value_list_t *value;
92 
93 	assert(conf);
94 	assert(type);
95 	assert(type_instance);
96 
97 	for (len = 0; values[len].src; len++) {} ;
98 	assert(len > 0);
99 
100 	/*
101 	 *	Initialise hostname once so we don't call gethostname every time
102 	 */
103 	if (*fqdn == '\0') {
104 		int ret;
105 		struct addrinfo hints, *info = NULL;
106 
107 		if (gethostname(hostname, sizeof(hostname)) < 0) {
108 			ERROR("Error getting hostname: %s", fr_syserror(errno));
109 
110 			return NULL;
111 		}
112 
113 		memset(&hints, 0, sizeof hints);
114 		hints.ai_family = AF_UNSPEC; /*either IPV4 or IPV6*/
115 		hints.ai_socktype = SOCK_STREAM;
116 		hints.ai_flags = AI_CANONNAME;
117 
118 		if ((ret = getaddrinfo(hostname, "radius", &hints, &info)) != 0) {
119 			ERROR("Error getting hostname: %s", gai_strerror(ret));
120 			return NULL;
121 		}
122 
123 		strlcpy(fqdn, info->ai_canonname, sizeof(fqdn));
124 
125 		freeaddrinfo(info);
126 	}
127 
128 	tmpl = talloc_zero(ctx, rs_stats_tmpl_t);
129 	if (!tmpl) {
130 		return NULL;
131 	}
132 
133 	tmpl->value_tmpl = talloc_zero_array(tmpl, rs_stats_value_tmpl_t, len);
134 	if (!tmpl->value_tmpl) {
135 		goto error;
136 	}
137 
138 	tmpl->stats = stats;
139 
140 	value = talloc_zero(tmpl, lcc_value_list_t);
141 	if (!value) {
142 		goto error;
143 	}
144 	tmpl->value = value;
145 
146 	value->interval = conf->stats.interval;
147 	value->values_len = len;
148 
149 	value->values_types = talloc_zero_array(value, int, len);
150 	if (!value->values_types) {
151 		goto error;
152 	}
153 
154 	value->values = talloc_zero_array(value, value_t, len);
155 	if (!value->values) {
156 		goto error;
157 	}
158 
159 	for (i = 0; i < (int) len; i++) {
160 		assert(values[i].src);
161 		assert(values[i].cb);
162 
163 		tmpl->value_tmpl[i] = values[i];
164 		switch (tmpl->value_tmpl[i].type) {
165 		case LCC_TYPE_COUNTER:
166 			tmpl->value_tmpl[i].dst = &value->values[i].counter;
167 			break;
168 
169 		case LCC_TYPE_GAUGE:
170 			tmpl->value_tmpl[i].dst = &value->values[i].gauge;
171 			break;
172 
173 		case LCC_TYPE_DERIVE:
174 			tmpl->value_tmpl[i].dst = &value->values[i].derive;
175 			break;
176 
177 		case LCC_TYPE_ABSOLUTE:
178 			tmpl->value_tmpl[i].dst = &value->values[i].absolute;
179 			break;
180 
181 		default:
182 			assert(0);
183 		}
184 		value->values_types[i] = tmpl->value_tmpl[i].type;
185 	}
186 
187 	/*
188 	 *	These should be OK as is
189 	 */
190 	strlcpy(value->identifier.host, fqdn, sizeof(value->identifier.host));
191 
192 	/*
193 	 *	Plugin is ASCII only and no '/'
194 	 */
195 	fr_prints(value->identifier.plugin, sizeof(value->identifier.plugin),
196 		  conf->stats.prefix, strlen(conf->stats.prefix), '\0');
197 	for (p = value->identifier.plugin; *p; ++p) {
198 		if ((*p == '-') || (*p == '/'))*p = '_';
199 	}
200 
201 	/*
202 	 *	Plugin instance is ASCII only (assuming printable only) and no '/'
203 	 */
204 	fr_prints(value->identifier.plugin_instance, sizeof(value->identifier.plugin_instance),
205 		  plugin_instance, strlen(plugin_instance), '\0');
206 	for (p = value->identifier.plugin_instance; *p; ++p) {
207 		if ((*p == '-') || (*p == '/')) *p = '_';
208 	}
209 
210 	/*
211 	 *	Type is ASCII only (assuming printable only) and no '/' or '-'
212 	 */
213 	fr_prints(value->identifier.type, sizeof(value->identifier.type),
214 		  type, strlen(type), '\0');
215 	for (p = value->identifier.type; *p; ++p) {
216 		if ((*p == '-') || (*p == '/')) *p = '_';
217 	}
218 
219 	fr_prints(value->identifier.type_instance, sizeof(value->identifier.type_instance),
220 		  type_instance, strlen(type_instance), '\0');
221 	for (p = value->identifier.type_instance; *p; ++p) {
222 		if ((*p == '-') || (*p == '/')) *p = '_';
223 	}
224 
225 
226 	return tmpl;
227 
228 error:
229 	talloc_free(tmpl);
230 	return NULL;
231 }
232 
233 
234 /** Setup stats templates for latency
235  *
236  */
rs_stats_collectd_init_latency(TALLOC_CTX * ctx,rs_stats_tmpl_t ** out,rs_t * conf,char const * type,rs_latency_t * stats,PW_CODE code)237 rs_stats_tmpl_t *rs_stats_collectd_init_latency(TALLOC_CTX *ctx, rs_stats_tmpl_t **out, rs_t *conf,
238 						char const *type, rs_latency_t *stats, PW_CODE code)
239 {
240 	rs_stats_tmpl_t **tmpl, *last;
241 	char *p;
242 	char buffer[LCC_NAME_LEN];
243 	tmpl = out;
244 
245 	rs_stats_value_tmpl_t rtx[(RS_RETRANSMIT_MAX + 1) + 1 + 1];	// RTX bins + 0 bin + lost + NULL
246 	int i;
247 
248 	/* not static so were thread safe */
249 	rs_stats_value_tmpl_t const _packet_count[] = {
250 		{ &stats->interval.received, LCC_TYPE_GAUGE,  _copy_double_to_double, NULL },
251 		{ &stats->interval.linked, LCC_TYPE_GAUGE,  _copy_double_to_double, NULL },
252 		{ &stats->interval.unlinked, LCC_TYPE_GAUGE,  _copy_double_to_double, NULL },
253 		{ &stats->interval.reused, LCC_TYPE_GAUGE,  _copy_double_to_double, NULL },
254 		{ NULL, 0, NULL, NULL }
255 	};
256 
257 	rs_stats_value_tmpl_t const _latency[] = {
258 		{ &stats->latency_smoothed, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
259 		{ &stats->interval.latency_average, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
260 		{ &stats->interval.latency_high, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
261 		{ &stats->interval.latency_low, LCC_TYPE_GAUGE, _copy_double_to_double, NULL },
262 		{ NULL, 0, NULL, NULL }
263 	};
264 
265 #define INIT_STATS(_ti, _v) do {\
266 		strlcpy(buffer, fr_packet_codes[code], sizeof(buffer)); \
267 		for (p = buffer; *p; ++p) *p = tolower(*p);\
268 		last = *tmpl = rs_stats_collectd_init(ctx, conf, type, _ti, buffer, stats, _v);\
269 		if (!*tmpl) {\
270 			TALLOC_FREE(*out);\
271 			return NULL;\
272 		}\
273 		tmpl = &(*tmpl)->next;\
274 		ctx = *tmpl;\
275 		} while (0)
276 
277 
278 	INIT_STATS("radius_count", _packet_count);
279 	INIT_STATS("radius_latency", _latency);
280 
281 	for (i = 0; i < (RS_RETRANSMIT_MAX + 1); i++) {
282 		rtx[i].src = &stats->interval.rt[i];
283 		rtx[i].type = LCC_TYPE_GAUGE;
284 		rtx[i].cb = _copy_double_to_double;
285 		rtx[i].dst = NULL;
286 	}
287 
288 	rtx[i].src = &stats->interval.lost;
289 	rtx[i].type = LCC_TYPE_GAUGE;
290 	rtx[i].cb = _copy_double_to_double;
291 	rtx[i].dst = NULL;
292 
293 	memset(&rtx[++i], 0, sizeof(rs_stats_value_tmpl_t));
294 
295 	INIT_STATS("radius_rtx", rtx);
296 
297 	return last;
298 }
299 
300 /** Refresh and send the stats to the collectd server
301  *
302  */
rs_stats_collectd_do_stats(rs_t * conf,rs_stats_tmpl_t * tmpls,struct timeval * now)303 void rs_stats_collectd_do_stats(rs_t *conf, rs_stats_tmpl_t *tmpls, struct timeval *now)
304 {
305 	rs_stats_tmpl_t *tmpl = tmpls;
306 	char identifier[6 * LCC_NAME_LEN];
307 	int i;
308 
309 	while (tmpl) {
310 		/*
311 		 *	Refresh the value of whatever were sending
312 		 */
313 		for (i = 0; i < (int) tmpl->value->values_len; i++) {
314 			tmpl->value_tmpl[i].cb(conf, &tmpl->value_tmpl[i]);
315 		}
316 
317 		tmpl->value->time = now->tv_sec;
318 
319 		lcc_identifier_to_string(conf->stats.handle, identifier, sizeof(identifier), &tmpl->value->identifier);
320 
321 		if (lcc_putval(conf->stats.handle, tmpl->value) < 0) {
322 			char const *error;
323 
324 			error = lcc_strerror(conf->stats.handle);
325 			ERROR("Failed PUTVAL \"%s\" interval=%i %" PRIu64 " : %s",
326 			      identifier,
327 			      (int) tmpl->value->interval,
328 			      (uint64_t) tmpl->value->time,
329 			      error ? error : "unknown error");
330 		}
331 
332 		tmpl = tmpl->next;
333 	}
334 }
335 
336 /** Connect to a collectd server for stats output
337  *
338  * @param[in,out] conf radsniff configuration, we write the generated handle here.
339  * @return 0 on success -1 on failure.
340  */
rs_stats_collectd_open(rs_t * conf)341 int rs_stats_collectd_open(rs_t *conf)
342 {
343 	assert(conf->stats.collectd);
344 
345 	/*
346 	 *	Tear down stale connections gracefully.
347 	 */
348 	rs_stats_collectd_close(conf);
349 
350 	/*
351 	 *	There's no way to get the error from the connection handle
352 	 *	because it's freed on failure, before lcc returns.
353 	 */
354 	if (lcc_connect(conf->stats.collectd, &conf->stats.handle) < 0) {
355 		ERROR("Failed opening connection to collectd: %s", fr_syserror(errno));
356 		return -1;
357 	}
358 	DEBUG2("Connected to \"%s\"", conf->stats.collectd);
359 
360 	assert(conf->stats.handle);
361 	return 0;
362 }
363 
364 /** Close connection
365  *
366  * @param[in,out] conf radsniff configuration.
367  * @return 0 on success -1 on failure.
368  */
rs_stats_collectd_close(rs_t * conf)369 int rs_stats_collectd_close(rs_t *conf)
370 {
371 	assert(conf->stats.collectd);
372 
373 	int ret = 0;
374 
375 	if (conf->stats.handle) {
376 		ret = lcc_disconnect(conf->stats.handle);
377 		conf->stats.handle = NULL;
378 	}
379 
380 	return ret;
381 }
382 #endif
383