openvpn plugin: Implement the “ImprovedNamingSchema” config option.
[collectd.git] / src / openvpn.c
1 /**
2  * collectd - src/openvpn.c
3  * Copyright (C) 2008  Doug MacEachern
4  * Copyright (C) 2009  Florian octo Forster
5  * Copyright (C) 2009  Marco Chiappero
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by the
9  * Free Software Foundation; only version 2 of the License is applicable.
10  *
11  * This program is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License along
17  * with this program; if not, write to the Free Software Foundation, Inc.,
18  * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
19  *
20  * Authors:
21  *   Doug MacEachern <dougm at hyperic.com>
22  *   Florian octo Forster <octo at verplant.org>
23  *   Marco Chiappero <marco at absence.it>
24  **/
25
26 #include "collectd.h"
27 #include "common.h"
28 #include "plugin.h"
29
30 #define V1STRING "Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since\n"
31 #define V2STRING "HEADER,CLIENT_LIST,Common Name,Real Address,Virtual Address,Bytes Received,Bytes Sent,Connected Since,Connected Since (time_t)\n"
32 #define V3STRING "HEADER CLIENT_LIST Common Name Real Address Virtual Address Bytes Received Bytes Sent Connected Since Connected Since (time_t)\n"
33 #define VSSTRING "OpenVPN STATISTICS\n"
34
35
36 struct vpn_status_s
37 {
38         char *file;
39         enum
40         {
41                 MULTI1 = 1, /* status-version 1 */
42                 MULTI2,     /* status-version 2 */
43                 MULTI3,     /* status-version 3 */
44                 SINGLE = 10 /* currently no versions for single mode, maybe in the future */
45         } version;
46         char *name;
47 };
48 typedef struct vpn_status_s vpn_status_t;
49
50 static vpn_status_t **vpn_list = NULL;
51 static int vpn_num = 0;
52
53 static int store_compression = 1;
54 static int new_naming_schema = 0;
55
56 static const char *config_keys[] =
57 {
58         "StatusFile",
59         "Compression",
60         "ImprovedNamingSchema"
61 };
62 static int config_keys_num = STATIC_ARRAY_SIZE (config_keys);
63
64
65 /* Helper function
66  * copy-n-pasted from common.c - changed delim to ","  */
67 static int openvpn_strsplit (char *string, char **fields, size_t size)
68 {
69         size_t i;
70         char *ptr;
71         char *saveptr;
72
73         i = 0;
74         ptr = string;
75         saveptr = NULL;
76         while ((fields[i] = strtok_r (ptr, ",", &saveptr)) != NULL)
77         {
78                 ptr = NULL;
79                 i++;
80
81                 if (i >= size)
82                         break;
83         }
84
85         return (i);
86 } /* int openvpn_strsplit */
87
88
89 /* dispatches stats about traffic (TCP or UDP) generated by the tunnel per single endpoint */
90 static void iostats_submit (char *name, char *type, counter_t rx, counter_t tx)
91 {
92         value_t values[2];
93         value_list_t vl = VALUE_LIST_INIT;
94
95         values[0].counter = rx;
96         values[1].counter = tx;
97
98         /* NOTE ON THE NEW NAMING SCHEMA:
99          *       using plugin_instance to identify each vpn config (and
100          *       status) file; using type_instance to identify the endpoint
101          *       host when in multimode, traffic or overhead when in single.
102          */
103
104         vl.values = values;
105         vl.values_len = STATIC_ARRAY_SIZE (values);
106         sstrncpy (vl.host, hostname_g, sizeof (vl.host));
107         sstrncpy (vl.plugin, "openvpn", sizeof (vl.plugin));
108         sstrncpy (vl.plugin_instance, name, sizeof (vl.plugin_instance));
109
110         if (type) /* new naming schema - improved naming schema option */
111         {
112                 sstrncpy (vl.type, "io_octets", sizeof (vl.type));
113                 sstrncpy (vl.type_instance, type, sizeof (vl.type_instance));
114         }
115         else /* old naming schema (multicontext only): plugin_instance = hostname rather than filename */
116         {
117                 sstrncpy (vl.type, "if_octets", sizeof (vl.type));
118         }
119
120         plugin_dispatch_values (&vl);
121 } /* void traffic_submit */
122
123 /* dispatches stats about data compression shown when in single mode */
124 static void compression_submit (char *name, char *type, counter_t uncompressed, counter_t compressed)
125 {
126         value_t values[2];
127         value_list_t vl = VALUE_LIST_INIT;
128
129         values[0].counter = uncompressed;
130         values[1].counter = compressed;
131
132         vl.values = values;
133         vl.values_len = STATIC_ARRAY_SIZE (values);
134         sstrncpy (vl.host, hostname_g, sizeof (vl.host));
135         sstrncpy (vl.plugin, "openvpn", sizeof (vl.plugin));
136         sstrncpy (vl.plugin_instance, name, sizeof (vl.plugin_instance));
137         sstrncpy (vl.type, "compression", sizeof (vl.type));
138         sstrncpy (vl.type_instance, type, sizeof (vl.type_instance));
139
140         plugin_dispatch_values (&vl);
141 } /* void compression_submit */
142
143 static int single_read (char *name, FILE *fh)
144 {
145         char buffer[1024];
146         char *fields[4];
147         const int max_fields = STATIC_ARRAY_SIZE (fields);
148         int  fields_num, read = 0;
149
150         counter_t link_rx, link_tx;
151         counter_t tun_rx, tun_tx;
152         counter_t pre_compress, post_compress;
153         counter_t pre_decompress, post_decompress;
154         counter_t overhead_rx, overhead_tx;
155
156         link_rx = 0;
157         link_tx = 0;
158         tun_rx = 0;
159         tun_tx = 0;
160         pre_compress = 0;
161         post_compress = 0;
162         pre_decompress = 0;
163         post_decompress = 0;
164         overhead_rx = 0;
165         overhead_tx = 0;
166
167         while (fgets (buffer, sizeof (buffer), fh) != NULL)
168         {
169                 fields_num = openvpn_strsplit (buffer, fields, max_fields);
170
171                 /* status file is generated by openvpn/sig.c:print_status()
172                  * http://svn.openvpn.net/projects/openvpn/trunk/openvpn/sig.c
173                  *
174                  * The line we're expecting has 2 fields. We ignore all lines
175                  *  with more or less fields.
176                  */
177                 if (fields_num != 2)
178                 {
179                         continue;
180                 }
181
182                 if (strcmp (fields[0], "TUN/TAP read bytes") == 0)
183                 {
184                         /* read from the system and sent over the tunnel */
185                         tun_tx = atoll (fields[1]);
186                 }
187                 else if (strcmp (fields[0], "TUN/TAP write bytes") == 0)
188                 {
189                         /* read from the tunnel and written in the system */
190                         tun_rx = atoll (fields[1]);
191                 }
192                 else if (strcmp (fields[0], "TCP/UDP read bytes") == 0)
193                 {
194                         link_rx = atoll (fields[1]);
195                 }
196                 else if (strcmp (fields[0], "TCP/UDP write bytes") == 0)
197                 {
198                         link_tx = atoll (fields[1]);
199                 }
200                 else if (strcmp (fields[0], "pre-compress bytes") == 0)
201                 {
202                         pre_compress = atoll (fields[1]);
203                 }
204                 else if (strcmp (fields[0], "post-compress bytes") == 0)
205                 {
206                         post_compress = atoll (fields[1]);
207                 }
208                 else if (strcmp (fields[0], "pre-decompress bytes") == 0)
209                 {
210                         pre_decompress = atoll (fields[1]);
211                 }
212                 else if (strcmp (fields[0], "post-decompress bytes") == 0)
213                 {
214                         post_decompress = atoll (fields[1]);
215                 }
216         }
217
218         iostats_submit (name, "traffic", link_rx, link_tx);
219
220         /* we need to force this order to avoid negative values with these unsigned */
221         overhead_rx = (((link_rx - pre_decompress) + post_decompress) - tun_rx);
222         overhead_tx = (((link_tx - post_compress) + pre_compress) - tun_tx);
223
224         iostats_submit (name, "overhead", overhead_rx, overhead_tx);
225
226         if (store_compression)
227         {
228                 compression_submit (name, "data_in", post_decompress, pre_decompress);
229                 compression_submit (name, "data_out", pre_compress, post_compress);
230         }
231
232         read = 1;
233
234         return (read);
235 } /* int single_read */
236
237 /* for reading status version 1 */
238 static int multi1_read (char *name, FILE *fh)
239 {
240         char buffer[1024];
241         char *fields[10];
242         int  fields_num, read = 0, found_header = 0;
243
244         /* read the file until the "ROUTING TABLE" line is found (no more info after) */
245         while (fgets (buffer, sizeof (buffer), fh) != NULL)
246         {
247                 if (strcmp (buffer, "ROUTING TABLE\n") == 0)
248                         break;
249
250                 if (strcmp (buffer, V1STRING) == 0)
251                 {
252                         found_header = 1;
253                         continue;
254                 }
255
256                 /* skip the first lines until the client list section is found */
257                 if (found_header == 0)
258                         /* we can't start reading data until this string is found */
259                         continue;
260
261                 fields_num = openvpn_strsplit (buffer,
262                                 fields, STATIC_ARRAY_SIZE (fields));
263                 if (fields_num < 4)
264                         continue;
265
266                 if (new_naming_schema)
267                 {
268                         iostats_submit (fields[0],          /* "Common Name" */
269                                         NULL,               /* unused when in multimode */
270                                         atoll (fields[2]),  /* "Bytes Received" */
271                                         atoll (fields[3])); /* "Bytes Sent" */
272                 }
273                 else
274                 {
275                         iostats_submit (name,               /* vpn instance */
276                                         fields[0],          /* "Common Name" */
277                                         atoll (fields[2]),  /* "Bytes Received" */
278                                         atoll (fields[3])); /* "Bytes Sent" */
279                 }
280
281                 read = 1;
282         }
283
284         return (read);
285 } /* int multi1_read */
286
287 /* for reading status version 2 */
288 static int multi2_read (char *name, FILE *fh)
289 {
290         char buffer[1024];
291         char *fields[10];
292         const int max_fields = STATIC_ARRAY_SIZE (fields);
293         int  fields_num, read = 0;
294
295         while (fgets (buffer, sizeof (buffer), fh) != NULL)
296         {
297                 fields_num = openvpn_strsplit (buffer, fields, max_fields);
298
299                 /* status file is generated by openvpn/multi.c:multi_print_status()
300                  * http://svn.openvpn.net/projects/openvpn/trunk/openvpn/multi.c
301                  *
302                  * The line we're expecting has 8 fields. We ignore all lines
303                  *  with more or less fields.
304                  */
305                 if (fields_num != 8)
306                 {
307                         continue;
308                 }
309                 else
310                 {
311                         if (strcmp (fields[0], "CLIENT_LIST") == 0)
312                         {
313                                 if (new_naming_schema)
314                                 {
315                                         iostats_submit (name,               /* vpn instance */
316                                                         fields[1],          /* "Common Name" */
317                                                         atoll (fields[4]),  /* "Bytes Received" */
318                                                         atoll (fields[5])); /* "Bytes Sent" */
319                                 }
320                                 else
321                                 {
322                                         iostats_submit (fields[1],          /* "Common Name" */
323                                                         NULL,               /* unused when in multimode */
324                                                         atoll (fields[4]),  /* "Bytes Received" */
325                                                         atoll (fields[5])); /* "Bytes Sent" */
326                                 }
327
328                                 read = 1;
329                         }
330                 }
331         }
332
333         return (read);
334 } /* int multi2_read */
335
336 /* for reading status version 3 */
337 static int multi3_read (char *name, FILE *fh)
338 {
339         char buffer[1024];
340         char *fields[15];
341         const int max_fields = STATIC_ARRAY_SIZE (fields);
342         int  fields_num, read = 0;
343
344         while (fgets (buffer, sizeof (buffer), fh) != NULL)
345         {
346                 fields_num = strsplit (buffer, fields, max_fields);
347
348                 /* status file is generated by openvpn/multi.c:multi_print_status()
349                  * http://svn.openvpn.net/projects/openvpn/trunk/openvpn/multi.c
350                  *
351                  * The line we're expecting has 12 fields. We ignore all lines
352                  *  with more or less fields.
353                  */
354                 if (fields_num != 12)
355                 {
356                         continue;
357                 }
358                 else
359                 {
360                         if (strcmp (fields[0], "CLIENT_LIST") == 0)
361                         {
362                                 if (new_naming_schema)
363                                 {
364                                         iostats_submit (name,               /* vpn instance */
365                                                         fields[1],          /* "Common Name" */
366                                                         atoll (fields[4]),  /* "Bytes Received" */
367                                                         atoll (fields[5])); /* "Bytes Sent" */
368                                 }
369                                 else
370                                 {
371                                         iostats_submit (fields[1],          /* "Common Name" */
372                                                         NULL,               /* unused when in multimode */
373                                                         atoll (fields[4]),  /* "Bytes Received" */
374                                                         atoll (fields[5])); /* "Bytes Sent" */
375                                 }
376
377                                 read = 1;
378                         }
379                 }
380         }
381
382         return (read);
383 } /* int multi3_read */
384
385 /* read callback */
386 static int openvpn_read (void)
387 {
388         FILE *fh;
389         int  i, read;
390
391         read = 0;
392
393         /* call the right read function for every status entry in the list */
394         for (i = 0; i < vpn_num; i++)
395         {
396                 fh = fopen (vpn_list[i]->file, "r");
397                 if (fh == NULL)
398                 {
399                         char errbuf[1024];
400                         WARNING ("openvpn plugin: fopen(%s) failed: %s", vpn_list[i]->file,
401                                 sstrerror (errno, errbuf, sizeof (errbuf)));
402
403                         continue;
404                 }
405
406                 switch (vpn_list[i]->version)
407                 {
408                         case SINGLE:
409                                 read = single_read(vpn_list[i]->name, fh);
410                                 break;
411
412                         case MULTI1:
413                                 read = multi1_read(vpn_list[i]->name, fh);
414                                 break;
415
416                         case MULTI2:
417                                 read = multi2_read(vpn_list[i]->name, fh);
418                                 break;
419
420                         case MULTI3:
421                                 read = multi3_read(vpn_list[i]->name, fh);
422                                 break;
423                 }
424
425                 fclose (fh);
426         }
427
428         return (read ? 0 : -1);
429 } /* int openvpn_read */
430
431 static int version_detect (const char *filename)
432 {
433         FILE *fh;
434         char buffer[1024];
435         int version = 0;
436
437         /* Sanity checking. We're called from the config handling routine, so
438          * better play it save. */
439         if ((filename == NULL) || (*filename == 0))
440                 return (0);
441
442         fh = fopen (filename, "r");
443         if (fh == NULL)
444         {
445                 char errbuf[1024];
446                 WARNING ("openvpn plugin: Unable to read \"%s\": %s", filename,
447                                 sstrerror (errno, errbuf, sizeof (errbuf)));
448                 return (0);
449         }
450
451         /* now search for the specific multimode data format */
452         while ((fgets (buffer, sizeof (buffer), fh)) != NULL)
453         {
454                 /* we look at the first line searching for SINGLE mode configuration */
455                 if (strcmp (buffer, VSSTRING) == 0)
456                 {
457                         DEBUG ("openvpn plugin: found status file version SINGLE");
458                         version = SINGLE;
459                         break;
460                 }
461                 /* searching for multi version 1 */
462                 else if (strcmp (buffer, V1STRING) == 0)
463                 {
464                         DEBUG ("openvpn plugin: found status file version MULTI1");
465                         version = MULTI1;
466                         break;
467                 }
468                 /* searching for multi version 2 */
469                 else if (strcmp (buffer, V2STRING) == 0)
470                 {
471                         DEBUG ("openvpn plugin: found status file version MULTI2");
472                         version = MULTI2;
473                         break;
474                 }
475                 /* searching for multi version 3 */
476                 else if (strcmp (buffer, V3STRING) == 0)
477                 {
478                         DEBUG ("openvpn plugin: found status file version MULTI3");
479                         version = MULTI3;
480                         break;
481                 }
482         }
483
484         if (version == 0)
485         {
486                 /* This is only reached during configuration, so complaining to
487                  * the user is in order. */
488                 NOTICE ("openvpn plugin: %s: Unknown file format, please "
489                                 "report this as bug. Make sure to include "
490                                 "your status file, so the plugin can "
491                                 "be adapted.", filename);
492         }
493
494         fclose (fh);
495
496         return version;
497 } /* int version_detect */
498
499 static int openvpn_config (const char *key, const char *value)
500 {
501         if (strcasecmp ("StatusFile", key) == 0)
502         {
503                 char    *status_file, *status_name, *filename;
504                 int     status_version, i;
505                 vpn_status_t *temp;
506
507                 /* try to detect the status file format */
508                 status_version = version_detect (value);
509
510                 if (status_version == 0)
511                 {
512                         WARNING ("openvpn plugin: unable to detect status version, \
513                                 discarding status file \"%s\".", value);
514                         return (1);
515                 }
516
517                 status_file = sstrdup (value);
518                 if (status_file == NULL)
519                 {
520                         char errbuf[1024];
521                         WARNING ("openvpn plugin: sstrdup failed: %s",
522                                 sstrerror (errno, errbuf, sizeof (errbuf)));
523                         return (1);
524                 }
525
526                 /* it determines the file name as string starting at location filename + 1 */
527                 filename = strrchr (status_file, (int) '/');
528                 if (filename == NULL)
529                 {
530                         /* status_file is already the file name only */
531                         status_name = status_file;
532                 }
533                 else
534                 {
535                         /* doesn't waste memory, uses status_file starting at filename + 1 */
536                         status_name = filename + 1;
537                 }
538
539                 /* scan the list looking for a clone */
540                 for (i = 0; i < vpn_num; i++)
541                 {
542                         if (strcasecmp (vpn_list[i]->name, status_name) == 0)
543                         {
544                                 WARNING ("openvpn plugin: status filename \"%s\" "
545                                                 "already used, please choose a "
546                                                 "different one.", status_name);
547                                 sfree (status_file);
548                                 return (1);
549                         }
550                 }
551
552                 /* create a new vpn element since file, version and name are ok */
553                 temp = (vpn_status_t *) malloc (sizeof (vpn_status_t));
554                 temp->file = status_file;
555                 temp->version = status_version;
556                 temp->name = status_name;
557
558                 vpn_list = (vpn_status_t **) realloc (vpn_list, (vpn_num + 1) * sizeof (vpn_status_t *));
559                 if (vpn_list == NULL)
560                 {
561                         char errbuf[1024];
562                         ERROR ("openvpn plugin: malloc failed: %s",
563                                 sstrerror (errno, errbuf, sizeof (errbuf)));
564
565                         sfree (temp->file);
566                         sfree (temp);
567                         return (1);
568                 }
569
570                 vpn_list[vpn_num] = temp;
571                 vpn_num++;
572
573                 DEBUG ("openvpn plugin: status file \"%s\" added", temp->file);
574
575         }
576         else if (strcasecmp ("Compression", key) == 0)
577         {
578                 if (IS_TRUE (value))
579                         store_compression = 1;
580                 else
581                 {
582                         store_compression = 0;
583                         DEBUG ("openvpn plugin: no 'compression statistcs' collected");
584                 }
585         }
586         else if (strcasecmp ("ImprovedNamingSchema", key) == 0)
587         {
588                 if (IS_TRUE (value))
589                 {
590                         DEBUG ("openvpn plugin: using the new naming schema");
591                         new_naming_schema = 1;
592                 }
593                 else
594                 {
595                         new_naming_schema = 0;
596                 }
597         }
598         else
599         {
600                 return (-1);
601         }
602
603         return (0);
604 } /* int openvpn_config */
605
606 /* shutdown callback */
607 static int openvpn_shutdown (void)
608 {
609         int i;
610
611         for (i = 0; i < vpn_num; i++)
612         {
613                 sfree (vpn_list[i]->file);
614                 sfree (vpn_list[i]);
615         }
616
617         sfree (vpn_list);
618
619         return (0);
620 } /* int openvpn_shutdown */
621
622 void module_register (void)
623 {
624         plugin_register_config ("openvpn", openvpn_config,
625                         config_keys, config_keys_num);
626         plugin_register_read ("openvpn", openvpn_read);
627         plugin_register_shutdown ("openvpn", openvpn_shutdown);
628 } /* void module_register */