Hi all,

I have some news regarding the Openvpn plugin.
I recently upgraded to 4.7.2 and enabled the new (to me) openvpn plugin,
but unfortunately it didn't work and I couldn't understand why. So had a
look at the code and applied some changes. Here a list of things
added:

- support for multiple vpn/status files
- support for single endpoint status file and for multicontext version
1,3 (currently every status file type is supported)
- status file type auto-detection
- collection of overhead and compression stats for single mode status files
- "NoCompression" option to disable compression statistics collection
- new file naming schema correcting the previous wrong one
- debug and error output

The plugin has already been tested with my own openvpn setup and many
different files found on the net, but two major aspects have to be taken
into consideration. The first one is the new overhead collection and the
change in the compression statistics (now data is stored as they are
[1]). However, due to a bug in the previous code, no compression
collection was working and so the new one is not going to brake backward
compatibility. Unfortunately backward compatibility is broken by the
new naming schema, more collectd compliant (and necessary with multiple
conf/status files support). So plugin_istance is now the status file
name (or the status entry number), while the plugin_type is now the
hostname when in multicontext mode or the traffic and overhead when in
single.
About the plugin_instance value, I started using numbers like the cpu
plugin does, but I prefer to use the self explanatory file name, helping
in finding the right vpn and avoiding to mess things up when adding a
new status file. The downside is that filenames have to be unique (man
pages should mention this [2]), therefore I added a check. Nonetheless I
think it's very unlikely to happen, it is quite rare to find many vpn on
the same system and it's really hard to have the same log filename
stored in different dirs rather than the opposite; usually to different
tunnels should already correspond different status log name.
However, Florian, feel free to choose the other method if you prefer, I can send you a patch to revert to the number based schema.
Hope you like the code.

Regards,
Marco



[1] This allows to retrieve multiple informations, not just the pure
compression ratio. For example we can now calculate the pure compression
ratio, the global compression level, the byte saved (eg. for a
overhead vs. saved bytes graph) and so on.

[2] Man pages should be updated as well, but I haven't written anything yet.


--- collectd-4.7.2/src/types.db	2009-07-18 18:09:56.000000000 +0200
+++ /usr/share/collectd/types.db	2009-09-17 12:02:12.000000000 +0200
@@ -7,7 +7,7 @@ bytes			value:GAUGE:0:U
 cache_result		value:COUNTER:0:4294967295
 cache_size		value:GAUGE:0:4294967295
 charge			value:GAUGE:0:U
-compression_ratio	value:GAUGE:0:2
+compression		uncompressed:COUNTER:0:U, compressed:COUNTER:0:U
 connections		value:COUNTER:0:U
 conntrack		entropy:GAUGE:0:4294967295
 counter			value:COUNTER:U:U
/**
 * collectd - src/openvpn.c
 * Copyright (C) 2008  Doug MacEachern
 * Copyright (C) 2009  Florian octo Forster
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; only version 2 of the License is applicable.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
 *
 * Authors:
 *   Doug MacEachern <dougm at hyperic.com>
 *   Florian octo Forster <octo at verplant.org>
 *   Marco Chiappero <marco at absence.it>
 **/

#include "collectd.h"
#include "common.h"
#include "plugin.h"

#define V1STRING "Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since\n"
#define V2STRING "HEADER,CLIENT_LIST,Common Name,Real Address,Virtual Address,Bytes Received,Bytes Sent,Connected Since,Connected Since (time_t)\n"
#define V3STRING "HEADER CLIENT_LIST Common Name Real Address Virtual Address Bytes Received Bytes Sent Connected Since Connected Since (time_t)\n"


struct vpn_status_s
{
	char	*file;
	enum
	{
		MULTI1 = 1,	/* status-version 1 */
		MULTI2,		/* status-version 2 */
		MULTI3,		/* status-version 3 */
		SINGLE = 10	/* currently no versions for single mode, maybe in the future */
	} version;
	char	*name;
};
typedef struct vpn_status_s vpn_status_t;

static vpn_status_t **vpn_list = NULL;
static int vpn_num = 0;

static int store_compression = 1;

static const char *config_keys[] =
{
	"StatusFile",
	"NoCompression"
};
static int config_keys_num = STATIC_ARRAY_SIZE (config_keys);


/*			Helper function			*/
/*  copy-n-pasted from common.c - changed delim to ","  */
static int openvpn_strsplit (char *string, char **fields, size_t size)
{
	size_t i;
	char *ptr;
	char *saveptr;

	i = 0;
	ptr = string;
	saveptr = NULL;
	while ((fields[i] = strtok_r (ptr, ",", &saveptr)) != NULL)
	{
		ptr = NULL;
		i++;

		if (i >= size)
			break;
	}

	return (i);
} /* int openvpn_strsplit */


/* dispatches stats about traffic (TCP or UDP) generated by the tunnel per single endpoint */
static void iostats_submit (char *name, char *type, counter_t rx, counter_t tx)
{
	value_t values[2];
	value_list_t vl = VALUE_LIST_INIT;

	values[0].counter = rx;
	values[1].counter = tx;

	/* NOTE: using plugin_instance to identify each vpn config (and
	 *	 status) file; using type_instance to identify the endpoint
	 *	 host when in multimode, traffic or overhead when in single.
	 */

	vl.values = values;
	vl.values_len = STATIC_ARRAY_SIZE (values);
	sstrncpy (vl.host, hostname_g, sizeof (vl.host));
	sstrncpy (vl.plugin, "openvpn", sizeof (vl.plugin));
	sstrncpy (vl.plugin_instance, name, sizeof (vl.plugin_instance));
	sstrncpy (vl.type, "io_octets", sizeof (vl.type));
	sstrncpy (vl.type_instance, type, sizeof (vl.type_instance));

	plugin_dispatch_values (&vl);
} /* void traffic_submit */

/* dispatches stats about data compression shown when in single mode */
static void compression_submit (char *name, char *type, counter_t uncompressed, counter_t compressed)
{
	value_t values[2];
	value_list_t vl = VALUE_LIST_INIT;

	values[0].counter = uncompressed;
	values[1].counter = compressed;

	vl.values = values;
	vl.values_len = STATIC_ARRAY_SIZE (values);
	sstrncpy (vl.host, hostname_g, sizeof (vl.host));
	sstrncpy (vl.plugin, "openvpn", sizeof (vl.plugin));
	sstrncpy (vl.plugin_instance, name, sizeof (vl.plugin_instance));
	sstrncpy (vl.type, "compression", sizeof (vl.type));
	sstrncpy (vl.type_instance, type, sizeof (vl.type_instance));

	plugin_dispatch_values (&vl);
} /* void compression_submit */

static int single_read (char *name, FILE *fh)
{
	char buffer[1024];
	char *fields[4];
	const int max_fields = STATIC_ARRAY_SIZE (fields);
	int  fields_num, read = 0;

	counter_t link_rx, link_tx;
	counter_t tun_rx, tun_tx;
	counter_t pre_compress, post_compress;
	counter_t pre_decompress, post_decompress;
	counter_t overhead_rx, overhead_tx;

	link_rx = 0;
	link_tx = 0;
	tun_rx = 0;
	tun_tx = 0;
	pre_compress = 0;
	post_compress = 0;
	pre_decompress = 0;
	post_decompress = 0;
	overhead_rx = 0;
	overhead_tx = 0;


	while (fgets (buffer, sizeof (buffer), fh) != NULL)
	{
		fields_num = openvpn_strsplit (buffer, fields, max_fields);

		/* status file is generated by openvpn/sig.c:print_status()
		 * http://svn.openvpn.net/projects/openvpn/trunk/openvpn/sig.c
		 *
		 * The line we're expecting has 2 fields. We ignore all lines
		 *  with more or less fields.
		 */
		if (fields_num != 2)
		{
			continue;
		}
		else
		{
			if (strcmp (fields[0], "TUN/TAP read bytes") == 0)
			{
				/* read from the system and sent over the tunnel */
				tun_tx = atoll (fields[1]);
			}
			else if (strcmp (fields[0], "TUN/TAP write bytes") == 0)
			{
				/* read from the tunnel and written in the system */
				tun_rx = atoll (fields[1]);
			}
			else if (strcmp (fields[0], "TCP/UDP read bytes") == 0)
			{
				link_rx = atoll (fields[1]);
			}
			else if (strcmp (fields[0], "TCP/UDP write bytes") == 0)
			{
				link_tx = atoll (fields[1]);
			}
			else if (strcmp (fields[0], "pre-compress bytes") == 0)
			{
				pre_compress = atoll (fields[1]);
			}
			else if (strcmp (fields[0], "post-compress bytes") == 0)
			{
				post_compress = atoll (fields[1]);
			}
			else if (strcmp (fields[0], "pre-decompress bytes") == 0)
			{
				pre_decompress = atoll (fields[1]);
			}
			else if (strcmp (fields[0], "post-decompress bytes") == 0)
			{
				post_decompress = atoll (fields[1]);
			}
		}
	}

	iostats_submit (name, "traffic", link_rx, link_tx);

	/* we need to force this order to avoid negative values with these unsigned */
	overhead_rx = (((link_rx - pre_decompress) + post_decompress) - tun_rx);
	overhead_tx = (((link_tx - post_compress) + pre_compress) - tun_tx);

	iostats_submit (name, "overhead", overhead_rx, overhead_tx);

	if (store_compression)
	{
		compression_submit (name, "data_in", post_decompress, pre_decompress);
		compression_submit (name, "data_out", pre_compress, post_compress);
	}

	read = 1;

	return (read);
} /* int single_read */

/* for reading status version 1 */
static int multi1_read (char *name, FILE *fh)
{
	char buffer[1024];
	char *fields[10];
	const int max_fields = STATIC_ARRAY_SIZE (fields);
	int  fields_num, read = 0, skip = 1;

	/* read the file until the "ROUTING TABLE" line is found (no more info after) */
	for ( ; strcmp (buffer, "ROUTING TABLE\n"); fgets (buffer, sizeof (buffer), fh))
	{
		if (skip) /* skip the first lines until the client list section is found */
		{
			/* we can't start reading data until this string is found */
			if (strcmp (buffer, V1STRING) == 0)
				skip = 0;

			continue;
		}
		else
		{
			fields_num = openvpn_strsplit (buffer, fields, max_fields);

			iostats_submit (name,			/* vpn instance */
					fields[0],		/* "Common Name" */
					atoll (fields[2]),	/* "Bytes Received" */
					atoll (fields[3]));	/* "Bytes Sent" */
			read = 1;
		}
	}

	return (read);
} /* int multi1_read */

/* for reading status version 2 */
static int multi2_read (char *name, FILE *fh)
{
	char buffer[1024];
	char *fields[10];
	const int max_fields = STATIC_ARRAY_SIZE (fields);
	int  fields_num, read = 0;

	while (fgets (buffer, sizeof (buffer), fh) != NULL)
	{
		fields_num = openvpn_strsplit (buffer, fields, max_fields);

		/* status file is generated by openvpn/multi.c:multi_print_status()
		 * http://svn.openvpn.net/projects/openvpn/trunk/openvpn/multi.c
		 *
		 * The line we're expecting has 8 fields. We ignore all lines
		 *  with more or less fields.
		 */
		if (fields_num != 8)
		{
			continue;
		}
		else
		{
			if (strcmp (fields[0], "CLIENT_LIST") == 0)
			{
				iostats_submit (name,			/* vpn instance */
						fields[1],		/* "Common Name" */
						atoll (fields[4]),	/* "Bytes Received" */
						atoll (fields[5]));	/* "Bytes Sent" */
				read = 1;
			}
		}
	}

	return (read);
} /* int multi2_read */

/* for reading status version 3 */
static int multi3_read (char *name, FILE *fh)
{
	char buffer[1024];
	char *fields[15];
	const int max_fields = STATIC_ARRAY_SIZE (fields);
	int  fields_num, read = 0;

	while (fgets (buffer, sizeof (buffer), fh) != NULL)
	{
		fields_num = strsplit (buffer, fields, max_fields);

		/* status file is generated by openvpn/multi.c:multi_print_status()
		 * http://svn.openvpn.net/projects/openvpn/trunk/openvpn/multi.c
		 *
		 * The line we're expecting has 12 fields. We ignore all lines
		 *  with more or less fields.
		 */
		if (fields_num != 12)
		{
			continue;
		}
		else
		{
			if (strcmp (fields[0], "CLIENT_LIST") == 0)
			{
				iostats_submit (name,			/* vpn instance */
						fields[1],		/* "Common Name" */
						atoll (fields[4]),	/* "Bytes Received" */
						atoll (fields[5]));	/* "Bytes Sent" */
				read = 1;
			}
		}
	}

	return (read);
} /* int multi3_read */

/* read callback */
static int openvpn_read (void)
{
	FILE *fh;
	int  i, read;

	read = 0;

	/* call the right read function for every status entry in the list */
	for (i = 0; i < vpn_num; i++)
	{
		fh = fopen (vpn_list[i]->file, "r");
		if (fh == NULL)
		{
			char errbuf[1024];
			WARNING ("openvpn plugin: fopen(%s) failed: %s", vpn_list[i]->file,
				sstrerror (errno, errbuf, sizeof (errbuf)));

			continue;
		}

		switch (vpn_list[i]->version)
		{
			case SINGLE:
				read = single_read(vpn_list[i]->name, fh);
				break;

			case MULTI1:
				read = multi1_read(vpn_list[i]->name, fh);
				break;

			case MULTI2:
				read = multi2_read(vpn_list[i]->name, fh);
				break;

			case MULTI3:
				read = multi3_read(vpn_list[i]->name, fh);
				break;
		}

		fclose (fh);
	}

	return (read ? 0 : -1);
} /* int openvpn_read */

static int version_detect (FILE *fh)
{
	char buffer[1024];
	int version = 0;

	/* we look at the first line searching for SINGLE mode configuration */
	if ((fscanf (fh, "%*s %s", buffer) == 1) && (strcmp (buffer, "STATISTICS") == 0))
	{
		DEBUG ("openvpn plugin: found status file version SINGLE");
		version = SINGLE;
	}
	else    /* else multimode */
	{
		/* now search for the specific multimode data format */
		while ((fgets (buffer, sizeof (buffer), fh)) != NULL)
		{

			/* searching for multi version 1 */
			if (strcmp (buffer, V1STRING) == 0)
			{
				DEBUG ("openvpn plugin: found status file version MULTI1");
				version = MULTI1;
				break;
			}
			/* searching for multi version 2 */
			else if (strcmp (buffer, V2STRING) == 0)
			{
				DEBUG ("openvpn plugin: found status file version MULTI2");
				version = MULTI2;
				break;
			}
			/* searching for multi version 3 */
			else if (strcmp (buffer, V3STRING) == 0)
			{
				DEBUG ("openvpn plugin: found status file version MULTI3");
				version = MULTI3;
				break;
			}
		}
	}

	if (version == 0)
	{
		DEBUG ("openvpn plugin: unknown file format, please report this as bug");
	}

	return version;
} /* int version_detect */

static int openvpn_config (const char *key, const char *value)
{
	if (strcasecmp ("StatusFile", key) == 0)
	{
		FILE    *fh;
		char    *status_file, *status_name, *filename;
		int     status_version, i;
		vpn_status_t *temp;

		/* check whether the status file provided is readable */
		fh = fopen (value, "r");
		if (fh == NULL)
		{
			char errbuf[1024];
			WARNING ("openvpn plugin: unable to read \"%s\": %s",
			value, sstrerror (errno, errbuf, sizeof (errbuf)));
			return (1);
		}

		/* once open try to detect the status file format */
		status_version = version_detect (fh);

		fclose (fh);

		if (status_version == 0)
		{	
			WARNING ("openvpn plugin: unable to detect status version, \
				discarding status file \"%s\".", value);
			return (1);
		}

		status_file = sstrdup (value);
		if (status_file == NULL)
		{
			char errbuf[1024];
			WARNING ("openvpn plugin: sstrdup failed: %s",
				sstrerror (errno, errbuf, sizeof (errbuf)));
			return (1);
		}
	
		/* it determines the file name as string starting at location filename + 1 */
		filename = strrchr (status_file, (int) '/');
		if (filename == NULL)
		{
			/* status_file is already the file name only */
			status_name = status_file;
		}
		else
		{
			/* doesn't waist memory, uses status_file starting at filename + 1 */
			status_name = filename + 1;
		}

		/* if not empty, it scans the list looking for a clone */
		if (vpn_num)
		{
			for (i = 0; i < vpn_num; i++)
			{
				if (strcasecmp (vpn_list[i]->name, status_name) == 0)
				{
					WARNING ("status filename \"%s\" already used, \
						please choose a different one.", status_name);
					return (1);
				}
			}
		}

		/* create a new vpn element since file and version are ok */	
		temp = (vpn_status_t *) malloc (sizeof (vpn_status_t));
		temp->file = status_file;
		temp->version = status_version;
		temp->name = status_name;

		vpn_list = (vpn_status_t **) realloc (vpn_list, (vpn_num + 1) * sizeof (vpn_status_t *));
		if (vpn_list == NULL)
		{
		    char errbuf[1024];
		    ERROR ("openvpn plugin: malloc failed: %s",
			    sstrerror (errno, errbuf, sizeof (errbuf)));
		    return (1);
		}

		vpn_list[vpn_num] = temp;
		vpn_num++;

		DEBUG ("openvpn plugin: status file \"%s\" added", temp->file);

	}
	else if (strcasecmp ("NoCompression", key) == 0)
	{
		if ((strcasecmp ("True", value) == 0)
			|| (strcasecmp ("Yes", value) == 0)
			|| (strcasecmp ("On", value) == 0))
		{
			store_compression = 0;
			DEBUG ("openvpn plugin: no 'compression statistcs' collected");
		}
		else
		{
			store_compression = 1;
		}
	}
	else
	{
		return (-1);
	}

	return (0);
} /* int openvpn_config */

/* shutdown callback */
static int openvpn_shutdown (void)
{
	int i;
	
	for (i = 0; i < vpn_num; i++)
	{
		sfree (vpn_list[i]->file);
		sfree (vpn_list[i]);
	}

	sfree (vpn_list);

	return (0);
} /* int openvpn_shutdown */

void module_register (void)
{
	plugin_register_config ("openvpn", openvpn_config,
				config_keys, config_keys_num);
	plugin_register_read ("openvpn", openvpn_read);
	plugin_register_shutdown ("openvpn", openvpn_shutdown);
} /* void module_register */
_______________________________________________
collectd mailing list
[email protected]
http://mailman.verplant.org/listinfo/collectd

Reply via email to