]> git.ipfire.org Git - thirdparty/collectd.git/commitdiff
capabilities plugin: new plugin to read static platform data
authorKamil Wiatrowski <kamilx.wiatrowski@intel.com>
Fri, 17 May 2019 13:05:03 +0000 (15:05 +0200)
committerDagobert Michelsen <dam@opencsw.org>
Tue, 25 Feb 2020 16:18:16 +0000 (17:18 +0100)
Read decoded SMBIOS data from dmidecode and expose it through httpd.
Using libmicrohttpd and libjansson.

Change-Id: I7bcbb8a0747fbaaa3d5e4affe78b828ca0d8837d
Signed-off-by: Kamil Wiatrowski <kamilx.wiatrowski@intel.com>
Makefile.am
README
configure.ac
src/capabilities.c [new file with mode: 0644]
src/capabilities_test.c [new file with mode: 0644]
src/collectd.conf.in
src/collectd.conf.pod
src/utils/dmi/dmi.c [new file with mode: 0644]
src/utils/dmi/dmi.h [new file with mode: 0644]

index 0b34088776e6dba1ca83ad01b6847ced60101d21..dabc08ecf080b4ae79b3e97458c1b97ffa7b34cf 100644 (file)
@@ -757,6 +757,32 @@ buddyinfo_la_LDFLAGS = $(PLUGIN_LDFLAGS)
 buddyinfo_la_LIBADD = libignorelist.la
 endif
 
+if BUILD_PLUGIN_CAPABILITIES
+pkglib_LTLIBRARIES += capabilities.la
+capabilities_la_SOURCES = src/capabilities.c \
+                          src/utils/dmi/dmi.c \
+                          src/utils/dmi/dmi.h
+capabilities_la_CPPFLAGS = $(AM_CPPFLAGS) \
+       $(BUILD_WITH_LIBMICROHTTPD_CPPFLAGS) $(BUILD_WITH_LIBJANSSON_CPPFLAGS)
+capabilities_la_LDFLAGS = $(PLUGIN_LDFLAGS) \
+       $(BUILD_WITH_LIBMICROHTTPD_LDFLAGS) $(BUILD_WITH_LIBJANSSON_LDFLAGS)
+capabilities_la_LIBADD = $(BUILD_WITH_LIBMICROHTTPD_LIBS) \
+       $(BUILD_WITH_LIBJANSSON_LIBS)
+
+test_plugin_capabilities_SOURCES = \
+        src/capabilities_test.c \
+        src/daemon/configfile.c \
+        src/daemon/types_list.c
+test_plugin_capabilities_CPPFLAGS = $(AM_CPPFLAGS) \
+        $(BUILD_WITH_LIBJANSSON_CPPFLAGS)
+test_plugin_capabilities_LDFLAGS = $(PLUGIN_LDFLAGS) \
+        $(BUILD_WITH_LIBJANSSON_LDFLAGS)
+test_plugin_capabilities_LDADD = liboconfig.la libplugin_mock.la \
+        $(BUILD_WITH_LIBJANSSON_LIBS)
+check_PROGRAMS += test_plugin_capabilities
+TESTS += test_plugin_capabilities
+endif
+
 if BUILD_PLUGIN_CEPH
 pkglib_LTLIBRARIES += ceph.la
 ceph_la_SOURCES = src/ceph.c
diff --git a/README b/README
index bd75b071ce90df1e3bed736e40fcd2c6a818d305..54babf6ac5182fc9e82e726c6118a40241c868fb 100644 (file)
--- a/README
+++ b/README
@@ -48,6 +48,10 @@ Features
     - buddyinfo
       Statistics from buddyinfo file about memory fragmentation.
 
+    - capabilities
+      Platform static capabilities decoded from SMBIOS using dmidecode.
+      <https://www.nongnu.org/dmidecode/>
+
     - ceph
       Statistics from the Ceph distributed storage system.
 
@@ -843,7 +847,8 @@ Prerequisites
     <http://netfilter.org/>
 
   * libjansson (optional)
-    Used by the `dpdk_telemetry` plugin.
+    Parse JSON data. This is used for the `capabilities' and `dpdk_telemetry` plugins.
+    <http://www.digip.org/jansson/>
 
   * libjevents (optional)
     The jevents library is used by the `intel_pmu' plugin to access the Linux
index 6e886b94cda1ae466fdc4b8a4e5972ff96725ee5..9c1d0461f85fff513e638dcc2e311fd57d3e6d82 100644 (file)
@@ -6425,6 +6425,7 @@ plugin_barometer="no"
 plugin_battery="no"
 plugin_bind="no"
 plugin_buddyinfo="no"
+plugin_capabilities="no"
 plugin_ceph="no"
 plugin_cgroups="no"
 plugin_connectivity="no"
@@ -6559,6 +6560,10 @@ if test "x$ac_system" = "xLinux"; then
   if test "x$have_pci_regs_h" = "xyes"; then
     plugin_pcie_errors="yes"
   fi
+
+  if test "x$with_libmicrohttpd" = "xyes" && test "x$with_libjansson" = "xyes"; then
+    plugin_capabilities="yes"
+  fi
 fi
 
 if test "x$ac_system" = "xOpenBSD"; then
@@ -6868,6 +6873,7 @@ AC_PLUGIN([barometer],           [$plugin_barometer],         [Barometer sensor
 AC_PLUGIN([battery],             [$plugin_battery],           [Battery statistics])
 AC_PLUGIN([bind],                [$plugin_bind],              [ISC Bind nameserver statistics])
 AC_PLUGIN([buddyinfo],           [$plugin_buddyinfo],         [buddyinfo statistics])
+AC_PLUGIN([capabilities],        [$plugin_capabilities],      [Platform static capabilities])
 AC_PLUGIN([ceph],                [$plugin_ceph],              [Ceph daemon statistics])
 AC_PLUGIN([cgroups],             [$plugin_cgroups],           [CGroups CPU usage accounting])
 AC_PLUGIN([chrony],              [yes],                       [Chrony statistics])
@@ -7306,6 +7312,7 @@ AC_MSG_RESULT([    barometer . . . . . . $enable_barometer])
 AC_MSG_RESULT([    battery . . . . . . . $enable_battery])
 AC_MSG_RESULT([    bind  . . . . . . . . $enable_bind])
 AC_MSG_RESULT([    buddyinfo . . . . . . $enable_buddyinfo])
+AC_MSG_RESULT([    capabilities  . . . . $enable_capabilities])
 AC_MSG_RESULT([    ceph  . . . . . . . . $enable_ceph])
 AC_MSG_RESULT([    cgroups . . . . . . . $enable_cgroups])
 AC_MSG_RESULT([    chrony. . . . . . . . $enable_chrony])
diff --git a/src/capabilities.c b/src/capabilities.c
new file mode 100644 (file)
index 0000000..29bba04
--- /dev/null
@@ -0,0 +1,408 @@
+/*
+ * MIT License
+ *
+ * Copyright(c) 2019 Intel Corporation. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * Authors:
+ * Kamil Wiatrowski <kamilx.wiatrowski@intel.com>
+ *
+ */
+
+#include "collectd.h"
+
+#include "plugin.h"
+#include "utils/common/common.h"
+#include "utils/dmi/dmi.h"
+
+#include <microhttpd.h>
+
+#include <jansson.h>
+#include <netdb.h>
+
+#define CAP_PLUGIN "capabilities"
+#define CONTENT_TYPE_JSON "application/json"
+#define LISTEN_BACKLOG 16
+
+typedef struct dmi_type_name_s {
+  dmi_type type;
+  const char *name;
+} dmi_type_name_t;
+
+static char *g_cap_string = NULL;
+static char *httpd_host = NULL;
+static unsigned short httpd_port = 9104;
+static struct MHD_Daemon *httpd;
+
+static dmi_type_name_t types_list[] = {
+    {BIOS, "BIOS"},
+    {SYSTEM, "SYSTEM"},
+    {BASEBOARD, "BASEBOARD"},
+    {PROCESSOR, "PROCESSORS"},
+    {CACHE, "CACHE"},
+    {PHYSICAL_MEMORY_ARRAY, "PHYSICAL MEMORY ARRAYS"},
+    {MEMORY_DEVICE, "MEMORY DEVICES"},
+    {IPMI_DEVICE, "IPMI DEVICE"},
+    {ONBOARD_DEVICES_EXTENDED_INFORMATION,
+     "ONBOARD DEVICES EXTENDED INFORMATION"}};
+
+static int cap_get_dmi_variables(json_t *parent, const dmi_type type,
+                                 const char *json_name) {
+  DEBUG(CAP_PLUGIN ": cap_get_dmi_variables: %d/%s.", type, json_name);
+  dmi_reader_t reader;
+  if (dmi_reader_init(&reader, type) != DMI_OK) {
+    ERROR(CAP_PLUGIN ": dmi_reader_init failed.");
+    return -1;
+  }
+
+  json_t *section = NULL;
+  json_t *entries = NULL;
+  json_t *attributes = NULL;
+  json_t *arr = json_array();
+  if (arr == NULL) {
+    ERROR(CAP_PLUGIN ": Failed to allocate json array.");
+    dmi_reader_clean(&reader);
+    return -1;
+  }
+  if (json_object_set_new(parent, json_name, arr)) {
+    ERROR(CAP_PLUGIN ": Failed to set array to parent.");
+    dmi_reader_clean(&reader);
+    return -1;
+  }
+
+  while (reader.current_type != DMI_ENTRY_END) {
+    int status = dmi_read_next(&reader);
+    if (status != DMI_OK) {
+      ERROR(CAP_PLUGIN ": dmi_read_next failed.");
+      return -1;
+    }
+
+    switch (reader.current_type) {
+
+    case DMI_ENTRY_NAME:
+      DEBUG("%s", reader.name);
+      attributes = NULL;
+      section = json_object();
+      if (section == NULL) {
+        ERROR(CAP_PLUGIN ": Failed to allocate json object.");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      if (json_array_append_new(arr, section)) {
+        ERROR(CAP_PLUGIN ": Failed to append json entry.");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      entries = json_object();
+      if (entries == NULL) {
+        ERROR(CAP_PLUGIN ": Failed to allocate json object.");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      if (json_object_set_new(section, reader.name, entries)) {
+        ERROR(CAP_PLUGIN ": Failed to set json entry.");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      break;
+
+    case DMI_ENTRY_MAP:
+      DEBUG("    %s:%s", reader.name, reader.value);
+      attributes = NULL;
+      if (entries == NULL) {
+        ERROR(CAP_PLUGIN ": unexpected dmi output format");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      if (json_object_set_new(entries, reader.name,
+                              json_string(reader.value))) {
+        ERROR(CAP_PLUGIN ": Failed to set json entry.");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      break;
+
+    case DMI_ENTRY_LIST_NAME:
+      DEBUG("    %s:", reader.name);
+      if (entries == NULL) {
+        ERROR(CAP_PLUGIN ": unexpected dmi output format");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      attributes = json_array();
+      if (attributes == NULL) {
+        ERROR(CAP_PLUGIN ": Failed to allocate json array for attributes.");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      if (json_object_set_new(entries, reader.name, attributes)) {
+        ERROR(CAP_PLUGIN ": Failed to set json entry.");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      break;
+
+    case DMI_ENTRY_LIST_VALUE:
+      DEBUG("        %s", reader.value);
+      if (attributes == NULL) {
+        ERROR(CAP_PLUGIN ": unexpected dmi output format");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      if (json_array_append_new(attributes, json_string(reader.value))) {
+        ERROR(CAP_PLUGIN ": Failed to append json attribute.");
+        dmi_reader_clean(&reader);
+        return -1;
+      }
+      break;
+
+    default:
+      section = NULL;
+      entries = NULL;
+      attributes = NULL;
+      break;
+    }
+  }
+
+  return 0;
+}
+
+/* http_handler is the callback called by the microhttpd library. It essentially
+ * handles all HTTP request aspects and creates an HTTP response. */
+static int cap_http_handler(void *cls, struct MHD_Connection *connection,
+                            const char *url, const char *method,
+                            const char *version, const char *upload_data,
+                            size_t *upload_data_size, void **connection_state) {
+  if (strcmp(method, MHD_HTTP_METHOD_GET) != 0) {
+    return MHD_NO;
+  }
+
+  /* On the first call for each connection, return without anything further.
+   * The first time only the headers are valid, do not respond in the first
+   * round. The docs are not very specific on the issue. */
+  if (*connection_state == NULL) {
+    /* set to a random non-NULL pointer. */
+    *connection_state = &(int){44};
+    return MHD_YES;
+  }
+  DEBUG(CAP_PLUGIN ": formatted response: %s", g_cap_string);
+
+#if defined(MHD_VERSION) && MHD_VERSION >= 0x00090500
+  struct MHD_Response *res = MHD_create_response_from_buffer(
+      strlen(g_cap_string), g_cap_string, MHD_RESPMEM_PERSISTENT);
+#else
+  struct MHD_Response *res =
+      MHD_create_response_from_data(strlen(g_cap_string), g_cap_string, 0, 0);
+#endif
+
+  MHD_add_response_header(res, MHD_HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON);
+  int status = MHD_queue_response(connection, MHD_HTTP_OK, res);
+
+  MHD_destroy_response(res);
+
+  return status;
+}
+
+static void cap_logger(__attribute__((unused)) void *arg, char const *fmt,
+                       va_list ap) {
+  char errbuf[1024];
+  vsnprintf(errbuf, sizeof(errbuf), fmt, ap);
+
+  ERROR(CAP_PLUGIN ": libmicrohttpd: %s", errbuf);
+}
+
+#if defined(MHD_VERSION) && MHD_VERSION >= 0x00090000
+static int cap_open_socket() {
+  char service[NI_MAXSERV];
+  snprintf(service, sizeof(service), "%hu", httpd_port);
+
+  struct addrinfo *res;
+  int status = getaddrinfo(httpd_host, service,
+                           &(struct addrinfo){
+                               .ai_flags = AI_PASSIVE | AI_ADDRCONFIG,
+                               .ai_family = PF_INET,
+                               .ai_socktype = SOCK_STREAM,
+                           },
+                           &res);
+  if (status != 0) {
+    return -1;
+  }
+
+  int fd = -1;
+  for (struct addrinfo *ai = res; ai != NULL; ai = ai->ai_next) {
+    int flags = ai->ai_socktype;
+#ifdef SOCK_CLOEXEC
+    flags |= SOCK_CLOEXEC;
+#endif
+
+    fd = socket(ai->ai_family, flags, 0);
+    if (fd == -1)
+      continue;
+
+    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) != 0) {
+      WARNING(CAP_PLUGIN ": setsockopt(SO_REUSEADDR) failed: %s", STRERRNO);
+      close(fd);
+      fd = -1;
+      continue;
+    }
+
+    if (bind(fd, ai->ai_addr, ai->ai_addrlen) != 0) {
+      close(fd);
+      fd = -1;
+      continue;
+    }
+
+    if (listen(fd, LISTEN_BACKLOG) != 0) {
+      close(fd);
+      fd = -1;
+      continue;
+    }
+
+    char str_node[NI_MAXHOST];
+    char str_service[NI_MAXSERV];
+
+    getnameinfo(ai->ai_addr, ai->ai_addrlen, str_node, sizeof(str_node),
+                str_service, sizeof(str_service),
+                NI_NUMERICHOST | NI_NUMERICSERV);
+
+    INFO(CAP_PLUGIN ": Listening on [%s]:%s.", str_node, str_service);
+    break;
+  }
+
+  freeaddrinfo(res);
+
+  return fd;
+}
+#endif
+
+static struct MHD_Daemon *cap_start_daemon() {
+#if defined(MHD_VERSION) && MHD_VERSION >= 0x00090000
+  int fd = cap_open_socket();
+  if (fd == -1) {
+    ERROR(CAP_PLUGIN ": Opening a listening socket for [%s]:%hu failed.",
+          (httpd_host != NULL) ? httpd_host : "0.0.0.0", httpd_port);
+    return NULL;
+  }
+#endif
+  unsigned int flags = MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DEBUG;
+#if defined(MHD_VERSION) && MHD_VERSION >= 0x00095300
+  flags |= MHD_USE_POLL_INTERNAL_THREAD;
+#endif
+
+  struct MHD_Daemon *d = MHD_start_daemon(
+      flags, httpd_port, NULL, NULL, cap_http_handler, NULL,
+#if defined(MHD_VERSION) && MHD_VERSION >= 0x00090000
+      MHD_OPTION_LISTEN_SOCKET, fd,
+#endif
+      MHD_OPTION_EXTERNAL_LOGGER, cap_logger, NULL, MHD_OPTION_END);
+
+  if (d == NULL) {
+    ERROR(CAP_PLUGIN ": MHD_start_daemon() failed.");
+    return NULL;
+  }
+
+  return d;
+}
+
+static int cap_config(oconfig_item_t *ci) {
+  int status = 0;
+  for (int i = 0; i < ci->children_num; i++) {
+    oconfig_item_t *child = ci->children + i;
+
+    if (strcasecmp("Host", child->key) == 0) {
+#if defined(MHD_VERSION) && MHD_VERSION >= 0x00090000
+      status = cf_util_get_string(child, &httpd_host);
+#else
+      ERROR(CAP_PLUGIN ": Option `Host' not supported. Please upgrade "
+                       "libmicrohttpd to at least 0.9.0");
+      status = -1;
+#endif
+    } else if (strcasecmp("Port", child->key) == 0) {
+      int port = cf_util_get_port_number(child);
+      if (port > 0)
+        httpd_port = (unsigned short)port;
+      else {
+        ERROR(CAP_PLUGIN ": Wrong port number, correct range is 1-65535.");
+        status = -1;
+      }
+    } else {
+      ERROR(CAP_PLUGIN ": Unknown configuration option \"%s\".", child->key);
+      status = -1;
+    }
+
+    if (status) {
+      ERROR(CAP_PLUGIN ": Invalid configuration parameter \"%s\".", child->key);
+      sfree(httpd_host);
+      break;
+    }
+  }
+
+  return status;
+}
+
+static int cap_shutdown() {
+  if (httpd != NULL) {
+    MHD_stop_daemon(httpd);
+    httpd = NULL;
+  }
+
+  sfree(httpd_host);
+  sfree(g_cap_string);
+  return 0;
+}
+
+static int cap_init(void) {
+  json_t *root = json_object();
+  if (root == NULL) {
+    ERROR(CAP_PLUGIN ": Failed to allocate json root.");
+    cap_shutdown();
+    return -1;
+  }
+
+  for (int i = 0; i < STATIC_ARRAY_SIZE(types_list); i++)
+    if (cap_get_dmi_variables(root, types_list[i].type, types_list[i].name)) {
+      json_decref(root);
+      cap_shutdown();
+      return -1;
+    }
+
+  g_cap_string = json_dumps(root, JSON_COMPACT);
+  json_decref(root);
+
+  if (g_cap_string == NULL) {
+    ERROR(CAP_PLUGIN ": json_dumps() failed.");
+    cap_shutdown();
+    return -1;
+  }
+
+  httpd = cap_start_daemon();
+  if (httpd == NULL) {
+    cap_shutdown();
+    return -1;
+  }
+
+  return 0;
+}
+
+void module_register(void) {
+  plugin_register_complex_config(CAP_PLUGIN, cap_config);
+  plugin_register_init(CAP_PLUGIN, cap_init);
+  plugin_register_shutdown(CAP_PLUGIN, cap_shutdown);
+}
diff --git a/src/capabilities_test.c b/src/capabilities_test.c
new file mode 100644 (file)
index 0000000..95ac666
--- /dev/null
@@ -0,0 +1,273 @@
+/*
+ * collectd - src/capabilities_test.c
+ *
+ * Copyright(c) 2019 Intel Corporation. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * Authors:
+ *   Kamil Wiatrowski <kamilx.wiatrowski@intel.com>
+ */
+
+#include "capabilities.c" /* sic */
+#include "testing.h"
+
+#define RESULT_STRING_JSON                                                     \
+  "{\"TEST_TYPE\":[{\"Name\":{\"MapName1\":\"MapValue1\",\"ListName1\":["      \
+  "\"ListValue1\",\"ListValue2\"],\"MapName2\":\"MapValue2\"}},{\"Name\":{"    \
+  "\"MapName1\":\"MapValue1\"}}]}"
+
+static int idx = 0;
+static char *test_dmi[][2] = {{NULL, NULL},
+                              {"Name", NULL},
+                              {"MapName1", "MapValue1"},
+                              {"ListName1", NULL},
+                              {NULL, "ListValue1"},
+                              {NULL, "ListValue2"},
+                              {"MapName2", "MapValue2"},
+                              {NULL, NULL},
+                              {"Name", NULL},
+                              {"MapName1", "MapValue1"},
+                              {NULL, NULL}};
+static entry_type entry[] = {
+    DMI_ENTRY_NONE,      DMI_ENTRY_NAME,       DMI_ENTRY_MAP,
+    DMI_ENTRY_LIST_NAME, DMI_ENTRY_LIST_VALUE, DMI_ENTRY_LIST_VALUE,
+    DMI_ENTRY_MAP,       DMI_ENTRY_NONE,       DMI_ENTRY_NAME,
+    DMI_ENTRY_MAP,       DMI_ENTRY_END};
+static size_t len = STATIC_ARRAY_SIZE(entry);
+
+/* mock functions */
+int dmi_reader_init(dmi_reader_t *reader, const dmi_type type) {
+  reader->current_type = DMI_ENTRY_NONE;
+  return DMI_OK;
+}
+
+void dmi_reader_clean(dmi_reader_t *reader) {}
+
+int dmi_read_next(dmi_reader_t *reader) {
+  if (idx >= len)
+    return DMI_ERROR;
+  reader->current_type = entry[idx];
+  reader->name = test_dmi[idx][0];
+  reader->value = test_dmi[idx][1];
+  idx++;
+  return DMI_OK;
+}
+
+struct MHD_Daemon *MHD_start_daemon(unsigned int flags, unsigned short port,
+                                    MHD_AcceptPolicyCallback apc, void *apc_cls,
+                                    MHD_AccessHandlerCallback dh, void *dh_cls,
+                                    ...) {
+  return NULL;
+}
+
+void MHD_stop_daemon(struct MHD_Daemon *daemon) {}
+
+struct MHD_Response *
+MHD_create_response_from_buffer(size_t size, void *data,
+                                enum MHD_ResponseMemoryMode mode) {
+  return NULL;
+}
+
+struct MHD_Response *MHD_create_response_from_data(size_t size, void *data,
+                                                   int must_free,
+                                                   int must_copy) {
+  return NULL;
+}
+
+int MHD_add_response_header(struct MHD_Response *response, const char *header,
+                            const char *content) {
+  return 0;
+}
+
+int MHD_queue_response(struct MHD_Connection *connection,
+                       unsigned int status_code,
+                       struct MHD_Response *response) {
+  return MHD_HTTP_OK;
+}
+
+void MHD_destroy_response(struct MHD_Response *response) {}
+/* end mock functions */
+
+DEF_TEST(plugin_config) {
+  oconfig_item_t test_cfg_parent = {"capabilities", NULL, 0, NULL, NULL, 0};
+  char value_buff[256] = "1234";
+  char key_buff[256] = "port";
+  oconfig_value_t test_cfg_value = {{value_buff}, OCONFIG_TYPE_STRING};
+  oconfig_item_t test_cfg = {
+      key_buff, &test_cfg_value, 1, &test_cfg_parent, NULL, 0};
+
+  test_cfg_parent.children = &test_cfg;
+  test_cfg_parent.children_num = 1;
+
+  int ret = cap_config(&test_cfg_parent);
+  EXPECT_EQ_INT(0, ret);
+  EXPECT_EQ_INT(1234, httpd_port);
+  OK(NULL == httpd_host);
+
+  strncpy(value_buff, "1", STATIC_ARRAY_SIZE(value_buff));
+  ret = cap_config(&test_cfg_parent);
+  EXPECT_EQ_INT(0, ret);
+  EXPECT_EQ_INT(1, httpd_port);
+
+  strncpy(value_buff, "65535", STATIC_ARRAY_SIZE(value_buff));
+  ret = cap_config(&test_cfg_parent);
+  EXPECT_EQ_INT(0, ret);
+  EXPECT_EQ_INT(65535, httpd_port);
+
+#if defined(MHD_VERSION) && MHD_VERSION >= 0x00090000
+  strncpy(value_buff, "127.0.0.1", STATIC_ARRAY_SIZE(value_buff));
+  strncpy(key_buff, "host", STATIC_ARRAY_SIZE(key_buff));
+
+  ret = cap_config(&test_cfg_parent);
+  EXPECT_EQ_INT(0, ret);
+  EXPECT_EQ_STR("127.0.0.1", httpd_host);
+  EXPECT_EQ_INT(65535, httpd_port);
+
+  free(httpd_host);
+  strncpy(key_buff, "port", STATIC_ARRAY_SIZE(key_buff));
+#endif
+
+  double port_value = 65535;
+  oconfig_value_t test_cfg_value2 = {{.number = port_value},
+                                     OCONFIG_TYPE_NUMBER};
+  test_cfg.values = &test_cfg_value2;
+  ret = cap_config(&test_cfg_parent);
+  EXPECT_EQ_INT(0, ret);
+  EXPECT_EQ_INT(65535, httpd_port);
+
+  return 0;
+}
+
+DEF_TEST(plugin_config_fail) {
+  oconfig_item_t test_cfg_parent = {"capabilities", NULL, 0, NULL, NULL, 0};
+  char value_buff[256] = "1";
+  char key_buff[256] = "aport";
+  oconfig_value_t test_cfg_value = {{value_buff}, OCONFIG_TYPE_STRING};
+  oconfig_item_t test_cfg = {
+      key_buff, &test_cfg_value, 1, &test_cfg_parent, NULL, 0};
+
+  test_cfg_parent.children = &test_cfg;
+  test_cfg_parent.children_num = 1;
+
+  unsigned short default_port = httpd_port;
+  int ret = cap_config(&test_cfg_parent);
+  EXPECT_EQ_INT(-1, ret);
+  EXPECT_EQ_INT(default_port, httpd_port);
+  OK(NULL == httpd_host);
+
+  /* Correct port range is 1 - 65535 */
+  strncpy(key_buff, "port", STATIC_ARRAY_SIZE(key_buff));
+  strncpy(value_buff, "-1", STATIC_ARRAY_SIZE(value_buff));
+  ret = cap_config(&test_cfg_parent);
+  EXPECT_EQ_INT(-1, ret);
+  EXPECT_EQ_INT(default_port, httpd_port);
+  OK(NULL == httpd_host);
+
+  strncpy(value_buff, "65536", STATIC_ARRAY_SIZE(value_buff));
+  ret = cap_config(&test_cfg_parent);
+  EXPECT_EQ_INT(-1, ret);
+  EXPECT_EQ_INT(default_port, httpd_port);
+  OK(NULL == httpd_host);
+
+#if defined(MHD_VERSION) && MHD_VERSION >= 0x00090000
+  strncpy(value_buff, "127.0.0.1", STATIC_ARRAY_SIZE(value_buff));
+  strncpy(key_buff, "host", STATIC_ARRAY_SIZE(key_buff));
+  test_cfg_value.type = OCONFIG_TYPE_NUMBER;
+  ret = cap_config(&test_cfg_parent);
+  EXPECT_EQ_INT(-1, ret);
+  EXPECT_EQ_INT(default_port, httpd_port);
+  OK(NULL == httpd_host);
+
+  strncpy(key_buff, "port", STATIC_ARRAY_SIZE(key_buff));
+#endif
+
+  double port_value = 65536;
+  oconfig_value_t test_cfg_value2 = {{.number = port_value},
+                                     OCONFIG_TYPE_NUMBER};
+  test_cfg.values = &test_cfg_value2;
+  ret = cap_config(&test_cfg_parent);
+  EXPECT_EQ_INT(-1, ret);
+  EXPECT_EQ_INT(default_port, httpd_port);
+  OK(NULL == httpd_host);
+
+  return 0;
+}
+
+DEF_TEST(http_handler) {
+  void *state = NULL;
+  g_cap_string = "TEST";
+  int ret = cap_http_handler(NULL, NULL, NULL, MHD_HTTP_METHOD_PUT, NULL, NULL,
+                             NULL, &state);
+  EXPECT_EQ_INT(MHD_NO, ret);
+  OK(NULL == state);
+
+  ret = cap_http_handler(NULL, NULL, NULL, MHD_HTTP_METHOD_GET, NULL, NULL,
+                         NULL, &state);
+  EXPECT_EQ_INT(MHD_YES, ret);
+  CHECK_NOT_NULL(state);
+
+  ret = cap_http_handler(NULL, NULL, NULL, MHD_HTTP_METHOD_GET, NULL, NULL,
+                         NULL, &state);
+  EXPECT_EQ_INT(MHD_HTTP_OK, ret);
+  CHECK_NOT_NULL(state);
+
+  g_cap_string = NULL;
+
+  return 0;
+}
+
+DEF_TEST(get_dmi_variables) {
+  json_t *root = json_object();
+  CHECK_NOT_NULL(root);
+
+  int ret = cap_get_dmi_variables(root, 0, "TEST_TYPE");
+  EXPECT_EQ_INT(0, ret);
+
+  char *test_str = json_dumps(root, JSON_COMPACT);
+  CHECK_NOT_NULL(test_str);
+  json_decref(root);
+  EXPECT_EQ_STR(RESULT_STRING_JSON, test_str);
+
+  free(test_str);
+
+  root = json_object();
+  CHECK_NOT_NULL(root);
+  ret = cap_get_dmi_variables(root, 1, "TEST_TYPE2");
+  EXPECT_EQ_INT(-1, ret);
+
+  test_str = json_dumps(root, JSON_COMPACT);
+  CHECK_NOT_NULL(test_str);
+  json_decref(root);
+  EXPECT_EQ_STR("{\"TEST_TYPE2\":[]}", test_str);
+
+  free(test_str);
+
+  return 0;
+}
+
+int main(void) {
+  RUN_TEST(plugin_config_fail);
+  RUN_TEST(plugin_config);
+
+  RUN_TEST(http_handler);
+  RUN_TEST(get_dmi_variables);
+
+  END_TEST;
+}
index f6a62060b1473b267e6697194617c9f34a95905b..aae6ccdc41654fd93f42625f66b0beba3ef00c3a 100644 (file)
 #@BUILD_PLUGIN_BATTERY_TRUE@LoadPlugin battery
 #@BUILD_PLUGIN_BIND_TRUE@LoadPlugin bind
 #@BUILD_PLUGIN_BUDDYINFO_TRUE@LoadPlugin buddyinfo
+#@BUILD_PLUGIN_CAPABILITIES_TRUE@LoadPlugin capabilities
 #@BUILD_PLUGIN_CEPH_TRUE@LoadPlugin ceph
 #@BUILD_PLUGIN_CGROUPS_TRUE@LoadPlugin cgroups
 #@BUILD_PLUGIN_CHRONY_TRUE@LoadPlugin chrony
 #</Plugin>
 
 #<Plugin buddyinfo>
-#      Zone "Normal"
+#  Zone "Normal"
+#</Plugin>
+
+#<Plugin capabilities>
+#  Port "9104"
 #</Plugin>
 
 #<Plugin ceph>
index 1e94a8c9da36e40fd4a32aed1641a3209460b860..92d6e3e4a9ca88dc04f07f7ea73376a05e1d3e2a 100644 (file)
@@ -1469,6 +1469,36 @@ Zone to collect info about. Will collect all zones by default.
 
 =back
 
+=head2 Plugin C<capabilities>
+
+The C<capabilities> plugin collects selected static platform data using
+I<dmidecode> and expose it through micro embedded webserver. The data
+returned by plugin is in json format.
+
+B<Synopsis:>
+
+  <Plugin capabilities>
+    Host "<hostname>"
+    Port "9104"
+  </Plugin>
+
+Available configuration options for the C<capabilities> plugin:
+
+=over 4
+
+=item B<Host> I<Hostname>
+
+Bind to the hostname / address I<Host>. By default, the plugin will bind to the
+"any" address, i.e. accept packets sent to any of the hosts addresses.
+
+This option is supported only for libmicrohttpd newer than 0.9.0.
+
+=item B<Port> I<Port>
+
+Port the embedded webserver should listen on. Defaults to B<9104>.
+
+=back
+
 =head2 Plugin C<ceph>
 
 The ceph plugin collects values from JSON data to be parsed by B<libyajl>
diff --git a/src/utils/dmi/dmi.c b/src/utils/dmi/dmi.c
new file mode 100644 (file)
index 0000000..12e9884
--- /dev/null
@@ -0,0 +1,189 @@
+/*
+ * MIT License
+ *
+ * Copyright(c) 2019 Intel Corporation. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * Authors:
+ * Kamil Wiatrowski <kamilx.wiatrowski@intel.com>
+ *
+ */
+
+/*
+ * The util helps to parse the output from dmidecode command.
+ * It expects that the output follows the given format, the size of
+ * tabulations does not matter:
+ *
+ * Handle 1(...)
+ * SMBIOS type name
+ *     item1: value
+ *     item2: value
+ *     list name:
+ *         list elem1
+ *         list elem2
+ *         (...)
+ *     item3: value
+ *     (...)
+ *
+ * Handle 2(...)
+ * SMBIOS type name
+ * (and so on ...)
+ *
+ */
+
+#include "collectd.h"
+
+#include "utils/common/common.h"
+#include "utils/dmi/dmi.h"
+
+#define UTIL_NAME "DMI_READER"
+
+#define DMIDECODE_CMD_FMT_LEN DMI_MAX_LEN
+
+static int dmi_look_for_handle(dmi_reader_t *r);
+
+static int dmi_read_entry(dmi_reader_t *r) {
+  while (fgets(r->_buff, STATIC_ARRAY_SIZE(r->_buff), r->_fd) != NULL) {
+    char *buff = r->_buff;
+    while (isspace(*buff))
+      buff++;
+    if (*buff == '\0') {
+      r->current_type = DMI_ENTRY_NONE;
+      r->_read_callback = dmi_look_for_handle;
+      return DMI_OK;
+    }
+
+    strstripnewline(buff);
+    char *sep = strchr(buff, ':');
+    if (sep == NULL) {
+      r->value = buff;
+      r->current_type = DMI_ENTRY_LIST_VALUE;
+      return DMI_OK;
+    }
+
+    sep[0] = '\0';
+    r->name = buff;
+    if (sep[1] == '\0') {
+      r->current_type = DMI_ENTRY_LIST_NAME;
+    } else {
+      sep++;
+      while (isspace(*sep))
+        sep++;
+      if (*sep == '\0')
+        INFO(UTIL_NAME ": value is empty for: \'%s\'.", r->_buff);
+      r->value = sep;
+      r->current_type = DMI_ENTRY_MAP;
+    }
+    return DMI_OK;
+  }
+
+  r->current_type = DMI_ENTRY_END;
+  return DMI_OK;
+}
+
+static int dmi_read_type_name(dmi_reader_t *r) {
+  while (fgets(r->_buff, STATIC_ARRAY_SIZE(r->_buff), r->_fd) != NULL) {
+    strstripnewline(r->_buff);
+    if (strlen(r->_buff) == 0) {
+      ERROR(UTIL_NAME ": unexpected format of dmidecode output.");
+      return DMI_ERROR;
+    }
+
+    r->name = r->_buff;
+    r->current_type = DMI_ENTRY_NAME;
+    r->_read_callback = dmi_read_entry;
+
+    return DMI_OK;
+  }
+
+  r->current_type = DMI_ENTRY_END;
+  return DMI_OK;
+}
+
+static int dmi_look_for_handle(dmi_reader_t *r) {
+  while (fgets(r->_buff, STATIC_ARRAY_SIZE(r->_buff), r->_fd) != NULL) {
+    const char *handle = "Handle";
+    if (strncmp(handle, r->_buff, strlen(handle)) != 0)
+      continue;
+
+    r->_read_callback = dmi_read_type_name;
+    return DMI_OK;
+  }
+
+  r->current_type = DMI_ENTRY_END;
+  return DMI_OK;
+}
+
+int dmi_reader_init(dmi_reader_t *reader, const dmi_type type) {
+  if (reader == NULL) {
+    ERROR(UTIL_NAME ".%s: NULL pointer.", __func__);
+    return DMI_ERROR;
+  }
+  char cmd[DMIDECODE_CMD_FMT_LEN];
+
+  if (type == DMI_TYPE_ALL)
+    strncpy(cmd, "dmidecode 2>/dev/null", STATIC_ARRAY_SIZE(cmd));
+  else
+    snprintf(cmd, STATIC_ARRAY_SIZE(cmd), "dmidecode -t %d 2>/dev/null", type);
+
+  DEBUG(UTIL_NAME ": dmidecode cmd=\'%s\'.", cmd);
+
+  reader->_fd = popen(cmd, "r");
+  if (reader->_fd == NULL) {
+    ERROR(UTIL_NAME ": popen failed.");
+    return DMI_ERROR;
+  }
+
+  reader->name = NULL;
+  reader->value = NULL;
+  reader->_read_callback = dmi_look_for_handle;
+  reader->current_type = DMI_ENTRY_NONE;
+
+  return DMI_OK;
+}
+
+void dmi_reader_clean(dmi_reader_t *reader) {
+  if (reader == NULL) {
+    WARNING(UTIL_NAME ".%s: NULL pointer.", __func__);
+    return;
+  }
+
+  if (reader->_fd)
+    pclose(reader->_fd);
+
+  reader->_fd = NULL;
+  reader->_read_callback = NULL;
+}
+
+int dmi_read_next(dmi_reader_t *reader) {
+  if (reader == NULL || reader->_read_callback == NULL || reader->_fd == NULL) {
+    ERROR(UTIL_NAME ".%s: NULL pointer.", __func__);
+    return DMI_ERROR;
+  }
+  int ret = reader->_read_callback(reader);
+
+  if (reader->current_type == DMI_ENTRY_END || ret == DMI_ERROR) {
+    pclose(reader->_fd);
+    reader->_read_callback = NULL;
+    DEBUG(UTIL_NAME ": dmidecode reader finished, status=%d.", ret);
+  }
+
+  return ret;
+}
diff --git a/src/utils/dmi/dmi.h b/src/utils/dmi/dmi.h
new file mode 100644 (file)
index 0000000..181477f
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * MIT License
+ *
+ * Copyright(c) 2019 Intel Corporation. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * Authors:
+ * Kamil Wiatrowski <kamilx.wiatrowski@intel.com>
+ */
+
+#ifndef UTILS_DMI_H
+#define UTILS_DMI_H
+
+#define DMI_MAX_LEN 256
+
+#define DMI_OK 0
+#define DMI_ERROR -1
+
+/* Following values should match Types assigned in System Management BIOS
+ * (SMBIOS) Reference Specification
+ */
+typedef enum dmi_type_e {
+  BIOS = 0,
+  SYSTEM,
+  BASEBOARD,
+  CHASSIS,
+  PROCESSOR,
+  MEMORY_CONTROLLER,
+  MEMORY_MODULE,
+  CACHE,
+  PORT_CONNECTOR,
+  SYSTEM_SLOTS,
+  ON_BOARD_DEVICES,
+  OEM_STRINGS,
+  SYSTEM_CONFIGURATION_OPTIONS,
+  BIOS_LANGUAGE,
+  GROUP_ASSOCIATIONS,
+  SYSTEM_EVENT_LOG,
+  PHYSICAL_MEMORY_ARRAY,
+  MEMORY_DEVICE,
+  MEMORY_ERROR_32_BIT,
+  MEMORY_ARRAY_MAPPED_ADDRESS,
+  MEMORY_DEVICE_MAPPED_ADDRESS,
+  BUILT_IN_POINTING_DEVICE,
+  PORTABLE_BATTERY,
+  SYSTEM_RESET,
+  HARDWARE_SECURITY,
+  SYSTEM_POWER_CONTROLS,
+  VOLTAGE_PROBE,
+  COOLING_DEVICE,
+  TEMPERATURE_PROBE,
+  ELECTRICAL_CURRENT_PROBE,
+  OUT_OF_BAND_REMOTE_ACCESS,
+  BOOT_INTEGRITY_SERVICES,
+  SYSTEM_BOOT,
+  MEMORY_ERROR_64_BIT,
+  MANAGEMENT_DEVICE,
+  MANAGEMENT_DEVICE_COMPONENT,
+  MANAGEMENT_DEVICE_THRESHOLD_DATA,
+  MEMORY_CHANNEL,
+  IPMI_DEVICE,
+  POWER_SUPPLY,
+  ADDITIONAL_INFORMATION,
+  ONBOARD_DEVICES_EXTENDED_INFORMATION,
+  MANAGEMENT_CONTROLLER_HOST_INTERFACE,
+  DMI_TYPE_ALL /* special type to read all SMBIOS handles */
+} dmi_type;
+
+typedef enum entry_type_e {
+  DMI_ENTRY_NONE,
+  DMI_ENTRY_NAME,
+  DMI_ENTRY_MAP,
+  DMI_ENTRY_LIST_NAME,
+  DMI_ENTRY_LIST_VALUE,
+  DMI_ENTRY_END
+} entry_type;
+
+typedef struct dmi_reader_s {
+  FILE *_fd;
+  char _buff[DMI_MAX_LEN];
+  int (*_read_callback)(struct dmi_reader_s *reader);
+  /* Type of current entry */
+  entry_type current_type;
+  /* Entry name, the pointer changes after every read. */
+  char *name;
+  /* Entry value, the pointer changes after every read. */
+  char *value;
+} dmi_reader_t;
+
+/*
+ * NAME
+ *   dmi_reader_init
+ *
+ * DESCRIPTION
+ *   Initialize reader structure and use popen to read SMBIOS using dmidecode.
+ *
+ * PARAMETERS
+ *   `reader'     Pointer to dmi_reader structure, can't be NULL.
+ *   `type'       Type of DMI entries to read.
+ *
+ * RETURN VALUE
+ *    DMI_OK upon success or DMI_ERROR if an error occurred.
+ *
+ * NOTES
+ *    The dmi_reader_clean needs to be called to close pipe if
+ *    reader hasn't finished yet. In case of error cleanup is
+ *    done by the function itself.
+ */
+int dmi_reader_init(dmi_reader_t *reader, const dmi_type type);
+
+/*
+ * NAME
+ *   dmi_reader_clean
+ *
+ * DESCRIPTION
+ *   Clean up reader structure and close pipe.
+ *
+ * PARAMETERS
+ *   `reader'     Pointer to dmi_reader structure, can't be NULL.
+ */
+void dmi_reader_clean(dmi_reader_t *reader);
+
+/*
+ * NAME
+ *   dmi_reader_next
+ *
+ * DESCRIPTION
+ *   Read next entry and store values.
+ *
+ * PARAMETERS
+ *   `reader'     Pointer to dmi_reader structure, can't be NULL.
+ *
+ * RETURN VALUE
+ *    DMI_OK upon success or DMI_ERROR if an error occurred.
+ *
+ * NOTES
+ *    In case of error the function cleans the reader structure.
+ *    After all entries are read the function sets state to DMI_ENTRY_END
+ *    and no further clean-up is required.
+ */
+int dmi_read_next(dmi_reader_t *reader);
+
+#endif /* UTILS_DMI_H */