Merge branch 'collectd-4.9'
[collectd.git] / src / curl_json.c
1 /**
2  * collectd - src/curl_json.c
3  * Copyright (C) 2009       Doug MacEachern
4  * Copyright (C) 2006-2009  Florian octo Forster
5  *
6  * This program is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU General Public License as published by the
8  * Free Software Foundation; only version 2 of the License is applicable.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
18  *
19  * Authors:
20  *   Doug MacEachern <dougm at hyperic.com>
21  *   Florian octo Forster <octo at verplant.org>
22  **/
23
24 #include "collectd.h"
25 #include "common.h"
26 #include "plugin.h"
27 #include "configfile.h"
28 #include "utils_avltree.h"
29
30 #include <curl/curl.h>
31 #include <yajl/yajl_parse.h>
32
33 #define CJ_DEFAULT_HOST "localhost"
34 #define CJ_KEY_MAGIC 0x43484b59UL /* CHKY */
35 #define CJ_IS_KEY(key) (key)->magic == CJ_KEY_MAGIC
36 #define CJ_ANY "*"
37 #define COUCH_MIN(x,y) ((x) < (y) ? (x) : (y))
38
39 struct cj_key_s;
40 typedef struct cj_key_s cj_key_t;
41 struct cj_key_s /* {{{ */
42 {
43   char *path;
44   char *type;
45   char *instance;
46   unsigned long magic;
47 };
48 /* }}} */
49
50 struct cj_s /* {{{ */
51 {
52   char *instance;
53   char *host;
54
55   char *url;
56   char *user;
57   char *pass;
58   char *credentials;
59   int   verify_peer;
60   int   verify_host;
61   char *cacert;
62
63   CURL *curl;
64   char curl_errbuf[CURL_ERROR_SIZE];
65
66   yajl_handle yajl;
67   c_avl_tree_t *tree;
68   cj_key_t *key;
69   int depth;
70   struct {
71     union {
72       c_avl_tree_t *tree;
73       cj_key_t *key;
74     };
75     char name[DATA_MAX_NAME_LEN];
76   } state[YAJL_MAX_DEPTH];
77 };
78 typedef struct cj_s cj_t; /* }}} */
79
80 static int cj_read (user_data_t *ud);
81 static int cj_curl_perform (cj_t *db, CURL *curl);
82 static void cj_submit (cj_t *db, cj_key_t *key, value_t *value);
83
84 static size_t cj_curl_callback (void *buf, /* {{{ */
85     size_t size, size_t nmemb, void *user_data)
86 {
87   cj_t *db;
88   size_t len;
89   yajl_status status;
90
91   len = size * nmemb;
92
93   if (len <= 0)
94     return (len);
95
96   db = user_data;
97   if (db == NULL)
98     return (0);
99
100   status = yajl_parse(db->yajl, (unsigned char *)buf, len);
101   if (status == yajl_status_ok)
102   {
103     status = yajl_parse_complete(db->yajl);
104     return (len);
105   }
106   else if (status == yajl_status_insufficient_data)
107     return (len);
108
109   if (status != yajl_status_ok)
110   {
111     unsigned char *msg =
112       yajl_get_error(db->yajl, 1, (unsigned char *)buf, len);
113     ERROR ("curl_json plugin: yajl_parse failed: %s", msg);
114     yajl_free_error(db->yajl, msg);
115     return (0); /* abort write callback */
116   }
117
118   return (len);
119 } /* }}} size_t cj_curl_callback */
120
121 static int cj_get_type (cj_key_t *key)
122 {
123   const data_set_t *ds;
124
125   ds = plugin_get_ds (key->type);
126   if (ds == NULL)
127     return -1; /* let plugin_write do the complaining */
128   else
129     return ds->ds[0].type; /* XXX support ds->ds_len > 1 */
130 }
131
132 /* yajl callbacks */
133 static int cj_cb_integer (void *ctx, long val)
134 {
135   cj_t *db = (cj_t *)ctx;
136   cj_key_t *key = db->state[db->depth].key;
137
138   if (key != NULL)
139   {
140     value_t vt;
141     int type;
142
143     type = cj_get_type (key);
144     if (type == DS_TYPE_COUNTER)
145       vt.counter = (counter_t) val;
146     else if (type == DS_TYPE_GAUGE)
147       vt.gauge = (gauge_t) val;
148     else if (type == DS_TYPE_DERIVE)
149       vt.derive = (derive_t) val;
150     else if (type == DS_TYPE_ABSOLUTE)
151       vt.absolute = (absolute_t) val;
152     else
153       return 0;
154
155     cj_submit (db, key, &vt);
156   }
157   return 1;
158 }
159
160 static int cj_cb_double (void *ctx, double val)
161 {
162   cj_t *db = (cj_t *)ctx;
163   cj_key_t *key = db->state[db->depth].key;
164
165   if (key != NULL)
166   {
167     value_t vt;
168     int type;
169
170     type = cj_get_type (key);
171     if (type == DS_TYPE_COUNTER)
172       vt.counter = (counter_t) val;
173     else if (type == DS_TYPE_GAUGE)
174       vt.gauge = (gauge_t) val;
175     else if (type == DS_TYPE_DERIVE)
176       vt.derive = (derive_t) val;
177     else if (type == DS_TYPE_ABSOLUTE)
178       vt.absolute = (absolute_t) val;
179     else
180       return 0;
181
182     cj_submit (db, key, &vt);
183   }
184   return 1;
185 }
186
187 static int cj_cb_map_key (void *ctx, const unsigned char *val,
188                             unsigned int len)
189 {
190   cj_t *db = (cj_t *)ctx;
191   c_avl_tree_t *tree;
192
193   tree = db->state[db->depth-1].tree;
194
195   if (tree != NULL)
196   {
197     cj_key_t *value;
198     char *name;
199
200     name = db->state[db->depth].name;
201     len = COUCH_MIN(len, sizeof (db->state[db->depth].name)-1);
202     sstrncpy (name, (char *)val, len+1);
203
204     if (c_avl_get (tree, name, (void *) &value) == 0)
205       db->state[db->depth].key = value;
206     else if (c_avl_get (tree, CJ_ANY, (void *) &value) == 0)
207       db->state[db->depth].key = value;
208     else
209       db->state[db->depth].key = NULL;
210   }
211
212   return 1;
213 }
214
215 static int cj_cb_string (void *ctx, const unsigned char *val,
216                            unsigned int len)
217 {
218   cj_t *db = (cj_t *)ctx;
219   c_avl_tree_t *tree;
220   char *ptr;
221
222   if (db->depth != 1) /* e.g. _all_dbs */
223     return 1;
224
225   cj_cb_map_key (ctx, val, len); /* same logic */
226
227   tree = db->state[db->depth].tree;
228
229   if ((tree != NULL) && (ptr = rindex (db->url, '/')))
230   {
231     char url[PATH_MAX];
232     CURL *curl;
233
234     /* url =~ s,[^/]+$,$name, */
235     len = (ptr - db->url) + 1;
236     ptr = url;
237     sstrncpy (ptr, db->url, sizeof (url));
238     sstrncpy (ptr + len, db->state[db->depth].name, sizeof (url) - len);
239
240     curl = curl_easy_duphandle (db->curl);
241     curl_easy_setopt (curl, CURLOPT_URL, url);
242     cj_curl_perform (db, curl);
243     curl_easy_cleanup (curl);
244   }
245   return 1;
246 }
247
248 static int cj_cb_start (void *ctx)
249 {
250   cj_t *db = (cj_t *)ctx;
251   if (++db->depth >= YAJL_MAX_DEPTH)
252   {
253     ERROR ("curl_json plugin: %s depth exceeds max, aborting.", db->url);
254     return 0;
255   }
256   return 1;
257 }
258
259 static int cj_cb_end (void *ctx)
260 {
261   cj_t *db = (cj_t *)ctx;
262   db->state[db->depth].tree = NULL;
263   --db->depth;
264   return 1;
265 }
266
267 static int cj_cb_start_map (void *ctx)
268 {
269   return cj_cb_start (ctx);
270 }
271
272 static int cj_cb_end_map (void *ctx)
273 {
274   return cj_cb_end (ctx);
275 }
276
277 static int cj_cb_start_array (void * ctx)
278 {
279   return cj_cb_start (ctx);
280 }
281
282 static int cj_cb_end_array (void * ctx)
283 {
284   return cj_cb_start (ctx);
285 }
286
287 static yajl_callbacks ycallbacks = {
288   NULL, /* null */
289   NULL, /* boolean */
290   cj_cb_integer,
291   cj_cb_double,
292   NULL, /* number */
293   cj_cb_string,
294   cj_cb_start_map,
295   cj_cb_map_key,
296   cj_cb_end_map,
297   cj_cb_start_array,
298   cj_cb_end_array
299 };
300
301 /* end yajl callbacks */
302
303 static void cj_key_free (cj_key_t *key) /* {{{ */
304 {
305   if (key == NULL)
306     return;
307
308   sfree (key->path);
309   sfree (key->type);
310   sfree (key->instance);
311
312   sfree (key);
313 } /* }}} void cj_key_free */
314
315 static void cj_tree_free (c_avl_tree_t *tree) /* {{{ */
316 {
317   char *name;
318   void *value;
319
320   while (c_avl_pick (tree, (void *) &name, (void *) &value) == 0)
321   {
322     cj_key_t *key = (cj_key_t *)value;
323
324     if (CJ_IS_KEY(key))
325       cj_key_free (key);
326     else
327       cj_tree_free ((c_avl_tree_t *)value);
328
329     sfree (name);
330   }
331
332   c_avl_destroy (tree);
333 } /* }}} void cj_tree_free */
334
335 static void cj_free (void *arg) /* {{{ */
336 {
337   cj_t *db;
338
339   DEBUG ("curl_json plugin: cj_free (arg = %p);", arg);
340
341   db = (cj_t *) arg;
342
343   if (db == NULL)
344     return;
345
346   if (db->curl != NULL)
347     curl_easy_cleanup (db->curl);
348   db->curl = NULL;
349
350   if (db->tree != NULL)
351     cj_tree_free (db->tree);
352   db->tree = NULL;
353
354   sfree (db->instance);
355   sfree (db->host);
356
357   sfree (db->url);
358   sfree (db->user);
359   sfree (db->pass);
360   sfree (db->credentials);
361   sfree (db->cacert);
362
363   sfree (db);
364 } /* }}} void cj_free */
365
366 /* Configuration handling functions {{{ */
367
368 static int cj_config_add_string (const char *name, char **dest, /* {{{ */
369                                       oconfig_item_t *ci)
370 {
371   if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_STRING))
372   {
373     WARNING ("curl_json plugin: `%s' needs exactly one string argument.", name);
374     return (-1);
375   }
376
377   sfree (*dest);
378   *dest = strdup (ci->values[0].value.string);
379   if (*dest == NULL)
380     return (-1);
381
382   return (0);
383 } /* }}} int cj_config_add_string */
384
385 static int cj_config_set_boolean (const char *name, int *dest, /* {{{ */
386                                        oconfig_item_t *ci)
387 {
388   if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_BOOLEAN))
389   {
390     WARNING ("curl_json plugin: `%s' needs exactly one boolean argument.", name);
391     return (-1);
392   }
393
394   *dest = ci->values[0].value.boolean ? 1 : 0;
395
396   return (0);
397 } /* }}} int cj_config_set_boolean */
398
399 static c_avl_tree_t *cj_avl_create(void)
400 {
401   return c_avl_create ((int (*) (const void *, const void *)) strcmp);
402 }
403
404 static int cj_config_add_key (cj_t *db, /* {{{ */
405                                    oconfig_item_t *ci)
406 {
407   cj_key_t *key;
408   int status;
409   int i;
410
411   if ((ci->values_num != 1)
412       || (ci->values[0].type != OCONFIG_TYPE_STRING))
413   {
414     WARNING ("curl_json plugin: The `Key' block "
415              "needs exactly one string argument.");
416     return (-1);
417   }
418
419   key = (cj_key_t *) malloc (sizeof (*key));
420   if (key == NULL)
421   {
422     ERROR ("curl_json plugin: malloc failed.");
423     return (-1);
424   }
425   memset (key, 0, sizeof (*key));
426   key->magic = CJ_KEY_MAGIC;
427
428   if (strcasecmp ("Key", ci->key) == 0)
429   {
430     status = cj_config_add_string ("Key", &key->path, ci);
431     if (status != 0)
432     {
433       sfree (key);
434       return (status);
435     }
436   }
437   else
438   {
439     ERROR ("curl_json plugin: cj_config: "
440            "Invalid key: %s", ci->key);
441     return (-1);
442   }
443
444   status = 0;
445   for (i = 0; i < ci->children_num; i++)
446   {
447     oconfig_item_t *child = ci->children + i;
448
449     if (strcasecmp ("Type", child->key) == 0)
450       status = cj_config_add_string ("Type", &key->type, child);
451     else if (strcasecmp ("Instance", child->key) == 0)
452       status = cj_config_add_string ("Instance", &key->instance, child);
453     else
454     {
455       WARNING ("curl_json plugin: Option `%s' not allowed here.", child->key);
456       status = -1;
457     }
458
459     if (status != 0)
460       break;
461   } /* for (i = 0; i < ci->children_num; i++) */
462
463   while (status == 0)
464   {
465     if (key->type == NULL)
466     {
467       WARNING ("curl_json plugin: `Type' missing in `Key' block.");
468       status = -1;
469     }
470
471     break;
472   } /* while (status == 0) */
473
474   /* store path in a tree that will match the json map structure, example:
475    * "httpd/requests/count",
476    * "httpd/requests/current" ->
477    * { "httpd": { "requests": { "count": $key, "current": $key } } }
478    */
479   if (status == 0)
480   {
481     char *ptr;
482     char *name;
483     char ent[PATH_MAX];
484     c_avl_tree_t *tree;
485
486     if (db->tree == NULL)
487       db->tree = cj_avl_create();
488
489     tree = db->tree;
490     name = key->path;
491     ptr = key->path;
492     if (*ptr == '/')
493       ++ptr;
494
495     name = ptr;
496     while (*ptr)
497     {
498       if (*ptr == '/')
499       {
500         c_avl_tree_t *value;
501         int len;
502
503         len = ptr-name;
504         if (len == 0)
505           break;
506         sstrncpy (ent, name, len+1);
507
508         if (c_avl_get (tree, ent, (void *) &value) != 0)
509         {
510           value = cj_avl_create ();
511           c_avl_insert (tree, strdup (ent), value);
512         }
513
514         tree = value;
515         name = ptr+1;
516       }
517       ++ptr;
518     }
519     if (*name)
520       c_avl_insert (tree, strdup(name), key);
521     else
522     {
523       ERROR ("curl_json plugin: invalid key: %s", key->path);
524       status = -1;
525     }
526   }
527
528   return (status);
529 } /* }}} int cj_config_add_key */
530
531 static int cj_init_curl (cj_t *db) /* {{{ */
532 {
533   db->curl = curl_easy_init ();
534   if (db->curl == NULL)
535   {
536     ERROR ("curl_json plugin: curl_easy_init failed.");
537     return (-1);
538   }
539
540   curl_easy_setopt (db->curl, CURLOPT_WRITEFUNCTION, cj_curl_callback);
541   curl_easy_setopt (db->curl, CURLOPT_WRITEDATA, db);
542   curl_easy_setopt (db->curl, CURLOPT_USERAGENT,
543                     PACKAGE_NAME"/"PACKAGE_VERSION);
544   curl_easy_setopt (db->curl, CURLOPT_ERRORBUFFER, db->curl_errbuf);
545   curl_easy_setopt (db->curl, CURLOPT_URL, db->url);
546
547   if (db->user != NULL)
548   {
549     size_t credentials_size;
550
551     credentials_size = strlen (db->user) + 2;
552     if (db->pass != NULL)
553       credentials_size += strlen (db->pass);
554
555     db->credentials = (char *) malloc (credentials_size);
556     if (db->credentials == NULL)
557     {
558       ERROR ("curl_json plugin: malloc failed.");
559       return (-1);
560     }
561
562     ssnprintf (db->credentials, credentials_size, "%s:%s",
563                db->user, (db->pass == NULL) ? "" : db->pass);
564     curl_easy_setopt (db->curl, CURLOPT_USERPWD, db->credentials);
565   }
566
567   curl_easy_setopt (db->curl, CURLOPT_SSL_VERIFYPEER, db->verify_peer);
568   curl_easy_setopt (db->curl, CURLOPT_SSL_VERIFYHOST,
569                     db->verify_host ? 2 : 0);
570   if (db->cacert != NULL)
571     curl_easy_setopt (db->curl, CURLOPT_CAINFO, db->cacert);
572
573   return (0);
574 } /* }}} int cj_init_curl */
575
576 static int cj_config_add_url (oconfig_item_t *ci) /* {{{ */
577 {
578   cj_t *db;
579   int status = 0;
580   int i;
581
582   if ((ci->values_num != 1)
583       || (ci->values[0].type != OCONFIG_TYPE_STRING))
584   {
585     WARNING ("curl_json plugin: The `URL' block "
586              "needs exactly one string argument.");
587     return (-1);
588   }
589
590   db = (cj_t *) malloc (sizeof (*db));
591   if (db == NULL)
592   {
593     ERROR ("curl_json plugin: malloc failed.");
594     return (-1);
595   }
596   memset (db, 0, sizeof (*db));
597
598   if (strcasecmp ("URL", ci->key) == 0)
599   {
600     status = cj_config_add_string ("URL", &db->url, ci);
601     if (status != 0)
602     {
603       sfree (db);
604       return (status);
605     }
606   }
607   else
608   {
609     ERROR ("curl_json plugin: cj_config: "
610            "Invalid key: %s", ci->key);
611     return (-1);
612   }
613
614   /* Fill the `cj_t' structure.. */
615   for (i = 0; i < ci->children_num; i++)
616   {
617     oconfig_item_t *child = ci->children + i;
618
619     if (strcasecmp ("Instance", child->key) == 0)
620       status = cj_config_add_string ("Instance", &db->instance, child);
621     else if (strcasecmp ("Host", child->key) == 0)
622       status = cj_config_add_string ("Host", &db->host, child);
623     else if (strcasecmp ("User", child->key) == 0)
624       status = cj_config_add_string ("User", &db->user, child);
625     else if (strcasecmp ("Password", child->key) == 0)
626       status = cj_config_add_string ("Password", &db->pass, child);
627     else if (strcasecmp ("VerifyPeer", child->key) == 0)
628       status = cj_config_set_boolean ("VerifyPeer", &db->verify_peer, child);
629     else if (strcasecmp ("VerifyHost", child->key) == 0)
630       status = cj_config_set_boolean ("VerifyHost", &db->verify_host, child);
631     else if (strcasecmp ("CACert", child->key) == 0)
632       status = cj_config_add_string ("CACert", &db->cacert, child);
633     else if (strcasecmp ("Key", child->key) == 0)
634       status = cj_config_add_key (db, child);
635     else
636     {
637       WARNING ("curl_json plugin: Option `%s' not allowed here.", child->key);
638       status = -1;
639     }
640
641     if (status != 0)
642       break;
643   }
644
645   if (status == 0)
646   {
647     if (db->tree == NULL)
648     {
649       WARNING ("curl_json plugin: No (valid) `Key' block "
650                "within `URL' block `%s'.", db->url);
651       status = -1;
652     }
653     if (status == 0)
654       status = cj_init_curl (db);
655   }
656
657   /* If all went well, register this database for reading */
658   if (status == 0)
659   {
660     user_data_t ud;
661     char cb_name[DATA_MAX_NAME_LEN];
662
663     if (db->instance == NULL)
664       db->instance = strdup("default");
665
666     DEBUG ("curl_json plugin: Registering new read callback: %s",
667            db->instance);
668
669     memset (&ud, 0, sizeof (ud));
670     ud.data = (void *) db;
671     ud.free_func = cj_free;
672
673     ssnprintf (cb_name, sizeof (cb_name), "curl_json-%s-%s",
674                db->instance, db->url);
675
676     plugin_register_complex_read (/* group = */ NULL, cb_name, cj_read,
677                                   /* interval = */ NULL, &ud);
678   }
679   else
680   {
681     cj_free (db);
682     return (-1);
683   }
684
685   return (0);
686 }
687  /* }}} int cj_config_add_database */
688
689 static int cj_config (oconfig_item_t *ci) /* {{{ */
690 {
691   int success;
692   int errors;
693   int status;
694   int i;
695
696   success = 0;
697   errors = 0;
698
699   for (i = 0; i < ci->children_num; i++)
700   {
701     oconfig_item_t *child = ci->children + i;
702
703     if (strcasecmp ("URL", child->key) == 0)
704     {
705       status = cj_config_add_url (child);
706       if (status == 0)
707         success++;
708       else
709         errors++;
710     }
711     else
712     {
713       WARNING ("curl_json plugin: Option `%s' not allowed here.", child->key);
714       errors++;
715     }
716   }
717
718   if ((success == 0) && (errors > 0))
719   {
720     ERROR ("curl_json plugin: All statements failed.");
721     return (-1);
722   }
723
724   return (0);
725 } /* }}} int cj_config */
726
727 /* }}} End of configuration handling functions */
728
729 static void cj_submit (cj_t *db, cj_key_t *key, value_t *value) /* {{{ */
730 {
731   value_list_t vl = VALUE_LIST_INIT;
732   char *host;
733
734   vl.values     = value;
735   vl.values_len = 1;
736
737   if ((db->host == NULL)
738       || (strcmp ("", db->host) == 0)
739       || (strcmp (CJ_DEFAULT_HOST, db->host) == 0))
740     host = hostname_g;
741   else
742     host = db->host;
743
744   if (key->instance == NULL)
745     ssnprintf (vl.type_instance, sizeof (vl.type_instance), "%s-%s",
746                db->state[db->depth-1].name, db->state[db->depth].name);
747   else
748     sstrncpy (vl.type_instance, key->instance, sizeof (vl.type_instance));
749
750   sstrncpy (vl.host, host, sizeof (vl.host));
751   sstrncpy (vl.plugin, "curl_json", sizeof (vl.plugin));
752   sstrncpy (vl.plugin_instance, db->instance, sizeof (vl.plugin_instance));
753   sstrncpy (vl.type, key->type, sizeof (vl.type));
754
755   plugin_dispatch_values (&vl);
756 } /* }}} int cj_submit */
757
758 static int cj_curl_perform (cj_t *db, CURL *curl) /* {{{ */
759 {
760   int status;
761   long rc;
762   char *url;
763   yajl_handle yprev = db->yajl;
764
765   db->yajl = yajl_alloc (&ycallbacks, NULL, NULL, (void *)db);
766   if (db->yajl == NULL)
767   {
768     ERROR ("curl_json plugin: yajl_alloc failed.");
769     return (-1);
770   }
771
772   status = curl_easy_perform (curl);
773
774   yajl_free (db->yajl);
775   db->yajl = yprev;
776
777   curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &url);
778   curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &rc);
779
780   if (rc != 200)
781   {
782     ERROR ("curl_json plugin: curl_easy_perform failed with response code %ld (%s)",
783            rc, url);
784     return (-1);
785   }
786
787   if (status != 0)
788   {
789     ERROR ("curl_json plugin: curl_easy_perform failed with status %i: %s (%s)",
790            status, db->curl_errbuf, url);
791     return (-1);
792   }
793
794   return (0);
795 } /* }}} int cj_curl_perform */
796
797 static int cj_read (user_data_t *ud) /* {{{ */
798 {
799   cj_t *db;
800
801   if ((ud == NULL) || (ud->data == NULL))
802   {
803     ERROR ("curl_json plugin: cj_read: Invalid user data.");
804     return (-1);
805   }
806
807   db = (cj_t *) ud->data;
808
809   db->depth = 0;
810   memset (&db->state, 0, sizeof(db->state));
811   db->state[db->depth].tree = db->tree;
812   db->key = NULL;
813
814   return cj_curl_perform (db, db->curl);
815 } /* }}} int cj_read */
816
817 void module_register (void)
818 {
819   plugin_register_complex_config ("curl_json", cj_config);
820 } /* void module_register */
821
822 /* vim: set sw=2 sts=2 et fdm=marker : */