mirror of
https://github.com/Cacti/spine.git
synced 2026-10-23 03:27:10 +09:00
The runtime/probe changes from #642 and GNU C17/build changes from #643 were squash-merged into #641's source branch, which made the result-normalization review include later stages. This follow-up preserves those approved changes while restoring #641's focused diff against develop. No force-push or approved-code deletion is required. Merge after #641 and before #644. #644 will target this branch. This combines the already reviewed steps 5 and 6; it introduces no new worker-stack or module-extraction changes. The UDP follow-up also corrects a validated finding: EHOSTUNREACH on a connected socket is ambiguous and can represent an unreachable host or failed neighbour lookup. It now reports down; ECONNREFUSED/ECONNRESET and actual peer datagrams retain their existing alive behavior. The production-linked test rejects the original EHOSTUNREACH behavior. Validation: Ubuntu 24.04/aarch64, clean GCC build and make check: 17 passed, one existing setuid prerequisite skip, zero failures. The full tracked source tree is byte-identical to the independently validated #641 functional-fix snapshot. The UDP and poll-host regressions fail against the original code and pass after the fixes. The isolated step-4 tree independently passes 15 tests with one existing skip. No live endpoints, host installation, privilege changes, or test suppression. Repository squash merges still require restacking/retargeting the next child after each merge; clean ordinary merge simulations are insufficient by themselves. --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2561 lines
82 KiB
C
2561 lines
82 KiB
C
/*
|
|
ex: set tabstop=4 shiftwidth=4 autoindent:
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU Lesser General Public |
|
|
| License as published by the Free Software Foundation; either |
|
|
| version 2.1 of the License, or (at your option) any later version. |
|
|
| |
|
|
| 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 Lesser General Public License for more details. |
|
|
| |
|
|
| You should have received a copy of the GNU Lesser General Public |
|
|
| License along with this library; if not, write to the Free Software |
|
|
| Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
|
|
| 02110-1301, USA |
|
|
| |
|
|
+-------------------------------------------------------------------------+
|
|
| spine: a backend data gatherer for cacti |
|
|
+-------------------------------------------------------------------------+
|
|
| This poller would not have been possible without: |
|
|
| - Larry Adams (current development and enhancements) |
|
|
| - Rivo Nurges (rrd support, mysql poller cache, misc functions) |
|
|
| - RTG (core poller code, pthreads, snmp, autoconf examples) |
|
|
| - Brady Alleman/Doug Warner (threading ideas, implementation details) |
|
|
+-------------------------------------------------------------------------+
|
|
| - Cacti - http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "spine.h"
|
|
#include "regex.h"
|
|
#include <stddef.h>
|
|
#include <limits.h>
|
|
|
|
#define SPINE_STRINGIFY_INNER(value) #value
|
|
#define SPINE_STRINGIFY(value) SPINE_STRINGIFY_INNER(value)
|
|
|
|
/* Bounded formatting must reject truncation before a partial SQL statement or
|
|
* command can be used. Return the number actually written, never the size that
|
|
* would have been needed, so append callers cannot advance past the buffer. */
|
|
int spine_snprintf(char *output, size_t capacity, const char *format, ...) {
|
|
va_list args;
|
|
int length;
|
|
|
|
va_start(args, format);
|
|
length = vsnprintf(output, capacity, format, args);
|
|
va_end(args);
|
|
if (length < 0 || (size_t) length >= capacity) {
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
die("ERROR: Formatted output exceeds its destination buffer");
|
|
}
|
|
return length;
|
|
}
|
|
|
|
int spine_count_to_int(unsigned long long count) {
|
|
if (count > INT_MAX) {
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
die("ERROR: Result count exceeds supported integer range");
|
|
}
|
|
return (int)count;
|
|
}
|
|
|
|
void spine_sleep_usec(unsigned int microseconds) {
|
|
struct timespec requested = {
|
|
(time_t)(microseconds / 1000000), (long)(microseconds % 1000000) * 1000
|
|
};
|
|
while (nanosleep(&requested, &requested) != 0) {
|
|
if (errno != EINTR) {
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
die("ERROR: Unable to wait for retry delay");
|
|
}
|
|
}
|
|
}
|
|
|
|
double spine_monotonic_time(void) {
|
|
struct timespec now;
|
|
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
die("ERROR: Unable to read monotonic clock");
|
|
}
|
|
return (double)now.tv_sec + (double)now.tv_nsec / 1000000000;
|
|
}
|
|
|
|
static int spine_wait_fd(int fd, double deadline, bool writable) {
|
|
if (fd < 0 || fd >= FD_SETSIZE) {
|
|
errno = EBADF;
|
|
return -1;
|
|
}
|
|
if (!isfinite(deadline)) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
for (;;) {
|
|
double remaining = deadline - spine_monotonic_time();
|
|
if (remaining <= 0) return 0;
|
|
if (remaining > INT_MAX) {
|
|
errno = EINVAL;
|
|
return -1;
|
|
}
|
|
struct timeval timeout;
|
|
timeout.tv_sec = (time_t)remaining;
|
|
timeout.tv_usec = (suseconds_t)((remaining - (double)timeout.tv_sec) * 1000000);
|
|
fd_set fds;
|
|
FD_ZERO(&fds);
|
|
FD_SET(fd, &fds);
|
|
int status = select(fd + 1, writable ? NULL : &fds, writable ? &fds : NULL, NULL, &timeout);
|
|
if (status < 0 && errno == EINTR) continue;
|
|
return status;
|
|
}
|
|
}
|
|
|
|
int spine_wait_readable(int fd, double deadline) {
|
|
return spine_wait_fd(fd, deadline, FALSE);
|
|
}
|
|
|
|
int spine_wait_writable(int fd, double deadline) {
|
|
return spine_wait_fd(fd, deadline, TRUE);
|
|
}
|
|
|
|
|
|
int spine_permits_init(spine_permits_t *permits, int count) {
|
|
if (count < 0) return EINVAL;
|
|
int status = pthread_mutex_init(&permits->mutex, NULL);
|
|
if (status == 0) permits->available = count;
|
|
return status;
|
|
}
|
|
|
|
int spine_permits_destroy(spine_permits_t *permits) {
|
|
return pthread_mutex_destroy(&permits->mutex);
|
|
}
|
|
|
|
static void spine_permits_lock(spine_permits_t *permits) {
|
|
if (pthread_mutex_lock(&permits->mutex) != 0) {
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
die("ERROR: Unable to lock process permits");
|
|
}
|
|
}
|
|
|
|
static void spine_permits_unlock(spine_permits_t *permits) {
|
|
if (pthread_mutex_unlock(&permits->mutex) != 0) {
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
die("ERROR: Unable to unlock process permits");
|
|
}
|
|
}
|
|
|
|
int spine_permits_try_acquire(spine_permits_t *permits) {
|
|
spine_permits_lock(permits);
|
|
int status = EAGAIN;
|
|
if (permits->available > 0) {
|
|
permits->available--;
|
|
status = 0;
|
|
}
|
|
spine_permits_unlock(permits);
|
|
return status;
|
|
}
|
|
|
|
int spine_permits_release(spine_permits_t *permits) {
|
|
spine_permits_lock(permits);
|
|
int status = EOVERFLOW;
|
|
if (permits->available < INT_MAX) {
|
|
permits->available++;
|
|
status = 0;
|
|
}
|
|
spine_permits_unlock(permits);
|
|
return status;
|
|
}
|
|
|
|
int spine_permits_available(spine_permits_t *permits) {
|
|
spine_permits_lock(permits);
|
|
int available = permits->available;
|
|
spine_permits_unlock(permits);
|
|
return available;
|
|
}
|
|
|
|
/* Raise, never lower, the platform default: glibc already gives 8 MiB, and a
|
|
* smaller stack there would only add risk in code nobody has measured. */
|
|
int spine_thread_attr_init(pthread_attr_t *attributes) {
|
|
size_t current = 0;
|
|
size_t wanted = SPINE_THREAD_STACK_SIZE;
|
|
int status;
|
|
|
|
#ifdef PTHREAD_STACK_MIN
|
|
if (wanted < (size_t) PTHREAD_STACK_MIN) wanted = (size_t) PTHREAD_STACK_MIN;
|
|
#endif
|
|
|
|
status = pthread_attr_init(attributes);
|
|
if (status != 0) {
|
|
return status;
|
|
}
|
|
|
|
if (pthread_attr_getstacksize(attributes, ¤t) == 0 && current >= wanted) {
|
|
return 0;
|
|
}
|
|
|
|
status = pthread_attr_setstacksize(attributes, wanted);
|
|
if (status != 0) {
|
|
pthread_attr_destroy(attributes);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
void spine_clear_sensitive(void *buffer, size_t length) {
|
|
volatile unsigned char *bytes = buffer;
|
|
while (length > 0) {
|
|
*bytes++ = 0;
|
|
length--;
|
|
}
|
|
}
|
|
|
|
/* Preserve printable device output while keeping each log record on one line.
|
|
* Also remove terminal controls and Unicode line/paragraph separators. */
|
|
void spine_sanitize_log_message(char *message) {
|
|
for (unsigned char *p = (unsigned char *)message; *p != 0; p++) {
|
|
if (*p < 0x20 || *p == 0x7f) {
|
|
*p = ' ';
|
|
} else if (p[0] == 0xc2 && p[1] == 0x85) {
|
|
p[0] = ' ';
|
|
p[1] = ' ';
|
|
} else if (p[0] == 0xe2 && p[1] == 0x80 && (p[2] == 0xa8 || p[2] == 0xa9)) {
|
|
p[0] = ' ';
|
|
p[1] = ' ';
|
|
p[2] = ' ';
|
|
}
|
|
}
|
|
}
|
|
|
|
static int nopts = 0;
|
|
|
|
/*! Override Options Structure
|
|
*
|
|
* When we fetch a setting from the database, we allow the user to override
|
|
* it from the command line. These overrides are provided with the --option
|
|
* parameter and stored in this table: we *use* them when the config code
|
|
* reads from the DB.
|
|
*
|
|
* It's not an error to set an option which is unknown, but maybe should be.
|
|
*
|
|
*/
|
|
static struct {
|
|
const char *opt;
|
|
const char *val;
|
|
} opttable[256];
|
|
|
|
/*! \fn void set_option(const char *option, const char *value)
|
|
* \brief Override spine setting from the Cacti settings table.
|
|
*
|
|
* Called from the command-line processing code, this provides a value
|
|
* to replace any DB-stored option settings.
|
|
*
|
|
*/
|
|
void set_option(const char *option, const char *value) {
|
|
if (option == NULL || value == NULL || nopts >= (int)(sizeof(opttable) / sizeof(opttable[0]))) {
|
|
die("ERROR: Invalid or excessive command-line setting overrides");
|
|
}
|
|
opttable[nopts ].opt = option;
|
|
opttable[nopts++].val = value;
|
|
}
|
|
|
|
/* Settings cache.
|
|
*
|
|
* read_config_options() looks up two dozen settings and each one was its own
|
|
* round trip. The settings table is small, so it is read once up front and
|
|
* served from memory for the duration of that call. Only ever populated and
|
|
* used from read_config_options(), which runs before any poller thread
|
|
* exists, so no locking is required.
|
|
*/
|
|
typedef struct setting_cache_entry {
|
|
char *name;
|
|
char *value;
|
|
} setting_cache_t;
|
|
|
|
static setting_cache_t *settings_cache = NULL;
|
|
static int settings_cache_count = 0;
|
|
|
|
/* Settings are read in the main thread before polling starts, so a database
|
|
* error here is a configuration failure and ends the process, as it did when
|
|
* db_query() exited on its own. An empty or missing result is not an error. */
|
|
static MYSQL_RES *config_query(MYSQL *psql, int mode, const char *query) {
|
|
MYSQL_RES *result = db_query(psql, mode, query);
|
|
|
|
if (result == NULL && mysql_errno(psql) != 0) {
|
|
die("FATAL: Unable to read the Cacti configuration from the database");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static void settings_cache_free(void) {
|
|
int i;
|
|
|
|
if (settings_cache == NULL) return;
|
|
|
|
for (i = 0; i < settings_cache_count; i++) {
|
|
free(settings_cache[i].name);
|
|
free(settings_cache[i].value);
|
|
}
|
|
|
|
free(settings_cache);
|
|
settings_cache = NULL;
|
|
settings_cache_count = 0;
|
|
}
|
|
|
|
static void settings_cache_load(MYSQL *psql, int mode) {
|
|
MYSQL_RES *result;
|
|
MYSQL_ROW row;
|
|
my_ulonglong rows;
|
|
setting_cache_t *table;
|
|
int i = 0;
|
|
|
|
assert(psql != 0);
|
|
|
|
settings_cache_free();
|
|
|
|
result = config_query(psql, mode, "SELECT SQL_NO_CACHE name, value FROM settings");
|
|
|
|
if (result == NULL) return;
|
|
|
|
rows = mysql_num_rows(result);
|
|
|
|
if (rows == 0 || rows > (my_ulonglong) INT_MAX) {
|
|
db_free_result(result);
|
|
return;
|
|
}
|
|
|
|
table = (setting_cache_t *) calloc((size_t) rows, sizeof(setting_cache_t));
|
|
|
|
if (table == NULL) {
|
|
db_free_result(result);
|
|
return;
|
|
}
|
|
|
|
while ((row = mysql_fetch_row(result)) != NULL && i < (int) rows) {
|
|
if (row[0] == NULL) continue;
|
|
|
|
table[i].name = strdup(row[0]);
|
|
table[i].value = strdup(row[1] != NULL ? row[1] : "");
|
|
|
|
if (table[i].name == NULL || table[i].value == NULL) {
|
|
free(table[i].name);
|
|
free(table[i].value);
|
|
break;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
db_free_result(result);
|
|
|
|
settings_cache = table;
|
|
settings_cache_count = i;
|
|
|
|
SPINE_LOG_DEBUG(("DEBUG: Loaded %i Cacti settings in one query", i));
|
|
}
|
|
|
|
/*! \fn static char *getsetting(MYSQL *psql, int mode, const char *setting)
|
|
* \brief Returns a character pointer to a Cacti setting.
|
|
*
|
|
* Given a pointer to a database and the name of a setting, return the string
|
|
* which represents the value from the settings table. Return NULL if we
|
|
* can't find a setting for whatever reason.
|
|
*
|
|
* NOTE: if the user has provided one of these options on the command line,
|
|
* it's intercepted here and returned, overriding the database setting.
|
|
*
|
|
* \return the database option setting
|
|
*
|
|
*/
|
|
static char *getsetting(MYSQL *psql, int mode, const char *setting) {
|
|
char qstring[BUFSIZE];
|
|
char *retval;
|
|
MYSQL_RES *result;
|
|
MYSQL_ROW mysql_row;
|
|
|
|
assert(psql != 0);
|
|
assert(setting != 0);
|
|
|
|
/* see if it's in the option table */
|
|
for (int i = 0; i < nopts; i++) {
|
|
if (STRIMATCH(setting, opttable[i].opt)) {
|
|
/* FOUND IT! */
|
|
retval = strdup(opttable[i].val);
|
|
return retval;
|
|
}
|
|
}
|
|
|
|
/* served from the bulk-loaded table when read_config_options() is running */
|
|
if (settings_cache != NULL) {
|
|
for (int i = 0; i < settings_cache_count; i++) {
|
|
if (STRIMATCH(setting, settings_cache[i].name)) {
|
|
return strdup(settings_cache[i].value);
|
|
}
|
|
}
|
|
|
|
return strdup("");
|
|
}
|
|
|
|
spine_snprintf(qstring, sizeof(qstring), "SELECT SQL_NO_CACHE value FROM settings WHERE name = '%s'", setting);
|
|
|
|
result = config_query(psql, mode, qstring);
|
|
if (result == NULL) return strdup("");
|
|
mysql_row = mysql_num_rows(result) > 0 ? mysql_fetch_row(result) : NULL;
|
|
retval = mysql_row != NULL && mysql_row[0] != NULL ? strdup(mysql_row[0]) : strdup("");
|
|
db_free_result(result);
|
|
return retval;
|
|
}
|
|
|
|
/*! \fn int putsetting(MYSQL *psql, const char *setting, const char *value)
|
|
* \brief Set's a specific Cacti setting.
|
|
*
|
|
* Given a pointer to a database and the name of a setting, and value of that setting
|
|
* set the Cacti setting in the database to the value.
|
|
*
|
|
* \return true for successful or false for failed
|
|
*
|
|
*/
|
|
int putsetting(MYSQL *psql, int mode, const char *mysetting, const char *myvalue) {
|
|
char qstring[BUFSIZE];
|
|
int result = 0;
|
|
|
|
assert(psql != 0);
|
|
assert(mysetting != 0);
|
|
assert(myvalue != 0);
|
|
|
|
if (set.database.onupdate == 0) {
|
|
spine_snprintf(qstring, sizeof(qstring), "INSERT INTO settings (name, value) "
|
|
"VALUES ('%s', '%s') "
|
|
"ON DUPLICATE KEY UPDATE value = VALUES(value)", mysetting, myvalue);
|
|
} else {
|
|
spine_snprintf(qstring, sizeof(qstring), "INSERT INTO settings (name, value) "
|
|
"VALUES ('%s', '%s') AS rs "
|
|
"ON DUPLICATE KEY UPDATE value = rs.value", mysetting, myvalue);
|
|
}
|
|
|
|
result = db_insert(psql, mode, qstring);
|
|
|
|
return result;
|
|
}
|
|
|
|
/*! \fn static char *getpsetting(MYSQL *psql, const char *setting)
|
|
* \brief Returns a character pointer to a Cacti poller setting.
|
|
*
|
|
* Given a pointer to a database and the name of a setting,
|
|
* return the string which represents the value from the poller table.
|
|
* Return NULL if we can't find a setting for whatever reason.
|
|
*
|
|
* NOTE: if the user has provided one of these options on the command line,
|
|
* it's intercepted here and returned, overriding the database setting.
|
|
*
|
|
* \return the database option setting
|
|
*
|
|
*/
|
|
static char *getpsetting(MYSQL *psql, int mode, const char *setting) {
|
|
char qstring[BUFSIZE];
|
|
char *retval;
|
|
MYSQL_RES *result;
|
|
MYSQL_ROW mysql_row;
|
|
|
|
assert(psql != 0);
|
|
assert(setting != 0);
|
|
|
|
/* see if it's in the option table */
|
|
for (int i = 0; i < nopts; i++) {
|
|
if (STRIMATCH(setting, opttable[i].opt)) {
|
|
/* FOUND IT! */
|
|
retval = strdup(opttable[i].val);
|
|
return retval;
|
|
}
|
|
}
|
|
|
|
spine_snprintf(qstring, sizeof(qstring), "SELECT SQL_NO_CACHE %s FROM poller WHERE id = '%d'", setting, set.poller.poller_id);
|
|
|
|
result = config_query(psql, mode, qstring);
|
|
if (result == NULL) return NULL;
|
|
mysql_row = mysql_num_rows(result) > 0 ? mysql_fetch_row(result) : NULL;
|
|
retval = mysql_row != NULL && mysql_row[0] != NULL ? strdup(mysql_row[0]) : NULL;
|
|
db_free_result(result);
|
|
return retval;
|
|
}
|
|
|
|
/*! \fn static int getboolsetting(MYSQL *psql, int mode, const char *setting, int dflt)
|
|
* \brief Obtains a boolean option from the database.
|
|
*
|
|
* Given the parameters for fetching a setting from the database,
|
|
* do so for a *Boolean* value. We parse the usual set of words
|
|
* meaning true/false, and if we don't get a value, or if we don't
|
|
* understand what we fetched, we use the default value provided.
|
|
*
|
|
* \return boolean TRUE or FALSE based upon database setting or the DEFAULT if not found
|
|
*/
|
|
static int getboolsetting(MYSQL *psql, int mode, const char *setting, int dflt) {
|
|
char *rc;
|
|
|
|
assert(psql != 0);
|
|
assert(setting != 0);
|
|
|
|
rc = getsetting(psql, mode, setting);
|
|
|
|
if (rc == 0) return dflt;
|
|
|
|
if (STRIMATCH(rc, "on" ) ||
|
|
STRIMATCH(rc, "yes" ) ||
|
|
STRIMATCH(rc, "true") ||
|
|
STRIMATCH(rc, "1" ) ) {
|
|
free(rc);
|
|
return TRUE;
|
|
}
|
|
|
|
if (STRIMATCH(rc, "off" ) ||
|
|
STRIMATCH(rc, "no" ) ||
|
|
STRIMATCH(rc, "false") ||
|
|
STRIMATCH(rc, "0" ) ) {
|
|
free(rc);
|
|
return FALSE;
|
|
}
|
|
|
|
/* doesn't really match one of our keywords: what to do? */
|
|
free(rc);
|
|
|
|
return dflt;
|
|
}
|
|
|
|
/*! \fn static char *getglobalvariable(MYSQL *psql, const char *setting)
|
|
* \brief Returns a character pointer to a MySQL global variable setting.
|
|
*
|
|
* Given a pointer to a database and the name of a global variable, return the string
|
|
* which represents that value from the settings table. Return NULL if we
|
|
* can't find a variable for whatever reason.
|
|
*
|
|
* \return the database global variable setting
|
|
*
|
|
*/
|
|
static char *getglobalvariable(MYSQL *psql, int mode, const char *setting) {
|
|
char qstring[BUFSIZE];
|
|
char *retval;
|
|
MYSQL_RES *result;
|
|
MYSQL_ROW mysql_row;
|
|
|
|
assert(psql != 0);
|
|
assert(setting != 0);
|
|
|
|
/* see if it's in the option table */
|
|
for (int i = 0; i < nopts; i++) {
|
|
if (STRIMATCH(setting, opttable[i].opt)) {
|
|
/* FOUND IT! */
|
|
return strdup(opttable[i].val);
|
|
}
|
|
}
|
|
|
|
spine_snprintf(qstring, sizeof(qstring), "SHOW GLOBAL VARIABLES LIKE '%s'", setting);
|
|
|
|
result = config_query(psql, mode, qstring);
|
|
if (result == NULL) return NULL;
|
|
mysql_row = mysql_num_rows(result) > 0 ? mysql_fetch_row(result) : NULL;
|
|
retval = mysql_row != NULL && mysql_row[1] != NULL ? strdup(mysql_row[1]) : NULL;
|
|
db_free_result(result);
|
|
return retval;
|
|
}
|
|
|
|
/*! \fn int is_debug_device(int device_id)
|
|
* \brief Determine if a device is a debug device
|
|
*
|
|
*/
|
|
int is_debug_device(int device_id) {
|
|
extern int *debug_devices;
|
|
int i = 0;
|
|
|
|
/* main() releases the list during shutdown; later logging must not read it. */
|
|
if (debug_devices == NULL) return FALSE;
|
|
|
|
while (i < MAX_DEBUG_DEVICES) {
|
|
if (debug_devices[i] == '\0') break;
|
|
if (debug_devices[i] == device_id) {
|
|
return TRUE;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
void parse_debug_devices(char *device_list, int *devices, size_t capacity) {
|
|
if (capacity == 0) return;
|
|
devices[0] = 0;
|
|
char *saveptr = NULL;
|
|
const char *token = strtok_r(device_list, ",", &saveptr);
|
|
for (size_t i = 0; token != NULL && i < capacity - 1; i++, token = strtok_r(NULL, ",", &saveptr)) {
|
|
devices[i] = atoi(token);
|
|
devices[i + 1] = 0;
|
|
}
|
|
}
|
|
|
|
static void read_logging_options(MYSQL *mysql) {
|
|
char *res;
|
|
char web_root[BUFSIZE] = {0};
|
|
/* get logging level from database - overrides spine.conf */
|
|
if ((res = getsetting(mysql, LOCAL, "log_verbosity")) != 0) {
|
|
const int n = atoi(res);
|
|
free(res);
|
|
if (n != 0) set.logging.log_level = n;
|
|
}
|
|
|
|
/* determine script server path operation and default log file processing */
|
|
if ((res = getsetting(mysql, LOCAL, "path_webroot")) != 0) {
|
|
snprintf(set.php.path_php_server, BUFSIZE, "%s/script_server.php", res);
|
|
snprintf(web_root, BUFSIZE, "%s", res);
|
|
free(res);
|
|
}
|
|
|
|
/* determine logfile path */
|
|
if ((res = getsetting(mysql, LOCAL, "path_cactilog")) != 0) {
|
|
if (strlen(res) != 0) {
|
|
snprintf(set.logging.path_logfile, DBL_BUFSIZE, "%s", res);
|
|
} else {
|
|
if (strlen(web_root) != 0) {
|
|
snprintf(set.logging.path_logfile, DBL_BUFSIZE, "%s/log/cacti.log", web_root);
|
|
} else {
|
|
set.logging.path_logfile[0] ='\0';
|
|
}
|
|
}
|
|
free(res);
|
|
} else {
|
|
snprintf(set.logging.path_logfile, DBL_BUFSIZE, "%s/log/cacti.log", web_root);
|
|
}
|
|
|
|
/* get log separator */
|
|
if ((res = getsetting(mysql, LOCAL, "default_datechar")) != 0) {
|
|
set.logging.log_datetime_separator = atoi(res);
|
|
free(res);
|
|
|
|
if (set.logging.log_datetime_separator < GDC_MIN || set.logging.log_datetime_separator > GDC_MAX) {
|
|
set.logging.log_datetime_separator = GDC_DEFAULT;
|
|
}
|
|
}
|
|
|
|
/* get log date format */
|
|
if ((res = getsetting(mysql, LOCAL, "default_date_format")) != 0) {
|
|
set.logging.log_datetime_format = atoi(res);
|
|
free(res);
|
|
|
|
if (set.logging.log_datetime_format < GD_MIN || set.logging.log_datetime_format > GD_MAX) {
|
|
set.logging.log_datetime_format = GD_DEFAULT;
|
|
}
|
|
}
|
|
|
|
/* The remaining option reads can log at debug verbosity. Refresh the
|
|
* cached format before any of those messages are emitted. */
|
|
set_date_format();
|
|
|
|
/* determine log file, syslog or both, default is 1 or log file only */
|
|
if ((res = getsetting(mysql, LOCAL, "log_destination")) != 0) {
|
|
set.logging.log_destination = parse_logdest(res, LOGDEST_FILE);
|
|
free(res);
|
|
} else {
|
|
set.logging.log_destination = LOGDEST_FILE;
|
|
}
|
|
|
|
/* log the path_webroot variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The path_php_server variable is %s", set.php.path_php_server));
|
|
|
|
/* log the path_cactilog variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The path_cactilog variable is %s", set.logging.path_logfile));
|
|
|
|
/* the version variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The version variable is %s", set.database.version));
|
|
|
|
/* log the log_destination variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The log_destination variable is %i (%s)",
|
|
set.logging.log_destination,
|
|
printable_logdest(set.logging.log_destination)));
|
|
|
|
set.logging.logfile_processed = TRUE;
|
|
|
|
}
|
|
|
|
static void read_ping_options(MYSQL *mysql) {
|
|
char *res;
|
|
/* set availability_method */
|
|
if ((res = getsetting(mysql, LOCAL, "availability_method")) != 0) {
|
|
set.availability.availability_method = atoi(res);
|
|
free(res);
|
|
}
|
|
|
|
/* log the availability_method variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The availability_method variable is %i", set.availability.availability_method));
|
|
|
|
/* set ping_recovery_count */
|
|
if ((res = getsetting(mysql, LOCAL, "ping_recovery_count")) != 0) {
|
|
set.availability.ping_recovery_count = atoi(res);
|
|
free(res);
|
|
}
|
|
|
|
/* log the ping_recovery_count variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The ping_recovery_count variable is %i", set.availability.ping_recovery_count));
|
|
|
|
/* set ping_failure_count */
|
|
if ((res = getsetting(mysql, LOCAL, "ping_failure_count")) != 0) {
|
|
set.availability.ping_failure_count = atoi(res);
|
|
free(res);
|
|
}
|
|
|
|
/* log the ping_failure_count variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The ping_failure_count variable is %i", set.availability.ping_failure_count));
|
|
|
|
/* set ping_method */
|
|
if ((res = getsetting(mysql, LOCAL, "ping_method")) != 0) {
|
|
set.availability.ping_method = atoi(res);
|
|
free(res);
|
|
}
|
|
|
|
/* log the ping_method variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The ping_method variable is %i", set.availability.ping_method));
|
|
|
|
/* set ping_retries */
|
|
if ((res = getsetting(mysql, LOCAL, "ping_retries")) != 0) {
|
|
set.availability.ping_retries = atoi(res);
|
|
free(res);
|
|
}
|
|
|
|
/* log the ping_retries variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The ping_retries variable is %i", set.availability.ping_retries));
|
|
|
|
/* set ping_timeout */
|
|
if ((res = getsetting(mysql, LOCAL, "ping_timeout")) != 0) {
|
|
set.availability.ping_timeout = atoi(res);
|
|
free(res);
|
|
} else {
|
|
set.availability.ping_timeout = 400;
|
|
}
|
|
|
|
/* log the ping_timeout variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The ping_timeout variable is %i", set.availability.ping_timeout));
|
|
|
|
/* set snmp_retries */
|
|
if ((res = getsetting(mysql, LOCAL, "snmp_retries")) != 0) {
|
|
set.snmp.snmp_retries = atoi(res);
|
|
free(res);
|
|
} else {
|
|
set.snmp.snmp_retries = 3;
|
|
}
|
|
|
|
/* log the snmp_retries variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The snmp_retries variable is %i", set.snmp.snmp_retries));
|
|
|
|
}
|
|
|
|
static void read_process_options(MYSQL *mysql, int mode) {
|
|
char *res;
|
|
/* get Cacti defined max threads override spine.conf */
|
|
res = set.poller.threads_set == FALSE ? getpsetting(mysql, mode, "threads") : NULL;
|
|
if (res != NULL) {
|
|
set.poller.threads = atoi(res);
|
|
free(res);
|
|
if (set.poller.threads > MAX_THREADS) {
|
|
set.poller.threads = MAX_THREADS;
|
|
}
|
|
}
|
|
|
|
/* log the threads variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The threads variable is %i", set.poller.threads));
|
|
|
|
/* get the poller_interval for those who have elected to go with a 1 minute polling interval */
|
|
if ((res = getsetting(mysql, LOCAL, "poller_interval")) != 0) {
|
|
set.poller.poller_interval = atoi(res);
|
|
free(res);
|
|
} else {
|
|
set.poller.poller_interval = 0;
|
|
}
|
|
|
|
/* log the poller_interval variable */
|
|
if (set.poller.poller_interval == 0) {
|
|
SPINE_LOG_DEBUG(("DEBUG: The polling interval is the system default"));
|
|
} else {
|
|
SPINE_LOG_DEBUG(("DEBUG: The polling interval is %i seconds", set.poller.poller_interval));
|
|
}
|
|
|
|
/* get the concurrent_processes variable to determine thread sleep values */
|
|
if ((res = getsetting(mysql, LOCAL, "concurrent_processes")) != 0) {
|
|
set.poller.num_parent_processes = atoi(res);
|
|
free(res);
|
|
} else {
|
|
set.poller.num_parent_processes = 1;
|
|
}
|
|
|
|
/* log the concurrent processes variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The number of concurrent processes is %i", set.poller.num_parent_processes));
|
|
|
|
/* get the script timeout to establish timeouts */
|
|
if ((res = getsetting(mysql, LOCAL, "script_timeout")) != 0) {
|
|
set.php.script_timeout = atoi(res);
|
|
free(res);
|
|
if (set.php.script_timeout < 5) {
|
|
set.php.script_timeout = 5;
|
|
}
|
|
} else {
|
|
set.php.script_timeout = 25;
|
|
}
|
|
|
|
/* log the script timeout value */
|
|
SPINE_LOG_DEBUG(("DEBUG: The script timeout is %i", set.php.script_timeout));
|
|
|
|
}
|
|
|
|
static void read_script_options(MYSQL *mysql) {
|
|
char *res;
|
|
/* get selective_device_debug string */
|
|
if ((res = getsetting(mysql, LOCAL, "selective_device_debug")) != 0) {
|
|
STRNCOPY(set.logging.selective_device_debug, res);
|
|
free(res);
|
|
}
|
|
|
|
/* log the selective_device_debug variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The selective_device_debug variable is %s", set.logging.selective_device_debug));
|
|
|
|
/* get spine_log_level */
|
|
if ((res = getsetting(mysql, LOCAL, "spine_log_level")) != 0) {
|
|
set.logging.spine_log_level = atoi(res);
|
|
free(res);
|
|
}
|
|
|
|
/* log the spine_log_level variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The spine_log_level variable is %i", set.logging.spine_log_level));
|
|
|
|
/* get the number of script server processes to run */
|
|
if ((res = getsetting(mysql, LOCAL, "php_servers")) != 0) {
|
|
set.php.php_servers = atoi(res);
|
|
free(res);
|
|
|
|
if (set.php.php_servers > MAX_PHP_SERVERS) {
|
|
set.php.php_servers = MAX_PHP_SERVERS;
|
|
}
|
|
|
|
if (set.php.php_servers <= 0) {
|
|
set.php.php_servers = 1;
|
|
}
|
|
} else {
|
|
set.php.php_servers = 2;
|
|
}
|
|
|
|
/* log the script timeout value */
|
|
SPINE_LOG_DEBUG(("DEBUG: The number of php script servers to run is %i", set.php.php_servers));
|
|
|
|
/* get the number of active profiles on the system run */
|
|
if ((res = getsetting(mysql, LOCAL, "active_profiles")) != 0) {
|
|
set.poller.active_profiles = atoi(res);
|
|
free(res);
|
|
|
|
if (set.poller.active_profiles <= 0) {
|
|
set.poller.active_profiles = 0;
|
|
}
|
|
} else {
|
|
set.poller.active_profiles = 0;
|
|
}
|
|
|
|
/* log the script timeout value */
|
|
SPINE_LOG_DEBUG(("DEBUG: The number of active data source profiles is %i", set.poller.active_profiles));
|
|
|
|
/* get the number of snmp_ports in use */
|
|
if ((res = getsetting(mysql, LOCAL, "total_snmp_ports")) != 0) {
|
|
set.snmp.total_snmp_ports = atoi(res);
|
|
free(res);
|
|
|
|
if (set.snmp.total_snmp_ports <= 0) {
|
|
set.snmp.total_snmp_ports = 0;
|
|
}
|
|
} else {
|
|
set.snmp.total_snmp_ports = 0;
|
|
}
|
|
|
|
/* log the script timeout value */
|
|
SPINE_LOG_DEBUG(("DEBUG: The number of snmp ports on the system is %i", set.snmp.total_snmp_ports));
|
|
|
|
}
|
|
|
|
static void read_php_requirement(MYSQL *mysql) {
|
|
MYSQL_RES *result;
|
|
int num_rows;
|
|
char sqlbuf[HUGE_BUFSIZE];
|
|
char *sqlp;
|
|
/*----------------------------------------------------------------
|
|
* determine if the php script server is required by searching for
|
|
* all the host records for an action of POLLER_ACTION_PHP_SCRIPT_SERVER.
|
|
* If we get even one, it means we have to deal with the PHP script
|
|
* server.
|
|
*
|
|
*/
|
|
set.php.php_required = FALSE; /* assume no */
|
|
|
|
/* log the requirement for the script server */
|
|
if (!strlen(set.hosts.host_id_list)) {
|
|
sqlp = sqlbuf;
|
|
sqlp += spine_snprintf(sqlp, sizeof(sqlbuf) - (size_t)(sqlp - sqlbuf), "SELECT SQL_NO_CACHE action FROM poller_item");
|
|
sqlp += spine_snprintf(sqlp, sizeof(sqlbuf) - (size_t)(sqlp - sqlbuf), " WHERE action=%d", POLLER_ACTION_PHP_SCRIPT_SERVER);
|
|
sqlp += append_hostrange(sqlp, sizeof(sqlbuf) - (size_t)(sqlp - sqlbuf), "host_id");
|
|
sqlp += spine_snprintf(sqlp, sizeof(sqlbuf) - (size_t)(sqlp - sqlbuf), " AND poller_id=%i", set.poller.poller_id);
|
|
spine_snprintf(sqlp, sizeof(sqlbuf) - (size_t)(sqlp - sqlbuf), " LIMIT 1");
|
|
|
|
result = config_query(mysql, LOCAL, sqlbuf);
|
|
num_rows = result != NULL ? spine_count_to_int(mysql_num_rows(result)) : 0;
|
|
db_free_result(result);
|
|
|
|
if (num_rows > 0) set.php.php_required = TRUE;
|
|
|
|
SPINE_LOG_DEBUG(("DEBUG: StartDevice='%i', EndDevice='%i', TotalPHPScripts='%i'",
|
|
set.hosts.start_host_id,
|
|
set.hosts.end_host_id,
|
|
num_rows));
|
|
} else {
|
|
sqlp = sqlbuf;
|
|
sqlp += spine_snprintf(sqlp, sizeof(sqlbuf) - (size_t)(sqlp - sqlbuf), "SELECT SQL_NO_CACHE action FROM poller_item");
|
|
sqlp += spine_snprintf(sqlp, sizeof(sqlbuf) - (size_t)(sqlp - sqlbuf), " WHERE action=%d", POLLER_ACTION_PHP_SCRIPT_SERVER);
|
|
sqlp += spine_snprintf(sqlp, sizeof(sqlbuf) - (size_t)(sqlp - sqlbuf), " AND host_id IN(%s)", set.hosts.host_id_list);
|
|
sqlp += spine_snprintf(sqlp, sizeof(sqlbuf) - (size_t)(sqlp - sqlbuf), " AND poller_id=%i", set.poller.poller_id);
|
|
spine_snprintf(sqlp, sizeof(sqlbuf) - (size_t)(sqlp - sqlbuf), " LIMIT 1");
|
|
|
|
result = config_query(mysql, LOCAL, sqlbuf);
|
|
num_rows = result != NULL ? spine_count_to_int(mysql_num_rows(result)) : 0;
|
|
db_free_result(result);
|
|
|
|
if (num_rows > 0) set.php.php_required = TRUE;
|
|
|
|
SPINE_LOG_DEBUG(("DEBUG: Device List to be polled='%s', TotalPHPScripts='%i'",
|
|
set.hosts.host_id_list,
|
|
num_rows));
|
|
}
|
|
|
|
SPINE_LOG_DEBUG(("DEBUG: The PHP Script Server is %sRequired",
|
|
set.php.php_required
|
|
? ""
|
|
: "Not "));
|
|
|
|
}
|
|
|
|
static void read_snmp_batch_size(MYSQL *mysql) {
|
|
char *res;
|
|
/* determine the maximum oid's to obtain in a single get request */
|
|
if ((res = getsetting(mysql, LOCAL, "max_get_size")) != 0) {
|
|
set.snmp.snmp_max_get_size = atoi(res);
|
|
free(res);
|
|
|
|
if (set.snmp.snmp_max_get_size > 128) {
|
|
set.snmp.snmp_max_get_size = 128;
|
|
}
|
|
} else {
|
|
set.snmp.snmp_max_get_size = 25;
|
|
}
|
|
|
|
/* log the snmp_max_get_size variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The Maximum SNMP OID Get Size is %i", set.snmp.snmp_max_get_size));
|
|
|
|
}
|
|
|
|
static void publish_snmp_capabilities(MYSQL *mysql) {
|
|
char spine_auth[BUFSIZE] = {0};
|
|
char spine_priv[BUFSIZE] = {0};
|
|
char spine_capabilities[BUFSIZE] = {0};
|
|
|
|
#ifndef NETSNMP_DISABLE_MD5
|
|
strcat(spine_auth, "MD5");
|
|
#endif
|
|
|
|
strcat(spine_auth, (strlen(spine_auth) > 0 ? ",SHA":"SHA"));
|
|
|
|
#if defined(NETSNMP_USMAUTH_HMAC128SHA224)
|
|
strcat(spine_auth, ",SHA224,SHA256");
|
|
#endif
|
|
|
|
#if defined(NETSNMP_USMAUTH_HMAC192SHA256)
|
|
strcat(spine_auth, ",SHA384,SHA512");
|
|
#endif
|
|
|
|
#ifndef NETSNMP_DISABLE_DES
|
|
strcat(spine_priv, "DES");
|
|
#endif
|
|
|
|
#ifdef HAVE_AES
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
strcat(spine_priv, (strlen(spine_priv) > 0 ? ",AES128":"AES128"));
|
|
#endif
|
|
|
|
#if defined(NETSNMP_DRAFT_BLUMENTHAL_AES_04)
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
strcat(spine_priv, (strlen(spine_priv) > 0 ? ",AES192":"AES192"));
|
|
#endif
|
|
|
|
#if defined(NETSNMP_DRAFT_BLUMENTHAL_AES_04)
|
|
// cppcheck-suppress knownConditionTrueFalse
|
|
strcat(spine_priv, (strlen(spine_priv) > 0 ? ",AES256":"AES256"));
|
|
#endif
|
|
|
|
/* Each source buffer can be BUFSIZE bytes. Bound both fields so the
|
|
* combined capability document always fits in its destination. */
|
|
if (!format_spine_capabilities(spine_capabilities,
|
|
sizeof(spine_capabilities), spine_auth, spine_priv)) {
|
|
SPINE_LOG(("ERROR: Unable to format Spine SNMP capabilities"));
|
|
spine_capabilities[0] = '\0';
|
|
}
|
|
|
|
if (set.poller.poller_id == 1 && spine_capabilities[0] != '\0') {
|
|
putsetting(mysql, LOCAL, "spine_capabilities", spine_capabilities);
|
|
}
|
|
|
|
}
|
|
|
|
/*! \fn void read_config_options(void)
|
|
* \brief Reads the default Spine runtime parameters from the database and set's the global array
|
|
*
|
|
* load default values from the database for poller processing
|
|
*
|
|
*/
|
|
/*! \fn int db_row_alias_upsert_supported(const char *version, unsigned long version_number)
|
|
* \brief whether the server accepts INSERT ... AS alias ON DUPLICATE KEY UPDATE
|
|
*/
|
|
int db_row_alias_upsert_supported(const char *version, unsigned long version_number) {
|
|
if (version == NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
for (const char *p = version; *p != '\0'; p++) {
|
|
if (strncasecmp(p, "mariadb", 7) == 0) {
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return version_number >= 80020 && version_number < 100000;
|
|
}
|
|
|
|
void read_config_options() {
|
|
MYSQL mysql;
|
|
MYSQL mysqlr;
|
|
int mode;
|
|
char *res;
|
|
|
|
if (!db_connect(LOCAL, &mysql)) die("FATAL: Unable to connect to the local database");
|
|
|
|
/* one round trip instead of one per setting */
|
|
settings_cache_load(&mysql, LOCAL);
|
|
|
|
if (set.poller.poller_id > 1 && set.poller.mode == REMOTE_ONLINE) {
|
|
if (!db_connect(REMOTE, &mysqlr)) die("FATAL: Unable to connect to the remote database");
|
|
mode = REMOTE;
|
|
} else {
|
|
mode = LOCAL;
|
|
}
|
|
|
|
/* get the mysql server version */
|
|
if ((res = getglobalvariable(&mysql, LOCAL, "version")) != 0) {
|
|
snprintf(set.database.version, BUFSIZE, "%s", res);
|
|
free(res);
|
|
}
|
|
|
|
/* The row alias form, INSERT ... AS rs, is a syntax error before
|
|
MySQL 8.0.20 and is not MariaDB syntax. */
|
|
set.database.onupdate = db_row_alias_upsert_supported(set.database.version, mysql_get_server_version(&mysql));
|
|
|
|
/* get the cacti version from the database */
|
|
set.cacti_version = get_cacti_version(&mysql, LOCAL);
|
|
|
|
/* log the path_webroot variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The binary Cacti version is %d", set.cacti_version));
|
|
|
|
/* Warn when Spine and Cacti come from different release lines. Point
|
|
* releases are expected to drift, so only major.minor is compared;
|
|
* cacti_version is encoded as major*1000 + minor*100 + point. */
|
|
if (set.cacti_version > 0) {
|
|
int spine_major = 0, spine_minor = 0, spine_point = 0;
|
|
|
|
if (sscanf(VERSION, "%d.%d.%d", &spine_major, &spine_minor, &spine_point) >= 2) {
|
|
int spine_line = (spine_major * 1000) + (spine_minor * 100);
|
|
int cacti_line = (set.cacti_version / 100) * 100;
|
|
|
|
if (spine_line != cacti_line) {
|
|
SPINE_LOG(("WARNING: Spine %s does not match the Cacti release line (Cacti reports %d.%d). Use the Spine built for this Cacti version.",
|
|
VERSION, set.cacti_version / 1000, (set.cacti_version / 100) % 10));
|
|
}
|
|
}
|
|
}
|
|
|
|
read_logging_options(&mysql);
|
|
|
|
/* get PHP Path Information for Scripting */
|
|
if ((res = getsetting(&mysql, LOCAL, "path_php_binary")) != 0) {
|
|
STRNCOPY(set.php.path_php, res);
|
|
free(res);
|
|
}
|
|
|
|
/* log the path_php variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The path_php variable is %s", set.php.path_php));
|
|
|
|
read_ping_options(&mysql);
|
|
|
|
/* set logging option for errors */
|
|
set.logging.log_perror = getboolsetting(&mysql, LOCAL, "log_perror", FALSE);
|
|
|
|
/* log the log_perror variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The log_perror variable is %i", set.logging.log_perror));
|
|
|
|
/* set logging option for errors */
|
|
set.logging.log_pwarn = getboolsetting(&mysql, LOCAL, "log_pwarn", FALSE);
|
|
|
|
/* log the log_pwarn variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The log_pwarn variable is %i", set.logging.log_pwarn));
|
|
|
|
/* set option to increase insert performance */
|
|
set.boost.boost_redirect = getboolsetting(&mysql, LOCAL, "boost_redirect", FALSE);
|
|
|
|
/* log the boost_redirect variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The boost_redirect variable is %i", set.boost.boost_redirect));
|
|
|
|
/* set option for determining if boost is enabled */
|
|
set.boost.boost_enabled = getboolsetting(&mysql, LOCAL, "boost_rrd_update_enable", FALSE);
|
|
|
|
/* log the boost_rrd_update_enable variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The boost_rrd_update_enable variable is %i", set.boost.boost_enabled));
|
|
|
|
/* set logging option for statistics */
|
|
set.logging.log_pstats = getboolsetting(&mysql, LOCAL, "log_pstats", FALSE);
|
|
|
|
/* log the log_pstats variable */
|
|
SPINE_LOG_DEBUG(("DEBUG: The log_pstats variable is %i", set.logging.log_pstats));
|
|
|
|
read_process_options(&mysql, mode);
|
|
|
|
read_script_options(&mysql);
|
|
|
|
read_php_requirement(&mysql);
|
|
|
|
read_snmp_batch_size(&mysql);
|
|
|
|
publish_snmp_capabilities(&mysql);
|
|
|
|
db_disconnect(&mysql);
|
|
|
|
if (set.poller.poller_id > 1 && set.poller.mode == REMOTE_ONLINE) {
|
|
db_disconnect(&mysqlr);
|
|
}
|
|
|
|
settings_cache_free();
|
|
}
|
|
|
|
typedef struct {
|
|
const char *table;
|
|
const char *columns;
|
|
const char *filter_column;
|
|
const char *order_column;
|
|
const char *const *updates;
|
|
size_t update_count;
|
|
size_t field_count;
|
|
size_t row_limit;
|
|
} poller_transfer_t;
|
|
|
|
static void transfer_queries(const poller_transfer_t *plan, char *query, size_t query_capacity, char *prefix, size_t prefix_capacity, char *suffix, size_t suffix_capacity) {
|
|
size_t used = (size_t)spine_snprintf(query, query_capacity, "SELECT SQL_NO_CACHE %s FROM %s WHERE poller_id = %d", plan->columns, plan->table, set.poller.poller_id);
|
|
if (set.hosts.host_id_list[0] != '\0') used += (size_t)spine_snprintf(query + used, query_capacity - used, " AND %s IN (%s)", plan->filter_column, set.hosts.host_id_list);
|
|
spine_snprintf(query + used, query_capacity - used, " ORDER BY %s", plan->order_column);
|
|
spine_snprintf(prefix, prefix_capacity, "INSERT INTO %s (%s) VALUES ", plan->table, plan->columns);
|
|
/* These rows go to the main server. The cached upsert capability describes the
|
|
* local server, so the row-alias form could reach a MariaDB main server
|
|
* that rejects it. VALUES() is accepted by both (#590). */
|
|
used = (size_t)spine_snprintf(suffix, suffix_capacity, " ON DUPLICATE KEY UPDATE ");
|
|
for (size_t index = 0; index < plan->update_count; index++) {
|
|
const char *column = plan->updates[index];
|
|
used += (size_t)spine_snprintf(suffix + used, suffix_capacity - used, "%s%s=VALUES(%s)", index == 0 ? "" : ", ", column, column);
|
|
}
|
|
}
|
|
|
|
static size_t transfer_row(MYSQL *destination, MYSQL_ROW row, size_t field_count, char *output, size_t capacity) {
|
|
/* The selected columns are at most 300 UTF-8 characters: allow 4 bytes per
|
|
* character and doubling for SQL escaping. Preserve SQL NULL explicitly. */
|
|
char escaped[DBL_BUFSIZE * 2];
|
|
size_t used = (size_t)spine_snprintf(output, capacity, "(");
|
|
for (size_t index = 0; index < field_count; index++) {
|
|
if (row[index] == NULL) {
|
|
used += (size_t)spine_snprintf(output + used, capacity - used, "%sNULL", index == 0 ? "" : ", ");
|
|
} else {
|
|
db_escape(destination, escaped, sizeof(escaped), row[index]);
|
|
used += (size_t)spine_snprintf(output + used, capacity - used, "%s'%s'", index == 0 ? "" : ", ", escaped);
|
|
}
|
|
}
|
|
used += (size_t)spine_snprintf(output + used, capacity - used, ")");
|
|
return used;
|
|
}
|
|
|
|
static bool transfer_batch(MYSQL *destination, char *buffer, size_t used, const char *suffix) {
|
|
spine_snprintf(buffer + used, HUGE_BUFSIZE - used, "%s", suffix);
|
|
return db_insert(destination, REMOTE, buffer);
|
|
}
|
|
|
|
static bool transfer_table(MYSQL *source, MYSQL *destination, const poller_transfer_t *plan) {
|
|
char query[MEGA_BUFSIZE];
|
|
char prefix[BUFSIZE];
|
|
char suffix[BUFSIZE];
|
|
transfer_queries(plan, query, sizeof(query), prefix, sizeof(prefix), suffix, sizeof(suffix));
|
|
MYSQL_RES *result = db_query(source, LOCAL, query);
|
|
if (result == NULL) return FALSE;
|
|
if (mysql_num_fields(result) != plan->field_count) {
|
|
db_free_result(result);
|
|
return FALSE;
|
|
}
|
|
/* Size one row for every column at its escaped maximum plus separators;
|
|
* a fixed few kilobytes could not hold a host row with wide SNMP strings. */
|
|
size_t row_capacity = plan->field_count * (DBL_BUFSIZE * 2 + 4) + 2;
|
|
char *row_sql = malloc(row_capacity);
|
|
char *buffer = malloc(HUGE_BUFSIZE);
|
|
if (buffer == NULL || row_sql == NULL) {
|
|
free(row_sql);
|
|
free(buffer);
|
|
db_free_result(result);
|
|
return FALSE;
|
|
}
|
|
size_t used = 0;
|
|
size_t rows = 0;
|
|
size_t suffix_length = strlen(suffix);
|
|
MYSQL_ROW row;
|
|
bool success = TRUE;
|
|
while ((row = mysql_fetch_row(result)) != NULL) {
|
|
size_t length = transfer_row(destination, row, plan->field_count, row_sql, row_capacity);
|
|
if (rows > 0 && (rows == plan->row_limit || length + suffix_length + 3 > HUGE_BUFSIZE - used)) {
|
|
if (!transfer_batch(destination, buffer, used, suffix)) {
|
|
success = FALSE;
|
|
break;
|
|
}
|
|
rows = 0;
|
|
}
|
|
if (rows == 0) used = (size_t)spine_snprintf(buffer, HUGE_BUFSIZE, "%s", prefix);
|
|
else used += (size_t)spine_snprintf(buffer + used, HUGE_BUFSIZE - used, ", ");
|
|
used += (size_t)spine_snprintf(buffer + used, HUGE_BUFSIZE - used, "%s", row_sql);
|
|
rows++;
|
|
}
|
|
if (mysql_errno(source) != 0) success = FALSE;
|
|
if (success && rows > 0) success = transfer_batch(destination, buffer, used, suffix);
|
|
free(row_sql);
|
|
free(buffer);
|
|
db_free_result(result);
|
|
return success;
|
|
}
|
|
|
|
bool poller_transfer_status(MYSQL *source, MYSQL *destination) {
|
|
static const char *const host_updates[] = {
|
|
"snmp_sysDescr", "snmp_sysObjectID", "snmp_sysUpTimeInstance", "snmp_sysContact", "snmp_sysName", "snmp_sysLocation",
|
|
"status", "status_event_count", "status_fail_date", "status_rec_date", "status_last_error", "min_time", "max_time",
|
|
"cur_time", "avg_time", "polling_time", "total_polls", "failed_polls", "availability", "last_updated"
|
|
};
|
|
static const char *const item_updates[] = {"rrd_next_step"};
|
|
static const poller_transfer_t host_plan = {
|
|
"host", "id, snmp_sysDescr, snmp_sysObjectID, snmp_sysUpTimeInstance, snmp_sysContact, snmp_sysName, snmp_sysLocation, status, status_event_count, status_fail_date, status_rec_date, status_last_error, min_time, max_time, cur_time, avg_time, polling_time, total_polls, failed_polls, availability, last_updated",
|
|
"id", "id", host_updates, sizeof(host_updates) / sizeof(host_updates[0]), 21, 500
|
|
};
|
|
static const poller_transfer_t item_plan = {
|
|
"poller_item", "local_data_id, host_id, rrd_name, rrd_step, rrd_next_step",
|
|
"host_id", "local_data_id, rrd_name", item_updates, sizeof(item_updates) / sizeof(item_updates[0]), 5, 10000
|
|
};
|
|
SPINE_LOG_MEDIUM(("Pushing Host Status to Main Server"));
|
|
if (!transfer_table(source, destination, &host_plan)) return FALSE;
|
|
SPINE_LOG_MEDIUM(("Pushing Poller Item RRD Next Step to Main Server"));
|
|
return transfer_table(source, destination, &item_plan);
|
|
}
|
|
|
|
void poller_push_data_to_main(void) {
|
|
MYSQL source;
|
|
MYSQL destination;
|
|
/* A failed connect leaves an initialized handle that still has to be closed. */
|
|
if (!db_connect(LOCAL, &source)) {
|
|
db_disconnect(&source);
|
|
SPINE_LOG(("ERROR: Collector synchronization skipped; the local database is unavailable. Local rows are retained for retry."));
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
return;
|
|
}
|
|
if (!db_connect(REMOTE, &destination)) {
|
|
db_disconnect(&destination);
|
|
db_disconnect(&source);
|
|
SPINE_LOG(("ERROR: Collector synchronization skipped; the main server is unavailable. Local rows are retained for retry."));
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
return;
|
|
}
|
|
/* Preserve the supported zero-date and GROUP BY session policies. */
|
|
bool configured = db_insert(&source, LOCAL, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE', ''))") &&
|
|
db_insert(&source, LOCAL, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY', ''))") &&
|
|
db_insert(&destination, REMOTE, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE', ''))") &&
|
|
db_insert(&destination, REMOTE, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY', ''))");
|
|
if (!configured || !poller_transfer_status(&source, &destination)) {
|
|
SPINE_LOG(("ERROR: Collector synchronization incomplete; earlier batches may have reached the main server. Local rows are retained for retry."));
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
}
|
|
db_disconnect(&source);
|
|
db_disconnect(&destination);
|
|
}
|
|
|
|
/*! \fn int read_spine_config(const char *file)
|
|
* \brief obtain default startup variables from the spine.conf file.
|
|
* \param file the spine config file
|
|
*
|
|
* \return 0 if successful or -1 if the file could not be opened
|
|
*/
|
|
static int apply_config_directive(const char *name, const char *value) {
|
|
/* Immutable metadata cannot be changed by configuration values written
|
|
* into set. Offsets and capacities describe the actual destination fields. */
|
|
static const struct {
|
|
const char *name;
|
|
size_t offset;
|
|
size_t capacity;
|
|
} strings[] = {
|
|
{"RDB_Host", offsetof(config_t, remote_database.host), sizeof(set.remote_database.host)},
|
|
{"RDB_Database", offsetof(config_t, remote_database.database), sizeof(set.remote_database.database)},
|
|
{"RDB_User", offsetof(config_t, remote_database.user), sizeof(set.remote_database.user)},
|
|
{"RDB_Pass", offsetof(config_t, remote_database.password), sizeof(set.remote_database.password)},
|
|
{"RDB_SSL_Key", offsetof(config_t, remote_database.ssl_key), sizeof(set.remote_database.ssl_key)},
|
|
{"RDB_SSL_Cert", offsetof(config_t, remote_database.ssl_cert), sizeof(set.remote_database.ssl_cert)},
|
|
{"RDB_SSL_CA", offsetof(config_t, remote_database.ssl_ca), sizeof(set.remote_database.ssl_ca)},
|
|
{"DB_Host", offsetof(config_t, database.host), sizeof(set.database.host)},
|
|
{"DB_Database", offsetof(config_t, database.database), sizeof(set.database.database)},
|
|
{"DB_User", offsetof(config_t, database.user), sizeof(set.database.user)},
|
|
{"DB_Pass", offsetof(config_t, database.password), sizeof(set.database.password)},
|
|
{"DB_SSL_Key", offsetof(config_t, database.ssl_key), sizeof(set.database.ssl_key)},
|
|
{"DB_SSL_Cert", offsetof(config_t, database.ssl_cert), sizeof(set.database.ssl_cert)},
|
|
{"DB_SSL_CA", offsetof(config_t, database.ssl_ca), sizeof(set.database.ssl_ca)},
|
|
{"SNMP_Clientaddr", offsetof(config_t, snmp.snmp_clientaddr), sizeof(set.snmp.snmp_clientaddr)},
|
|
};
|
|
for (size_t i = 0; i < sizeof(strings) / sizeof(strings[0]); i++) {
|
|
if (STRIMATCH(name, strings[i].name)) {
|
|
strncopy((char *)&set + strings[i].offset, value, strings[i].capacity);
|
|
return TRUE;
|
|
}
|
|
}
|
|
if (STRIMATCH(name, "RDB_Port")) set.remote_database.port = atoi(value);
|
|
else if (STRIMATCH(name, "RDB_UseSSL")) set.remote_database.ssl = atoi(value);
|
|
else if (STRIMATCH(name, "DB_Port")) set.database.port = atoi(value);
|
|
else if (STRIMATCH(name, "DB_UseSSL")) set.database.ssl = atoi(value);
|
|
else if (STRIMATCH(name, "Poller")) set.poller.poller_id = atoi(value);
|
|
else if (STRIMATCH(name, "DB_PreG")) {
|
|
if (!set.console.stderr_notty) fprintf(stderr, "WARNING: DB_PreG is no longer supported\n");
|
|
} else if (STRIMATCH(name, "Cacti_Log")) {
|
|
STRNCOPY(set.logging.path_logfile, value);
|
|
set.logging.logfile_processed = 1;
|
|
set.logging.log_destination = LOGDEST_BOTH;
|
|
} else {
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/* fgets() returns a line longer than its buffer in pieces, and each piece
|
|
* would be parsed as a directive of its own. Report whether the line just
|
|
* read was whole, consuming the remainder when it was not. The caller sets
|
|
* the last byte nonzero before reading: fgets() only clears it when it filled
|
|
* the buffer, which a string search cannot tell once the line holds a NUL. */
|
|
static bool config_line_complete(FILE *fp, const char *line, size_t capacity) {
|
|
int next;
|
|
|
|
if (line[capacity - 1] != '\0' || line[capacity - 2] == '\n') return TRUE;
|
|
next = getc(fp);
|
|
if (next == EOF || next == '\n') return TRUE;
|
|
while (next != EOF && next != '\n') next = getc(fp);
|
|
return FALSE;
|
|
}
|
|
|
|
int read_spine_config(const char *file) {
|
|
FILE *fp = fopen(file, "rb");
|
|
char buff[BUFSIZE];
|
|
char name[BUFSIZE];
|
|
char value[BUFSIZE];
|
|
char display_file[BUFSIZE];
|
|
int line = 0;
|
|
int fields;
|
|
strncopy(display_file, file, sizeof(display_file));
|
|
spine_sanitize_log_message(display_file);
|
|
|
|
if (fp == NULL) {
|
|
if (set.logging.log_level == POLLER_VERBOSITY_DEBUG && !set.console.stderr_notty) {
|
|
fprintf(stderr, "ERROR: Could not open config file [%s]\n", display_file);
|
|
}
|
|
return -1;
|
|
}
|
|
if (!set.console.stdout_notty) {
|
|
fprintf(stdout, "SPINE: Using spine config file [%s]\n", display_file);
|
|
}
|
|
for (;;) {
|
|
buff[sizeof(buff) - 1] = 1;
|
|
if (fgets(buff, sizeof(buff), fp) == NULL) break;
|
|
/* config_line_complete() consumes an overlong line whole, so each
|
|
* pass is exactly one line */
|
|
line++;
|
|
if (!config_line_complete(fp, buff, sizeof(buff))) {
|
|
if (!set.console.stderr_notty) {
|
|
fprintf(stderr, "WARNING: Ignoring line %d of %s, longer than %d characters\n", line, display_file, BUFSIZE - 2);
|
|
}
|
|
continue;
|
|
}
|
|
if (buff[0] == '#' || buff[0] == ' ' || buff[0] == '\n') continue;
|
|
/* Field widths match the line buffer, so no token is ever cut; the old
|
|
* %15s handed the tail of a long key over as its value. */
|
|
fields = sscanf(buff, "%1023s %1023s", name, value);
|
|
if (fields < 1) continue;
|
|
/* -C accepts any path, so report where the line is, never what it holds.
|
|
* A lone token is a key without a value, or a line that is no
|
|
* directive at all; the old %15s split such a line in two. */
|
|
if (fields == 1) {
|
|
if (!set.console.stderr_notty) {
|
|
fprintf(stderr, "WARNING: Directive without a value on line %d of %s\n", line, display_file);
|
|
}
|
|
} else if (!apply_config_directive(name, value) && !set.console.stderr_notty) {
|
|
fprintf(stderr, "WARNING: Unrecognized directive on line %d of %s\n", line, display_file);
|
|
}
|
|
}
|
|
int failed = ferror(fp);
|
|
if (fclose(fp) != 0) failed = TRUE;
|
|
return failed ? -1 : 0;
|
|
}
|
|
|
|
/*! \fn void config_defaults(void)
|
|
* \brief populates the global configuration structure with default spine.conf file settings
|
|
* \param *set global runtime parameters
|
|
*
|
|
*/
|
|
void config_defaults() {
|
|
set.poller.threads = DEFAULT_THREADS;
|
|
|
|
/* default server */
|
|
set.database.port = DEFAULT_DB_PORT;
|
|
|
|
STRNCOPY(set.database.host, DEFAULT_DB_HOST);
|
|
STRNCOPY(set.database.database, DEFAULT_DB_DB );
|
|
STRNCOPY(set.database.user, DEFAULT_DB_USER);
|
|
STRNCOPY(set.database.password, DEFAULT_DB_PASS);
|
|
|
|
/* remote default server */
|
|
set.remote_database.port = DEFAULT_DB_PORT;
|
|
|
|
STRNCOPY(set.remote_database.host, DEFAULT_DB_HOST);
|
|
STRNCOPY(set.remote_database.database, DEFAULT_DB_DB );
|
|
STRNCOPY(set.remote_database.user, DEFAULT_DB_USER);
|
|
STRNCOPY(set.remote_database.password, DEFAULT_DB_PASS);
|
|
|
|
STRNCOPY(config_paths[0], CONFIG_PATH_1);
|
|
STRNCOPY(config_paths[1], CONFIG_PATH_2);
|
|
STRNCOPY(config_paths[2], CONFIG_PATH_3);
|
|
STRNCOPY(config_paths[3], CONFIG_PATH_4);
|
|
|
|
set.logging.log_destination = LOGDEST_FILE;
|
|
}
|
|
|
|
/*! \fn void die(const char *format, ...)
|
|
* \brief a method to end Spine while returning the fatal error to stderr
|
|
*
|
|
* Given a printf-style argument list, format it to the standard
|
|
* error, append a newline, then exit Spine.
|
|
*
|
|
*/
|
|
void die(const char *format, ...) {
|
|
va_list args;
|
|
char logmessage[BUFSIZE];
|
|
char flogmessage[DBL_BUFSIZE];
|
|
int old_errno = errno;
|
|
|
|
va_start(args, format);
|
|
vsnprintf(logmessage, sizeof(logmessage), format, args);
|
|
va_end(args);
|
|
|
|
if (set.logging.log_perror) {
|
|
char perr[BUFSIZE];
|
|
snprintf(perr, BUFSIZE, " [%d, %s]", old_errno, strerror(old_errno));
|
|
size_t used = strlen(logmessage);
|
|
snprintf(logmessage + used, sizeof(logmessage) - used, "%s", perr);
|
|
}
|
|
spine_sanitize_log_message(logmessage);
|
|
|
|
if (set.logging.logfile_processed) {
|
|
if (set.poller.parent_fork == SPINE_PARENT) {
|
|
snprintf(flogmessage, DBL_BUFSIZE, "%s (Spine parent)", logmessage);
|
|
} else {
|
|
snprintf(flogmessage, DBL_BUFSIZE, "%s (Spine thread)", logmessage);
|
|
}
|
|
} else {
|
|
snprintf(flogmessage, DBL_BUFSIZE, "%s (Spine init)", logmessage);
|
|
}
|
|
|
|
fprintf(stderr, "%s\n", flogmessage);
|
|
|
|
if ((set.poller.parent_fork == SPINE_PARENT) && (set.php.php_initialized)) {
|
|
php_close(PHP_INIT);
|
|
}
|
|
|
|
exit(set.exit.exit_code == EXIT_SUCCESS ? EXIT_FAILURE : set.exit.exit_code);
|
|
}
|
|
|
|
/* The log timestamp format depends only on two settings read once in
|
|
* read_config_options(), so it is built there, while spine is still single
|
|
* threaded, and only read afterwards. Rebuilding it per log line cost a
|
|
* malloc/free pair on every message. */
|
|
static char log_date_format[GD_FMT_SIZE] = "%Y/%b/%d %H:%M:%S - ";
|
|
|
|
/*! \fn void set_date_format(void)
|
|
* \brief build the cached log timestamp format from the current settings
|
|
*
|
|
* Not safe to call after the poller threads have started.
|
|
*/
|
|
void set_date_format(void) {
|
|
static const char separators[] = {'-', '/', '.'};
|
|
static const char *const formats[][3] = {
|
|
{"%m", "%d", "%Y"}, {"%b", "%d", "%Y"},
|
|
{"%d", "%m", "%Y"}, {"%d", "%b", "%Y"},
|
|
{"%Y", "%m", "%d"}, {"%Y", "%b", "%d"}
|
|
};
|
|
if (set.logging.log_datetime_separator < GDC_MIN || set.logging.log_datetime_separator > GDC_MAX) {
|
|
set.logging.log_datetime_separator = GDC_DEFAULT;
|
|
}
|
|
if (set.logging.log_datetime_format < GD_MIN || set.logging.log_datetime_format > GD_MAX) {
|
|
set.logging.log_datetime_format = GD_DEFAULT;
|
|
}
|
|
char separator = separators[set.logging.log_datetime_separator];
|
|
const char *const *parts = formats[set.logging.log_datetime_format];
|
|
spine_snprintf(log_date_format, sizeof(log_date_format), "%s%c%s%c%s %%H:%%M:%%S - ",
|
|
parts[0], separator, parts[1], separator, parts[2]);
|
|
}
|
|
|
|
char *get_date_format(void) {
|
|
return log_date_format;
|
|
}
|
|
|
|
/*! \fn void spine_log(const char *format, ...)
|
|
* \brief output's log information to the desired cacti logfile.
|
|
* \param *logmessage a pointer to the pre-formatted log message.
|
|
*
|
|
*/
|
|
bool spine_should_log_device(int host_id, int verbosity) {
|
|
return is_debug_device(host_id) || set.logging.log_level >= verbosity;
|
|
}
|
|
|
|
static FILE *log_error_stream(void) {
|
|
#ifdef DISABLE_STDERR
|
|
return stdout;
|
|
#else
|
|
return stderr;
|
|
#endif
|
|
}
|
|
|
|
static bool log_stream_available(const FILE *stream) {
|
|
return (stream == stdout && !set.console.stdout_notty) || (stream == stderr && !set.console.stderr_notty);
|
|
}
|
|
|
|
static void log_format_error(const char *message) {
|
|
FILE *stream = log_error_stream();
|
|
if (log_stream_available(stream)) fprintf(stream, "%s\n", message);
|
|
}
|
|
|
|
static bool log_format_message(char *output, size_t capacity, const char *message) {
|
|
char prefix[SMALL_BUFSIZE];
|
|
snprintf(prefix, sizeof(prefix), "SPINE: Poller[%i] PID[%i] PT[%lu] ", set.poller.poller_id, getpid(), (unsigned long int)pthread_self());
|
|
time_t now = time(NULL);
|
|
struct tm local;
|
|
const char *date_format = get_date_format();
|
|
size_t date_length = 0;
|
|
if (localtime_r(&now, &local) != NULL) date_length = strftime(output, 50, date_format, &local);
|
|
if (date_length == 0) {
|
|
output[0] = '\0';
|
|
log_format_error("ERROR: Could not get string from strftime()");
|
|
}
|
|
int prefix_length = spine_count_to_int(strlen(prefix));
|
|
int message_length = spine_count_to_int(strlen(message));
|
|
int available = spine_count_to_int(capacity) - spine_count_to_int(date_length) - 2;
|
|
if (prefix_length > available) prefix_length = available;
|
|
if (message_length > available - prefix_length) message_length = available - prefix_length;
|
|
snprintf(output + date_length, capacity - date_length, "%.*s%.*s", prefix_length, prefix, message_length, message);
|
|
return date_length != 0;
|
|
}
|
|
|
|
static void log_to_syslog(const char *message) {
|
|
if (!IS_LOGGING_TO_SYSLOG()) return;
|
|
openlog("Cacti", LOG_NDELAY | LOG_PID, LOG_SYSLOG);
|
|
if ((strstr(message, "ERROR") || strstr(message, "FATAL")) && set.logging.log_perror) syslog(LOG_CRIT, "%s\n", message);
|
|
if (strstr(message, "WARNING") && set.logging.log_pwarn) syslog(LOG_WARNING, "%s\n", message);
|
|
if (strstr(message, "STATS") && set.logging.log_pstats) syslog(LOG_NOTICE, "%s\n", message);
|
|
closelog();
|
|
}
|
|
|
|
static bool log_to_file(const char *message) {
|
|
if (!IS_LOGGING_TO_FILE() || set.logging.log_level == POLLER_VERBOSITY_NONE || !set.logging.path_logfile[0] || !set.logging.logfile_processed) return TRUE;
|
|
/* Serialize complete records and the once-only diagnostic. Append mode
|
|
* creates missing files without a stat/open race that can truncate them. */
|
|
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
static bool reported_error = FALSE;
|
|
if (pthread_mutex_lock(&mutex) != 0) return FALSE;
|
|
/* O_APPEND selects the end atomically and one write() keeps another
|
|
* process sharing the log from splicing into a record. Reopening per
|
|
* message preserves log rotation. */
|
|
int oldstate;
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
|
|
int fd = open(set.logging.path_logfile, O_WRONLY | O_CREAT | O_APPEND, 0666);
|
|
bool success = FALSE;
|
|
if (fd >= 0) {
|
|
size_t length = strlen(message);
|
|
ssize_t written = write(fd, message, length);
|
|
success = written >= 0 && (size_t) written == length;
|
|
if (close(fd) != 0) success = FALSE;
|
|
}
|
|
pthread_setcancelstate(oldstate, NULL);
|
|
if (!success && !reported_error) {
|
|
printf("ERROR: Spine Log File Could Not Be Opened/Created or Written\n");
|
|
reported_error = TRUE;
|
|
}
|
|
if (pthread_mutex_unlock(&mutex) != 0) return FALSE;
|
|
return success;
|
|
}
|
|
|
|
/* Three LOGSIZE records on the stack overran the 128 KiB musl default thread
|
|
* stack. Each thread takes its buffers from the heap on its first message
|
|
* and keeps them until it exits; if that allocation fails the record is
|
|
* still written, cut to BUFSIZE. */
|
|
typedef struct {
|
|
char message[LOGSIZE];
|
|
char formatted[LOGSIZE];
|
|
} log_buffers_t;
|
|
|
|
static pthread_key_t log_buffers_key;
|
|
static pthread_once_t log_buffers_once = PTHREAD_ONCE_INIT;
|
|
static bool log_buffers_keyed = FALSE;
|
|
|
|
static void log_buffers_create_key(void) {
|
|
log_buffers_keyed = pthread_key_create(&log_buffers_key, free) == 0;
|
|
}
|
|
|
|
static log_buffers_t *log_buffers(void) {
|
|
log_buffers_t *buffers;
|
|
if (pthread_once(&log_buffers_once, log_buffers_create_key) != 0 || !log_buffers_keyed) return NULL;
|
|
buffers = pthread_getspecific(log_buffers_key);
|
|
if (buffers != NULL) return buffers;
|
|
buffers = malloc(sizeof(*buffers));
|
|
if (buffers != NULL && pthread_setspecific(log_buffers_key, buffers) != 0) {
|
|
free(buffers);
|
|
buffers = NULL;
|
|
}
|
|
return buffers;
|
|
}
|
|
|
|
int spine_log(const char *format, ...) {
|
|
char fallback_message[BUFSIZE];
|
|
char fallback_formatted[BUFSIZE];
|
|
log_buffers_t *buffers = log_buffers();
|
|
char *message = buffers != NULL ? buffers->message : fallback_message;
|
|
char *formatted = buffers != NULL ? buffers->formatted : fallback_formatted;
|
|
size_t capacity = buffers != NULL ? LOGSIZE : BUFSIZE;
|
|
va_list args;
|
|
va_start(args, format);
|
|
vsnprintf(message, capacity - 1, format, args);
|
|
va_end(args);
|
|
spine_sanitize_log_message(message);
|
|
if (IS_LOGGING_TO_STDOUT()) {
|
|
return printf("Total[%3.4f] %s\n", get_time_as_double() - start_time, message) < 0 ? FALSE : TRUE;
|
|
}
|
|
bool date_valid = log_format_message(formatted, capacity, message);
|
|
log_to_syslog(formatted);
|
|
if (strchr(formatted, '\n') == NULL) {
|
|
size_t used = strlen(formatted);
|
|
snprintf(formatted + used, capacity - used, "\n");
|
|
}
|
|
bool success = log_to_file(formatted);
|
|
if (set.logging.log_level >= POLLER_VERBOSITY_NONE) {
|
|
FILE *stream = stdout;
|
|
if (!date_valid || strstr(formatted, "ERROR") || strstr(formatted, "WARNING") || strstr(formatted, "FATAL")) stream = log_error_stream();
|
|
if (log_stream_available(stream) && fprintf(stream, "%s", formatted) < 0) success = FALSE;
|
|
}
|
|
return success;
|
|
}
|
|
|
|
/*! \fn int file_exists(const char *filename)
|
|
* \brief checks for the existence of a file.
|
|
* \param *filename the name of the file to check for.
|
|
*
|
|
* \return TRUE if found FALSE if not.
|
|
*
|
|
*/
|
|
int file_exists(const char *filename) {
|
|
struct stat file_stat;
|
|
|
|
if (stat(filename, &file_stat)) {
|
|
return FALSE;
|
|
} else {
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
/*! \fn all_digits(const char *string)
|
|
* \brief verifies that a string is contains only numeric characters
|
|
* \param string the string to check
|
|
*
|
|
* This function has no leeway: spaces and minus signs and decimal points
|
|
* are not digits, and an empty string is (by convention) not
|
|
* all-digits too.
|
|
*
|
|
* \return TRUE if not alpha or special characters found, FALSE if non numeric found
|
|
*
|
|
*/
|
|
int all_digits(const char *string) {
|
|
/* empty string is not all digits */
|
|
if ( *string == '\0' ) return FALSE;
|
|
|
|
while ( isdigit((int)*string) )
|
|
string++;
|
|
|
|
return *string == '\0';
|
|
}
|
|
|
|
/*! \fn is_ipaddress(const char *string)
|
|
* \brief verifies that a string is an ip address either v4 or v6
|
|
* \param string the string to check
|
|
*
|
|
* This function simply checks to see if a string object is an ip address.
|
|
* If it is, it returns true else false.
|
|
*
|
|
* \return TRUE if an ip address, or FALSE if non
|
|
*
|
|
*/
|
|
int is_ipaddress(const char *string) {
|
|
while (*string) {
|
|
if ((isdigit((int)*string)) ||
|
|
(*string == '.') ||
|
|
(*string == ':')) {
|
|
string++;
|
|
|
|
continue;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Cacti's prepare_validate_result() trims quotes and line ends; Spine has
|
|
* always trimmed blanks, tabs and backslashes as well. */
|
|
static const char result_padding[] = " \"'\\\t\n\r";
|
|
|
|
/* PHP is_numeric() grammar: no hex, no "0x", no inf or nan, which strtod()
|
|
* would all accept. */
|
|
static bool decimal_syntax(const char *text, bool *integral) {
|
|
const char *cursor = text;
|
|
size_t digits = 0;
|
|
|
|
*integral = TRUE;
|
|
if (*cursor == '+' || *cursor == '-') cursor++;
|
|
while (isdigit((unsigned char)*cursor)) {
|
|
cursor++;
|
|
digits++;
|
|
}
|
|
if (*cursor == '.') {
|
|
*integral = FALSE;
|
|
cursor++;
|
|
while (isdigit((unsigned char)*cursor)) {
|
|
cursor++;
|
|
digits++;
|
|
}
|
|
}
|
|
if (digits == 0) return FALSE;
|
|
if (*cursor == 'e' || *cursor == 'E') {
|
|
*integral = FALSE;
|
|
cursor++;
|
|
if (*cursor == '+' || *cursor == '-') cursor++;
|
|
if (!isdigit((unsigned char)*cursor)) return FALSE;
|
|
while (isdigit((unsigned char)*cursor)) cursor++;
|
|
}
|
|
return *cursor == '\0';
|
|
}
|
|
|
|
static bool parse_decimal(const char *text, classified_result_t *out) {
|
|
bool integral;
|
|
char *end;
|
|
double real;
|
|
|
|
if (!decimal_syntax(text, &integral)) return FALSE;
|
|
|
|
if (integral) {
|
|
errno = 0;
|
|
if (text[0] == '-') {
|
|
long long value = strtoll(text, &end, 10);
|
|
if (errno == 0 && *end == '\0') {
|
|
out->kind = RESULT_SIGNED;
|
|
out->value.integer = (int64_t)value;
|
|
return TRUE;
|
|
}
|
|
} else {
|
|
unsigned long long value = strtoull(text, &end, 10);
|
|
if (errno == 0 && *end == '\0') {
|
|
out->kind = RESULT_COUNTER;
|
|
out->value.counter = (uint64_t)value;
|
|
return TRUE;
|
|
}
|
|
}
|
|
/* Wider than 64 bits is still a number; the text is stored as is. */
|
|
}
|
|
|
|
errno = 0;
|
|
real = strtod(text, &end);
|
|
if (*end != '\0' || !isfinite(real) || (errno == ERANGE && fabs(real) == HUGE_VAL)) {
|
|
return FALSE;
|
|
}
|
|
out->kind = RESULT_FLOAT;
|
|
out->value.real = real;
|
|
return TRUE;
|
|
}
|
|
|
|
/* Cacti's "name:value name:value" form: one ':' or '!' per field, fields
|
|
* separated by single spaces. */
|
|
static bool multipart_syntax(const char *text) {
|
|
size_t spaces = 0;
|
|
size_t delimiters = 0;
|
|
|
|
if (strchr(text, ':') == NULL && strchr(text, '!') == NULL) return FALSE;
|
|
for (const char *cursor = text; *cursor != '\0'; cursor++) {
|
|
if (*cursor == ':' || *cursor == '!') delimiters++;
|
|
else if (*cursor == ' ') spaces++;
|
|
}
|
|
return spaces == 0 || spaces + 1 == delimiters;
|
|
}
|
|
|
|
static int hex_digit(int c) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
typedef enum {
|
|
OCTETS_NONE,
|
|
OCTETS_VALUE,
|
|
OCTETS_OVERFLOW
|
|
} octets_t;
|
|
|
|
/* An OctetString such as a Hex-STRING Counter64: two-digit octets separated
|
|
* by one space, '-' or ':', as Cacti's is_hexadecimal() requires. Like Cacti,
|
|
* a lone octet such as "FF" counts; "42" never gets here as it is decimal. */
|
|
static octets_t parse_octets(const char *text, uint64_t *value) {
|
|
const char *cursor = text;
|
|
uint64_t number = 0;
|
|
bool overflow = FALSE;
|
|
|
|
for (;;) {
|
|
int high = hex_digit((unsigned char)cursor[0]);
|
|
int low = high < 0 ? -1 : hex_digit((unsigned char)cursor[1]);
|
|
|
|
if (low < 0) return OCTETS_NONE;
|
|
if (number > (UINT64_MAX >> 8)) overflow = TRUE;
|
|
number = (number << 8) | (uint64_t)(high * 16 + low);
|
|
cursor += 2;
|
|
if (*cursor == '\0') break;
|
|
if (*cursor != ' ' && *cursor != ':' && *cursor != '-') {
|
|
return OCTETS_NONE;
|
|
}
|
|
cursor++;
|
|
}
|
|
|
|
if (overflow) return OCTETS_OVERFLOW;
|
|
*value = number;
|
|
return OCTETS_VALUE;
|
|
}
|
|
|
|
/*! \fn result_kind_t classify_result(const char *raw, classified_result_t *out)
|
|
* \brief decide what a script or SNMP agent returned and what to store
|
|
* \param raw the output as received; it is not modified
|
|
* \param out receives the kind, the parsed value and the text to store
|
|
*
|
|
* The order is the one Spine has always used: a decimal number, Cacti's
|
|
* multipart output, a hex octet string, then a decimal number wrapped in
|
|
* text such as "4096 Bytes". Anything else, including output too long to
|
|
* store whole, is unknown and stored as U rather than as a guess.
|
|
*
|
|
* \return the kind, also stored in out->kind
|
|
*/
|
|
result_kind_t classify_result(const char *raw, classified_result_t *out) {
|
|
const char *start;
|
|
const char *end;
|
|
const char *core;
|
|
size_t length;
|
|
uint64_t octets;
|
|
|
|
assert(out != NULL);
|
|
out->kind = RESULT_UNKNOWN;
|
|
out->value.counter = 0;
|
|
SET_UNDEFINED(out->text);
|
|
|
|
if (raw == NULL) return RESULT_UNKNOWN;
|
|
|
|
start = raw + strspn(raw, result_padding);
|
|
if (*start == '\0') return RESULT_UNKNOWN;
|
|
/* start[0] is not padding, so this stops with at least one byte left */
|
|
end = start + strlen(start);
|
|
while (strchr(result_padding, end[-1]) != NULL) end--;
|
|
|
|
length = (size_t)(end - start);
|
|
if (length >= sizeof(out->text)) return RESULT_UNKNOWN;
|
|
memcpy(out->text, start, length);
|
|
out->text[length] = '\0';
|
|
|
|
if (parse_decimal(out->text, out)) return out->kind;
|
|
|
|
if (multipart_syntax(out->text)) {
|
|
out->kind = RESULT_MULTIPART;
|
|
return out->kind;
|
|
}
|
|
|
|
switch (parse_octets(out->text, &octets)) {
|
|
case OCTETS_VALUE:
|
|
out->kind = RESULT_HEX_COUNTER;
|
|
out->value.counter = octets;
|
|
snprintf(out->text, sizeof(out->text), "%llu", (unsigned long long)octets);
|
|
return out->kind;
|
|
case OCTETS_OVERFLOW:
|
|
/* Stripping letters from a counter wider than 64 bits would leave
|
|
* its leading octet looking like a small decimal sample. */
|
|
break;
|
|
case OCTETS_NONE:
|
|
/* strip_alpha() points into out->text, so move the number to the front. */
|
|
core = strip_alpha(out->text);
|
|
if (parse_decimal(core, out)) {
|
|
memmove(out->text, core, strlen(core) + 1);
|
|
return out->kind;
|
|
}
|
|
break;
|
|
}
|
|
|
|
out->kind = RESULT_UNKNOWN;
|
|
out->value.counter = 0;
|
|
SET_UNDEFINED(out->text);
|
|
return RESULT_UNKNOWN;
|
|
}
|
|
|
|
/*! \fn char *strip_alpha(char *string)
|
|
* \brief remove trailing alpha characters from a string.
|
|
* \param string the string to strip characters from
|
|
*
|
|
* \return a pointer to the modified string
|
|
*
|
|
*/
|
|
char *strip_alpha(char *string) {
|
|
size_t end = strlen(string);
|
|
while (end > 0 && !isdigit((unsigned char)string[end - 1])) string[--end] = '\0';
|
|
size_t start = 0;
|
|
while (start < end && !isdigit((unsigned char)string[start]) && string[start] != '-') start++;
|
|
return string + start;
|
|
}
|
|
|
|
/*! \fn char *add_slashes(const char *string)
|
|
* \brief add escaping to back slashes on for Windows type commands.
|
|
* \param string the string to replace slashes
|
|
*
|
|
* \return a pointer to the modified string. Variable must be freed by parent.
|
|
*
|
|
*/
|
|
char *add_slashes(const char *string) {
|
|
size_t length = strlen(string);
|
|
if (length > (SIZE_MAX - 1) / 2) {
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
die("ERROR: Escaped command exceeds addressable memory");
|
|
}
|
|
char *result = malloc(length * 2 + 1);
|
|
if (result == NULL) die("ERROR: Fatal malloc error: util.c add_slashes!");
|
|
size_t used = 0;
|
|
for (size_t i = 0; i < length; i++) {
|
|
if (string[i] == '\\') result[used++] = '\\';
|
|
result[used++] = string[i];
|
|
}
|
|
result[used] = '\0';
|
|
return result;
|
|
}
|
|
|
|
/*! \fn char *strncopy(char *dst, const char *src, size_t obuf)
|
|
* \brief copies source to destination add a NUL terminator
|
|
*
|
|
* Copy from source to destination, insuring a NUL termination.
|
|
* The size of the buffer *includes* the terminating NUL. Note
|
|
* that strncpy() does NOT NUL terminate if the source is the
|
|
* size of the destination (yuck).
|
|
*
|
|
* NOTE: it's very common to call this as:
|
|
*
|
|
* strncopy(buf, src, sizeof buf)
|
|
*
|
|
* so we provide an STRNCOPY() macro which adds the size.
|
|
*
|
|
* \return pointer to destination string
|
|
*
|
|
*/
|
|
char *strncopy(char *dst, const char *src, size_t obuf) {
|
|
assert(dst != 0);
|
|
assert(src != 0);
|
|
|
|
size_t len;
|
|
|
|
if (obuf == 0) return dst;
|
|
/* Bound the scan by the usable capacity; src may be large or unterminated. */
|
|
len = strnlen(src, obuf - 1);
|
|
if (len) {
|
|
memcpy(dst, src, len);
|
|
}
|
|
|
|
dst[len] = '\0';
|
|
return dst;
|
|
}
|
|
|
|
/*! \fn double get_time_as_double()
|
|
* \brief fetches system time as a double-precision value
|
|
*
|
|
* \return system time (at microsecond resolution) as a double
|
|
*/
|
|
double get_time_as_double(void) {
|
|
struct timeval now;
|
|
|
|
gettimeofday(&now, NULL);
|
|
|
|
return (double)now.tv_sec + (double)now.tv_usec / 1000000;
|
|
}
|
|
|
|
/*! \fn trim()
|
|
* \brief removes leading and trailing blanks, tabs, line feeds and
|
|
* carriage returns from a string.
|
|
*
|
|
* \return the trimmed string.
|
|
*/
|
|
char *trim(char *str) {
|
|
return ltrim(rtrim(str));
|
|
}
|
|
|
|
/*! \fn rtrim()
|
|
* \brief removes trailing blanks, tabs, line feeds, carriage returns
|
|
* single and double quotes and back-slashed from a string.
|
|
*
|
|
* \return the trimmed string.
|
|
*/
|
|
char *rtrim(char *str) {
|
|
char *end;
|
|
const char *trim_chars = " \"\'\\\t\n\r";
|
|
|
|
if (!str) return NULL;
|
|
|
|
end = str + strlen(str);
|
|
|
|
while (end-- > str) {
|
|
if (!strchr(trim_chars, *end)) return str;
|
|
|
|
*end = 0;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
/*! \fn ltrim()
|
|
* \brief removes leading blanks, tabs, line feeds, carriage returns
|
|
* single and double quotes and back-slashed from a string.
|
|
*
|
|
* \return the trimmed string.
|
|
*/
|
|
char *ltrim(char *str) {
|
|
const char *trim_chars = " \"\'\\\t\n\r";
|
|
|
|
if (!str) return NULL;
|
|
|
|
while (*str) {
|
|
if (!strchr(trim_chars, *str)) return str;
|
|
|
|
++str;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
/*! \fn reverse()
|
|
* \brief reverses a string in place.
|
|
*
|
|
* \return the reversed string.
|
|
*/
|
|
char *reverse(char* str) {
|
|
size_t start = 0;
|
|
size_t end = strlen(str);
|
|
while (start < end) {
|
|
end--;
|
|
if (start >= end) break;
|
|
char byte = str[start];
|
|
str[start++] = str[end];
|
|
str[end] = byte;
|
|
}
|
|
return str;
|
|
}
|
|
|
|
/*! \fn strpos()
|
|
* \brief looks for the position of needle in haystack
|
|
*
|
|
* \return the position of -1 if not found
|
|
*/
|
|
int strpos(const char *haystack, const char *needle) {
|
|
const char *p = strstr(haystack, needle);
|
|
|
|
if (p) {
|
|
return p - haystack <= INT_MAX ? (int)(p - haystack) : -1;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/*! \fn char_count()
|
|
* \brief counts occurrences of char in string.
|
|
*
|
|
* \return number of occurrences.
|
|
*/
|
|
int char_count(const char *str, int chr) {
|
|
const unsigned char *my_str = (const unsigned char *) str;
|
|
const unsigned char my_chr = (unsigned char)chr;
|
|
int count = 0;
|
|
|
|
if (!my_chr) return 1;
|
|
|
|
while (*my_str) {
|
|
if (*my_str++ == my_chr) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
#ifdef HAVE_LCAP
|
|
/* This patch is adapted (copied) patch for ntpd from Jarno Huuskonen and
|
|
* Pekka Savola that was adapted (copied) from a patch by Chris Wings to drop
|
|
* root for xntpd.
|
|
*/
|
|
/* Returns TRUE if CAP_NET_RAW was kept. Exits if the ids cannot be changed. */
|
|
static int drop_root(uid_t server_uid, gid_t server_gid) {
|
|
cap_t caps;
|
|
int kept;
|
|
|
|
/* Runs before logging is configured, so failures go to stderr via die(). */
|
|
if (prctl(PR_SET_KEEPCAPS, 1)) {
|
|
die("ERROR: prctl(PR_SET_KEEPCAPS, 1) failed; refusing to run");
|
|
}
|
|
|
|
if (setgroups(0, NULL) == -1) {
|
|
die("ERROR: setgroups failed; refusing to run");
|
|
}
|
|
|
|
if (setegid(server_gid) == -1 || seteuid(server_uid) == -1) {
|
|
die("ERROR: setegid/seteuid to uid=%d/gid=%d failed; refusing to run", server_uid, server_gid);
|
|
}
|
|
|
|
caps = cap_from_text("cap_net_raw=eip");
|
|
kept = caps != NULL && cap_set_proc(caps) == 0;
|
|
|
|
if (caps != NULL) {
|
|
cap_free(caps);
|
|
}
|
|
|
|
/* Without the narrowed set, PR_SET_KEEPCAPS would carry every one of
|
|
* root's permitted capabilities across the uid change below. Clearing
|
|
* it lets the kernel empty the set, and spine continues without ICMP
|
|
* unless datagram ICMP is allowed. */
|
|
if (!kept && prctl(PR_SET_KEEPCAPS, 0)) {
|
|
die("ERROR: prctl(PR_SET_KEEPCAPS, 0) failed; refusing to run");
|
|
}
|
|
|
|
if ( setregid(server_gid, server_gid) == -1 ||
|
|
setreuid(server_uid, server_uid) == -1 ) {
|
|
die("ERROR: setregid/setreuid to uid=%d/gid=%d failed; refusing to run",
|
|
server_uid, server_gid);
|
|
}
|
|
|
|
SPINE_LOG_LOW(("running as uid(%d)/gid(%d) euid(%d)/egid(%d)%s.",
|
|
getuid(), getgid(), geteuid(), getegid(), kept ? " with cap_net_raw=eip" : ""));
|
|
|
|
return kept;
|
|
}
|
|
#endif /* HAVE_LCAP */
|
|
|
|
/* Set when a setuid root start could not get raw ICMP before dropping root,
|
|
* so checkAsRoot() can say so once logging is configured. */
|
|
static int setuid_icmp_lost = FALSE;
|
|
|
|
/*! \fn int privileges_dropped(uid_t uid, gid_t gid)
|
|
* \brief confirms root is gone for good rather than set aside
|
|
*
|
|
* seteuid(0) succeeds while the real or saved uid is still 0, and setegid(0)
|
|
* while a group id is, so a failed attempt is the proof.
|
|
*
|
|
* \return TRUE if every id is the invoking user's and root cannot come back
|
|
*/
|
|
int privileges_dropped(uid_t uid, gid_t gid) {
|
|
#ifdef HAVE_LCAP
|
|
cap_t caps;
|
|
cap_flag_value_t flag;
|
|
cap_value_t value;
|
|
int only_net_raw = TRUE;
|
|
#endif
|
|
|
|
if (uid == 0 || getuid() != uid || geteuid() != uid || getgid() != gid || getegid() != gid) {
|
|
return FALSE;
|
|
}
|
|
|
|
#ifdef HAVE_LCAP
|
|
/* A permitted capability could be raised again without any uid change,
|
|
* so anything beyond CAP_NET_RAW means root was not given up. */
|
|
caps = cap_get_proc();
|
|
if (caps == NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
for (value = 0; value < 64; value++) {
|
|
if (cap_get_flag(caps, value, CAP_PERMITTED, &flag) == 0 && flag == CAP_SET && value != CAP_NET_RAW) {
|
|
only_net_raw = FALSE;
|
|
}
|
|
}
|
|
|
|
cap_free(caps);
|
|
|
|
if (!only_net_raw) {
|
|
return FALSE;
|
|
}
|
|
#endif
|
|
|
|
if (seteuid(0) == 0) {
|
|
return FALSE;
|
|
}
|
|
|
|
if (gid != 0 && setegid(0) == 0) {
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/*! \fn void drop_privileges(void)
|
|
* \brief gives up setuid root before spine reads any input
|
|
*
|
|
* Spine is installed setuid root only to open raw ICMP sockets. Options and
|
|
* the config file can name files that spine then creates or writes, so all
|
|
* input is handled as the invoking user. This runs first in main(), opens
|
|
* what ICMP needs while still root, then drops root permanently. A real root user
|
|
* (uid 0, for example from cron) keeps running as root, as before; with
|
|
* libcap it is narrowed to CAP_NET_RAW, also as before.
|
|
*
|
|
* Never returns while spine is setuid root: it drops or exits.
|
|
*/
|
|
void drop_privileges(void) {
|
|
uid_t uid = getuid();
|
|
gid_t gid = getgid();
|
|
int icmp_ready;
|
|
|
|
if (geteuid() != 0 || uid == 0) {
|
|
#ifdef HAVE_LCAP
|
|
/* --enable-lcap confines a root run to CAP_NET_RAW. Running on with
|
|
* all of root's capabilities would silently undo that, so refuse. */
|
|
if (geteuid() == 0 && !drop_root(uid, gid)) {
|
|
die("ERROR: Spine runs as root but could not confine itself to CAP_NET_RAW; refusing to run");
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
#ifdef HAVE_LCAP
|
|
icmp_ready = drop_root(uid, gid);
|
|
#else
|
|
icmp_ready = ping_icmp_open_shared();
|
|
|
|
/* With an effective uid of 0, POSIX setgid() and setuid() replace the
|
|
* real, effective and saved ids; seteuid() would keep root in reserve.
|
|
* Unlike drop_root(), this keeps the supplementary groups: a setuid exec
|
|
* does not change them, so they are the caller's own. */
|
|
if (setgid(gid) != 0 || setuid(uid) != 0) {
|
|
die("ERROR: Spine is setuid root and could not drop to uid %d/gid %d; refusing to run", (int) uid, (int) gid);
|
|
}
|
|
#endif
|
|
|
|
/* The one case that stops spine: running on with root still reachable. */
|
|
if (!privileges_dropped(uid, gid)) {
|
|
die("ERROR: Spine is setuid root and could not drop root permanently; refusing to run");
|
|
}
|
|
|
|
/* Missing ICMP is not a reason to stop polling: checkAsRoot() tries
|
|
* datagram ICMP and otherwise warns, and ICMP pings fall back to UDP. */
|
|
setuid_icmp_lost = !icmp_ready;
|
|
}
|
|
|
|
int hasCaps(void) {
|
|
#ifdef HAVE_LCAP
|
|
cap_t caps;
|
|
cap_flag_value_t capflag;
|
|
|
|
/* Recommended caps: cap_net_raw=eip */
|
|
caps = cap_get_proc();
|
|
if (caps == NULL) {
|
|
SPINE_LOG(("ERROR: cap_get_proc failed."));
|
|
return FALSE;
|
|
}
|
|
|
|
/* check if cap_net_raw is in effective set */
|
|
if (cap_get_flag(caps, CAP_NET_RAW, CAP_EFFECTIVE, &capflag)) {
|
|
SPINE_LOG(("ERROR: cap_get_flag for CAP_NET_RAW failed. Falling back to unprivileged ICMP where available."));
|
|
cap_free(caps);
|
|
return FALSE;
|
|
}
|
|
|
|
if (capflag != CAP_SET) {
|
|
SPINE_LOG_MEDIUM(("WARNING: Capability CAP_NET_RAW is not set. Falling back to unprivileged ICMP where available."));
|
|
cap_free(caps);
|
|
return FALSE;
|
|
}
|
|
|
|
SPINE_LOG_DEBUG(("DEBUG: Capability CAP_NET_RAW is set."));
|
|
cap_free(caps);
|
|
|
|
return TRUE;
|
|
#else
|
|
return FALSE;
|
|
#endif
|
|
}
|
|
|
|
void checkAsRoot(void) {
|
|
set.availability.icmp_uses_caps = FALSE;
|
|
#ifndef __CYGWIN__
|
|
#ifdef SOLAR_PRIV
|
|
priv_set_t *privset;
|
|
char *p;
|
|
|
|
/* Get the basic set */
|
|
privset = priv_str_to_set("basic", ",", NULL);
|
|
if (privset == NULL) {
|
|
die("ERROR: Could not get basic privset from priv_str_to_set().");
|
|
} else {
|
|
p = priv_set_to_str(privset, ',', 0);
|
|
SPINE_LOG_DEBUG(("DEBUG: Basic privset is: '%s'.", p != NULL ? p : "Unknown"));
|
|
}
|
|
|
|
/* Add privilege to send/receive ICMP packets */
|
|
if (priv_addset(privset, PRIV_NET_ICMPACCESS) < 0) {
|
|
SPINE_LOG_DEBUG(("WARNING: Addition of PRIV_NET_ICMPACCESS to privset failed: '%s'.", strerror(errno)));
|
|
}
|
|
|
|
/* Compute the set of privileges that are never needed */
|
|
priv_inverse(privset);
|
|
|
|
/* Remove the set of unneeded privs from Permitted (and by
|
|
* implication from Effective) */
|
|
if (setppriv(PRIV_OFF, PRIV_PERMITTED, privset) < 0) {
|
|
SPINE_LOG_DEBUG(("WARNING: Dropping privileges from PRIV_PERMITTED failed: '%s'.", strerror(errno)));
|
|
}
|
|
|
|
/* Remove unneeded priv set from Limit to be safe */
|
|
if (setppriv(PRIV_OFF, PRIV_LIMIT, privset) < 0) {
|
|
SPINE_LOG_DEBUG(("WARNING: Dropping privileges from PRIV_LIMIT failed: '%s'.", strerror(errno)));
|
|
}
|
|
|
|
boolean_t pe = priv_ineffect(PRIV_NET_ICMPACCESS);
|
|
SPINE_LOG_DEBUG(("DEBUG: Privilege PRIV_NET_ICMPACCESS is: '%s'.", pe != 0 ? "Enabled" : "Disabled"));
|
|
|
|
set.availability.icmp_avail = pe || ping_icmp_shared_available();
|
|
|
|
/* Free the privset */
|
|
priv_freeset(privset);
|
|
free(p);
|
|
#else
|
|
/* Never regains root here: a setuid install gave it up in drop_privileges(). */
|
|
if (hasCaps() != TRUE) {
|
|
SPINE_LOG_DEBUG(("DEBUG: Spine running as %d UID, %d EUID", getuid(), geteuid()));
|
|
|
|
if (ping_icmp_shared_available()) {
|
|
SPINE_LOG_DEBUG(("DEBUG: Spine uses the raw ICMP sockets it opened before dropping root."));
|
|
set.availability.icmp_avail = TRUE;
|
|
} else if (geteuid() != 0) {
|
|
int probe;
|
|
|
|
/* A file capability (setcap cap_net_raw+ep) works without libcap
|
|
* support too, so ask the kernel rather than hasCaps(). Root is
|
|
* not the only way in either: net.ipv4.ping_group_range lists the
|
|
* groups allowed to open datagram ICMP sockets. */
|
|
probe = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
|
|
|
if (probe != -1) {
|
|
close(probe);
|
|
SPINE_LOG_DEBUG(("DEBUG: Spine may open raw ICMP sockets."));
|
|
set.availability.icmp_avail = TRUE;
|
|
} else if ((probe = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)) != -1) {
|
|
close(probe);
|
|
SPINE_LOG_DEBUG(("DEBUG: Spine may use unprivileged ICMP sockets."));
|
|
set.availability.icmp_avail = TRUE;
|
|
} else if (setuid_icmp_lost) {
|
|
SPINE_LOG(("WARNING: Spine is setuid root but could not get raw ICMP access before dropping root, and net.ipv4.ping_group_range does not cover this user. ICMP pings fall back to UDP; SNMP polling continues."));
|
|
set.availability.icmp_avail = FALSE;
|
|
} else {
|
|
SPINE_LOG_DEBUG(("WARNING: Spine has no ICMP access. Install it setuid root (chown root:root spine; chmod u+s spine), grant it CAP_NET_RAW (setcap cap_net_raw+ep spine), or widen net.ipv4.ping_group_range to cover this user."));
|
|
set.availability.icmp_avail = FALSE;
|
|
}
|
|
} else {
|
|
SPINE_LOG_DEBUG(("DEBUG: Spine is running as root."));
|
|
set.availability.icmp_avail = TRUE;
|
|
}
|
|
} else {
|
|
SPINE_LOG_DEBUG(("DEBUG: Spine has cap_net_raw capability."));
|
|
set.availability.icmp_avail = TRUE;
|
|
set.availability.icmp_uses_caps = TRUE;
|
|
}
|
|
SPINE_LOG_DEBUG(("DEBUG: Spine has %sgot ICMP", set.availability.icmp_avail?"":"not "));
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
/*! \fn int get_cacti_version(MYSQL *psql, int mode, const char *setting)
|
|
* \brief Returns the version of Cacti as a decimal
|
|
*
|
|
* Given a pointer to a database get the version of Cacti and convert
|
|
* to an integer.
|
|
*
|
|
* \return the cacti version
|
|
*
|
|
*/
|
|
static int parse_cacti_version(const char *value) {
|
|
const unsigned long weights[] = {1000, 100, 1};
|
|
unsigned long version = 0;
|
|
for (size_t index = 0; index < sizeof(weights) / sizeof(weights[0]); index++) {
|
|
if (*value < '0' || *value > '9') return 0;
|
|
errno = 0;
|
|
char *end;
|
|
unsigned long component = strtoul(value, &end, 10);
|
|
if (errno == ERANGE || component > ((unsigned long)INT_MAX - version) / weights[index]) return 0;
|
|
version += component * weights[index];
|
|
if (index < 2 && *end != '.') return 0;
|
|
value = end + (index < 2 ? 1 : 0);
|
|
}
|
|
/* Preserve suffixes such as develop/beta accepted by the previous three-component scan. */
|
|
return (int)version;
|
|
}
|
|
|
|
int get_cacti_version(MYSQL *psql, int mode) {
|
|
assert(psql != NULL);
|
|
MYSQL_RES *result = config_query(psql, mode, "SELECT cacti FROM version LIMIT 1");
|
|
if (result == NULL) return 0;
|
|
MYSQL_ROW row = mysql_fetch_row(result);
|
|
int version = row != NULL && row[0] != NULL ? parse_cacti_version(row[0]) : 0;
|
|
db_free_result(result);
|
|
return version;
|
|
}
|
|
|
|
/* pthread storage keeps the borrowed match valid without requiring C11 TLS.
|
|
* Each polling thread owns its buffer, which is freed when the thread exits. */
|
|
static pthread_key_t regex_result_key;
|
|
static pthread_once_t regex_result_once = PTHREAD_ONCE_INIT;
|
|
|
|
static void initialize_regex_result_key(void) {
|
|
if (pthread_key_create(®ex_result_key, free) != 0) {
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
die("ERROR: Unable to create thread-local regex storage");
|
|
}
|
|
}
|
|
|
|
static char *regex_result_buffer(void) {
|
|
if (pthread_once(®ex_result_once, initialize_regex_result_key) != 0) {
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
die("ERROR: Unable to initialize thread-local regex storage");
|
|
}
|
|
char *buffer = pthread_getspecific(regex_result_key);
|
|
if (buffer == NULL) {
|
|
buffer = malloc(RESULTS_BUFFER);
|
|
if (buffer == NULL) die("ERROR: Fatal malloc error: regex result buffer!");
|
|
if (pthread_setspecific(regex_result_key, buffer) != 0) {
|
|
free(buffer);
|
|
set.exit.exit_code = EXIT_FAILURE;
|
|
die("ERROR: Unable to retain thread-local regex storage");
|
|
}
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
char *regex_replace(const char *exp, char *value) {
|
|
regex_t regex;
|
|
int reti;
|
|
char *msgbuf = NULL;
|
|
|
|
/* Stored output_regex values use the historical basic-regex dialect. */
|
|
reti = regcomp(®ex, exp, 0);
|
|
if (reti) {
|
|
return value;
|
|
}
|
|
|
|
/* Execute regular expression */
|
|
regmatch_t matches[MAX_MATCHES];
|
|
reti = regexec(®ex, value, MAX_MATCHES, matches, 0);
|
|
if (!reti) {
|
|
// regex matched
|
|
size_t length = (size_t) (matches[0].rm_eo - matches[0].rm_so);
|
|
if (length >= RESULTS_BUFFER) {
|
|
regfree(®ex);
|
|
return value;
|
|
}
|
|
msgbuf = regex_result_buffer();
|
|
memcpy(msgbuf, value + matches[0].rm_so, length);
|
|
msgbuf[length] = '\0';
|
|
}
|
|
|
|
/* Free memory allocated to the pattern buffer by regcomp() */
|
|
regfree(®ex);
|
|
|
|
return reti ? value : msgbuf;
|
|
}
|
|
|
|
int format_spine_capabilities(char *output, size_t output_size,
|
|
const char *auth_protocols, const char *priv_protocols) {
|
|
int written;
|
|
|
|
if (output == NULL || output_size == 0 ||
|
|
auth_protocols == NULL || priv_protocols == NULL) {
|
|
return FALSE;
|
|
}
|
|
|
|
written = snprintf(output, output_size,
|
|
"{ authProtocols: \"%." SPINE_STRINGIFY(CAPABILITY_PROTOCOL_LIST_MAX)
|
|
"s\", privProtocols: \"%." SPINE_STRINGIFY(CAPABILITY_PROTOCOL_LIST_MAX) "s\" }",
|
|
auth_protocols, priv_protocols);
|
|
|
|
return written >= 0 && (size_t)written < output_size;
|
|
}
|
|
|
|
/*! \fn int spine_appendf(char **cursor, size_t *remaining, const char *fmt, ...)
|
|
* \brief append to a bounded buffer without walking off the end
|
|
*
|
|
* See util.h for why the `p += snprintf(...)` idiom this replaces is unsafe.
|
|
*
|
|
* \return TRUE when the whole string was appended, FALSE otherwise
|
|
*/
|
|
int spine_appendf(char **cursor, size_t *remaining, const char *fmt, ...) {
|
|
va_list args;
|
|
int written;
|
|
|
|
if (cursor == NULL || *cursor == NULL || remaining == NULL || *remaining == 0) {
|
|
return FALSE;
|
|
}
|
|
|
|
va_start(args, fmt);
|
|
written = vsnprintf(*cursor, *remaining, fmt, args);
|
|
va_end(args);
|
|
|
|
if (written < 0) {
|
|
/* the buffer is untouched on an encoding error, but vsnprintf may have
|
|
written a partial result, so re-terminate where the cursor stands */
|
|
**cursor = '\0';
|
|
return FALSE;
|
|
}
|
|
|
|
if ((size_t) written >= *remaining) {
|
|
/* Truncated. Leave the cursor on the terminator vsnprintf wrote, so
|
|
the buffer stays a valid string and every later append fails here
|
|
rather than running past the end. */
|
|
*cursor += *remaining - 1;
|
|
*remaining = 1;
|
|
return FALSE;
|
|
}
|
|
|
|
*cursor += written;
|
|
*remaining -= (size_t) written;
|
|
|
|
return TRUE;
|
|
}
|