Merge branch 'ps/http'
authorFlorian Forster <octo@leeloo.lan.home.verplant.org>
Sat, 29 Aug 2009 08:45:38 +0000 (10:45 +0200)
committerFlorian Forster <octo@leeloo.lan.home.verplant.org>
Sat, 29 Aug 2009 08:45:38 +0000 (10:45 +0200)
AUTHORS
README
configure.in
src/Makefile.am
src/collectd.conf.in
src/collectd.conf.pod
src/plugin.c
src/write_http.c [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
index b896914..f86d6d3 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -110,6 +110,7 @@ Ondrej Zajicek <santiago at crfreenet.org>
 Paul Sadauskas <psadauskas at gmail.com>
  - tokyotyrant plugin.
  - `ReportByDevice' option of the df plugin.
+ - write_http plugin.
 
 Peter Holik <peter at holik.at>
  - cpufreq plugin.
diff --git a/README b/README
index d5b796c..270c3c4 100644 (file)
--- a/README
+++ b/README
@@ -262,6 +262,9 @@ Features
     - wireless
       Link quality of wireless cards. Linux only.
 
+    - write_http
+      Send data to a web-server using POST requests.
+
     - xmms
       Bitrate and frequency of music played with XMMS.
 
@@ -442,7 +445,8 @@ Prerequisites
     Used by the `oracle' plugin.
 
   * libcurl (optional)
-    If you want to use the `apache', `ascent', `curl' or `nginx' plugin.
+    If you want to use the `apache', `ascent', `curl', `nginx', or `write_http'
+    plugin.
     <http://curl.haxx.se/>
 
   * libdbi (optional)
index fbbe488..4732313 100644 (file)
@@ -3785,6 +3785,7 @@ AC_PLUGIN([uuid],        [yes],                [UUID as hostname plugin])
 AC_PLUGIN([vmem],        [$plugin_vmem],       [Virtual memory statistics])
 AC_PLUGIN([vserver],     [$plugin_vserver],    [Linux VServer statistics])
 AC_PLUGIN([wireless],    [$plugin_wireless],   [Wireless statistics])
+AC_PLUGIN([write_http],  [$with_libcurl],      [HTTP output plugin])
 AC_PLUGIN([xmms],        [$with_libxmms],      [XMMS statistics])
 
 dnl Default configuration file
@@ -4083,6 +4084,7 @@ Configuration:
     vmem  . . . . . . . . $enable_vmem
     vserver . . . . . . . $enable_vserver
     wireless  . . . . . . $enable_wireless
+    write_http  . . . . . $enable_write_http
     xmms  . . . . . . . . $enable_xmms
 
 EOF
index beef39a..722d3c5 100644 (file)
@@ -1042,6 +1042,20 @@ collectd_LDADD += "-dlopen" wireless.la
 collectd_DEPENDENCIES += wireless.la
 endif
 
+if BUILD_PLUGIN_WRITE_HTTP
+pkglib_LTLIBRARIES += write_http.la
+write_http_la_SOURCES = write_http.c
+write_http_la_LDFLAGS = -module -avoid-version
+write_http_la_CFLAGS = $(AM_CFLAGS)
+write_http_la_LIBADD =
+collectd_LDADD += "-dlopen" write_http.la
+if BUILD_WITH_LIBCURL
+write_http_la_CFLAGS += $(BUILD_WITH_LIBCURL_CFLAGS)
+write_http_la_LIBADD += $(BUILD_WITH_LIBCURL_LIBS)
+endif
+collectd_DEPENDENCIES += write_http.la
+endif
+
 if BUILD_PLUGIN_XMMS
 pkglib_LTLIBRARIES += xmms.la
 xmms_la_SOURCES = xmms.c
index 003cf16..9a54245 100644 (file)
@@ -73,6 +73,7 @@ FQDNLookup   true
 #@BUILD_PLUGIN_FSCACHE_TRUE@LoadPlugin fscache
 #@BUILD_PLUGIN_GMOND_TRUE@LoadPlugin gmond
 #@BUILD_PLUGIN_HDDTEMP_TRUE@LoadPlugin hddtemp
+#@BUILD_PLUGIN_HTTP_TRUE@LoadPlugin http
 @BUILD_PLUGIN_INTERFACE_TRUE@@BUILD_PLUGIN_INTERFACE_TRUE@LoadPlugin interface
 #@BUILD_PLUGIN_IPTABLES_TRUE@LoadPlugin iptables
 #@BUILD_PLUGIN_IPMI_TRUE@LoadPlugin ipmi
@@ -126,6 +127,7 @@ FQDNLookup   true
 #@BUILD_PLUGIN_VMEM_TRUE@LoadPlugin vmem
 #@BUILD_PLUGIN_VSERVER_TRUE@LoadPlugin vserver
 #@BUILD_PLUGIN_WIRELESS_TRUE@LoadPlugin wireless
+#@BUILD_PLUGIN_WRITE_HTTP_TRUE@LoadPlugin write_http
 #@BUILD_PLUGIN_XMMS_TRUE@LoadPlugin xmms
 
 ##############################################################################
@@ -311,6 +313,12 @@ FQDNLookup   true
 #  TranslateDevicename false
 #</Plugin>
 
+#<Plugin http>
+#      URL "http://example.com/collectd-import"
+#      User "www-user"
+#      Password "secret"
+#</Plugin>
+
 #<Plugin interface>
 #      Interface "eth0"
 #      IgnoreSelected false
@@ -706,6 +714,13 @@ FQDNLookup   true
 #      Verbose false
 #</Plugin>
 
+#<Plugin write_http>
+#      <URL "http://example.com/collectd-post">
+#              User "collectd"
+#              Password "weCh3ik0"
+#      </URL>
+#</Plugin>
+
 ##############################################################################
 # Filter configuration                                                       #
 #----------------------------------------------------------------------------#
index bb95c31..9419fb9 100644 (file)
@@ -3454,6 +3454,56 @@ traffic (e.E<nbsp>g. due to headers and retransmission). If you want to
 collect on-wire traffic you could, for example, use the logging facilities of
 iptables to feed data for the guest IPs into the iptables plugin.
 
+=head2 Plugin C<write_http>
+
+This output plugin submits values to an http server by POST them using the
+PUTVAL plain-text protocol. Each destination you want to post data to needs to
+have one B<URL> block, within which the destination can be configured further,
+for example by specifying authentication data.
+
+Synopsis:
+
+ <Plugin "write_http">
+   <URL "http://example.com/post-collectd">
+     User "collectd"
+     Password "weCh3ik0"
+   </URL>
+ </Plugin>
+
+B<URL> blocks need one string argument which is used as the URL to which data
+is posted. The following options are understood within B<URL> blocks.
+
+=over 4
+
+=item B<User> I<Username>
+
+Optional user name needed for authentication.
+
+=item B<Password> I<Password>
+
+Optional password needed for authentication.
+
+=item B<VerifyPeer> B<true>|B<false>
+
+Enable or disable peer SSL certificate verification. See
+L<http://curl.haxx.se/docs/sslcerts.html> for details. Enabled by default.
+
+=item B<VerifyHost> B<true|false>
+
+Enable or disable peer host name verification. If enabled, the plugin checks if
+the C<Common Name> or a C<Subject Alternate Name> field of the SSL certificate
+matches the host name provided by the B<URL> option. If this identity check
+fails, the connection is aborted. Obviously, only works when connecting to a
+SSL enabled server. Enabled by default.
+
+=item B<CACert> I<File>
+
+File that holds one or more SSL certificates. If you want to use HTTPS you will
+possibly need this option. What CA certificates come bundled with C<libcurl>
+and are checked by default depends on the distribution you use.
+
+=back
+
 =head1 THRESHOLD CONFIGURATION
 
 Starting with version C<4.3.0> collectd has support for B<monitoring>. By that
index b150cf6..7f37fa7 100644 (file)
@@ -1205,8 +1205,14 @@ void plugin_shutdown_all (void)
                (*callback) ();
        }
 
-       destroy_all_callbacks (&list_write);
+       /* Write plugins which use the `user_data' pointer usually need the
+        * same data available to the flush callback. If this is the case, set
+        * the free_function to NULL when registering the flush callback and to
+        * the real free function when registering the write callback. This way
+        * the data isn't freed twice. */
        destroy_all_callbacks (&list_flush);
+       destroy_all_callbacks (&list_write);
+
        destroy_all_callbacks (&list_notification);
        destroy_all_callbacks (&list_shutdown);
        destroy_all_callbacks (&list_log);
diff --git a/src/write_http.c b/src/write_http.c
new file mode 100644 (file)
index 0000000..b17a342
--- /dev/null
@@ -0,0 +1,501 @@
+/**
+ * collectd - src/write_http.c
+ * Copyright (C) 2009       Paul Sadauskas
+ * Copyright (C) 2009       Doug MacEachern
+ * Copyright (C) 2007-2009  Florian octo Forster
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; only version 2 of the License is applicable.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+ *
+ * Authors:
+ *   Florian octo Forster <octo at verplant.org>
+ *   Doug MacEachern <dougm@hyperic.com>
+ *   Paul Sadauskas <psadauskas@gmail.com>
+ **/
+
+#include "collectd.h"
+#include "plugin.h"
+#include "common.h"
+#include "utils_cache.h"
+#include "utils_parse_option.h"
+
+#if HAVE_PTHREAD_H
+# include <pthread.h>
+#endif
+
+#include <curl/curl.h>
+
+/*
+ * Private variables
+ */
+struct wh_callback_s
+{
+        char *location;
+
+        char *user;
+        char *pass;
+        char *credentials;
+        int   verify_peer;
+        int   verify_host;
+        char *cacert;
+
+        CURL *curl;
+        char curl_errbuf[CURL_ERROR_SIZE];
+
+        char   send_buffer[4096];
+        size_t send_buffer_free;
+        size_t send_buffer_fill;
+        time_t send_buffer_init_time;
+
+        pthread_mutex_t send_lock;
+};
+typedef struct wh_callback_s wh_callback_t;
+
+static void wh_reset_buffer (wh_callback_t *cb)  /* {{{ */
+{
+        memset (cb->send_buffer, 0, sizeof (cb->send_buffer));
+        cb->send_buffer_free = sizeof (cb->send_buffer);
+        cb->send_buffer_fill = 0;
+        cb->send_buffer_init_time = time (NULL);
+} /* }}} wh_reset_buffer */
+
+static int wh_send_buffer (wh_callback_t *cb) /* {{{ */
+{
+        int status = 0;
+
+        curl_easy_setopt (cb->curl, CURLOPT_POSTFIELDS, cb->send_buffer);
+        status = curl_easy_perform (cb->curl);
+        if (status != 0)
+        {
+                ERROR ("write_http plugin: curl_easy_perform failed with "
+                                "staus %i: %s",
+                                status, cb->curl_errbuf);
+        }
+        return (status);
+} /* }}} wh_send_buffer */
+
+static int wh_callback_init (wh_callback_t *cb) /* {{{ */
+{
+        struct curl_slist *headers;
+
+        if (cb->curl != NULL)
+                return (0);
+
+        cb->curl = curl_easy_init ();
+        if (cb->curl == NULL)
+        {
+                ERROR ("curl plugin: curl_easy_init failed.");
+                return (-1);
+        }
+
+        curl_easy_setopt (cb->curl, CURLOPT_USERAGENT, PACKAGE_NAME"/"PACKAGE_VERSION);
+
+        headers = NULL;
+        headers = curl_slist_append (headers, "Accept:  */*");
+        headers = curl_slist_append (headers, "Content-Type: text/plain");
+        curl_easy_setopt (cb->curl, CURLOPT_HTTPHEADER, headers);
+
+        curl_easy_setopt (cb->curl, CURLOPT_ERRORBUFFER, cb->curl_errbuf);
+        curl_easy_setopt (cb->curl, CURLOPT_URL, cb->location);
+
+        if (cb->user != NULL)
+        {
+                size_t credentials_size;
+
+                credentials_size = strlen (cb->user) + 2;
+                if (cb->pass != NULL)
+                        credentials_size += strlen (cb->pass);
+
+                cb->credentials = (char *) malloc (credentials_size);
+                if (cb->credentials == NULL)
+                {
+                        ERROR ("curl plugin: malloc failed.");
+                        return (-1);
+                }
+
+                ssnprintf (cb->credentials, credentials_size, "%s:%s",
+                                cb->user, (cb->pass == NULL) ? "" : cb->pass);
+                curl_easy_setopt (cb->curl, CURLOPT_USERPWD, cb->credentials);
+                curl_easy_setopt (cb->curl, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
+        }
+
+        curl_easy_setopt (cb->curl, CURLOPT_SSL_VERIFYPEER, cb->verify_peer);
+        curl_easy_setopt (cb->curl, CURLOPT_SSL_VERIFYHOST,
+                        cb->verify_host ? 2 : 0);
+        if (cb->cacert != NULL)
+                curl_easy_setopt (cb->curl, CURLOPT_CAINFO, cb->cacert);
+
+        wh_reset_buffer (cb);
+
+        return (0);
+} /* }}} int wh_callback_init */
+
+static int wh_flush_nolock (int timeout, wh_callback_t *cb) /* {{{ */
+{
+        int status;
+
+        DEBUG ("write_http plugin: wh_flush_nolock: timeout = %i; "
+                        "send_buffer_fill = %zu;",
+                        timeout, cb->send_buffer_fill);
+
+        if (timeout > 0)
+        {
+                time_t now;
+
+                now = time (NULL);
+                if ((cb->send_buffer_init_time + timeout) > now)
+                        return (0);
+        }
+
+        if (cb->send_buffer_fill <= 0)
+        {
+                cb->send_buffer_init_time = time (NULL);
+                return (0);
+        }
+
+        status = wh_send_buffer (cb);
+        wh_reset_buffer (cb);
+
+        return (status);
+} /* }}} wh_flush_nolock */
+
+static int wh_flush (int timeout, /* {{{ */
+                const char *identifier __attribute__((unused)),
+                user_data_t *user_data)
+{
+        wh_callback_t *cb;
+        int status;
+
+        if (user_data == NULL)
+                return (-EINVAL);
+
+        cb = user_data->data;
+
+        pthread_mutex_lock (&cb->send_lock);
+
+        if (cb->curl == NULL)
+        {
+                status = wh_callback_init (cb);
+                if (status != 0)
+                {
+                        ERROR ("write_http plugin: wh_callback_init failed.");
+                        pthread_mutex_unlock (&cb->send_lock);
+                        return (-1);
+                }
+        }
+
+        status = wh_flush_nolock (timeout, cb);
+        pthread_mutex_unlock (&cb->send_lock);
+
+        return (status);
+} /* }}} int wh_flush */
+
+static void wh_callback_free (void *data) /* {{{ */
+{
+        wh_callback_t *cb;
+
+        if (data == NULL)
+                return;
+
+        cb = data;
+
+        wh_flush_nolock (/* timeout = */ -1, cb);
+
+        curl_easy_cleanup (cb->curl);
+        sfree (cb->location);
+        sfree (cb->user);
+        sfree (cb->pass);
+        sfree (cb->credentials);
+        sfree (cb->cacert);
+
+        sfree (cb);
+} /* }}} void wh_callback_free */
+
+static int wh_value_list_to_string (char *buffer, /* {{{ */
+                size_t buffer_size,
+                const data_set_t *ds, const value_list_t *vl)
+{
+        size_t offset = 0;
+        int status;
+        int i;
+
+        assert (0 == strcmp (ds->type, vl->type));
+
+        memset (buffer, 0, buffer_size);
+
+#define BUFFER_ADD(...) do { \
+        status = ssnprintf (buffer + offset, buffer_size - offset, \
+                        __VA_ARGS__); \
+        if (status < 1) \
+                return (-1); \
+        else if (((size_t) status) >= (buffer_size - offset)) \
+                return (-1); \
+        else \
+                offset += ((size_t) status); \
+} while (0)
+
+        BUFFER_ADD ("%lu", (unsigned long) vl->time);
+
+        for (i = 0; i < ds->ds_num; i++)
+{
+        if (ds->ds[i].type == DS_TYPE_GAUGE)
+                BUFFER_ADD (":%f", vl->values[i].gauge);
+        else if (ds->ds[i].type == DS_TYPE_COUNTER)
+                BUFFER_ADD (":%llu", vl->values[i].counter);
+        else if (ds->ds[i].type == DS_TYPE_DERIVE)
+                BUFFER_ADD (":%"PRIi64, vl->values[i].derive);
+        else if (ds->ds[i].type == DS_TYPE_ABSOLUTE)
+                BUFFER_ADD (":%"PRIu64, vl->values[i].absolute);
+        else
+        {
+                ERROR ("write_http plugin: Unknown data source type: %i",
+                                ds->ds[i].type);
+                return (-1);
+        }
+} /* for ds->ds_num */
+
+#undef BUFFER_ADD
+
+return (0);
+} /* }}} int wh_value_list_to_string */
+
+static int wh_write_command (const data_set_t *ds, const value_list_t *vl, /* {{{ */
+                wh_callback_t *cb)
+{
+        char key[10*DATA_MAX_NAME_LEN];
+        char values[512];
+        char command[1024];
+        size_t command_len;
+
+        int status;
+
+        if (0 != strcmp (ds->type, vl->type)) {
+                ERROR ("write_http plugin: DS type does not match "
+                                "value list type");
+                return -1;
+        }
+
+        /* Copy the identifier to `key' and escape it. */
+        status = FORMAT_VL (key, sizeof (key), vl);
+        if (status != 0) {
+                ERROR ("write_http plugin: error with format_name");
+                return (status);
+        }
+        escape_string (key, sizeof (key));
+
+        /* Convert the values to an ASCII representation and put that into
+         * `values'. */
+        status = wh_value_list_to_string (values, sizeof (values), ds, vl);
+        if (status != 0) {
+                ERROR ("write_http plugin: error with "
+                                "wh_value_list_to_string");
+                return (status);
+        }
+
+        command_len = (size_t) ssnprintf (command, sizeof (command),
+                        "PUTVAL %s interval=%i %s\n",
+                        key, vl->interval, values);
+        if (command_len >= sizeof (command)) {
+                ERROR ("write_http plugin: Command buffer too small: "
+                                "Need %zu bytes.", command_len + 1);
+                return (-1);
+        }
+
+        pthread_mutex_lock (&cb->send_lock);
+
+        if (cb->curl == NULL)
+        {
+                status = wh_callback_init (cb);
+                if (status != 0)
+                {
+                        ERROR ("write_http plugin: wh_callback_init failed.");
+                        pthread_mutex_unlock (&cb->send_lock);
+                        return (-1);
+                }
+        }
+
+        if (command_len >= cb->send_buffer_free)
+        {
+                status = wh_flush_nolock (/* timeout = */ -1, cb);
+                if (status != 0)
+                {
+                        pthread_mutex_unlock (&cb->send_lock);
+                        return (status);
+                }
+        }
+        assert (command_len < cb->send_buffer_free);
+
+        /* `command_len + 1' because `command_len' does not include the
+         * trailing null byte. Neither does `send_buffer_fill'. */
+        memcpy (cb->send_buffer + cb->send_buffer_fill,
+                        command, command_len + 1);
+        cb->send_buffer_fill += command_len;
+        cb->send_buffer_free -= command_len;
+
+        DEBUG ("write_http plugin: <%s> buffer %zu/%zu (%g%%) \"%s\"",
+                        cb->location,
+                        cb->send_buffer_fill, sizeof (cb->send_buffer),
+                        100.0 * ((double) cb->send_buffer_fill) / ((double) sizeof (cb->send_buffer)),
+                        command);
+
+        /* Check if we have enough space for this command. */
+        pthread_mutex_unlock (&cb->send_lock);
+
+        return (0);
+} /* }}} int wh_write_command */
+
+static int wh_write (const data_set_t *ds, const value_list_t *vl, /* {{{ */
+                user_data_t *user_data)
+{
+        wh_callback_t *cb;
+        int status;
+
+        if (user_data == NULL)
+                return (-EINVAL);
+
+        cb = user_data->data;
+
+        status = wh_write_command (ds, vl, cb);
+        return (status);
+} /* }}} int wh_write */
+
+static int config_set_string (char **ret_string, /* {{{ */
+                oconfig_item_t *ci)
+{
+        char *string;
+
+        if ((ci->values_num != 1)
+                        || (ci->values[0].type != OCONFIG_TYPE_STRING))
+        {
+                WARNING ("write_http plugin: The `%s' config option "
+                                "needs exactly one string argument.", ci->key);
+                return (-1);
+        }
+
+        string = strdup (ci->values[0].value.string);
+        if (string == NULL)
+        {
+                ERROR ("write_http plugin: strdup failed.");
+                return (-1);
+        }
+
+        if (*ret_string != NULL)
+                free (*ret_string);
+        *ret_string = string;
+
+        return (0);
+} /* }}} int config_set_string */
+
+static int config_set_boolean (int *dest, oconfig_item_t *ci) /* {{{ */
+{
+        if ((ci->values_num != 1) || (ci->values[0].type != OCONFIG_TYPE_BOOLEAN))
+        {
+                WARNING ("write_http plugin: The `%s' config option "
+                                "needs exactly one boolean argument.", ci->key);
+                return (-1);
+        }
+
+        *dest = ci->values[0].value.boolean ? 1 : 0;
+
+        return (0);
+} /* }}} int config_set_boolean */
+
+static int wh_config_url (oconfig_item_t *ci) /* {{{ */
+{
+        wh_callback_t *cb;
+        user_data_t user_data;
+        int i;
+
+        cb = malloc (sizeof (*cb));
+        if (cb == NULL)
+        {
+                ERROR ("write_http plugin: malloc failed.");
+                return (-1);
+        }
+        memset (cb, 0, sizeof (*cb));
+        cb->location = NULL;
+        cb->user = NULL;
+        cb->pass = NULL;
+        cb->credentials = NULL;
+        cb->verify_peer = 1;
+        cb->verify_host = 1;
+        cb->cacert = NULL;
+        cb->curl = NULL;
+
+        pthread_mutex_init (&cb->send_lock, /* attr = */ NULL);
+
+        config_set_string (&cb->location, ci);
+        if (cb->location == NULL)
+                return (-1);
+
+        for (i = 0; i < ci->children_num; i++)
+        {
+                oconfig_item_t *child = ci->children + i;
+
+                if (strcasecmp ("User", child->key) == 0)
+                        config_set_string (&cb->user, child);
+                else if (strcasecmp ("Password", child->key) == 0)
+                        config_set_string (&cb->pass, child);
+                else if (strcasecmp ("VerifyPeer", child->key) == 0)
+                        config_set_boolean (&cb->verify_peer, child);
+                else if (strcasecmp ("VerifyHost", child->key) == 0)
+                        config_set_boolean (&cb->verify_host, child);
+                else if (strcasecmp ("CACert", child->key) == 0)
+                        config_set_string (&cb->cacert, child);
+                else
+                {
+                        ERROR ("write_http plugin: Invalid configuration "
+                                        "option: %s.", child->key);
+                }
+        }
+
+        DEBUG ("write_http: Registering write callback with URL %s",
+                        cb->location);
+
+        memset (&user_data, 0, sizeof (user_data));
+        user_data.data = cb;
+        user_data.free_func = NULL;
+        plugin_register_flush ("write_http", wh_flush, &user_data);
+
+        user_data.free_func = wh_callback_free;
+        plugin_register_write ("write_http", wh_write, &user_data);
+
+        return (0);
+} /* }}} int wh_config_url */
+
+static int wh_config (oconfig_item_t *ci) /* {{{ */
+{
+        int i;
+
+        for (i = 0; i < ci->children_num; i++)
+        {
+                oconfig_item_t *child = ci->children + i;
+
+                if (strcasecmp ("URL", child->key) == 0)
+                        wh_config_url (child);
+                else
+                {
+                        ERROR ("write_http plugin: Invalid configuration "
+                                        "option: %s.", child->key);
+                }
+        }
+
+        return (0);
+} /* }}} int wh_config */
+
+void module_register (void) /* {{{ */
+{
+        plugin_register_complex_config ("write_http", wh_config);
+} /* }}} void module_register */
+
+/* vim: set fdm=marker sw=8 ts=8 tw=78 et : */