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