Merge branch 'collectd-4.4'
[collectd.git] / src / libvirt.c
1 /**
2  * collectd - src/libvirt.c
3  * Copyright (C) 2006-2008  Red Hat Inc.
4  *
5  * This program is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License as published by the
7  * Free Software Foundation; only version 2 of the license is applicable.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
17  *
18  * Authors:
19  *   Richard W.M. Jones <rjones@redhat.com>
20  **/
21
22 #include "collectd.h"
23 #include "common.h"
24 #include "plugin.h"
25 #include "configfile.h"
26 #include "utils_ignorelist.h"
27
28 #include <libvirt/libvirt.h>
29 #include <libvirt/virterror.h>
30 #include <libxml/parser.h>
31 #include <libxml/tree.h>
32 #include <libxml/xpath.h>
33
34 static const char *config_keys[] = {
35     "Connection",
36
37     "RefreshInterval",
38
39     "Domain",
40     "BlockDevice",
41     "InterfaceDevice",
42     "IgnoreSelected",
43
44     "HostnameFormat",
45
46     NULL
47 };
48 #define NR_CONFIG_KEYS ((sizeof config_keys / sizeof config_keys[0]) - 1)
49
50 /* Connection. */
51 static virConnectPtr conn = 0;
52
53 /* Seconds between list refreshes, 0 disables completely. */
54 static int interval = 60;
55
56 /* List of domains, if specified. */
57 static ignorelist_t *il_domains = NULL;
58 /* List of block devices, if specified. */
59 static ignorelist_t *il_block_devices = NULL;
60 /* List of network interface devices, if specified. */
61 static ignorelist_t *il_interface_devices = NULL;
62
63 static int ignore_device_match (ignorelist_t *,
64                                 const char *domname, const char *devpath);
65
66 /* Actual list of domains found on last refresh. */
67 static virDomainPtr *domains = NULL;
68 static int nr_domains = 0;
69
70 static void free_domains (void);
71 static int add_domain (virDomainPtr dom);
72
73 /* Actual list of block devices found on last refresh. */
74 struct block_device {
75     virDomainPtr dom;           /* domain */
76     char *path;                 /* name of block device */
77 };
78
79 static struct block_device *block_devices = NULL;
80 static int nr_block_devices = 0;
81
82 static void free_block_devices (void);
83 static int add_block_device (virDomainPtr dom, const char *path);
84
85 /* Actual list of network interfaces found on last refresh. */
86 struct interface_device {
87     virDomainPtr dom;           /* domain */
88     char *path;                 /* name of interface device */
89 };
90
91 static struct interface_device *interface_devices = NULL;
92 static int nr_interface_devices = 0;
93
94 static void free_interface_devices (void);
95 static int add_interface_device (virDomainPtr dom, const char *path);
96
97 /* HostnameFormat. */
98 #define HF_MAX_FIELDS 3
99
100 enum hf_field {
101     hf_none = 0,
102     hf_hostname,
103     hf_name,
104     hf_uuid
105 };
106
107 static enum hf_field hostname_format[HF_MAX_FIELDS] =
108     { hf_name };
109
110 /* Time that we last refreshed. */
111 static time_t last_refresh = (time_t) 0;
112
113 static int refresh_lists (void);
114
115 /* Submit functions. */
116 static void cpu_submit (unsigned long long cpu_time,
117                         time_t t,
118                         virDomainPtr dom, const char *type);
119 static void vcpu_submit (unsigned long long cpu_time,
120                          time_t t,
121                          virDomainPtr dom, int vcpu_nr, const char *type);
122 static void submit_counter2 (const char *type, counter_t v0, counter_t v1,
123              time_t t,
124              virDomainPtr dom, const char *devname);
125
126 /* ERROR(...) macro for virterrors. */
127 #define VIRT_ERROR(conn,s) do {                 \
128         virErrorPtr err;                        \
129         err = (conn) ? virConnGetLastError ((conn)) : virGetLastError (); \
130         if (err) ERROR ("%s: %s", (s), err->message);                   \
131     } while(0)
132
133 static int
134 lv_init (void)
135 {
136     if (virInitialize () != 0)
137         return -1;
138
139         return 0;
140 }
141
142 static int
143 lv_config (const char *key, const char *value)
144 {
145     if (virInitialize () != 0)
146         return 1;
147
148     if (il_domains == NULL)
149         il_domains = ignorelist_create (1);
150     if (il_block_devices == NULL)
151         il_block_devices = ignorelist_create (1);
152     if (il_interface_devices == NULL)
153         il_interface_devices = ignorelist_create (1);
154
155     if (strcasecmp (key, "Connection") == 0) {
156         if (conn != 0) {
157             ERROR ("Connection may only be given once in config file");
158             return 1;
159         }
160         conn = virConnectOpenReadOnly (value);
161         if (!conn) {
162             VIRT_ERROR (NULL, "connection failed");
163             return 1;
164         }
165         return 0;
166     }
167
168     if (strcasecmp (key, "RefreshInterval") == 0) {
169         char *eptr = NULL;
170         interval = strtol (value, &eptr, 10);
171         if (eptr == NULL || *eptr != '\0') return 1;
172         return 0;
173     }
174
175     if (strcasecmp (key, "Domain") == 0) {
176         if (ignorelist_add (il_domains, value)) return 1;
177         return 0;
178     }
179     if (strcasecmp (key, "BlockDevice") == 0) {
180         if (ignorelist_add (il_block_devices, value)) return 1;
181         return 0;
182     }
183     if (strcasecmp (key, "InterfaceDevice") == 0) {
184         if (ignorelist_add (il_interface_devices, value)) return 1;
185         return 0;
186     }
187
188     if (strcasecmp (key, "IgnoreSelected") == 0) {
189         if (strcasecmp (value, "True") == 0 ||
190             strcasecmp (value, "Yes") == 0 ||
191             strcasecmp (value, "On") == 0)
192         {
193             ignorelist_set_invert (il_domains, 0);
194             ignorelist_set_invert (il_block_devices, 0);
195             ignorelist_set_invert (il_interface_devices, 0);
196         }
197         else
198         {
199             ignorelist_set_invert (il_domains, 1);
200             ignorelist_set_invert (il_block_devices, 1);
201             ignorelist_set_invert (il_interface_devices, 1);
202         }
203         return 0;
204     }
205
206     if (strcasecmp (key, "HostnameFormat") == 0) {
207         char *value_copy;
208         char *fields[HF_MAX_FIELDS];
209         int i, n;
210
211         value_copy = strdup (value);
212         if (value_copy == NULL) {
213             ERROR ("libvirt plugin: strdup failed.");
214             return -1;
215         }
216
217         n = strsplit (value_copy, fields, HF_MAX_FIELDS);
218         if (n < 1) {
219             free (value_copy);
220             ERROR ("HostnameFormat: no fields");
221             return -1;
222         }
223
224         for (i = 0; i < n; ++i) {
225             if (strcasecmp (fields[i], "hostname") == 0)
226                 hostname_format[i] = hf_hostname;
227             else if (strcasecmp (fields[i], "name") == 0)
228                 hostname_format[i] = hf_name;
229             else if (strcasecmp (fields[i], "uuid") == 0)
230                 hostname_format[i] = hf_uuid;
231             else {
232                 free (value_copy);
233                 ERROR ("unknown HostnameFormat field: %s", fields[i]);
234                 return -1;
235             }
236         }
237         free (value_copy);
238
239         for (i = n; i < HF_MAX_FIELDS; ++i)
240             hostname_format[i] = hf_none;
241
242         return 0;
243     }
244
245     /* Unrecognised option. */
246     return -1;
247 }
248
249 static int
250 lv_read (void)
251 {
252     time_t t;
253     int i;
254
255     if (conn == NULL) {
256         ERROR ("libvirt plugin: Not connected. Use Connection in "
257                 "config file to supply connection URI.  For more information "
258                 "see <http://libvirt.org/uri.html>");
259         return -1;
260     }
261
262     time (&t);
263
264     /* Need to refresh domain or device lists? */
265     if ((last_refresh == (time_t) 0) ||
266             ((interval > 0) && ((last_refresh + interval) <= t))) {
267         if (refresh_lists () != 0)
268             return -1;
269         last_refresh = t;
270     }
271
272 #if 0
273     for (i = 0; i < nr_domains; ++i)
274         fprintf (stderr, "domain %s\n", virDomainGetName (domains[i]));
275     for (i = 0; i < nr_block_devices; ++i)
276         fprintf  (stderr, "block device %d %s:%s\n",
277                   i, virDomainGetName (block_devices[i].dom),
278                   block_devices[i].path);
279     for (i = 0; i < nr_interface_devices; ++i)
280         fprintf (stderr, "interface device %d %s:%s\n",
281                  i, virDomainGetName (interface_devices[i].dom),
282                  interface_devices[i].path);
283 #endif
284
285     /* Get CPU usage, VCPU usage for each domain. */
286     for (i = 0; i < nr_domains; ++i) {
287         virDomainInfo info;
288         virVcpuInfoPtr vinfo = NULL;
289         int j;
290
291         if (virDomainGetInfo (domains[i], &info) != 0)
292             continue;
293
294         cpu_submit (info.cpuTime, t, domains[i], "virt_cpu_total");
295
296         vinfo = malloc (info.nrVirtCpu * sizeof vinfo[0]);
297         if (vinfo == NULL) {
298             ERROR ("libvirt plugin: malloc failed.");
299             continue;
300         }
301
302         if (virDomainGetVcpus (domains[i], vinfo, info.nrVirtCpu,
303                     NULL, 0) != 0) {
304             free (vinfo);
305             continue;
306         }
307
308         for (j = 0; j < info.nrVirtCpu; ++j)
309             vcpu_submit (vinfo[j].cpuTime,
310                     t, domains[i], vinfo[j].number, "virt_vcpu");
311
312         free (vinfo);
313     }
314
315     /* Get block device stats for each domain. */
316     for (i = 0; i < nr_block_devices; ++i) {
317         struct _virDomainBlockStats stats;
318
319         if (virDomainBlockStats (block_devices[i].dom, block_devices[i].path,
320                     &stats, sizeof stats) != 0)
321             continue;
322
323         if ((stats.rd_req != -1) && (stats.wr_req != -1))
324             submit_counter2 ("disk_ops",
325                     (counter_t) stats.rd_req, (counter_t) stats.wr_req,
326                     t, block_devices[i].dom, block_devices[i].path);
327
328         if ((stats.rd_bytes != -1) && (stats.wr_bytes != -1))
329             submit_counter2 ("disk_octets",
330                     (counter_t) stats.rd_bytes, (counter_t) stats.wr_bytes,
331                     t, block_devices[i].dom, block_devices[i].path);
332     } /* for (nr_block_devices) */
333
334     /* Get interface stats for each domain. */
335     for (i = 0; i < nr_interface_devices; ++i) {
336         struct _virDomainInterfaceStats stats;
337
338         if (virDomainInterfaceStats (interface_devices[i].dom,
339                     interface_devices[i].path,
340                     &stats, sizeof stats) != 0)
341             continue;
342
343         if ((stats.rx_bytes != -1) && (stats.tx_bytes != -1))
344             submit_counter2 ("if_octets",
345                     (counter_t) stats.rx_bytes, (counter_t) stats.tx_bytes,
346                     t, interface_devices[i].dom, interface_devices[i].path);
347
348         if ((stats.rx_packets != -1) && (stats.tx_packets != -1))
349             submit_counter2 ("if_packets",
350                     (counter_t) stats.rx_packets, (counter_t) stats.tx_packets,
351                     t, interface_devices[i].dom, interface_devices[i].path);
352
353         if ((stats.rx_errs != -1) && (stats.tx_errs != -1))
354             submit_counter2 ("if_errors",
355                     (counter_t) stats.rx_errs, (counter_t) stats.tx_errs,
356                     t, interface_devices[i].dom, interface_devices[i].path);
357
358         if ((stats.rx_drop != -1) && (stats.tx_drop != -1))
359             submit_counter2 ("if_dropped",
360                     (counter_t) stats.rx_drop, (counter_t) stats.tx_drop,
361                     t, interface_devices[i].dom, interface_devices[i].path);
362     } /* for (nr_interface_devices) */
363
364     return 0;
365 }
366
367 static int
368 refresh_lists (void)
369 {
370     int n;
371
372     n = virConnectNumOfDomains (conn);
373     if (n < 0) {
374         VIRT_ERROR (conn, "reading number of domains");
375         return -1;
376     }
377
378     if (n > 0) {
379         int i;
380         int *domids;
381
382         /* Get list of domains. */
383         domids = malloc (sizeof (int) * n);
384         if (domids == 0) {
385             ERROR ("libvirt plugin: malloc failed.");
386             return -1;
387         }
388
389         n = virConnectListDomains (conn, domids, n);
390         if (n < 0) {
391             VIRT_ERROR (conn, "reading list of domains");
392             free (domids);
393             return -1;
394         }
395
396         free_block_devices ();
397         free_interface_devices ();
398         free_domains ();
399
400         /* Fetch each domain and add it to the list, unless ignore. */
401         for (i = 0; i < n; ++i) {
402             virDomainPtr dom = NULL;
403             const char *name;
404             char *xml = NULL;
405             xmlDocPtr xml_doc = NULL;
406             xmlXPathContextPtr xpath_ctx = NULL;
407             xmlXPathObjectPtr xpath_obj = NULL;
408             int j;
409
410             dom = virDomainLookupByID (conn, domids[i]);
411             if (dom == NULL) {
412                 VIRT_ERROR (conn, "virDomainLookupByID");
413                 /* Could be that the domain went away -- ignore it anyway. */
414                 continue;
415             }
416
417             name = virDomainGetName (dom);
418             if (name == NULL) {
419                 VIRT_ERROR (conn, "virDomainGetName");
420                 goto cont;
421             }
422
423             if (il_domains && ignorelist_match (il_domains, name) != 0)
424                 goto cont;
425
426             if (add_domain (dom) < 0) {
427                 ERROR ("libvirt plugin: malloc failed.");
428                 goto cont;
429             }
430
431             /* Get a list of devices for this domain. */
432             xml = virDomainGetXMLDesc (dom, 0);
433             if (!xml) {
434                 VIRT_ERROR (conn, "virDomainGetXMLDesc");
435                 goto cont;
436             }
437
438             /* Yuck, XML.  Parse out the devices. */
439             xml_doc = xmlReadDoc ((xmlChar *) xml, NULL, NULL, XML_PARSE_NONET);
440             if (xml_doc == NULL) {
441                 VIRT_ERROR (conn, "xmlReadDoc");
442                 goto cont;
443             }
444
445             xpath_ctx = xmlXPathNewContext (xml_doc);
446
447             /* Block devices. */
448             xpath_obj = xmlXPathEval
449                 ((xmlChar *) "/domain/devices/disk/target[@dev]",
450                  xpath_ctx);
451             if (xpath_obj == NULL || xpath_obj->type != XPATH_NODESET ||
452                 xpath_obj->nodesetval == NULL)
453                 goto cont;
454
455             for (j = 0; j < xpath_obj->nodesetval->nodeNr; ++j) {
456                 xmlNodePtr node;
457                 char *path = NULL;
458
459                 node = xpath_obj->nodesetval->nodeTab[j];
460                 if (!node) continue;
461                 path = (char *) xmlGetProp (node, (xmlChar *) "dev");
462                 if (!path) continue;
463
464                 if (il_block_devices &&
465                     ignore_device_match (il_block_devices, name, path) != 0)
466                     goto cont2;
467
468                 add_block_device (dom, path);
469             cont2:
470                 if (path) xmlFree (path);
471             }
472             xmlXPathFreeObject (xpath_obj);
473
474             /* Network interfaces. */
475             xpath_obj = xmlXPathEval
476                 ((xmlChar *) "/domain/devices/interface/target[@dev]",
477                  xpath_ctx);
478             if (xpath_obj == NULL || xpath_obj->type != XPATH_NODESET ||
479                 xpath_obj->nodesetval == NULL)
480                 goto cont;
481
482             for (j = 0; j < xpath_obj->nodesetval->nodeNr; ++j) {
483                 xmlNodePtr node;
484                 char *path = NULL;
485
486                 node = xpath_obj->nodesetval->nodeTab[j];
487                 if (!node) continue;
488                 path = (char *) xmlGetProp (node, (xmlChar *) "dev");
489                 if (!path) continue;
490
491                 if (il_interface_devices &&
492                     ignore_device_match (il_interface_devices, name, path) != 0)
493                     goto cont3;
494
495                 add_interface_device (dom, path);
496             cont3:
497                 if (path) xmlFree (path);
498             }
499
500         cont:
501             if (xpath_obj) xmlXPathFreeObject (xpath_obj);
502             if (xpath_ctx) xmlXPathFreeContext (xpath_ctx);
503             if (xml_doc) xmlFreeDoc (xml_doc);
504             if (xml) free (xml);
505         }
506
507         free (domids);
508     }
509
510     return 0;
511 }
512
513 static void
514 free_domains ()
515 {
516     int i;
517
518     if (domains) {
519         for (i = 0; i < nr_domains; ++i)
520             virDomainFree (domains[i]);
521         free (domains);
522     }
523     domains = NULL;
524     nr_domains = 0;
525 }
526
527 static int
528 add_domain (virDomainPtr dom)
529 {
530     virDomainPtr *new_ptr;
531     int new_size = sizeof (domains[0]) * (nr_domains+1);
532
533     if (domains)
534         new_ptr = realloc (domains, new_size);
535     else
536         new_ptr = malloc (new_size);
537
538     if (new_ptr == NULL)
539         return -1;
540
541     domains = new_ptr;
542     domains[nr_domains] = dom;
543     return nr_domains++;
544 }
545
546 static void
547 free_block_devices ()
548 {
549     int i;
550
551     if (block_devices) {
552         for (i = 0; i < nr_block_devices; ++i)
553             free (block_devices[i].path);
554         free (block_devices);
555     }
556     block_devices = NULL;
557     nr_block_devices = 0;
558 }
559
560 static int
561 add_block_device (virDomainPtr dom, const char *path)
562 {
563     struct block_device *new_ptr;
564     int new_size = sizeof (block_devices[0]) * (nr_block_devices+1);
565     char *path_copy;
566
567     path_copy = strdup (path);
568     if (!path_copy)
569         return -1;
570
571     if (block_devices)
572         new_ptr = realloc (block_devices, new_size);
573     else
574         new_ptr = malloc (new_size);
575
576     if (new_ptr == NULL) {
577         free (path_copy);
578         return -1;
579     }
580     block_devices = new_ptr;
581     block_devices[nr_block_devices].dom = dom;
582     block_devices[nr_block_devices].path = path_copy;
583     return nr_block_devices++;
584 }
585
586 static void
587 free_interface_devices ()
588 {
589     int i;
590
591     if (interface_devices) {
592         for (i = 0; i < nr_interface_devices; ++i)
593             free (interface_devices[i].path);
594         free (interface_devices);
595     }
596     interface_devices = NULL;
597     nr_interface_devices = 0;
598 }
599
600 static int
601 add_interface_device (virDomainPtr dom, const char *path)
602 {
603     struct interface_device *new_ptr;
604     int new_size = sizeof (interface_devices[0]) * (nr_interface_devices+1);
605     char *path_copy;
606
607     path_copy = strdup (path);
608     if (!path_copy) return -1;
609
610     if (interface_devices)
611         new_ptr = realloc (interface_devices, new_size);
612     else
613         new_ptr = malloc (new_size);
614
615     if (new_ptr == NULL) {
616         free (path_copy);
617         return -1;
618     }
619     interface_devices = new_ptr;
620     interface_devices[nr_interface_devices].dom = dom;
621     interface_devices[nr_interface_devices].path = path_copy;
622     return nr_interface_devices++;
623 }
624
625 static int
626 ignore_device_match (ignorelist_t *il, const char *domname, const char *devpath)
627 {
628     char *name;
629     int n, r;
630
631     n = sizeof (char) * (strlen (domname) + strlen (devpath) + 2);
632     name = malloc (n);
633     if (name == NULL) {
634         ERROR ("libvirt plugin: malloc failed.");
635         return 0;
636     }
637     ssnprintf (name, n, "%s:%s", domname, devpath);
638     r = ignorelist_match (il, name);
639     free (name);
640     return r;
641 }
642
643 static void
644 init_value_list (value_list_t *vl, time_t t, virDomainPtr dom)
645 {
646     int i, n;
647     const char *name;
648     char uuid[VIR_UUID_STRING_BUFLEN];
649     char  *host_ptr;
650     size_t host_len;
651
652     vl->time = t;
653     vl->interval = interval_g;
654
655     sstrncpy (vl->plugin, "libvirt", sizeof (vl->plugin));
656
657     vl->host[0] = '\0';
658     host_ptr = vl->host;
659     host_len = sizeof (vl->host);
660
661     /* Construct the hostname field according to HostnameFormat. */
662     for (i = 0; i < HF_MAX_FIELDS; ++i) {
663         if (hostname_format[i] == hf_none)
664             continue;
665
666         n = DATA_MAX_NAME_LEN - strlen (vl->host) - 2;
667
668         if (i > 0 && n >= 1) {
669             strncat (vl->host, ":", 1);
670             n--;
671         }
672
673         switch (hostname_format[i]) {
674         case hf_none: break;
675         case hf_hostname:
676             strncat (vl->host, hostname_g, n);
677             break;
678         case hf_name:
679             name = virDomainGetName (dom);
680             if (name)
681                 strncat (vl->host, name, n);
682             break;
683         case hf_uuid:
684             if (virDomainGetUUIDString (dom, uuid) == 0)
685                 strncat (vl->host, uuid, n);
686             break;
687         }
688     }
689
690     vl->host[sizeof (vl->host) - 1] = '\0';
691 } /* void init_value_list */
692
693 static void
694 cpu_submit (unsigned long long cpu_time,
695             time_t t,
696             virDomainPtr dom, const char *type)
697 {
698     value_t values[1];
699     value_list_t vl = VALUE_LIST_INIT;
700
701     init_value_list (&vl, t, dom);
702
703     values[0].counter = cpu_time;
704
705     vl.values = values;
706     vl.values_len = 1;
707
708     sstrncpy (vl.type, type, sizeof (vl.type));
709
710     plugin_dispatch_values (&vl);
711 }
712
713 static void
714 vcpu_submit (counter_t cpu_time,
715              time_t t,
716              virDomainPtr dom, int vcpu_nr, const char *type)
717 {
718     value_t values[1];
719     value_list_t vl = VALUE_LIST_INIT;
720
721     init_value_list (&vl, t, dom);
722
723     values[0].counter = cpu_time;
724     vl.values = values;
725     vl.values_len = 1;
726
727     sstrncpy (vl.type, type, sizeof (vl.type));
728     ssnprintf (vl.type_instance, sizeof (vl.type_instance), "%d", vcpu_nr);
729
730     plugin_dispatch_values (&vl);
731 }
732
733 static void
734 submit_counter2 (const char *type, counter_t v0, counter_t v1,
735              time_t t,
736              virDomainPtr dom, const char *devname)
737 {
738     value_t values[2];
739     value_list_t vl = VALUE_LIST_INIT;
740
741     init_value_list (&vl, t, dom);
742
743     values[0].counter = v0;
744     values[1].counter = v1;
745     vl.values = values;
746     vl.values_len = 2;
747
748     sstrncpy (vl.type, type, sizeof (vl.type));
749     sstrncpy (vl.type_instance, devname, sizeof (vl.type_instance));
750
751     plugin_dispatch_values (&vl);
752 } /* void submit_counter2 */
753
754 static int
755 lv_shutdown (void)
756 {
757     free_block_devices ();
758     free_interface_devices ();
759     free_domains ();
760
761     if (conn != NULL)
762         virConnectClose (conn);
763     conn = NULL;
764
765     ignorelist_free (il_domains);
766     il_domains = NULL;
767     ignorelist_free (il_block_devices);
768     il_block_devices = NULL;
769     ignorelist_free (il_interface_devices);
770     il_interface_devices = NULL;
771
772     return 0;
773 }
774
775 void
776 module_register (void)
777 {
778     plugin_register_config ("libvirt",
779             lv_config,
780             config_keys, NR_CONFIG_KEYS);
781     plugin_register_init ("libvirt", lv_init);
782     plugin_register_read ("libvirt", lv_read);
783     plugin_register_shutdown ("libvirt", lv_shutdown);
784 }
785
786 /*
787  * vim: shiftwidth=4 tabstop=8 softtabstop=4 expandtab fdm=marker
788  */