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>
2948 lines
129 KiB
C
2948 lines
129 KiB
C
/*
|
|
* 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.
|
|
*/
|
|
#include "common.h"
|
|
#include "spine.h"
|
|
#include <fcntl.h>
|
|
#include <sys/un.h>
|
|
#include <limits.h>
|
|
|
|
extern int spine_program_main(int argc, char **argv);
|
|
extern void test_output_write_contracts(MYSQL *mysql);
|
|
extern void test_additional_contracts(void);
|
|
extern void test_cli_alias_contracts(void);
|
|
extern void test_privilege_contracts(void);
|
|
extern void test_setuid_icmp(void);
|
|
extern void test_setuid_stress(int rounds);
|
|
extern int run_script_stream_fixture(const char *scenario);
|
|
extern void test_script_stream_contracts(void);
|
|
extern void test_additional_database_contracts(MYSQL *mysql);
|
|
extern void test_settings_write_contracts(MYSQL *mysql);
|
|
extern void test_additional_reindex_contracts(MYSQL *mysql);
|
|
extern void test_reindex_result_contracts(MYSQL *mysql);
|
|
extern void test_additional_snmp_session_boundaries(void);
|
|
extern void test_snmpv3_agent_contracts(void);
|
|
|
|
/* Threads that run poll_host() need the stack production workers get. */
|
|
void start_test_worker(pthread_t *worker, void *(*start)(void *), void *argument) {
|
|
pthread_attr_t attributes;
|
|
|
|
assert(spine_thread_attr_init(&attributes) == 0);
|
|
assert(pthread_create(worker, &attributes, start, argument) == 0);
|
|
assert(pthread_attr_destroy(&attributes) == 0);
|
|
}
|
|
|
|
static void test_copy_bounds(void) {
|
|
struct { char text[8]; unsigned char guard; } output;
|
|
memset(&output, 0xa5, sizeof output);
|
|
strncopy(output.text, "12345678", sizeof(output.text));
|
|
assert(strcmp(output.text, "1234567") == 0);
|
|
assert(output.guard == 0xa5);
|
|
strncopy(output.text, "abcdefghijk", sizeof(output.text));
|
|
assert(strcmp(output.text, "abcdefg") == 0);
|
|
assert(output.guard == 0xa5);
|
|
strncopy(output.text, "unchanged", 0);
|
|
assert(strcmp(output.text, "abcdefg") == 0);
|
|
strncopy(output.text, "x", 1);
|
|
assert(output.text[0] == '\0');
|
|
strncopy(output.text, "", sizeof(output.text));
|
|
assert(output.text[0] == '\0');
|
|
}
|
|
|
|
static void test_string_conversions(void) {
|
|
char empty[] = "";
|
|
char one[] = "x";
|
|
char word[] = "abcd";
|
|
assert(strcmp(reverse(empty), "") == 0);
|
|
assert(strcmp(reverse(one), "x") == 0);
|
|
assert(strcmp(reverse(word), "dcba") == 0);
|
|
classified_result_t hex;
|
|
assert(classify_result("\"FF ff\t\"", &hex) == RESULT_HEX_COUNTER && hex.value.counter == 65535);
|
|
assert(classify_result("FF:FF:FF:FF FF-FF-FF-FF", &hex) == RESULT_HEX_COUNTER && hex.value.counter == ULLONG_MAX);
|
|
assert(classify_result("01 00 00 00 00 00 00 00 00", &hex) == RESULT_UNKNOWN && IS_UNDEFINED(hex.text));
|
|
assert(classify_result("not hex", &hex) == RESULT_UNKNOWN);
|
|
char input[BUFSIZE * 2];
|
|
memset(input, '\\', sizeof(input) - 1);
|
|
input[sizeof(input) - 1] = '\0';
|
|
char *escaped = add_slashes(input);
|
|
assert(strlen(escaped) == 2 * (sizeof(input) - 1));
|
|
for (size_t i = 0; escaped[i] != '\0'; i++) assert(escaped[i] == '\\');
|
|
free(escaped);
|
|
escaped = add_slashes("a\\b");
|
|
assert(strcmp(escaped, "a\\\\b") == 0);
|
|
free(escaped);
|
|
escaped = add_slashes("");
|
|
assert(escaped[0] == '\0');
|
|
free(escaped);
|
|
char decorated[] = "text +12.5 Bytes";
|
|
assert(strcmp(strip_alpha(decorated), "12.5") == 0);
|
|
char negative[] = "text -12.5 Bytes";
|
|
assert(strcmp(strip_alpha(negative), "-12.5") == 0);
|
|
char nonnumeric[] = "no sample";
|
|
assert(strcmp(strip_alpha(nonnumeric), "") == 0);
|
|
}
|
|
|
|
static void test_result_count_range(void) {
|
|
assert(spine_count_to_int(0) == 0);
|
|
assert(spine_count_to_int(INT_MAX) == INT_MAX);
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
spine_count_to_int((unsigned long long)INT_MAX + 1);
|
|
_exit(99);
|
|
}
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == EXIT_FAILURE);
|
|
}
|
|
|
|
static void test_cli_case(const char *flag, const char *input, int expected, const char *error) {
|
|
int errors[2];
|
|
assert(pipe(errors) == 0);
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
assert(close(errors[0]) == 0);
|
|
assert(dup2(errors[1], STDERR_FILENO) == STDERR_FILENO);
|
|
assert(close(errors[1]) == 0);
|
|
char program[] = "spine";
|
|
char option[32];
|
|
strncopy(option, flag, sizeof(option));
|
|
char version[] = "--version";
|
|
char value[64];
|
|
strncopy(value, input, sizeof(value));
|
|
char *args[] = {program, option, value, version, NULL};
|
|
spine_program_main(4, args);
|
|
_exit(99);
|
|
}
|
|
assert(close(errors[1]) == 0);
|
|
char message[512];
|
|
ssize_t received = read(errors[0], message, sizeof(message) - 1);
|
|
if (received < 0 || received >= (ssize_t)sizeof(message)) abort();
|
|
message[received] = '\0';
|
|
assert(close(errors[0]) == 0);
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == expected);
|
|
if (expected == EXIT_FAILURE || error != NULL) assert(strstr(message, error) != NULL);
|
|
else assert(message[0] == '\0');
|
|
}
|
|
|
|
static void test_cli_option_shape(void) {
|
|
test_cli_case("--option", "missing-colon", EXIT_FAILURE, "ERROR: -O requires setting:value");
|
|
test_cli_case("--option", ":value", EXIT_FAILURE, "ERROR: -O requires setting:value");
|
|
test_cli_case("--option", "regression:", EXIT_SUCCESS, NULL);
|
|
test_cli_case("--option", "regression:value:colon", EXIT_SUCCESS, NULL);
|
|
test_cli_case("--mode", "online", EXIT_SUCCESS, NULL);
|
|
test_cli_case("--mode", "offline", EXIT_SUCCESS, NULL);
|
|
test_cli_case("--mode", "recovery", EXIT_SUCCESS, NULL);
|
|
test_cli_case("--mode", "invalid", EXIT_FAILURE, "ERROR: invalid polling mode 'invalid' specified");
|
|
static const char *const bad_threads[] = {"0", "-1", "abc", "5x", " 5", "+5", "", "99999999999999999999"};
|
|
for (size_t index = 0; index < sizeof(bad_threads) / sizeof(bad_threads[0]); index++) {
|
|
test_cli_case("--threads", bad_threads[index], EXIT_FAILURE, "is an invalid thread count, use 1 to 100");
|
|
test_cli_case("-t", bad_threads[index], EXIT_FAILURE, "is an invalid thread count, use 1 to 100");
|
|
}
|
|
test_cli_case("--threads", "1", EXIT_SUCCESS, NULL);
|
|
test_cli_case("-t", "100", EXIT_SUCCESS, NULL);
|
|
/* above the cap still runs, as it did with atoi(), but says so */
|
|
test_cli_case("-t", "101", EXIT_SUCCESS, "WARNING: '-t=101' exceeds the maximum thread count, using 100");
|
|
test_cli_case("--threads", "150", EXIT_SUCCESS, "WARNING: '--threads=150' exceeds the maximum thread count, using 100");
|
|
static const char *const bad_ids[] = {"abc", "1x", "-1", "+1", " 1", "", "2147483648", "99999999999999999999"};
|
|
for (size_t index = 0; index < sizeof(bad_ids) / sizeof(bad_ids[0]); index++) {
|
|
test_cli_case("--first", bad_ids[index], EXIT_FAILURE, "is invalid first-host ID");
|
|
test_cli_case("-l", bad_ids[index], EXIT_FAILURE, "is invalid last-host ID");
|
|
}
|
|
test_cli_case("-f", "0", EXIT_SUCCESS, NULL);
|
|
test_cli_case("--last", "2147483647", EXIT_SUCCESS, NULL);
|
|
/* the positional "first last" form poller.php uses */
|
|
test_cli_case("7", "99999999999999999999", EXIT_FAILURE, "is invalid last-host ID");
|
|
test_cli_case("99999999999999999999", "7", EXIT_FAILURE, "is invalid first-host ID");
|
|
test_cli_case("7", "9", EXIT_SUCCESS, NULL);
|
|
int errors[2];
|
|
assert(pipe(errors) == 0);
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
assert(close(errors[0]) == 0);
|
|
assert(dup2(errors[1], STDERR_FILENO) == STDERR_FILENO);
|
|
assert(close(errors[1]) == 0);
|
|
set.exit.exit_code = EXIT_SUCCESS;
|
|
for (int i = 0; i < 257; i++) set_option("regression", "value");
|
|
_exit(99);
|
|
}
|
|
assert(close(errors[1]) == 0);
|
|
char message[512];
|
|
ssize_t received = read(errors[0], message, sizeof(message) - 1);
|
|
if (received < 0 || received >= (ssize_t)sizeof(message)) abort();
|
|
message[received] = '\0';
|
|
assert(close(errors[0]) == 0);
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == EXIT_FAILURE);
|
|
assert(strstr(message, "Invalid or excessive command-line setting overrides") != NULL);
|
|
}
|
|
|
|
static spine_permits_t test_permits;
|
|
static pthread_mutex_t permit_test_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
static int active_permit_workers;
|
|
|
|
static void *exercise_permit(void *unused) {
|
|
(void)unused;
|
|
for (int iteration = 0; iteration < 100; iteration++) {
|
|
int status;
|
|
while ((status = spine_permits_try_acquire(&test_permits)) == EAGAIN) spine_sleep_usec(100);
|
|
assert(status == 0);
|
|
assert(pthread_mutex_lock(&permit_test_mutex) == 0);
|
|
assert(active_permit_workers == 0);
|
|
active_permit_workers++;
|
|
assert(pthread_mutex_unlock(&permit_test_mutex) == 0);
|
|
spine_sleep_usec(100);
|
|
assert(pthread_mutex_lock(&permit_test_mutex) == 0);
|
|
assert(active_permit_workers == 1);
|
|
active_permit_workers--;
|
|
assert(pthread_mutex_unlock(&permit_test_mutex) == 0);
|
|
assert(spine_permits_release(&test_permits) == 0);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void test_concurrent_permits(void) {
|
|
assert(spine_permits_init(&test_permits, -1) == EINVAL);
|
|
assert(spine_permits_init(&test_permits, INT_MAX) == 0);
|
|
assert(spine_permits_release(&test_permits) == EOVERFLOW);
|
|
assert(spine_permits_available(&test_permits) == INT_MAX);
|
|
assert(spine_permits_destroy(&test_permits) == 0);
|
|
assert(spine_permits_init(&test_permits, 1) == 0);
|
|
assert(spine_permits_try_acquire(&test_permits) == 0);
|
|
assert(spine_permits_try_acquire(&test_permits) == EAGAIN);
|
|
assert(spine_permits_available(&test_permits) == 0);
|
|
assert(spine_permits_release(&test_permits) == 0);
|
|
pthread_t workers[4];
|
|
for (size_t i = 0; i < sizeof(workers) / sizeof(workers[0]); i++) {
|
|
assert(pthread_create(&workers[i], NULL, exercise_permit, NULL) == 0);
|
|
}
|
|
for (size_t i = 0; i < sizeof(workers) / sizeof(workers[0]); i++) assert(pthread_join(workers[i], NULL) == 0);
|
|
assert(spine_permits_available(&test_permits) == 1 && active_permit_workers == 0);
|
|
assert(spine_permits_destroy(&test_permits) == 0);
|
|
}
|
|
|
|
static void test_database_escape(void) {
|
|
MYSQL *mysql = mysql_init(NULL);
|
|
assert(mysql != NULL);
|
|
char input[RESULTS_BUFFER];
|
|
char output[RESULTS_BUFFER * 2 + 1];
|
|
memset(input, 'x', sizeof(input) - 1);
|
|
input[sizeof(input) - 1] = '\0';
|
|
db_escape(mysql, output, sizeof(output), input);
|
|
assert(strcmp(output, input) == 0);
|
|
memset(input, '\'', sizeof(input) - 1);
|
|
db_escape(mysql, output, sizeof(output), input);
|
|
assert(strlen(output) == 2 * (sizeof(input) - 1));
|
|
for (size_t i = 0; i < sizeof(input) - 1; i++) {
|
|
assert(output[2 * i] == '\\' && output[2 * i + 1] == '\'');
|
|
}
|
|
unsigned char guarded[13];
|
|
memset(guarded, 0xa5, sizeof(guarded));
|
|
db_escape(mysql, (char *)guarded + 1, 11, input);
|
|
assert(guarded[0] == 0xa5 && guarded[12] == 0xa5);
|
|
assert(strlen((char *)guarded + 1) == 10);
|
|
char tiny[] = "abc";
|
|
db_escape(mysql, tiny, 0, input);
|
|
assert(strcmp(tiny, "abc") == 0);
|
|
db_escape(mysql, tiny, -1, input);
|
|
assert(strcmp(tiny, "abc") == 0);
|
|
db_escape(mysql, tiny, 1, input);
|
|
assert(tiny[0] == '\0' && tiny[1] == 'b');
|
|
db_escape(mysql, tiny, sizeof(tiny), NULL);
|
|
assert(tiny[1] == 'b');
|
|
db_escape(mysql, NULL, 16, input);
|
|
mysql_close(mysql);
|
|
}
|
|
|
|
static void test_database_addresses(void) {
|
|
db_address_t address;
|
|
db_address_init(&address, "localhost", true);
|
|
assert(strcmp(address.hostname, "localhost") == 0 && address.socket == NULL);
|
|
db_address_release(&address);
|
|
assert(address.storage == NULL && address.hostname == NULL && address.socket == NULL);
|
|
db_address_init(&address, "localhost:service", true);
|
|
assert(strcmp(address.hostname, "localhost") == 0 && strcmp(address.socket, "service") == 0);
|
|
db_address_release(&address);
|
|
db_address_init(&address, "remote:service", false);
|
|
assert(strcmp(address.hostname, "remote:service") == 0 && address.socket == NULL);
|
|
db_address_release(&address);
|
|
char directory[] = "spine-socket-test-XXXXXX";
|
|
assert(mkdtemp(directory) != NULL);
|
|
db_address_init(&address, directory, true);
|
|
assert(strcmp(address.hostname, directory) == 0 && address.socket == NULL);
|
|
db_address_release(&address);
|
|
struct sockaddr_un socket_address = {0};
|
|
socket_address.sun_family = AF_UNIX;
|
|
#ifdef __APPLE__
|
|
socket_address.sun_len = sizeof(socket_address);
|
|
#endif
|
|
spine_snprintf(socket_address.sun_path, sizeof(socket_address.sun_path), "%s/db.sock", directory);
|
|
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
assert(fd >= 0);
|
|
int bound = bind(fd, (struct sockaddr *)&socket_address, sizeof(socket_address));
|
|
if (bound != 0) perror("bind regression socket");
|
|
assert(bound == 0);
|
|
db_address_init(&address, socket_address.sun_path, true);
|
|
assert(address.hostname == NULL && strcmp(address.socket, socket_address.sun_path) == 0);
|
|
db_address_release(&address);
|
|
assert(close(fd) == 0);
|
|
assert(unlink(socket_address.sun_path) == 0);
|
|
assert(rmdir(directory) == 0);
|
|
}
|
|
|
|
static void test_database_option_failure(void) {
|
|
MYSQL *mysql = mysql_init(NULL);
|
|
assert(mysql != NULL);
|
|
unsigned int timeout = 5;
|
|
db_set_option(mysql, MYSQL_OPT_CONNECT_TIMEOUT, &timeout, "test timeout");
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
db_set_option(mysql, (enum mysql_option)999999, NULL, "unsupported test option");
|
|
_exit(0);
|
|
}
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == EXIT_FAILURE);
|
|
mysql_close(mysql);
|
|
}
|
|
|
|
static void test_regex_storage(void) {
|
|
char first[] = "abc123xyz";
|
|
char second[] = "x4y";
|
|
char absent[] = "no digits";
|
|
const char *match = regex_replace("[0-9][0-9]*", first);
|
|
assert(strcmp(match, "123") == 0);
|
|
/* The result remains valid across another function's stack use. */
|
|
char stack[4096];
|
|
memset(stack, 'z', sizeof stack);
|
|
assert(strcmp(match, "123") == 0);
|
|
assert(strcmp(regex_replace("[0-9][0-9]*", second), "4") == 0);
|
|
assert(regex_replace("[0-9][0-9]*", absent) == absent);
|
|
assert(regex_replace("[", first) == first);
|
|
char large[RESULTS_BUFFER + 1];
|
|
memset(large, '9', sizeof large - 1);
|
|
large[sizeof large - 1] = '\0';
|
|
assert(regex_replace("[0-9][0-9]*", large) == large);
|
|
}
|
|
|
|
static pthread_mutex_t regex_test_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
static pthread_cond_t regex_test_ready = PTHREAD_COND_INITIALIZER;
|
|
static int regex_threads_ready;
|
|
|
|
static void *test_regex_thread(void *argument) {
|
|
char *input = argument;
|
|
const char *match = regex_replace("[0-9][0-9]*", input);
|
|
assert(pthread_mutex_lock(®ex_test_mutex) == 0);
|
|
regex_threads_ready++;
|
|
assert(pthread_cond_broadcast(®ex_test_ready) == 0);
|
|
while (regex_threads_ready < 2) {
|
|
assert(pthread_cond_wait(®ex_test_ready, ®ex_test_mutex) == 0);
|
|
}
|
|
/* Both threads have produced a result; neither may overwrite the other. */
|
|
assert(strcmp(match, input) == 0);
|
|
assert(pthread_mutex_unlock(®ex_test_mutex) == 0);
|
|
return NULL;
|
|
}
|
|
|
|
static void test_regex_concurrency(void) {
|
|
char first[] = "123";
|
|
char second[] = "4567";
|
|
pthread_t threads[2];
|
|
assert(pthread_create(&threads[0], NULL, test_regex_thread, first) == 0);
|
|
assert(pthread_create(&threads[1], NULL, test_regex_thread, second) == 0);
|
|
assert(pthread_join(threads[0], NULL) == 0);
|
|
assert(pthread_join(threads[1], NULL) == 0);
|
|
}
|
|
|
|
static void test_formatting(void) {
|
|
char output[8];
|
|
assert(spine_snprintf(output, sizeof output, "%s", "1234567") == 7);
|
|
assert(strcmp(output, "1234567") == 0);
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
spine_snprintf(output, sizeof output, "%s", "12345678");
|
|
_exit(0);
|
|
}
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == EXIT_FAILURE);
|
|
}
|
|
|
|
static void test_secret_clear(void) {
|
|
unsigned char bytes[8];
|
|
memset(bytes, 0xa5, sizeof bytes);
|
|
spine_clear_sensitive(bytes + 1, 6);
|
|
assert(bytes[0] == 0xa5 && bytes[7] == 0xa5);
|
|
for (size_t i = 1; i < 7; i++) assert(bytes[i] == 0);
|
|
spine_clear_sensitive(NULL, 0);
|
|
}
|
|
|
|
static void test_log_boundary(void) {
|
|
char *message = malloc(LOGSIZE + 1);
|
|
assert(message != NULL);
|
|
memset(message, 'x', LOGSIZE);
|
|
message[LOGSIZE] = '\0';
|
|
set.logging.log_level = POLLER_VERBOSITY_NONE;
|
|
set.logging.log_destination = 0;
|
|
set.console.stdout_notty = TRUE;
|
|
spine_log("%s", message);
|
|
free(message);
|
|
}
|
|
|
|
static void test_log_sanitization(void) {
|
|
char message[] = "a\r\nb\t\033\177c\302\205d\342\200\250e\342\200\251f";
|
|
spine_sanitize_log_message(message);
|
|
assert(strcmp(message, "a b c d e f") == 0);
|
|
char unicode[] = "caf\303\251";
|
|
spine_sanitize_log_message(unicode);
|
|
assert(strcmp(unicode, "caf\303\251") == 0);
|
|
char truncated[] = "\342\200";
|
|
spine_sanitize_log_message(truncated);
|
|
assert(strcmp(truncated, "\342\200") == 0);
|
|
char path[] = "spine-log-test-XXXXXX";
|
|
int fd = mkstemp(path);
|
|
assert(fd >= 0 && close(fd) == 0);
|
|
set.logging.log_destination = LOGDEST_FILE;
|
|
set.logging.log_level = POLLER_VERBOSITY_LOW;
|
|
set.logging.logfile_processed = TRUE;
|
|
strncopy(set.logging.path_logfile, path, sizeof(set.logging.path_logfile));
|
|
spine_log("device: %s", "first\r\nsecond");
|
|
FILE *file = fopen(path, "r");
|
|
assert(file != NULL);
|
|
char line[BUFSIZE];
|
|
assert(fgets(line, sizeof(line), file) != NULL);
|
|
assert(strstr(line, "device: first second") != NULL);
|
|
assert(fgets(line, sizeof(line), file) == NULL && !ferror(file));
|
|
assert(fclose(file) == 0 && unlink(path) == 0);
|
|
set.logging.log_destination = 0;
|
|
}
|
|
|
|
static void *test_log_writer(void *argument) {
|
|
const char *message = argument;
|
|
for (int count = 0; count < 5; count++) assert(spine_log("%s", message));
|
|
return NULL;
|
|
}
|
|
|
|
static void test_log_append_and_failures(void) {
|
|
config_t previous = set;
|
|
char path[] = "spine-log-append-XXXXXX";
|
|
int fd = mkstemp(path);
|
|
assert(fd >= 0);
|
|
static const char retained[] = "retained record\n";
|
|
assert(write(fd, retained, sizeof(retained) - 1) == (ssize_t)(sizeof(retained) - 1));
|
|
assert(close(fd) == 0);
|
|
set.logging.log_destination = LOGDEST_FILE;
|
|
set.logging.log_level = POLLER_VERBOSITY_LOW;
|
|
set.logging.logfile_processed = TRUE;
|
|
set.console.stdout_notty = TRUE;
|
|
set.console.stderr_notty = TRUE;
|
|
strncopy(set.logging.path_logfile, path, sizeof(set.logging.path_logfile));
|
|
char first[20001];
|
|
char second[20001];
|
|
memset(first, 'a', sizeof(first) - 1);
|
|
memset(second, 'b', sizeof(second) - 1);
|
|
first[sizeof(first) - 1] = '\0';
|
|
second[sizeof(second) - 1] = '\0';
|
|
pthread_t threads[2];
|
|
assert(pthread_create(&threads[0], NULL, test_log_writer, first) == 0);
|
|
assert(pthread_create(&threads[1], NULL, test_log_writer, second) == 0);
|
|
assert(pthread_join(threads[0], NULL) == 0);
|
|
assert(pthread_join(threads[1], NULL) == 0);
|
|
FILE *file = fopen(path, "r");
|
|
assert(file != NULL);
|
|
char *line = NULL;
|
|
size_t capacity = 0;
|
|
assert(getline(&line, &capacity, file) == (ssize_t)(sizeof(retained) - 1));
|
|
assert(strcmp(line, retained) == 0);
|
|
int first_count = 0;
|
|
int second_count = 0;
|
|
while (getline(&line, &capacity, file) >= 0) {
|
|
const char *body = strstr(line, first);
|
|
if (body != NULL) first_count++;
|
|
else { body = strstr(line, second); second_count++; }
|
|
assert(body != NULL && strcmp(body + 20000, "\n") == 0);
|
|
}
|
|
assert(!ferror(file) && first_count == 5 && second_count == 5);
|
|
free(line);
|
|
assert(fclose(file) == 0 && unlink(path) == 0);
|
|
/* Append mode must also create a missing file. */
|
|
assert(spine_log("created record"));
|
|
assert(file_exists(path) && unlink(path) == 0);
|
|
char directory[] = "spine-log-directory-XXXXXX";
|
|
assert(mkdtemp(directory) != NULL);
|
|
strncopy(set.logging.path_logfile, directory, sizeof(set.logging.path_logfile));
|
|
assert(!spine_log("must fail to open directory"));
|
|
assert(rmdir(directory) == 0);
|
|
#ifdef __linux__
|
|
STRNCOPY(set.logging.path_logfile, "/dev/full");
|
|
assert(!spine_log("must detect buffered write/close failure"));
|
|
#endif
|
|
set = previous;
|
|
}
|
|
|
|
static void test_config_directives(void) {
|
|
char path[] = "spine-config-test-XXXXXX";
|
|
int fd = mkstemp(path);
|
|
assert(fd >= 0);
|
|
FILE *file = fdopen(fd, "w");
|
|
assert(file != NULL);
|
|
fputs("# comment\nDB_Host localhost\nDB_Port 3307\nRDB_UseSSL 1\n"
|
|
"Poller 2\nCacti_Log /tmp/spine-test.log\n"
|
|
"SNMP_Clientaddr 127.0.0.1\nDB_Pass\nDB_Database final_line", file);
|
|
assert(fclose(file) == 0);
|
|
memset(&set, 0, sizeof set);
|
|
set.console.stdout_notty = TRUE;
|
|
set.console.stderr_notty = TRUE;
|
|
config_defaults();
|
|
assert(read_spine_config(path) == 0);
|
|
assert(strcmp(set.database.host, "localhost") == 0);
|
|
assert(set.database.port == 3307 && set.remote_database.ssl == 1 && set.poller.poller_id == 2);
|
|
assert(strcmp(set.logging.path_logfile, "/tmp/spine-test.log") == 0);
|
|
assert(strcmp(set.snmp.snmp_clientaddr, "127.0.0.1") == 0);
|
|
assert(strcmp(set.database.database, "final_line") == 0);
|
|
assert(set.logging.logfile_processed == 1 && set.logging.log_destination == LOGDEST_BOTH);
|
|
assert(unlink(path) == 0);
|
|
assert(read_spine_config(path) == -1);
|
|
}
|
|
|
|
/* -C accepts any readable path, so a warning about a line that is not a
|
|
* directive must not reveal what the line holds. */
|
|
static void test_config_unknown_lines_not_echoed(void) {
|
|
char path[] = "spine-config-echo-XXXXXX";
|
|
int fd = mkstemp(path);
|
|
assert(fd >= 0);
|
|
FILE *file = fdopen(fd, "w");
|
|
assert(file != NULL);
|
|
assert(fputs("DB_Host localhost\nroot:$6$saltsecret:19000:0:99999:7:::\n", file) != EOF);
|
|
assert(fclose(file) == 0);
|
|
int errors[2];
|
|
assert(pipe(errors) == 0);
|
|
fflush(NULL);
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
assert(close(errors[0]) == 0);
|
|
assert(dup2(errors[1], STDERR_FILENO) == STDERR_FILENO);
|
|
assert(close(errors[1]) == 0);
|
|
memset(&set, 0, sizeof set);
|
|
set.console.stdout_notty = TRUE;
|
|
set.console.stderr_notty = FALSE;
|
|
config_defaults();
|
|
_exit(read_spine_config(path) == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
|
|
}
|
|
assert(close(errors[1]) == 0);
|
|
char message[1024];
|
|
size_t used = 0;
|
|
for (;;) {
|
|
ssize_t received = read(errors[0], message + used, sizeof(message) - used - 1);
|
|
if (received < 0 && errno == EINTR) continue;
|
|
assert(received >= 0);
|
|
if (received == 0) break;
|
|
used += (size_t)received;
|
|
assert(used < sizeof(message) - 1);
|
|
}
|
|
message[used] = '\0';
|
|
assert(close(errors[0]) == 0);
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS);
|
|
assert(unlink(path) == 0);
|
|
assert(strstr(message, "line 2") != NULL);
|
|
assert(strstr(message, "root") == NULL && strstr(message, "salt") == NULL);
|
|
}
|
|
|
|
static void test_config_structure_bindings(void) {
|
|
config_t previous = set;
|
|
memset(&set, 0, sizeof set);
|
|
set.console.stdout_notty = TRUE;
|
|
set.console.stderr_notty = TRUE;
|
|
set.d_b = 37;
|
|
set.cacti_version = 1232;
|
|
set.cygwinshloc = 1;
|
|
config_defaults();
|
|
assert(strcmp(set.database.host, DEFAULT_DB_HOST) == 0);
|
|
assert(strcmp(set.remote_database.host, DEFAULT_DB_HOST) == 0);
|
|
assert(set.database.port == DEFAULT_DB_PORT && set.remote_database.port == DEFAULT_DB_PORT);
|
|
assert(set.poller.threads == DEFAULT_THREADS && set.logging.log_destination == LOGDEST_FILE);
|
|
char path[] = "spine-config-bindings-XXXXXX";
|
|
int fd = mkstemp(path);
|
|
assert(fd >= 0);
|
|
FILE *file = fdopen(fd, "w");
|
|
assert(file != NULL);
|
|
assert(fputs("DB_Host local-fixture\nDB_Database local-schema\nDB_User local-user\n"
|
|
"DB_Pass fixture-local-password\nDB_SSL_Key local-key\nDB_SSL_Cert local-cert\nDB_SSL_CA local-ca\n"
|
|
"RDB_Host remote-fixture\nRDB_Database remote-schema\nRDB_User remote-user\n"
|
|
"RDB_Pass fixture-remote-password\nRDB_SSL_Key remote-key\nRDB_SSL_Cert remote-cert\nRDB_SSL_CA remote-ca\n"
|
|
"DB_Port 3307\nRDB_Port 3308\nDB_UseSSL 1\nRDB_UseSSL 2\nPoller 3\n"
|
|
"SNMP_Clientaddr 127.0.0.2\nCacti_Log /tmp/spine-bindings.log\n", file) != EOF);
|
|
assert(fclose(file) == 0 && read_spine_config(path) == 0);
|
|
assert(strcmp(set.database.host, "local-fixture") == 0);
|
|
assert(strcmp(set.database.database, "local-schema") == 0);
|
|
assert(strcmp(set.database.user, "local-user") == 0);
|
|
assert(strcmp(set.database.password, "fixture-local-password") == 0);
|
|
assert(strcmp(set.database.ssl_key, "local-key") == 0);
|
|
assert(strcmp(set.database.ssl_cert, "local-cert") == 0);
|
|
assert(strcmp(set.database.ssl_ca, "local-ca") == 0);
|
|
assert(strcmp(set.remote_database.host, "remote-fixture") == 0);
|
|
assert(strcmp(set.remote_database.database, "remote-schema") == 0);
|
|
assert(strcmp(set.remote_database.user, "remote-user") == 0);
|
|
assert(strcmp(set.remote_database.password, "fixture-remote-password") == 0);
|
|
assert(strcmp(set.remote_database.ssl_key, "remote-key") == 0);
|
|
assert(strcmp(set.remote_database.ssl_cert, "remote-cert") == 0);
|
|
assert(strcmp(set.remote_database.ssl_ca, "remote-ca") == 0);
|
|
assert(set.database.port == 3307 && set.remote_database.port == 3308);
|
|
assert(set.database.ssl == 1 && set.remote_database.ssl == 2);
|
|
assert(set.poller.poller_id == 3 && set.poller.threads == DEFAULT_THREADS);
|
|
assert(strcmp(set.snmp.snmp_clientaddr, "127.0.0.2") == 0);
|
|
assert(strcmp(set.logging.path_logfile, "/tmp/spine-bindings.log") == 0);
|
|
assert(set.logging.logfile_processed == 1 && set.logging.log_destination == LOGDEST_BOTH);
|
|
assert(set.d_b == 37 && set.cacti_version == 1232 && set.cygwinshloc == 1);
|
|
assert(set.console.stdout_notty && set.console.stderr_notty);
|
|
assert(unlink(path) == 0);
|
|
set = previous;
|
|
puts("production config structure bindings passed");
|
|
}
|
|
|
|
static void test_date_formats(void) {
|
|
const char *const expected[] = {
|
|
"%m/%d/%Y %H:%M:%S - ", "%b/%d/%Y %H:%M:%S - ",
|
|
"%d/%m/%Y %H:%M:%S - ", "%d/%b/%Y %H:%M:%S - ",
|
|
"%Y/%m/%d %H:%M:%S - ", "%Y/%b/%d %H:%M:%S - "
|
|
};
|
|
set.logging.log_datetime_separator = GDC_SLASH;
|
|
for (int i = GD_MIN; i <= GD_MAX; i++) {
|
|
set.logging.log_datetime_format = i;
|
|
set_date_format();
|
|
assert(strcmp(get_date_format(), expected[i]) == 0);
|
|
}
|
|
set.logging.log_datetime_format = -1;
|
|
set.logging.log_datetime_separator = -1;
|
|
set_date_format();
|
|
assert(strcmp(get_date_format(), expected[GD_DEFAULT]) == 0);
|
|
}
|
|
|
|
static void test_device_logging(void) {
|
|
extern int *debug_devices;
|
|
int devices[100] = {7, 0};
|
|
int *previous = debug_devices;
|
|
debug_devices = devices;
|
|
for (int level = POLLER_VERBOSITY_NONE; level <= POLLER_VERBOSITY_DEVDBG; level++) {
|
|
set.logging.log_level = level;
|
|
for (int minimum = POLLER_VERBOSITY_LOW; minimum <= POLLER_VERBOSITY_DEVDBG; minimum++) {
|
|
assert(spine_should_log_device(7, minimum));
|
|
assert(spine_should_log_device(8, minimum) == (level >= minimum));
|
|
}
|
|
}
|
|
set.logging.log_level = POLLER_VERBOSITY_NONE;
|
|
int evaluated = 0;
|
|
SPINE_LOG_DEVICE(8, POLLER_VERBOSITY_DEBUG, ("hidden %i", ++evaluated));
|
|
assert(evaluated == 0);
|
|
SPINE_LOG_DEVICE(7, POLLER_VERBOSITY_DEBUG, ("visible %i", ++evaluated));
|
|
assert(evaluated == 1);
|
|
debug_devices = previous;
|
|
}
|
|
|
|
static void test_debug_device_bounds(void) {
|
|
struct { int before; int devices[100]; int after; } output;
|
|
output.before = 123;
|
|
output.after = 456;
|
|
char list[2048];
|
|
size_t used = 0;
|
|
for (int i = 1; i <= 110; i++) {
|
|
used += (size_t)spine_snprintf(list + used, sizeof(list) - used, "%i,", i);
|
|
}
|
|
parse_debug_devices(list, output.devices, 100);
|
|
assert(output.before == 123 && output.after == 456);
|
|
for (int i = 0; i < 99; i++) assert(output.devices[i] == i + 1);
|
|
assert(output.devices[99] == 0);
|
|
char empty[] = "";
|
|
parse_debug_devices(empty, output.devices, 100);
|
|
assert(output.devices[0] == 0);
|
|
char single[] = "7";
|
|
parse_debug_devices(single, output.devices, 1);
|
|
assert(output.devices[0] == 0);
|
|
output.devices[0] = 7;
|
|
parse_debug_devices(single, output.devices, 0);
|
|
assert(output.devices[0] == 7);
|
|
}
|
|
|
|
static void test_poll_result_formats(void) {
|
|
const struct {
|
|
const char *input;
|
|
const char *output;
|
|
enum poll_result_status status;
|
|
} cases[] = {
|
|
{"123", "123", POLL_RESULT_VALID},
|
|
{"-12.5", "-12.5", POLL_RESULT_VALID},
|
|
{"4096 Bytes", "4096", POLL_RESULT_VALID},
|
|
{"a:1 b!2", "a:1 b!2", POLL_RESULT_VALID},
|
|
{"a!1", "a!1", POLL_RESULT_VALID},
|
|
{"U", "U", POLL_RESULT_UNDEFINED},
|
|
{"nonsense", "U", POLL_RESULT_INVALID},
|
|
/* Padding is not data: " 42" once parsed as the octets 0x42. */
|
|
{" 42", "42", POLL_RESULT_VALID},
|
|
{"42\n", "42", POLL_RESULT_VALID},
|
|
{" 10\n", "10", POLL_RESULT_VALID},
|
|
{"\"42\"", "42", POLL_RESULT_VALID},
|
|
{"1F FF FF FF FF FF FF F1", "2305843009213693937", POLL_RESULT_VALID},
|
|
{"0x1A", "U", POLL_RESULT_INVALID},
|
|
{"---", "U", POLL_RESULT_INVALID},
|
|
{" ", "U", POLL_RESULT_INVALID}
|
|
};
|
|
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
|
for (int snmp = 0; snmp <= 1; snmp++) {
|
|
char result[RESULTS_BUFFER];
|
|
strncopy(result, cases[i].input, sizeof(result));
|
|
assert(normalize_poll_result(result, snmp) == cases[i].status);
|
|
assert(strcmp(result, cases[i].output) == 0);
|
|
}
|
|
}
|
|
/* An agent's "u" or "NaN" is unknown, and poller_output stores it as U. */
|
|
static const char *const unknown[] = {"u", "NaN", "nan"};
|
|
for (size_t i = 0; i < sizeof(unknown) / sizeof(unknown[0]); i++) {
|
|
char undefined[RESULTS_BUFFER];
|
|
strncopy(undefined, unknown[i], sizeof(undefined));
|
|
assert(normalize_poll_result(undefined, true) == POLL_RESULT_UNDEFINED);
|
|
assert(strcmp(undefined, "U") == 0);
|
|
strncopy(undefined, unknown[i], sizeof(undefined));
|
|
assert(normalize_poll_result(undefined, false) == POLL_RESULT_INVALID);
|
|
assert(strcmp(undefined, "U") == 0);
|
|
}
|
|
}
|
|
|
|
static void test_hostnames(void) {
|
|
const char *const inputs[] = {"router.example", "router.example:161", "TCP:router.example:443",
|
|
"udp:router.example:53", "[2001:db8::1]"};
|
|
const char *const outputs[] = {"router.example", "router.example", "router.example", "router.example", "[2001:db8::1]"};
|
|
const int ports[] = {0, 161, 443, 53, 0};
|
|
const int methods[] = {0, 0, 1, 2, 0};
|
|
for (size_t i = 0; i < sizeof(inputs) / sizeof(inputs[0]); i++) {
|
|
char input[128];
|
|
strncopy(input, inputs[i], sizeof(input));
|
|
name_t *name = get_namebyhost(input, NULL);
|
|
assert(strcmp(name->hostname, outputs[i]) == 0);
|
|
assert(name->port == ports[i] && name->method == methods[i]);
|
|
free(name);
|
|
}
|
|
}
|
|
|
|
static void test_snmp_initialization_failure(void) {
|
|
/* These inputs fail before a network session opens. Temporary connection
|
|
* parameters and passphrase copies must be released on every error path. */
|
|
assert(snmp_host_init(&(snmp_connection_t){
|
|
.host_id = 1,
|
|
.hostname = "127.0.0.1",
|
|
.snmp_version = 99,
|
|
.snmp_community = "public",
|
|
.snmp_username = "",
|
|
.snmp_password = "",
|
|
.snmp_auth_protocol = "SHA",
|
|
.snmp_priv_passphrase = "",
|
|
.snmp_priv_protocol = "[None]",
|
|
.snmp_context = "",
|
|
.snmp_engine_id = "",
|
|
.snmp_port = 161,
|
|
.snmp_timeout = 1000,
|
|
}) == NULL);
|
|
assert(snmp_host_init(&(snmp_connection_t){
|
|
.host_id = 1,
|
|
.hostname = "127.0.0.1",
|
|
.snmp_version = 3,
|
|
.snmp_community = "public",
|
|
.snmp_username = "user",
|
|
.snmp_password = "short",
|
|
.snmp_auth_protocol = "INVALID",
|
|
.snmp_priv_passphrase = "",
|
|
.snmp_priv_protocol = "[None]",
|
|
.snmp_context = "",
|
|
.snmp_engine_id = "",
|
|
.snmp_port = 161,
|
|
.snmp_timeout = 1000,
|
|
}) == NULL);
|
|
assert(snmp_host_init(&(snmp_connection_t){
|
|
.host_id = 1,
|
|
.hostname = "127.0.0.1",
|
|
.snmp_version = 3,
|
|
.snmp_community = "public",
|
|
.snmp_username = "user",
|
|
.snmp_password = "short",
|
|
.snmp_auth_protocol = "SHA",
|
|
.snmp_priv_passphrase = "short",
|
|
.snmp_priv_protocol = "AES",
|
|
.snmp_context = "",
|
|
.snmp_engine_id = "",
|
|
.snmp_port = 161,
|
|
.snmp_timeout = 1000,
|
|
}) == NULL);
|
|
assert(snmp_host_init(&(snmp_connection_t){
|
|
.host_id = 1,
|
|
.hostname = "127.0.0.1",
|
|
.snmp_version = 3,
|
|
.snmp_community = "public",
|
|
.snmp_username = "user",
|
|
.snmp_password = "regression-password",
|
|
.snmp_auth_protocol = "SHA",
|
|
.snmp_priv_passphrase = "short",
|
|
.snmp_priv_protocol = "AES",
|
|
.snmp_context = "",
|
|
.snmp_engine_id = "",
|
|
.snmp_port = 161,
|
|
.snmp_timeout = 1000,
|
|
}) == NULL);
|
|
}
|
|
|
|
static void test_snmp_security_protocols(void) {
|
|
snmp_spine_init();
|
|
snmp_connection_t options = {
|
|
.host_id = 1, .hostname = "127.0.0.1", .snmp_version = 3,
|
|
.snmp_community = "", .snmp_username = "regression-user",
|
|
.snmp_password = "", .snmp_auth_protocol = "SHA",
|
|
.snmp_priv_passphrase = "", .snmp_priv_protocol = "[None]",
|
|
.snmp_context = "regression-context", .snmp_engine_id = "",
|
|
.snmp_port = 1161, .snmp_timeout = 500,
|
|
};
|
|
const int levels[] = {SNMP_SEC_LEVEL_NOAUTH, SNMP_SEC_LEVEL_AUTHNOPRIV, SNMP_SEC_LEVEL_AUTHPRIV};
|
|
for (size_t index = 0; index < sizeof(levels) / sizeof(levels[0]); index++) {
|
|
options.snmp_password = index == 0 ? "" : "regression-password";
|
|
options.snmp_priv_protocol = index == 2 ? "AES" : "[None]";
|
|
options.snmp_priv_passphrase = index == 2 ? "regression-privacy" : "";
|
|
void *handle = snmp_host_init(&options);
|
|
assert(handle != NULL);
|
|
const struct snmp_session *session = snmp_sess_session(handle);
|
|
assert(session != NULL && session->version == SNMP_VERSION_3);
|
|
assert(session->securityLevel == levels[index]);
|
|
assert(strcmp(session->securityName, options.snmp_username) == 0);
|
|
assert(strcmp(session->contextName, options.snmp_context) == 0);
|
|
assert(session->securityAuthProto != NULL && session->securityPrivProto != NULL);
|
|
if (index == 2) {
|
|
assert(session->securityAuthKeyLen == 20); /* SHA-1 Ku length. */
|
|
assert(session->securityPrivKeyLen == 20);
|
|
}
|
|
snmp_host_cleanup(handle);
|
|
}
|
|
test_additional_snmp_session_boundaries();
|
|
options.snmp_priv_protocol = "INVALID";
|
|
assert(snmp_host_init(&options) == NULL);
|
|
snmp_spine_close();
|
|
}
|
|
|
|
static void test_child_process(void) {
|
|
int fd = nft_popen("printf spine-test", "r");
|
|
assert(fd >= 0);
|
|
char output[32] = {0};
|
|
size_t used = 0;
|
|
ssize_t received;
|
|
while ((received = read(fd, output + used, sizeof(output) - 1 - used)) != 0) {
|
|
if (received < 0 && errno == EINTR) continue;
|
|
assert(received > 0);
|
|
used += (size_t)received;
|
|
assert(used < sizeof(output) - 1);
|
|
}
|
|
assert(used == 10);
|
|
assert(strcmp(output, "spine-test") == 0);
|
|
int status = nft_pclose(fd);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
assert(nft_popen("printf ignored", "invalid") == -1);
|
|
assert(nft_popen("printf ignored", "invalid+") == -1 && errno == EINVAL);
|
|
assert(nft_popen(NULL, "r") == -1 && errno == EINVAL);
|
|
assert(nft_popen("printf ignored", NULL) == -1 && errno == EINVAL);
|
|
}
|
|
|
|
static void test_duplex_process(void) {
|
|
int fd = nft_popen("IFS= read -r value; printf '%s' \"$value\"", "r+");
|
|
assert(fd >= 0);
|
|
assert(write(fd, "duplex\n", 7) == 7);
|
|
char output[7] = {0};
|
|
size_t used = 0;
|
|
while (used < 6) {
|
|
ssize_t received = read(fd, output + used, 6 - used);
|
|
if (received < 0 && errno == EINTR) continue;
|
|
assert(received > 0);
|
|
used += (size_t)received;
|
|
}
|
|
assert(strcmp(output, "duplex") == 0);
|
|
int status = nft_pclose(fd);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
}
|
|
|
|
static void test_write_process(void) {
|
|
char path[] = "spine-write-process-XXXXXX";
|
|
int temporary = mkstemp(path);
|
|
assert(temporary >= 0 && close(temporary) == 0);
|
|
char command[BUFSIZE];
|
|
spine_snprintf(command, sizeof(command), "cat > %s", path);
|
|
int fd = nft_popen(command, "w");
|
|
assert(fd >= 0 && write(fd, "written", 7) == 7);
|
|
/* cat copies each read straight through, so the data reaches the file
|
|
* before EOF. Check it while the writer is still open: nft_pclose() never
|
|
* blocks the poller on a slow child and may signal it once closed. */
|
|
char output[8] = {0};
|
|
FILE *file = NULL;
|
|
for (int attempt = 0; attempt < 200; attempt++) {
|
|
file = fopen(path, "r");
|
|
assert(file != NULL);
|
|
memset(output, 0, sizeof(output));
|
|
if (fread(output, 1, 7, file) == 7) break;
|
|
assert(fclose(file) == 0);
|
|
file = NULL;
|
|
spine_sleep_usec(10000);
|
|
}
|
|
assert(file != NULL && strcmp(output, "written") == 0);
|
|
int status = nft_pclose(fd);
|
|
assert(status == -1 || (WIFEXITED(status) && WEXITSTATUS(status) == 0) || WIFSIGNALED(status));
|
|
assert(fgetc(file) == EOF && !ferror(file));
|
|
assert(fclose(file) == 0 && unlink(path) == 0);
|
|
}
|
|
|
|
static void test_process_descriptor_aliases(void) {
|
|
for (int mask = 1; mask <= 3; mask++) {
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
alarm(5);
|
|
if (mask & 1) assert(close(STDIN_FILENO) == 0);
|
|
if (mask & 2) assert(close(STDOUT_FILENO) == 0);
|
|
/* Keep an earlier parent pipe in the list, possibly at 0 or 1. */
|
|
int retained = nft_popen("printf retained", "r");
|
|
assert(retained >= 0);
|
|
test_duplex_process();
|
|
char output[16] = {0};
|
|
assert(read(retained, output, sizeof(output) - 1) == 8);
|
|
assert(strcmp(output, "retained") == 0);
|
|
int status = nft_pclose(retained);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
_exit(0);
|
|
}
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
}
|
|
}
|
|
|
|
/* Drive the shipped php_readpipe() against a pipe that stands in for a
|
|
* script server. A failed read restarts the slot, so point the restart at a
|
|
* missing executable to keep it local. */
|
|
static char *read_php_pipe(int fd, int timeout) {
|
|
config_t previous_config = set;
|
|
php_t *previous_processes = php_processes;
|
|
php_t process;
|
|
php_processes_initialize(&process, 1);
|
|
process.php_read_fd = fd;
|
|
process.php_state = PHP_BUSY;
|
|
php_processes = &process;
|
|
set.php.php_servers = 1;
|
|
set.php.script_timeout = timeout;
|
|
STRNCOPY(set.php.path_php, "/nonexistent-spine-regression-executable");
|
|
char *result = php_readpipe(0, "regression");
|
|
if (process.php_read_fd >= 0) close(process.php_read_fd);
|
|
if (process.php_write_fd >= 0) close(process.php_write_fd);
|
|
php_processes = previous_processes;
|
|
set = previous_config;
|
|
return result;
|
|
}
|
|
|
|
static void test_php_response(size_t length, bool newline) {
|
|
int pipes[2];
|
|
assert(pipe(pipes) == 0);
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
close(pipes[0]);
|
|
char *data = malloc(length);
|
|
assert(data != NULL);
|
|
memset(data, '7', length);
|
|
if (newline) data[length - 1] = '\n';
|
|
size_t sent = 0;
|
|
while (sent < length) {
|
|
ssize_t count = write(pipes[1], data + sent, length - sent);
|
|
if (count < 0 && errno == EINTR) continue;
|
|
if (count < 0 && errno == EPIPE) break;
|
|
assert(count > 0);
|
|
sent += (size_t)count;
|
|
}
|
|
free(data);
|
|
close(pipes[1]);
|
|
_exit(0);
|
|
}
|
|
close(pipes[1]);
|
|
char *result = read_php_pipe(pipes[0], 5);
|
|
if (newline) {
|
|
assert(strlen(result) == length && result[length - 1] == '\n');
|
|
} else {
|
|
assert(strcmp(result, "U") == 0);
|
|
}
|
|
free(result);
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
}
|
|
|
|
static void test_php_partial_response_timeout(void) {
|
|
/* A server that writes part of a line and stalls must not hold the
|
|
* poller thread past the response deadline. */
|
|
int pipes[2];
|
|
assert(pipe(pipes) == 0);
|
|
assert(write(pipes[1], "7", 1) == 1);
|
|
double begin = spine_monotonic_time();
|
|
char *result = read_php_pipe(pipes[0], 1);
|
|
assert(spine_monotonic_time() - begin < 5);
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
close(pipes[1]);
|
|
}
|
|
|
|
/* Script server arguments can carry SNMP communities and v3 passphrases, so
|
|
* no failure line may repeat the command arguments, at any verbosity.
|
|
* Drives the timeout, partial-line and lost-server paths of the real code. */
|
|
static void run_php_failure_logs(int level) {
|
|
static const char secret[] = "regression-community-7d1f";
|
|
config_t previous_config = set;
|
|
php_t *previous_processes = php_processes;
|
|
char path[] = "spine-php-redaction-XXXXXX";
|
|
int log_fd = mkstemp(path);
|
|
assert(log_fd >= 0 && close(log_fd) == 0);
|
|
set.logging.log_destination = LOGDEST_FILE;
|
|
set.logging.log_level = level;
|
|
set.logging.logfile_processed = TRUE;
|
|
set.console.stdout_notty = TRUE;
|
|
set.console.stderr_notty = TRUE;
|
|
strncopy(set.logging.path_logfile, path, sizeof(set.logging.path_logfile));
|
|
set.php.php_servers = 1;
|
|
set.php.script_timeout = 1;
|
|
STRNCOPY(set.php.path_php, "/nonexistent-spine-regression-executable");
|
|
char command[BUFSIZE];
|
|
spine_snprintf(command, sizeof(command), "ss_regression.php ss_regression 127.0.0.1 %s\r\n", secret);
|
|
php_t *process = malloc(sizeof(*process));
|
|
assert(process != NULL);
|
|
php_processes = process;
|
|
for (int partial = 0; partial <= 1; partial++) {
|
|
int pipes[2];
|
|
assert(pipe(pipes) == 0);
|
|
if (partial) assert(write(pipes[1], "7", 1) == 1);
|
|
php_processes_initialize(process, 1);
|
|
process->php_read_fd = pipes[0];
|
|
char *result = php_readpipe(0, command);
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
if (process->php_read_fd >= 0) close(process->php_read_fd);
|
|
assert(close(pipes[1]) == 0);
|
|
}
|
|
int requests[2];
|
|
assert(pipe(requests) == 0 && close(requests[0]) == 0);
|
|
int responses[2];
|
|
assert(pipe(responses) == 0);
|
|
fflush(NULL);
|
|
pid_t server = fork();
|
|
assert(server >= 0);
|
|
if (server == 0) {
|
|
pause();
|
|
_exit(0);
|
|
}
|
|
php_processes_initialize(process, 1);
|
|
process->php_state = PHP_READY;
|
|
process->php_pid = server;
|
|
process->php_write_fd = requests[1];
|
|
process->php_read_fd = responses[0];
|
|
/* The write fails with EPIPE; restarts fail on the missing executable. */
|
|
char *result = php_cmd(command, 0);
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
assert(close(responses[1]) == 0);
|
|
int status;
|
|
assert(waitpid(server, &status, 0) == server);
|
|
/* The script poller logs the same commands; drive its empty-result and
|
|
* missing-file lines with the secret in the arguments. */
|
|
assert(spine_permits_init(&available_scripts, 1) == 0);
|
|
host_t host = {0};
|
|
STRNCOPY(host.hostname, "regression-device");
|
|
char script_command[BUFSIZE];
|
|
spine_snprintf(script_command, sizeof(script_command), "/usr/bin/printf '' %s", secret);
|
|
result = exec_poll(&host, script_command, 1, "DS");
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
spine_snprintf(script_command, sizeof(script_command), "/spine-regression/missing-script %s", secret);
|
|
result = exec_poll(&host, script_command, 1, "DS");
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
assert(spine_permits_destroy(&available_scripts) == 0);
|
|
FILE *file = fopen(path, "r");
|
|
assert(file != NULL);
|
|
char logged[16384];
|
|
size_t bytes = fread(logged, 1, sizeof(logged) - 1, file);
|
|
assert(!ferror(file) && fclose(file) == 0);
|
|
logged[bytes] = '\0';
|
|
assert(strstr(logged, "did not respond in time") != NULL);
|
|
assert(strstr(logged, "partial response") != NULL);
|
|
assert(strstr(logged, "communications lost") != NULL);
|
|
assert(strstr(logged, "Empty result [regression-device]: '/usr/bin/printf'") != NULL);
|
|
assert(strstr(logged, "File '/spine-regression/missing-script' does not exist") != NULL);
|
|
fprintf(stderr, "php failure logs at level %d contain command arguments: %s\n", level, strstr(logged, secret) != NULL ? "yes" : "no");
|
|
assert(strstr(logged, secret) == NULL);
|
|
/* Debug output still names the script, which is enough to find it. */
|
|
if (level == POLLER_VERBOSITY_DEBUG) {
|
|
assert(strstr(logged, "ss_regression.php") != NULL);
|
|
assert(strstr(logged, "The executable is '/usr/bin/printf'") != NULL);
|
|
}
|
|
assert(unlink(path) == 0);
|
|
php_processes = previous_processes;
|
|
free(process);
|
|
set = previous_config;
|
|
}
|
|
|
|
static void test_php_failure_logs_omit_command(void) {
|
|
run_php_failure_logs(POLLER_VERBOSITY_LOW);
|
|
run_php_failure_logs(POLLER_VERBOSITY_DEBUG);
|
|
}
|
|
|
|
static void test_script_execution(void) {
|
|
assert(spine_permits_init(&available_scripts, 1) == 0);
|
|
host_t host = {0};
|
|
STRNCOPY(host.hostname, "regression-device");
|
|
int previous_timeout = set.php.script_timeout;
|
|
set.php.script_timeout = 1;
|
|
char command[] = "/usr/bin/printf 7";
|
|
char *result = exec_poll(&host, command, 1, "DS");
|
|
assert(strcmp(result, "7") == 0);
|
|
free(result);
|
|
char delayed[] = "/bin/sleep 2";
|
|
double begin = spine_monotonic_time();
|
|
result = exec_poll(&host, delayed, 1, "DS");
|
|
assert(strcmp(result, "U") == 0);
|
|
assert(spine_monotonic_time() - begin < 5);
|
|
free(result);
|
|
assert(spine_permits_available(&available_scripts) == 1);
|
|
for (int timeout = -1; timeout <= 0; timeout++) {
|
|
set.php.script_timeout = timeout;
|
|
result = exec_poll(&host, command, 1, "DS");
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
assert(spine_permits_available(&available_scripts) == 1);
|
|
}
|
|
set.php.script_timeout = 1;
|
|
assert(spine_permits_try_acquire(&available_scripts) == 0);
|
|
begin = spine_monotonic_time();
|
|
result = exec_poll(&host, command, 1, "DS");
|
|
assert(strcmp(result, "U") == 0);
|
|
assert(spine_monotonic_time() - begin < 1);
|
|
free(result);
|
|
assert(spine_permits_available(&available_scripts) == 0);
|
|
assert(spine_permits_release(&available_scripts) == 0);
|
|
char output_command[128];
|
|
for (int excess = 0; excess <= 1; excess++) {
|
|
spine_snprintf(output_command, sizeof(output_command), "/usr/bin/printf '%%%ds' x", RESULTS_BUFFER - 1 + excess);
|
|
result = exec_poll(&host, output_command, 1, "DS");
|
|
if (excess) assert(strcmp(result, "U") == 0);
|
|
else {
|
|
assert(strlen(result) == RESULTS_BUFFER - 1);
|
|
assert(result[RESULTS_BUFFER - 1] == '\0');
|
|
}
|
|
free(result);
|
|
assert(spine_permits_available(&available_scripts) == 1);
|
|
}
|
|
char embedded_nul[] = "/usr/bin/printf '42\\000junk'";
|
|
result = exec_poll(&host, embedded_nul, 1, "DS");
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
char empty[] = "/usr/bin/printf ''";
|
|
result = exec_poll(&host, empty, 1, "DQ");
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
char missing[] = "/spine-regression/nonexistent-executable";
|
|
result = exec_poll(&host, missing, 1, "DS");
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
assert(spine_permits_available(&available_scripts) == 1);
|
|
assert(spine_permits_destroy(&available_scripts) == 0);
|
|
set.php.script_timeout = previous_timeout;
|
|
}
|
|
|
|
/* A timed-out script is killed with everything it started. The background
|
|
* sleep inherits the write end of the witness pipe, so EOF on the read end
|
|
* proves that no descendant outlived the timeout. */
|
|
static void test_script_timeout_kills_descendants(void) {
|
|
assert(spine_permits_init(&available_scripts, 1) == 0);
|
|
host_t host = {0};
|
|
STRNCOPY(host.hostname, "regression-device");
|
|
int previous_timeout = set.php.script_timeout;
|
|
set.php.script_timeout = 1;
|
|
int witness[2];
|
|
assert(pipe(witness) == 0);
|
|
char command[] = "/bin/sh -c '/bin/sleep 30 & /bin/sleep 30'";
|
|
char *result = exec_poll(&host, command, 1, "DS");
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
assert(close(witness[1]) == 0);
|
|
assert(witness[0] < FD_SETSIZE);
|
|
fd_set readable;
|
|
FD_ZERO(&readable);
|
|
FD_SET(witness[0], &readable);
|
|
struct timeval timeout = {3, 0};
|
|
int ready = select(witness[0] + 1, &readable, NULL, NULL, &timeout);
|
|
char byte;
|
|
fprintf(stderr, "script timeout descendants: ready=%d\n", ready);
|
|
assert(ready == 1 && read(witness[0], &byte, 1) == 0);
|
|
assert(close(witness[0]) == 0);
|
|
assert(spine_permits_available(&available_scripts) == 1);
|
|
assert(spine_permits_destroy(&available_scripts) == 0);
|
|
set.php.script_timeout = previous_timeout;
|
|
}
|
|
|
|
/* nft_pclose() kills a script that is still running after its output was
|
|
* read, and the kill must reach the script's descendants as well. */
|
|
static void test_pclose_kills_descendants(void) {
|
|
int witness[2];
|
|
assert(pipe(witness) == 0);
|
|
int fd = nft_popen("printf 7; /bin/sleep 30 & /bin/sleep 30", "r");
|
|
assert(fd >= 0);
|
|
char byte;
|
|
assert(read(fd, &byte, 1) == 1 && byte == '7');
|
|
assert(nft_pclose(fd) == -1 && errno == ETIMEDOUT);
|
|
assert(close(witness[1]) == 0);
|
|
assert(witness[0] < FD_SETSIZE);
|
|
fd_set readable;
|
|
FD_ZERO(&readable);
|
|
FD_SET(witness[0], &readable);
|
|
struct timeval timeout = {3, 0};
|
|
int ready = select(witness[0] + 1, &readable, NULL, NULL, &timeout);
|
|
fprintf(stderr, "pclose descendants: ready=%d\n", ready);
|
|
assert(ready == 1 && read(witness[0], &byte, 1) == 0);
|
|
assert(close(witness[0]) == 0);
|
|
}
|
|
|
|
static void test_php_command(size_t length) {
|
|
char command[BUFSIZE];
|
|
assert(length <= sizeof(command) - 3);
|
|
memset(command, 'c', length);
|
|
command[length] = '\0';
|
|
int requests[2];
|
|
int responses[2];
|
|
assert(pipe(requests) == 0 && pipe(responses) == 0);
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
close(requests[1]);
|
|
close(responses[0]);
|
|
char received[BUFSIZE] = {0};
|
|
size_t used = 0;
|
|
while (used < length + 2) {
|
|
ssize_t bytes = read(requests[0], received + used, length + 2 - used);
|
|
if (bytes < 0 && errno == EINTR) continue;
|
|
assert(bytes > 0);
|
|
used += (size_t)bytes;
|
|
}
|
|
assert(memcmp(received, command, length) == 0);
|
|
assert(received[length] == '\r' && received[length + 1] == '\n');
|
|
assert(write(responses[1], "7\n", 2) == 2);
|
|
close(requests[0]);
|
|
close(responses[1]);
|
|
_exit(0);
|
|
}
|
|
close(requests[0]);
|
|
close(responses[1]);
|
|
php_t process = {0};
|
|
/* php_cmd() only talks to a READY slot that owns a live child. */
|
|
process.php_state = PHP_READY;
|
|
process.php_pid = child;
|
|
process.php_write_fd = requests[1];
|
|
process.php_read_fd = responses[0];
|
|
php_t *previous = php_processes;
|
|
php_processes = &process;
|
|
set.php.php_servers = 1;
|
|
set.php.script_timeout = 5;
|
|
char *result = php_cmd(command, 0);
|
|
assert(strcmp(result, "7\n") == 0);
|
|
free(result);
|
|
php_processes = previous;
|
|
close(requests[1]);
|
|
close(responses[0]);
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
}
|
|
|
|
static void test_invalid_php_commands(void) {
|
|
/* Invalid requests must return before touching server state or writing. */
|
|
char *result = php_cmd("invalid\r\nline", 0);
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
char oversized[BUFSIZE];
|
|
memset(oversized, 'c', sizeof(oversized) - 1);
|
|
oversized[sizeof(oversized) - 1] = '\0';
|
|
result = php_cmd(oversized, 0);
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
result = php_cmd("valid", -1);
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
}
|
|
|
|
static int run_test_script_server(int argc, char **argv) {
|
|
assert(argc >= 5);
|
|
if (strncmp(argv[2], "regression-server:", 18) == 0) {
|
|
char *end;
|
|
errno = 0;
|
|
long descriptor = strtol(argv[2] + 18, &end, 10);
|
|
assert(errno == 0 && *end == 0 && descriptor >= 3 && descriptor <= INT_MAX);
|
|
char token;
|
|
ssize_t received = read((int)descriptor, &token, 1);
|
|
assert(close((int)descriptor) == 0);
|
|
if (received == 0) return 127;
|
|
assert(received == 1 && token == 82);
|
|
} else assert(strcmp(argv[2], "regression-server") == 0);
|
|
if (strcmp(argv[3], "spine") == 0) {
|
|
assert(argc == 5 && strcmp(argv[4], "1") == 0);
|
|
} else {
|
|
assert(strcmp(argv[3], "--environ=spine") == 0);
|
|
assert(strcmp(argv[4], "--poller=1") == 0 || strcmp(argv[4], "--poller=2") == 0);
|
|
if (strcmp(argv[4], "--poller=2") == 0) {
|
|
assert(argc == 6 && (strcmp(argv[5], "--mode=online") == 0 || strcmp(argv[5], "--mode=offline") == 0));
|
|
} else assert(argc == 5);
|
|
}
|
|
puts("Started");
|
|
fflush(stdout);
|
|
char command[BUFSIZE];
|
|
while (fgets(command, sizeof(command), stdin) != NULL) {
|
|
if (strcmp(command, "quit\r\n") == 0) return 0;
|
|
puts("7");
|
|
fflush(stdout);
|
|
}
|
|
return 17; /* EOF without quit must not count as graceful protocol shutdown. */
|
|
}
|
|
|
|
static void assert_php_responses(const php_t *processes, int count) {
|
|
for (int index = 0; index < count; index++) {
|
|
assert(processes[index].php_state == PHP_READY);
|
|
char *result = php_cmd("regression request", index);
|
|
assert(strcmp(result, "7\n") == 0);
|
|
free(result);
|
|
}
|
|
}
|
|
|
|
static void assert_php_children_reaped(const php_t *processes, const pid_t *children, size_t count) {
|
|
for (size_t index = 0; index < count; index++) {
|
|
int status;
|
|
assert(waitpid(children[index], &status, WNOHANG) == -1 && errno == ECHILD);
|
|
assert(processes[index].php_pid == -1);
|
|
}
|
|
}
|
|
|
|
static void test_php_startup(const char *executable) {
|
|
char *absolute = realpath(executable, NULL);
|
|
assert(absolute != NULL);
|
|
config_t previous_config = set;
|
|
php_t *previous_processes = php_processes;
|
|
php_t processes[2];
|
|
memset(processes, 0, sizeof(processes));
|
|
php_processes = processes;
|
|
STRNCOPY(set.php.path_php, absolute);
|
|
free(absolute);
|
|
STRNCOPY(set.php.path_php_server, "regression-server");
|
|
set.php.script_timeout = 5;
|
|
set.php.php_servers = 2;
|
|
const int versions[] = {1222, 1223, 1223, 1223};
|
|
const int pollers[] = {1, 1, 2, 2};
|
|
const int modes[] = {REMOTE_ONLINE, REMOTE_ONLINE, REMOTE_ONLINE, REMOTE_OFFLINE};
|
|
for (size_t i = 0; i < sizeof(versions) / sizeof(versions[0]); i++) {
|
|
set.cacti_version = versions[i];
|
|
set.poller.poller_id = pollers[i];
|
|
set.poller.mode = modes[i];
|
|
assert(php_init(PHP_INIT));
|
|
assert_php_responses(processes, set.php.php_servers);
|
|
pid_t children[] = {processes[0].php_pid, processes[1].php_pid};
|
|
php_close(PHP_INIT);
|
|
assert_php_children_reaped(processes, children, sizeof(children) / sizeof(children[0]));
|
|
}
|
|
/* An inherited one-byte pipe admits the first child and rejects the second. */
|
|
int admissions[2];
|
|
assert(pipe(admissions) == 0);
|
|
assert(write(admissions[1], "R", 1) == 1 && close(admissions[1]) == 0);
|
|
spine_snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "regression-server:%d", admissions[0]);
|
|
assert(php_init(PHP_INIT));
|
|
/* A server that never answered stays BUSY for the restart path; shutdown
|
|
* still reaps both children. */
|
|
assert(processes[0].php_state == PHP_READY && processes[1].php_state == PHP_BUSY);
|
|
php_close(PHP_INIT);
|
|
assert(processes[0].php_pid == -1 && processes[1].php_pid == -1);
|
|
assert(close(admissions[0]) == 0);
|
|
STRNCOPY(set.php.path_php_server, "regression-server");
|
|
assert(!php_init(-2) && !php_init(set.php.php_servers));
|
|
STRNCOPY(set.php.path_php, "/nonexistent-spine-regression-executable");
|
|
assert(!php_init(0));
|
|
assert(processes[0].php_state == PHP_BUSY);
|
|
assert(processes[0].php_read_fd == -1 && processes[0].php_write_fd == -1);
|
|
/* posix_spawn reports a missing executable to the parent (or the child
|
|
* exits 127 where the libc defers exec errors), so no server is left. */
|
|
assert(processes[0].php_pid == -1);
|
|
char *failed_command = php_cmd("request on stopped server", 0);
|
|
assert(strcmp(failed_command, "U") == 0);
|
|
free(failed_command);
|
|
assert(processes[0].php_pid == -1 && processes[0].php_state == PHP_BUSY);
|
|
php_processes = previous_processes;
|
|
set = previous_config;
|
|
}
|
|
|
|
static void test_php_owned_shutdown(void) {
|
|
int requests[2];
|
|
int responses[2];
|
|
assert(pipe(requests) == 0 && pipe(responses) == 0);
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
close(requests[1]);
|
|
close(responses[0]);
|
|
assert(signal(SIGTERM, SIG_IGN) != SIG_ERR);
|
|
assert(write(responses[1], "R", 1) == 1);
|
|
for (;;) pause();
|
|
}
|
|
close(requests[0]);
|
|
close(responses[1]);
|
|
char ready;
|
|
assert(read(responses[0], &ready, 1) == 1 && ready == 'R');
|
|
int flags = fcntl(requests[1], F_GETFL);
|
|
assert(flags >= 0 && fcntl(requests[1], F_SETFL, flags | O_NONBLOCK) == 0);
|
|
char padding[1024] = {0};
|
|
while (write(requests[1], padding, sizeof(padding)) > 0) { /* Fill the pipe until the nonblocking write refuses more data. */ }
|
|
assert(errno == EAGAIN || errno == EWOULDBLOCK);
|
|
assert(fcntl(requests[1], F_SETFL, flags) == 0);
|
|
php_t server = {0};
|
|
server.php_pid = child;
|
|
server.php_write_fd = requests[1];
|
|
server.php_read_fd = responses[0];
|
|
php_t *previous_processes = php_processes;
|
|
int previous_count = set.php.php_servers;
|
|
php_processes = &server;
|
|
set.php.php_servers = 1;
|
|
double begin = spine_monotonic_time();
|
|
alarm(5);
|
|
php_close(0);
|
|
alarm(0);
|
|
assert(spine_monotonic_time() - begin < 4.0);
|
|
assert(server.php_pid == -1 && server.php_read_fd == -1 && server.php_write_fd == -1);
|
|
assert(fcntl(requests[1], F_GETFD) == -1 && errno == EBADF);
|
|
assert(fcntl(responses[0], F_GETFD) == -1 && errno == EBADF);
|
|
/* php_close() sends SIGKILL and parks a child that has not exited yet
|
|
* instead of blocking the poller, so it is reaped here or by the sweep. */
|
|
int status;
|
|
pid_t reaped = waitpid(child, &status, 0);
|
|
assert((reaped == child && WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL) ||
|
|
(reaped == -1 && errno == ECHILD));
|
|
php_close(0);
|
|
/* A stale PID for a non-child must be retired without sending a signal. */
|
|
server.php_pid = getpid();
|
|
php_close(0);
|
|
assert(server.php_pid == -1);
|
|
php_processes = previous_processes;
|
|
set.php.php_servers = previous_count;
|
|
}
|
|
|
|
static void test_udp_deadline(void) {
|
|
struct sockaddr_in address = {0};
|
|
assert(init_sockaddr(&address, "127.0.0.1", 0));
|
|
assert(address.sin_family == AF_INET && address.sin_addr.s_addr == htonl(INADDR_LOOPBACK));
|
|
assert(!init_sockaddr(&address, "127.0.0.1", -1));
|
|
assert(!init_sockaddr(&address, "127.0.0.1", 65536));
|
|
int server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
|
assert(server >= 0);
|
|
assert(bind(server, (struct sockaddr *)&address, sizeof(address)) == 0);
|
|
socklen_t length = sizeof(address);
|
|
assert(getsockname(server, (struct sockaddr *)&address, &length) == 0);
|
|
host_t host = {0};
|
|
STRNCOPY(host.hostname, "127.0.0.1");
|
|
host.availability.port = ntohs(address.sin_port);
|
|
host.availability.timeout = 650;
|
|
host.availability.retries = 0;
|
|
ping_t ping = {0};
|
|
double begin = spine_monotonic_time();
|
|
assert(ping_udp(&host, &ping) == HOST_DOWN);
|
|
double elapsed = spine_monotonic_time() - begin;
|
|
assert(elapsed >= 0.60 && elapsed < 1.25);
|
|
assert(strcmp(ping.ping_response, "UDP: Ping timed out") == 0);
|
|
char request[64];
|
|
ssize_t received = recv(server, request, sizeof(request), 0);
|
|
static const char expected[] = "cacti-monitoring-system";
|
|
assert(received == (ssize_t)(sizeof(expected) - 1));
|
|
assert(memcmp(request, expected, sizeof(expected) - 1) == 0);
|
|
assert(close(server) == 0);
|
|
host.availability.timeout = 0;
|
|
assert(ping_udp(&host, &ping) == HOST_DOWN);
|
|
}
|
|
|
|
static bool icmp_reply_matches(const unsigned char *reply, size_t length, uint16_t id, uint16_t sequence) {
|
|
return spine_icmp_classify_reply(reply, (ssize_t) length, id, sequence, NULL) == SPINE_ICMP_REPLY_OK;
|
|
}
|
|
|
|
static void test_icmp_reply_bounds(void) {
|
|
unsigned char storage[29] = {0};
|
|
unsigned char *reply = storage + 1;
|
|
reply[0] = 0x45;
|
|
reply[9] = IPPROTO_ICMP;
|
|
uint16_t id = htons(123);
|
|
uint16_t sequence = htons(456);
|
|
memcpy(reply + 24, &id, sizeof(id));
|
|
memcpy(reply + 26, &sequence, sizeof(sequence));
|
|
assert(icmp_reply_matches(reply, 28, id, sequence));
|
|
assert(!icmp_reply_matches(NULL, 28, id, sequence));
|
|
for (size_t length = 0; length < 28; length++) assert(!icmp_reply_matches(reply, length, id, sequence));
|
|
assert(!icmp_reply_matches(reply, 28, htons(124), sequence));
|
|
assert(!icmp_reply_matches(reply, 28, id, htons(457)));
|
|
reply[0] = 0x44;
|
|
assert(!icmp_reply_matches(reply, 28, id, sequence));
|
|
reply[0] = 0x4f;
|
|
assert(!icmp_reply_matches(reply, 28, id, sequence));
|
|
reply[0] = 0x65;
|
|
assert(!icmp_reply_matches(reply, 28, id, sequence));
|
|
reply[0] = 0x45;
|
|
reply[20] = ICMP_ECHO;
|
|
assert(!icmp_reply_matches(reply, 28, id, sequence));
|
|
reply[20] = ICMP_ECHOREPLY;
|
|
reply[21] = 1;
|
|
assert(!icmp_reply_matches(reply, 28, id, sequence));
|
|
reply[21] = 0;
|
|
reply[9] = IPPROTO_TCP;
|
|
assert(!icmp_reply_matches(reply, 28, id, sequence));
|
|
}
|
|
|
|
static void test_icmp_loopback(void) {
|
|
host_t host = {0};
|
|
STRNCOPY(host.hostname, "127.0.0.1");
|
|
host.availability.timeout = 500;
|
|
host.availability.retries = 0;
|
|
ping_t ping = {0};
|
|
assert(ping_icmp(&host, &ping) == HOST_UP);
|
|
assert(strcmp(ping.ping_response, "ICMP: Device is Alive") == 0);
|
|
assert(geteuid() == getuid());
|
|
}
|
|
|
|
static void test_icmp_socket_failure(void) {
|
|
assert(geteuid() != 0);
|
|
host_t host = {0};
|
|
STRNCOPY(host.hostname, "127.0.0.1");
|
|
host.availability.timeout = 100;
|
|
ping_t ping = {0};
|
|
/* Where net.ipv4.ping_group_range admits this user, ICMP works through an
|
|
* unprivileged datagram socket and needs no capability at all. */
|
|
int datagram = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
|
|
if (datagram >= 0) {
|
|
close(datagram);
|
|
host.availability.timeout = 500;
|
|
assert(ping_icmp(&host, &ping) == HOST_UP);
|
|
assert(strcmp(ping.ping_response, "ICMP: Device is Alive") == 0);
|
|
assert(geteuid() == getuid());
|
|
return;
|
|
}
|
|
/* Failed socket retries must give up without regaining root. */
|
|
alarm(6);
|
|
assert(ping_icmp(&host, &ping) == HOST_DOWN);
|
|
alarm(0);
|
|
assert(strcmp(ping.ping_response, "ICMP: Ping unable to create ICMP Socket") == 0);
|
|
assert(geteuid() == getuid());
|
|
}
|
|
|
|
static unsigned int query_sample_count(MYSQL *mysql, const char *query) {
|
|
MYSQL_RES *result = db_query(mysql, LOCAL, query);
|
|
assert(result != NULL && mysql_num_fields(result) == 2);
|
|
unsigned int samples = 0;
|
|
MYSQL_ROW row;
|
|
while ((row = mysql_fetch_row(result))) samples += (unsigned int)atoi(row[1]);
|
|
db_free_result(result);
|
|
return samples;
|
|
}
|
|
|
|
static void test_poller_query_case(MYSQL *mysql, int poller, int active, int ports) {
|
|
static const unsigned int all_rows[] = {3, 2, 1};
|
|
static const unsigned int due_rows[] = {2, 1, 1};
|
|
set.poller.poller_id = poller;
|
|
set.poller.active_profiles = active;
|
|
set.snmp.total_snmp_ports = ports;
|
|
poller_queries_t queries;
|
|
poller_prepare_queries(&queries, 42, 1, 0);
|
|
MYSQL_RES *result = db_query(mysql, LOCAL, queries.items);
|
|
assert(result != NULL && mysql_num_fields(result) == 20);
|
|
assert(mysql_num_rows(result) == all_rows[poller]);
|
|
db_free_result(result);
|
|
result = db_query(mysql, LOCAL, queries.due_items);
|
|
unsigned int expected_due = active ? all_rows[poller] : due_rows[poller];
|
|
assert(result != NULL && mysql_num_rows(result) == expected_due);
|
|
db_free_result(result);
|
|
assert(query_sample_count(mysql, queries.agents) == all_rows[poller]);
|
|
assert(query_sample_count(mysql, queries.due_agents) == expected_due);
|
|
}
|
|
|
|
static void test_poller_schedule(MYSQL *mysql) {
|
|
poller_queries_t queries;
|
|
set.poller.poller_interval = 60;
|
|
set.poller.poller_id = 1;
|
|
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET rrd_step=300,rrd_next_step=180 WHERE host_id=42") == TRUE);
|
|
poller_prepare_queries(&queries, 42, 1, 0);
|
|
assert(db_insert(mysql, LOCAL, queries.schedule) == TRUE);
|
|
MYSQL_RES *result = db_query(mysql, LOCAL, "SELECT local_data_id,rrd_next_step FROM poller_item WHERE host_id=42 ORDER BY local_data_id");
|
|
assert(result != NULL && mysql_num_rows(result) == 3);
|
|
MYSQL_ROW row = mysql_fetch_row(result);
|
|
assert(row != NULL && strcmp(row[0], "101") == 0 && strcmp(row[1], "120") == 0);
|
|
row = mysql_fetch_row(result);
|
|
assert(row != NULL && strcmp(row[0], "102") == 0 && strcmp(row[1], "120") == 0);
|
|
row = mysql_fetch_row(result);
|
|
assert(row != NULL && strcmp(row[0], "103") == 0 && strcmp(row[1], "180") == 0);
|
|
db_free_result(result);
|
|
set.poller.poller_id = 0;
|
|
poller_prepare_queries(&queries, 42, 1, 0);
|
|
assert(db_insert(mysql, LOCAL, queries.schedule) == TRUE);
|
|
result = db_query(mysql, LOCAL, "SELECT rrd_next_step FROM poller_item WHERE local_data_id=103");
|
|
assert(result != NULL && mysql_num_rows(result) == 1);
|
|
row = mysql_fetch_row(result);
|
|
assert(row != NULL && strcmp(row[0], "120") == 0);
|
|
db_free_result(result);
|
|
}
|
|
|
|
static void test_poller_queries(MYSQL *mysql) {
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item") == TRUE);
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM host") == TRUE);
|
|
assert(db_insert(mysql, LOCAL, "INSERT INTO host (id,hostname) VALUES (42,'127.0.0.1')") == TRUE);
|
|
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_item (local_data_id,host_id,poller_id,rrd_next_step,snmp_port) VALUES (101,42,1,0,161),(102,42,1,1,162),(103,42,2,0,161),(104,99,1,0,161),(105,0,1,0,161)") == TRUE);
|
|
poller_queries_t queries;
|
|
for (int poller = 0; poller <= 2; poller++) {
|
|
for (int active = 0; active <= 1; active++) {
|
|
for (int ports = 1; ports <= 2; ports++) {
|
|
test_poller_query_case(mysql, poller, active, ports);
|
|
}
|
|
}
|
|
}
|
|
set.poller.poller_id = 1;
|
|
poller_prepare_queries(&queries, 42, 1, 1);
|
|
MYSQL_RES *result = db_query(mysql, LOCAL, queries.items);
|
|
assert(result != NULL && mysql_num_rows(result) == 1);
|
|
db_free_result(result);
|
|
poller_prepare_queries(&queries, 42, 3, 1);
|
|
result = db_query(mysql, LOCAL, queries.items);
|
|
assert(result != NULL && mysql_num_rows(result) == 0);
|
|
db_free_result(result);
|
|
poller_prepare_queries(&queries, 42, INT_MAX, INT_MAX);
|
|
result = db_query(mysql, LOCAL, queries.items);
|
|
assert(result != NULL && mysql_num_rows(result) == 0);
|
|
db_free_result(result);
|
|
poller_prepare_queries(&queries, 42, 1, 0);
|
|
result = db_query(mysql, LOCAL, queries.host);
|
|
assert(result != NULL && mysql_num_fields(result) == 37 && mysql_num_rows(result) == 1);
|
|
db_free_result(result);
|
|
assert(db_insert(mysql, LOCAL, "UPDATE host SET deleted='on' WHERE id=42") == TRUE);
|
|
result = db_query(mysql, LOCAL, queries.host);
|
|
assert(result != NULL && mysql_num_rows(result) == 0);
|
|
db_free_result(result);
|
|
result = db_query(mysql, LOCAL, queries.reindex);
|
|
assert(result != NULL && mysql_num_fields(result) == 5 && mysql_num_rows(result) == 0);
|
|
db_free_result(result);
|
|
poller_prepare_queries(&queries, 0, 1, 0);
|
|
result = db_query(mysql, LOCAL, queries.items);
|
|
assert(result != NULL && mysql_num_rows(result) == 1);
|
|
db_free_result(result);
|
|
test_poller_schedule(mysql);
|
|
}
|
|
|
|
static unsigned long database_count(MYSQL *mysql, const char *query) {
|
|
MYSQL_RES *result = db_query(mysql, LOCAL, query);
|
|
assert(result != NULL && mysql_num_rows(result) == 1 && mysql_num_fields(result) == 1);
|
|
MYSQL_ROW row = mysql_fetch_row(result);
|
|
assert(row != NULL && row[0] != NULL);
|
|
unsigned long value = strtoul(row[0], NULL, 10);
|
|
db_free_result(result);
|
|
return value;
|
|
}
|
|
|
|
static void seed_transfer_rows(MYSQL *source) {
|
|
assert(db_insert(source, LOCAL, "DELETE FROM host"));
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_item"));
|
|
for (int index = 1; index <= 502; index++) {
|
|
char query[BUFSIZE];
|
|
spine_snprintf(query, sizeof(query), "INSERT INTO host (id,poller_id,snmp_sysDescr,status_last_error,last_updated,min_time) VALUES (%d,%d,'quote\\\' and slash\\\\',NULL,NULL,NULL)", index, index == 502 ? 3 : 2);
|
|
assert(db_insert(source, LOCAL, query));
|
|
}
|
|
assert(db_insert(source, LOCAL, "UPDATE host SET snmp_sysDescr=REPEAT(CONVERT(0xF09F8CB5 USING utf8mb4),300) WHERE id=501"));
|
|
for (int begin = 1; begin <= 10002; begin += 100) {
|
|
char query[BUFSIZE * 8];
|
|
size_t used = (size_t)spine_snprintf(query, sizeof(query), "INSERT INTO poller_item (local_data_id,host_id,poller_id,rrd_name,rrd_step,rrd_next_step) VALUES ");
|
|
int end = begin + 100;
|
|
if (end > 10003) end = 10003;
|
|
for (int index = begin; index < end; index++) {
|
|
used += (size_t)spine_snprintf(query + used, sizeof(query) - used, "%s(%d,%d,%d,'r\\\'r',300,120)", index == begin ? "" : ",", index, index == 10002 ? 502 : 501, index == 10002 ? 3 : 2);
|
|
}
|
|
assert(db_insert(source, LOCAL, query));
|
|
}
|
|
}
|
|
|
|
static void test_transfer_boundary(MYSQL *source, MYSQL *destination) {
|
|
assert(poller_transfer_status(source, destination));
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM host") == 501);
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM poller_item") == 10001);
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM host WHERE status_last_error IS NULL AND min_time IS NULL") == 501);
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM host d JOIN spine_regressions.host s ON s.id=d.id WHERE NOT (d.last_updated <=> s.last_updated)") == 0);
|
|
assert(database_count(destination, "SELECT CHAR_LENGTH(snmp_sysDescr) FROM host WHERE id=501") == 300);
|
|
assert(database_count(destination, "SELECT OCTET_LENGTH(snmp_sysDescr) FROM host WHERE id=501") == 1200);
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM host WHERE snmp_sysDescr=CONCAT('quote',CHAR(39),' and slash',CHAR(92))") == 500);
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM poller_item WHERE rrd_name='r\\\'r' AND rrd_next_step=120") == 10001);
|
|
/* Upsert retries must update only the existing documented columns. */
|
|
assert(db_insert(destination, REMOTE, "UPDATE poller_item SET rrd_step=900,rrd_next_step=999"));
|
|
assert(poller_transfer_status(source, destination));
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM poller_item WHERE rrd_step=900 AND rrd_next_step=120") == 10001);
|
|
}
|
|
|
|
static void test_transfer_failure_and_filter(MYSQL *source, MYSQL *destination) {
|
|
assert(db_insert(destination, REMOTE, "DELETE FROM host"));
|
|
assert(db_insert(destination, REMOTE, "DELETE FROM poller_item"));
|
|
assert(db_insert(destination, REMOTE, "CREATE TRIGGER reject_transfer BEFORE INSERT ON host FOR EACH ROW BEGIN IF NEW.id=501 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='regression late-batch failure'; END IF; END"));
|
|
assert(!poller_transfer_status(source, destination));
|
|
set.exit.exit_code = EXIT_SUCCESS;
|
|
poller_push_data_to_main();
|
|
assert(set.exit.exit_code == EXIT_FAILURE);
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM host") == 500);
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM poller_item") == 0);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM host") == 502);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM poller_item") == 10002);
|
|
assert(db_insert(destination, REMOTE, "DROP TRIGGER reject_transfer"));
|
|
set.exit.exit_code = EXIT_SUCCESS;
|
|
poller_push_data_to_main();
|
|
assert(set.exit.exit_code == EXIT_SUCCESS);
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM host") == 501);
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM poller_item") == 10001);
|
|
assert(db_insert(destination, REMOTE, "DELETE FROM poller_item"));
|
|
assert(db_insert(destination, REMOTE, "CREATE TRIGGER reject_item_transfer BEFORE INSERT ON poller_item FOR EACH ROW BEGIN IF NEW.local_data_id=10001 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='regression final-item failure'; END IF; END"));
|
|
assert(!poller_transfer_status(source, destination));
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM poller_item") == 10000);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM poller_item") == 10002);
|
|
assert(db_insert(destination, REMOTE, "DROP TRIGGER reject_item_transfer"));
|
|
assert(poller_transfer_status(source, destination));
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM poller_item") == 10001);
|
|
/* Full-width Unicode across all text fields must flush by bytes before 500 rows. */
|
|
assert(db_insert(source, LOCAL, "UPDATE host SET snmp_sysDescr=REPEAT(CONVERT(0xF09F8CB5 USING utf8mb4),300),snmp_sysContact=snmp_sysDescr,snmp_sysName=snmp_sysDescr,snmp_sysLocation=snmp_sysDescr WHERE poller_id=2"));
|
|
assert(db_insert(destination, REMOTE, "DELETE FROM host"));
|
|
assert(poller_transfer_status(source, destination));
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM host WHERE OCTET_LENGTH(snmp_sysDescr)=1200 AND OCTET_LENGTH(snmp_sysLocation)=1200") == 501);
|
|
assert(db_insert(destination, REMOTE, "DELETE FROM host"));
|
|
assert(db_insert(destination, REMOTE, "DELETE FROM poller_item"));
|
|
STRNCOPY(set.hosts.host_id_list, "1");
|
|
assert(poller_transfer_status(source, destination));
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM host") == 1);
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM poller_item") == 0);
|
|
set.hosts.host_id_list[0] = '\0';
|
|
assert(db_insert(source, LOCAL, "DELETE FROM host"));
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_item"));
|
|
assert(poller_transfer_status(source, destination));
|
|
assert(database_count(destination, "SELECT COUNT(*) FROM host") == 1);
|
|
}
|
|
|
|
static void test_collector_transfer(MYSQL *source) {
|
|
config_t previous = set;
|
|
set.poller.poller_id = 2;
|
|
set.database.onupdate = 0;
|
|
set.hosts.host_id_list[0] = '\0';
|
|
char database[80];
|
|
char query[BUFSIZE];
|
|
spine_snprintf(database, sizeof(database), "spine_transfer_%ld", (long)getpid());
|
|
STRNCOPY(set.remote_database.host, set.database.host);
|
|
STRNCOPY(set.remote_database.user, set.database.user);
|
|
STRNCOPY(set.remote_database.password, set.database.password);
|
|
STRNCOPY(set.remote_database.database, database);
|
|
set.remote_database.port = set.database.port;
|
|
set.remote_database.ssl = FALSE;
|
|
spine_snprintf(query, sizeof(query), "CREATE DATABASE %s CHARACTER SET utf8mb4", database);
|
|
assert(db_insert(source, LOCAL, query));
|
|
MYSQL destination;
|
|
db_connect(LOCAL, &destination);
|
|
assert(mysql_select_db(&destination, database) == 0);
|
|
assert(mysql_set_character_set(source, "utf8mb4") == 0 && mysql_set_character_set(&destination, "utf8mb4") == 0);
|
|
assert(db_insert(&destination, REMOTE, "CREATE TABLE host LIKE spine_regressions.host"));
|
|
assert(db_insert(&destination, REMOTE, "CREATE TABLE poller_item LIKE spine_regressions.poller_item"));
|
|
seed_transfer_rows(source);
|
|
test_transfer_boundary(source, &destination);
|
|
test_transfer_failure_and_filter(source, &destination);
|
|
db_disconnect(&destination);
|
|
spine_snprintf(query, sizeof(query), "DROP DATABASE %s", database);
|
|
assert(db_insert(source, LOCAL, query));
|
|
set = previous;
|
|
}
|
|
|
|
static void test_database_version(MYSQL *mysql) {
|
|
const struct {
|
|
const char *text;
|
|
int expected;
|
|
} cases[] = {
|
|
{"1.2.32", 1232}, {"1.3.0_develop", 1300}, {"1.2.33-beta1", 1233},
|
|
{"2147483.6.47", INT_MAX}, {"2147483.6.48", 0}, {"2147483647.0.0", 0},
|
|
{"99999999999999999999", 0}, {"new_install", 0}, {"", 0},
|
|
{"1.2", 0}, {"1.x.3", 0}, {"1.2.-1", 0}, {"-1.2.3", 0}
|
|
};
|
|
for (size_t index = 0; index < sizeof(cases) / sizeof(cases[0]); index++) {
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM version"));
|
|
char query[BUFSIZE];
|
|
spine_snprintf(query, sizeof(query), "INSERT INTO version(cacti) VALUES ('%s')", cases[index].text);
|
|
assert(db_insert(mysql, LOCAL, query));
|
|
assert(get_cacti_version(mysql, LOCAL) == cases[index].expected);
|
|
}
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM version"));
|
|
assert(get_cacti_version(mysql, LOCAL) == 0);
|
|
assert(db_insert(mysql, LOCAL, "INSERT INTO version(cacti) VALUES ('1.2.32')"));
|
|
}
|
|
|
|
typedef struct {
|
|
poller_thread_t thread;
|
|
int errors;
|
|
} test_poll_work_t;
|
|
|
|
static void *test_poll_worker(void *argument) {
|
|
test_poll_work_t *work = argument;
|
|
assert(mysql_thread_init() == 0);
|
|
poll_host(&work->thread, &work->errors);
|
|
return NULL;
|
|
}
|
|
|
|
/* A worker without a connection fails its device and returns; the process
|
|
* keeps polling other devices and still exits with a failure status. */
|
|
static void test_poll_missing_connection(const poller_thread_t *work) {
|
|
for (int remote = 0; remote <= 1; remote++) {
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
pool_t unavailable = {0};
|
|
if (remote) {
|
|
set.poller.poller_id = 2;
|
|
set.poller.mode = REMOTE_ONLINE;
|
|
db_pool_remote = &unavailable;
|
|
} else db_pool_local[0].free = FALSE;
|
|
extern poller_thread_t **details;
|
|
poller_thread_t *device = details[work->device_counter];
|
|
device->complete = FALSE;
|
|
device->threads_complete = 0;
|
|
int errors = 0;
|
|
set.exit.exit_code = EXIT_SUCCESS;
|
|
poll_host(work, &errors);
|
|
if (!device->poll_failed || device->complete || device->threads_complete != 1) _exit(3);
|
|
/* The local slot borrowed for a remote poll goes back to the pool. */
|
|
if (remote && !db_pool_local[0].free) _exit(4);
|
|
/* The failure still reaches the process exit status. */
|
|
_exit(set.exit.exit_code == EXIT_FAILURE ? 0 : 5);
|
|
}
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
fprintf(stderr, "missing connection: remote=%d raw_status=%d\n", remote, status);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
|
}
|
|
}
|
|
|
|
static void test_reindex_pipeline(MYSQL *mysql, test_poll_work_t *work) {
|
|
static const struct {
|
|
const char *op;
|
|
const char *expected;
|
|
const char *command;
|
|
int action;
|
|
int queued;
|
|
const char *stored;
|
|
const char *output;
|
|
} cases[] = {
|
|
{"=", "123", "/usr/bin/printf 123", POLLER_ACTION_SCRIPT, 0, "123", "123"},
|
|
{"=", "122", "/usr/bin/printf 123", POLLER_ACTION_SCRIPT, 1, "123", "123"},
|
|
{">", "122", "/usr/bin/printf 123", POLLER_ACTION_SCRIPT, 1, "123", "123"},
|
|
{">", "124", "/usr/bin/printf 123", POLLER_ACTION_SCRIPT, 0, "123", "123"},
|
|
{"<", "124", "/usr/bin/printf 123", POLLER_ACTION_SCRIPT, 1, "123", "U"},
|
|
{"<", "122", "/usr/bin/printf 123", POLLER_ACTION_SCRIPT, 0, "123", "123"},
|
|
{"<", "0", "/usr/bin/printf 123", POLLER_ACTION_SCRIPT, 0, "123", "123"},
|
|
{"=", "123", "/usr/bin/printf 'a\\nb\\n'", POLLER_ACTION_SCRIPT_COUNT, 1, "2", "123"},
|
|
{"=", "123", "/usr/bin/printf U", POLLER_ACTION_SCRIPT, 0, "123", "123"},
|
|
{"<", "124", "/usr/bin/printf U", POLLER_ACTION_SCRIPT, 0, "124", "123"},
|
|
{"<", "124", "unknown", 99, 0, "124", "123"}
|
|
};
|
|
for (int level = 0; level <= 2; level++) {
|
|
set.logging.spine_log_level = level;
|
|
for (size_t index = 0; index < sizeof(cases) / sizeof(cases[0]); index++) {
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost"));
|
|
char escaped_command[BUFSIZE];
|
|
db_escape(mysql, escaped_command, sizeof(escaped_command), cases[index].command);
|
|
char query[LRG_BUFSIZE];
|
|
spine_snprintf(query, sizeof(query), "INSERT INTO poller_reindex(host_id,data_query_id,action,op,assert_value,arg1) VALUES (%d,7,%d,'%s','%s','%s')", work->thread.host_id, cases[index].action, cases[index].op, cases[index].expected, escaped_command);
|
|
assert(db_insert(mysql, LOCAL, query));
|
|
work->thread.complete = FALSE;
|
|
work->thread.threads_complete = 0;
|
|
work->errors = 0;
|
|
pthread_t worker;
|
|
start_test_worker(&worker, test_poll_worker, work);
|
|
assert(pthread_join(worker, NULL) == 0);
|
|
assert(work->thread.complete && work->thread.threads_complete == 1);
|
|
int expected_errors = 1;
|
|
if (level == 1) expected_errors += cases[index].queued + (cases[index].queued && STRMATCH(cases[index].output, "U"));
|
|
assert(work->errors == expected_errors);
|
|
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_command WHERE poller_id=1 AND action=%d AND command='%d:7'", POLLER_COMMAND_REINDEX, work->thread.host_id);
|
|
assert(database_count(mysql, query) == (unsigned long)cases[index].queued);
|
|
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_reindex WHERE assert_value='%s'", cases[index].stored);
|
|
assert(database_count(mysql, query) == 1);
|
|
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output WHERE local_data_id=601 AND output='%s'", cases[index].output);
|
|
assert(database_count(mysql, query) == 1);
|
|
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=601 AND output='%s'", cases[index].output);
|
|
assert(database_count(mysql, query) == 1);
|
|
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
|
|
}
|
|
}
|
|
set.logging.spine_log_level = 0;
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command"));
|
|
}
|
|
|
|
/* A reindex that could not be queued must leave the stored assertion alone;
|
|
* advancing it would hide the change from every later poll. Hiding the
|
|
* poller_command table makes the server itself reject the queue write. */
|
|
static void test_reindex_queue_failure(MYSQL *mysql, test_poll_work_t *work) {
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command"));
|
|
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_reindex(host_id,data_query_id,action,op,assert_value,arg1) VALUES (43,7,1,'=','122','/usr/bin/printf 123'),(43,8,1,'>','122','/usr/bin/printf 123')"));
|
|
assert(db_insert(mysql, LOCAL, "RENAME TABLE poller_command TO poller_command_regression_hidden"));
|
|
work->thread.complete = FALSE;
|
|
work->thread.threads_complete = 0;
|
|
work->errors = 0;
|
|
pthread_t worker;
|
|
start_test_worker(&worker, test_poll_worker, work);
|
|
assert(pthread_join(worker, NULL) == 0);
|
|
assert(db_insert(mysql, LOCAL, "RENAME TABLE poller_command_regression_hidden TO poller_command"));
|
|
long long kept = (long long)database_count(mysql, "SELECT COUNT(*) FROM poller_reindex WHERE (data_query_id=7 AND assert_value='122') OR (data_query_id=8 AND assert_value='122')");
|
|
fprintf(stderr, "reindex queue failure: assertions kept=%lld\n", kept);
|
|
assert(kept == 2);
|
|
assert(work->thread.threads_complete == 1);
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
}
|
|
|
|
static void test_reindex_query_shortcut(MYSQL *mysql, test_poll_work_t *work) {
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost"));
|
|
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_reindex(host_id,data_query_id,action,op,assert_value,arg1) VALUES (43,7,1,'=','122','/usr/bin/printf 123'),(43,7,1,'=','777','/usr/bin/printf 888'),(43,8,1,'=','455','/usr/bin/printf 456')"));
|
|
work->thread.complete = FALSE;
|
|
work->thread.threads_complete = 0;
|
|
work->errors = 0;
|
|
pthread_t worker;
|
|
start_test_worker(&worker, test_poll_worker, work);
|
|
assert(pthread_join(worker, NULL) == 0);
|
|
assert(work->thread.complete && work->thread.threads_complete == 1 && work->errors == 1);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_command") == 2);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_reindex WHERE (data_query_id=7 AND arg1='/usr/bin/printf 123' AND assert_value='123') OR (data_query_id=7 AND arg1='/usr/bin/printf 888' AND assert_value='777') OR (data_query_id=8 AND arg1='/usr/bin/printf 456' AND assert_value='456')") == 3);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE (local_data_id=601 AND output='123') OR (local_data_id=602 AND output='U')") == 2);
|
|
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
}
|
|
|
|
static void test_profile_schedule_completion(MYSQL *mysql, test_poll_work_t *aggregate) {
|
|
config_t previous = set;
|
|
test_poll_work_t original = *aggregate;
|
|
set.poller.active_profiles = 2;
|
|
set.poller.poller_interval = 5;
|
|
set.snmp.total_snmp_ports = 1;
|
|
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET rrd_step=300,rrd_next_step=0 WHERE host_id=43"));
|
|
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_item (local_data_id,host_id,poller_id,action,arg1,rrd_name,rrd_step,rrd_next_step) VALUES (603,43,1,1,'/usr/bin/printf 789','not-due',300,100)"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost"));
|
|
aggregate->thread.host_thread = 2;
|
|
aggregate->thread.host_threads = 2;
|
|
aggregate->thread.host_data_ids = 1;
|
|
aggregate->thread.complete = FALSE;
|
|
aggregate->thread.threads_complete = 0;
|
|
/* Deliberately finish partition 2 before partition 1 has selected its rows. */
|
|
test_poll_work_t second = *aggregate;
|
|
second.thread.host_thread = 2;
|
|
pthread_t worker;
|
|
start_test_worker(&worker, test_poll_worker, &second);
|
|
assert(pthread_join(worker, NULL) == 0);
|
|
assert(aggregate->thread.threads_complete == 1 && !aggregate->thread.complete);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=43 AND rrd_next_step=0") == 2);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=602 AND output='U'") == 1);
|
|
test_poll_work_t first = *aggregate;
|
|
first.thread.host_thread = 1;
|
|
start_test_worker(&worker, test_poll_worker, &first);
|
|
assert(pthread_join(worker, NULL) == 0);
|
|
assert(aggregate->thread.threads_complete == 2 && aggregate->thread.complete);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE (local_data_id=601 AND output='123') OR (local_data_id=602 AND output='U')") == 2);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE (local_data_id=601 AND output='123') OR (local_data_id=602 AND output='U')") == 2);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=603") == 0);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=43 AND ((local_data_id IN (601,602) AND rrd_next_step=295) OR (local_data_id=603 AND rrd_next_step=95))") == 3);
|
|
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
|
|
*aggregate = original;
|
|
set = previous;
|
|
}
|
|
|
|
/* A real, non-retryable SQL error inside a worker fails that device and the
|
|
* worker returns: the schedule is not advanced, nothing is written, and the
|
|
* connection goes back to the pool for the next device. The item query names
|
|
* a column the fixture schema lacks, so MariaDB itself rejects it. */
|
|
static void test_poll_database_failure(MYSQL *mysql, test_poll_work_t *aggregate) {
|
|
config_t previous = set;
|
|
test_poll_work_t original = *aggregate;
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='poller_item' AND COLUMN_NAME='output_regex'") == 0);
|
|
set.poller.active_profiles = 2;
|
|
set.poller.poller_interval = 5;
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item WHERE host_id=43 AND local_data_id NOT IN (601,602)"));
|
|
for (int failing = 1; failing >= 0; failing--) {
|
|
set.hosts.has_output_regex = failing;
|
|
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET rrd_step=300,rrd_next_step=0 WHERE host_id=43"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost"));
|
|
aggregate->thread.host_thread = 1;
|
|
aggregate->thread.host_threads = 1;
|
|
aggregate->thread.host_data_ids = 0;
|
|
aggregate->thread.complete = FALSE;
|
|
aggregate->thread.threads_complete = 0;
|
|
aggregate->thread.output_failed = FALSE;
|
|
aggregate->thread.poll_failed = FALSE;
|
|
set.exit.exit_code = EXIT_SUCCESS;
|
|
pthread_t worker;
|
|
start_test_worker(&worker, test_poll_worker, aggregate);
|
|
assert(pthread_join(worker, NULL) == 0);
|
|
assert(aggregate->thread.threads_complete == 1);
|
|
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
|
|
if (failing) {
|
|
assert(aggregate->thread.poll_failed && !aggregate->thread.complete);
|
|
assert(set.exit.exit_code == EXIT_FAILURE);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output") == 0);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=43 AND rrd_next_step=0") == 2);
|
|
} else {
|
|
assert(!aggregate->thread.poll_failed && aggregate->thread.complete);
|
|
assert(set.exit.exit_code == EXIT_SUCCESS);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE (local_data_id=601 AND output='123') OR (local_data_id=602 AND output='U')") == 2);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=43 AND rrd_next_step=295") == 2);
|
|
}
|
|
}
|
|
*aggregate = original;
|
|
set = previous;
|
|
}
|
|
|
|
static void prepare_nullable_snmp_profile(MYSQL *mysql) {
|
|
/* These fields are nullable in the shipped schema; missing values
|
|
* must survive the real row-loader/session handoff. */
|
|
assert(db_insert(mysql, LOCAL, "UPDATE host SET snmp_context=NULL,snmp_engine_id=NULL WHERE id=44"));
|
|
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET snmp_context=NULL,snmp_engine_id=NULL,arg2=NULL,arg3=NULL WHERE host_id=44"));
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=44 AND snmp_context IS NULL AND snmp_engine_id IS NULL AND arg2 IS NULL AND arg3 IS NULL") == 2);
|
|
}
|
|
|
|
static void assert_snmp_pipeline_samples(MYSQL *mysql, bool spike) {
|
|
char query[LRG_BUFSIZE];
|
|
/* A restarted agent invalidates every sample of the cycle, including
|
|
* those flushed early because the next item's credentials differ. */
|
|
const char *first_check = spike ? "output='U'" : "output REGEXP '^[0-9]+$' AND CAST(output AS UNSIGNED)>0";
|
|
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output WHERE local_data_id=601 AND %s", first_check);
|
|
assert(database_count(mysql, query) == 1);
|
|
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=601 AND %s", first_check);
|
|
assert(database_count(mysql, query) == 1);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=602 AND output='U'") == 1);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=602 AND output='U'") == 1);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output") == 2);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output_boost") == 2);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_command") == (spike ? 1 : 0));
|
|
}
|
|
|
|
static void test_snmp_item_pipeline(MYSQL *mysql, test_poll_work_t *work, const char *agent) {
|
|
char escaped_agent[BUFSIZE];
|
|
db_escape(mysql, escaped_agent, sizeof(escaped_agent), agent);
|
|
for (int scenario = 0; scenario < 9; scenario++) {
|
|
bool change_version = (scenario & 2) != 0;
|
|
bool spike = (scenario & 4) != 0;
|
|
char query[LRG_BUFSIZE];
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost"));
|
|
spine_snprintf(query, sizeof(query), "UPDATE host SET max_oids=%d WHERE id=44", 1 + (scenario & 1));
|
|
assert(db_insert(mysql, LOCAL, query));
|
|
spine_snprintf(query, sizeof(query), "UPDATE poller_item SET action=0,hostname='%s',snmp_community='regression',snmp_version=2,snmp_port=1161,snmp_timeout=500,arg1='.1.3.6.1.2.1.1.3.0' WHERE host_id=44", escaped_agent);
|
|
assert(db_insert(mysql, LOCAL, query));
|
|
spine_snprintf(query, sizeof(query), "UPDATE poller_item SET snmp_version=%d,arg1='%s' WHERE local_data_id=602", change_version ? 1 : 2, spike ? ".1.3.6.1.2.1.1.3.0" : ".1.3.6.1.2.1.1.999.0");
|
|
assert(db_insert(mysql, LOCAL, query));
|
|
if (spike) assert(db_insert(mysql, LOCAL, "INSERT INTO poller_reindex(host_id,data_query_id,action,op,assert_value,arg1) VALUES (44,7,1,'<','124','/usr/bin/printf 123')"));
|
|
if (scenario == 8) {
|
|
prepare_nullable_snmp_profile(mysql);
|
|
}
|
|
work->thread.complete = FALSE;
|
|
work->thread.threads_complete = 0;
|
|
work->errors = 0;
|
|
pthread_t worker;
|
|
start_test_worker(&worker, test_poll_worker, work);
|
|
assert(pthread_join(worker, NULL) == 0);
|
|
assert(work->thread.complete && work->thread.threads_complete == 1);
|
|
assert(work->errors == (spike ? 0 : 1));
|
|
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
|
|
assert_snmp_pipeline_samples(mysql, spike);
|
|
if (scenario == 8) puts("production nullable SNMP profile handoff passed");
|
|
}
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
assert(db_insert(mysql, LOCAL, "UPDATE host SET snmp_context='',snmp_engine_id='' WHERE id=44"));
|
|
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET snmp_context='',snmp_engine_id='' WHERE host_id=44"));
|
|
}
|
|
|
|
/* Each item leaves through a different batch flush: 601 through the
|
|
* batch-full flush (max_oids=1) or the credential-change flush (602 switches
|
|
* to v1), and 602 through the final flush. All three must apply the same
|
|
* output_regex and spike kill. */
|
|
static void test_snmp_flush_contracts(MYSQL *mysql, test_poll_work_t *work, const char *agent) {
|
|
char escaped_agent[BUFSIZE];
|
|
char query[LRG_BUFSIZE];
|
|
int previous_regex = set.hosts.has_output_regex;
|
|
db_escape(mysql, escaped_agent, sizeof(escaped_agent), agent);
|
|
assert(db_insert(mysql, LOCAL, "ALTER TABLE poller_item ADD COLUMN output_regex varchar(255) NOT NULL DEFAULT ''"));
|
|
set.hosts.has_output_regex = TRUE;
|
|
for (int scenario = 0; scenario < 4; scenario++) {
|
|
bool full_batch = (scenario & 1) != 0;
|
|
bool spike = (scenario & 2) != 0;
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost"));
|
|
spine_snprintf(query, sizeof(query), "UPDATE host SET max_oids=%d WHERE id=44", full_batch ? 1 : 5);
|
|
assert(db_insert(mysql, LOCAL, query));
|
|
/* The leading digit of sysUpTime: a visible, still numeric rewrite. */
|
|
spine_snprintf(query, sizeof(query), "UPDATE poller_item SET action=0,hostname='%s',snmp_community='regression',snmp_version=2,snmp_port=1161,snmp_timeout=500,arg1='.1.3.6.1.2.1.1.3.0',output_regex='^[0-9]' WHERE host_id=44", escaped_agent);
|
|
assert(db_insert(mysql, LOCAL, query));
|
|
if (!full_batch) assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET snmp_version=1 WHERE local_data_id=602"));
|
|
if (spike) assert(db_insert(mysql, LOCAL, "INSERT INTO poller_reindex(host_id,data_query_id,action,op,assert_value,arg1) VALUES (44,7,1,'<','124','/usr/bin/printf 123')"));
|
|
work->thread.complete = FALSE;
|
|
work->thread.threads_complete = 0;
|
|
work->errors = 0;
|
|
pthread_t worker;
|
|
start_test_worker(&worker, test_poll_worker, work);
|
|
assert(pthread_join(worker, NULL) == 0);
|
|
assert(work->thread.complete && work->thread.threads_complete == 1);
|
|
const char *expected = spike ? "output='U'" : "output REGEXP '^[0-9]$'";
|
|
for (int item = 601; item <= 602; item++) {
|
|
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output WHERE local_data_id=%d AND %s", item, expected);
|
|
if (database_count(mysql, query) != 1) {
|
|
fprintf(stderr, "flush contract: scenario=%d full_batch=%d spike=%d item=%d expected %s\n", scenario, full_batch, spike, item, expected);
|
|
assert(0);
|
|
}
|
|
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=%d AND %s", item, expected);
|
|
assert(database_count(mysql, query) == 1);
|
|
}
|
|
}
|
|
/* Existing regexes were written against the normalized value: a
|
|
* Hex-STRING becomes 266 first, then the regex keeps "266", not "00". */
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost"));
|
|
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET snmp_version=2,arg1='.1.3.6.1.2.1.1.5.0',output_regex='^[0-9][0-9]*' WHERE host_id=44"));
|
|
work->thread.complete = FALSE;
|
|
work->thread.threads_complete = 0;
|
|
work->errors = 0;
|
|
pthread_t regex_worker;
|
|
start_test_worker(®ex_worker, test_poll_worker, work);
|
|
assert(pthread_join(regex_worker, NULL) == 0);
|
|
if (database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id IN (601,602) AND output='266'") != 2) {
|
|
fprintf(stderr, "flush contract: output_regex did not see the normalized Hex-STRING\n");
|
|
assert(0);
|
|
}
|
|
set.hosts.has_output_regex = previous_regex;
|
|
assert(db_insert(mysql, LOCAL, "ALTER TABLE poller_item DROP COLUMN output_regex"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
|
|
puts("production SNMP batch flush contracts passed");
|
|
}
|
|
|
|
static void assert_polled_host_statistics(MYSQL *mysql, int host_id) {
|
|
if (host_id == 44) {
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM host WHERE id=44 AND total_polls=1 AND failed_polls=0 AND status_last_error='' AND snmp_sysUpTimeInstance>0 AND snmp_sysDescr!='' AND snmp_sysObjectID!='' AND snmp_sysContact='regression' AND snmp_sysName!='' AND snmp_sysLocation='isolated-regression-agent'") == 1);
|
|
} else if (host_id != 0) {
|
|
char query[256];
|
|
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM host WHERE id=%d AND total_polls=1 AND failed_polls=0 AND min_time=0 AND status_last_error=''", host_id);
|
|
assert(database_count(mysql, query) == 1);
|
|
}
|
|
}
|
|
|
|
static void test_poll_pipeline(MYSQL *mysql) {
|
|
extern poller_thread_t **details;
|
|
config_t previous = set;
|
|
pool_t *previous_pool = db_pool_local;
|
|
poller_thread_t **previous_details = details;
|
|
set.poller.threads = 1;
|
|
set.poller.poller_id = 1;
|
|
set.poller.poller_interval = 0;
|
|
set.poller.active_profiles = 1;
|
|
set.boost.boost_enabled = TRUE;
|
|
set.boost.boost_redirect = TRUE;
|
|
set.availability.ping_only = FALSE;
|
|
set.php.script_timeout = 2;
|
|
set.logging.spine_log_level = 0;
|
|
set.logging.log_destination = 0;
|
|
db_pool_local = calloc(1, sizeof(*db_pool_local));
|
|
assert(db_pool_local != NULL);
|
|
db_create_connection_pool(LOCAL);
|
|
assert(spine_permits_init(&available_scripts, 2) == 0);
|
|
set.snmp.mibs = FALSE;
|
|
set.availability.ping_recovery_count = 1;
|
|
set.availability.ping_failure_count = 1;
|
|
const char *agent = getenv("SPINE_TEST_SNMP_HOST");
|
|
const int hosts[] = {0, 42, 43, 44};
|
|
size_t host_count = sizeof(hosts) / sizeof(hosts[0]);
|
|
if (agent == NULL || agent[0] == '\0') host_count--;
|
|
else snmp_spine_init();
|
|
for (size_t index = 0; index < host_count; index++) {
|
|
int host_id = hosts[index];
|
|
set.snmp.mibs = host_id == 44;
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM host"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost"));
|
|
assert(db_insert(mysql, LOCAL, "DELETE FROM host_errors"));
|
|
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_item (local_data_id,host_id,poller_id,action,arg1,rrd_name) VALUES (601,0,1,1,'/usr/bin/printf 123','valid'),(602,0,1,1,'/usr/bin/printf invalid','invalid')"));
|
|
char host_query[BUFSIZE];
|
|
spine_snprintf(host_query, sizeof(host_query), "UPDATE poller_item SET host_id=%d", host_id);
|
|
assert(db_insert(mysql, LOCAL, host_query));
|
|
if (host_id != 0) {
|
|
spine_snprintf(host_query, sizeof(host_query), "INSERT INTO host (id,hostname,availability_method,status_fail_date,status_rec_date,max_oids,ping_method,ping_port,status_last_error,min_time,total_polls) VALUES (%d,'127.0.0.1',%d,'2026-10-05 00:00:00','2026-10-05 00:00:00',%d,NULL,NULL,NULL,NULL,NULL)", host_id, host_id == 42 ? AVAIL_STREAM : AVAIL_NONE, host_id == 42 ? 0 : 101);
|
|
assert(db_insert(mysql, LOCAL, host_query));
|
|
}
|
|
if (host_id == 44) {
|
|
char escaped_agent[BUFSIZE];
|
|
db_escape(mysql, escaped_agent, sizeof(escaped_agent), agent);
|
|
spine_snprintf(host_query, sizeof(host_query), "UPDATE host SET hostname='%s',availability_method=%d,snmp_version=2,snmp_community='regression',snmp_port=1161,snmp_timeout=500 WHERE id=44", escaped_agent, AVAIL_SNMP);
|
|
assert(db_insert(mysql, LOCAL, host_query));
|
|
}
|
|
test_poll_work_t *work = calloc(1, sizeof(*work));
|
|
assert(work != NULL);
|
|
work->thread.host_id = host_id;
|
|
work->thread.host_thread = 1;
|
|
work->thread.host_threads = 1;
|
|
work->thread.host_data_ids = 2;
|
|
work->thread.host_time_double = get_time_as_double();
|
|
STRNCOPY(work->thread.host_time, "1791158400");
|
|
poller_thread_t **owned_details = calloc(1, sizeof(*owned_details));
|
|
assert(owned_details != NULL);
|
|
owned_details[0] = &work->thread;
|
|
details = owned_details;
|
|
pthread_t worker;
|
|
start_test_worker(&worker, test_poll_worker, work);
|
|
assert(pthread_join(worker, NULL) == 0);
|
|
assert(work->thread.complete && work->thread.threads_complete == 1 && work->errors == 1);
|
|
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
|
|
if (host_id == 0) test_poll_missing_connection(&work->thread);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE (local_data_id=601 AND output='123') OR (local_data_id=602 AND output='U')") == 2);
|
|
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE (local_data_id=601 AND output='123') OR (local_data_id=602 AND output='U')") == 2);
|
|
char query[256];
|
|
spine_snprintf(query, sizeof(query), "SELECT errors FROM host_errors WHERE host_id=%d", host_id);
|
|
assert(database_count(mysql, query) == 1);
|
|
assert_polled_host_statistics(mysql, host_id);
|
|
if (host_id == 44) {
|
|
test_snmp_item_pipeline(mysql, work, agent);
|
|
test_snmp_flush_contracts(mysql, work, agent);
|
|
}
|
|
if (host_id == 43) {
|
|
test_reindex_pipeline(mysql, work);
|
|
test_reindex_query_shortcut(mysql, work);
|
|
test_reindex_queue_failure(mysql, work);
|
|
test_profile_schedule_completion(mysql, work);
|
|
test_poll_database_failure(mysql, work);
|
|
}
|
|
/* End this fixture binding before the next host or caller runs. */
|
|
details = previous_details;
|
|
free(owned_details);
|
|
free(work);
|
|
}
|
|
assert(spine_permits_destroy(&available_scripts) == 0);
|
|
db_close_connection_pool(LOCAL);
|
|
db_pool_local = previous_pool;
|
|
details = previous_details;
|
|
set = previous;
|
|
}
|
|
|
|
static void run_cli_poll_profile(const char *config, const char *poller, const char *threads, const char *interval, const char *profiles, const char *hostlist, int expected) {
|
|
pid_t child = fork();
|
|
assert(child >= 0);
|
|
if (child == 0) {
|
|
alarm(15);
|
|
if (hostlist != NULL) {
|
|
execl("./spine", "spine", "--conf", config, "-p", poller, "-t", threads, "-H", hostlist, "--mode=online", "-O", interval, "-O", profiles, "-S", "-V", "2", NULL);
|
|
_exit(127);
|
|
}
|
|
execl("./spine", "spine", "-C", "/nonexistent/spine-regression.conf", "--conf", config, "-p", poller, "-t", threads, "--mode=online", "-O", interval, "-O", profiles, "-S", "-V", "2", NULL);
|
|
_exit(127);
|
|
}
|
|
int status;
|
|
assert(waitpid(child, &status, 0) == child);
|
|
printf("Spine executable exit: expected=%d raw_status=%d\n", expected, status);
|
|
fflush(stdout);
|
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == expected);
|
|
}
|
|
|
|
static void run_cli_poll(const char *config, const char *poller, const char *threads, const char *interval, int expected) {
|
|
run_cli_poll_profile(config, poller, threads, interval, "active_profiles:1", NULL, expected);
|
|
}
|
|
|
|
static void test_cli_workers(MYSQL *source, const char *config) {
|
|
assert(db_insert(source, LOCAL, "REPLACE INTO poller(id,threads) VALUES (1,2)"));
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_time WHERE poller_id=1"));
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_item"));
|
|
char query[BUFSIZE];
|
|
spine_snprintf(query, sizeof(query), "UPDATE host SET hostname='127.0.0.1',poller_id=1,disabled='',device_threads=2,availability_method=%i,status_fail_date='2026-10-05 00:00:00',status_rec_date='2026-10-05 00:00:00' WHERE id=42", AVAIL_NONE);
|
|
assert(db_insert(source, LOCAL, query));
|
|
assert(db_insert(source, LOCAL, "INSERT INTO poller_item (local_data_id,host_id,poller_id,action,arg1,rrd_name) VALUES (701,42,1,1,'/usr/bin/printf 123','first'),(702,42,1,1,'/usr/bin/printf 456','second')"));
|
|
run_cli_poll(config, "1", "2", "poller_interval:5", EXIT_SUCCESS);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM poller_output WHERE (local_data_id=701 AND output='123') OR (local_data_id=702 AND output='456')") == 2);
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(source, LOCAL, "INSERT INTO host(id,hostname,poller_id,disabled,device_threads,availability_method,status_fail_date,status_rec_date) VALUES(43,'127.0.0.1',1,'',1,0,'2026-10-05 00:00:00','2026-10-05 00:00:00')"));
|
|
assert(db_insert(source, LOCAL, "INSERT INTO poller_item(local_data_id,host_id,poller_id,action,arg1,rrd_name) VALUES(704,43,1,1,'/usr/bin/printf 789','excluded')"));
|
|
run_cli_poll_profile(config, "1", "2", "poller_interval:5", "active_profiles:1", " 0042 , 42 ", EXIT_SUCCESS);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM poller_output WHERE (local_data_id=701 AND output='123') OR (local_data_id=702 AND output='456')") == 2);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=704") == 0);
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_item WHERE local_data_id=704"));
|
|
assert(db_insert(source, LOCAL, "DELETE FROM host WHERE id=43"));
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(source, LOCAL, "UPDATE poller_item SET rrd_next_step=0 WHERE local_data_id IN (701,702)"));
|
|
assert(db_insert(source, LOCAL, "INSERT INTO poller_item (local_data_id,host_id,poller_id,action,arg1,rrd_name,rrd_next_step) VALUES (703,42,1,1,'/usr/bin/printf 789','not-due',100)"));
|
|
run_cli_poll_profile(config, "1", "2", "poller_interval:5", "active_profiles:2", NULL, EXIT_SUCCESS);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM poller_output WHERE (local_data_id=701 AND output='123') OR (local_data_id=702 AND output='456')") == 2);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=703") == 0);
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_item WHERE local_data_id=703"));
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_output"));
|
|
assert(db_insert(source, LOCAL, "UPDATE poller_item SET arg1='/bin/sleep 3' WHERE local_data_id=702"));
|
|
double start = spine_monotonic_time();
|
|
run_cli_poll(config, "1", "2", "poller_interval:1", EXIT_FAILURE);
|
|
assert(spine_monotonic_time() - start < 3);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=701 AND output='123'") == 1);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=702") == 0);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM poller_time WHERE poller_id=1 AND end_time='0000-00-00 00:00:00'") == 1);
|
|
}
|
|
|
|
static void test_cli_transfer_exit(MYSQL *source) {
|
|
char database[80];
|
|
char username[80];
|
|
char query[BUFSIZE];
|
|
spine_snprintf(database, sizeof(database), "spine_exit_%ld", (long)getpid());
|
|
spine_snprintf(username, sizeof(username), "spine_cli_%ld", (long)getpid());
|
|
spine_snprintf(query, sizeof(query), "CREATE DATABASE %s CHARACTER SET utf8mb4", database);
|
|
assert(db_insert(source, LOCAL, query));
|
|
/* Public fixture credentials, restricted to the two isolated regression databases. */
|
|
spine_snprintf(query, sizeof(query), "CREATE USER '%s'@'%%' IDENTIFIED BY 'regression-only'", username);
|
|
assert(db_insert(source, LOCAL, query));
|
|
spine_snprintf(query, sizeof(query), "GRANT SELECT,INSERT,UPDATE,DELETE ON spine_regressions.* TO '%s'@'%%'", username);
|
|
assert(db_insert(source, LOCAL, query));
|
|
spine_snprintf(query, sizeof(query), "GRANT SELECT,INSERT,UPDATE,DELETE ON %s.* TO '%s'@'%%'", database, username);
|
|
assert(db_insert(source, LOCAL, query));
|
|
MYSQL destination;
|
|
db_connect(LOCAL, &destination);
|
|
assert(mysql_select_db(&destination, database) == 0);
|
|
const char *const tables[] = {"settings", "poller", "poller_item", "version", "host", "poller_time"};
|
|
for (size_t index = 0; index < sizeof(tables) / sizeof(tables[0]); index++) {
|
|
spine_snprintf(query, sizeof(query), "CREATE TABLE %s LIKE spine_regressions.%s", tables[index], tables[index]);
|
|
assert(db_insert(&destination, REMOTE, query));
|
|
}
|
|
assert(db_insert(&destination, REMOTE, "INSERT INTO version VALUES ('1.2.32')"));
|
|
assert(db_insert(source, LOCAL, "REPLACE INTO poller(id,threads) VALUES (2,1)"));
|
|
assert(db_insert(&destination, REMOTE, "INSERT INTO poller(id,threads) VALUES (2,1)"));
|
|
assert(db_insert(source, LOCAL, "DELETE FROM host"));
|
|
assert(db_insert(source, LOCAL, "DELETE FROM poller_item"));
|
|
/* Disabled devices are excluded from CLI polling, but their status still syncs. */
|
|
assert(db_insert(source, LOCAL, "INSERT INTO host (id,poller_id,disabled) VALUES (42,2,'on')"));
|
|
assert(db_insert(&destination, REMOTE, "CREATE TRIGGER reject_cli_transfer BEFORE INSERT ON host FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='regression transfer failure'"));
|
|
char config[] = "spine-cli-exit-XXXXXX";
|
|
int fd = mkstemp(config);
|
|
assert(fd >= 0);
|
|
FILE *file = fdopen(fd, "w");
|
|
assert(file != NULL);
|
|
assert(fprintf(file, "DB_Host %s\nDB_Database spine_regressions\nDB_User %s\nDB_Pass regression-only\nDB_Port 3306\nRDB_Host %s\nRDB_Database %s\nRDB_User %s\nRDB_Pass regression-only\nRDB_Port 3306\nCacti_Log %s.log\n", set.database.host, username, set.database.host, database, username, config) > 0);
|
|
assert(fclose(file) == 0);
|
|
run_cli_poll(config, "2", "1", "poller_interval:5", EXIT_FAILURE);
|
|
assert(database_count(&destination, "SELECT COUNT(*) FROM host") == 0);
|
|
assert(database_count(source, "SELECT COUNT(*) FROM host") == 1);
|
|
assert(db_insert(&destination, REMOTE, "DROP TRIGGER reject_cli_transfer"));
|
|
run_cli_poll(config, "2", "1", "poller_interval:5", EXIT_SUCCESS);
|
|
assert(database_count(&destination, "SELECT COUNT(*) FROM host WHERE id=42") == 1);
|
|
test_cli_workers(source, config);
|
|
assert(unlink(config) == 0);
|
|
spine_snprintf(query, sizeof(query), "%s.log", config);
|
|
if (file_exists(query)) assert(unlink(query) == 0);
|
|
db_disconnect(&destination);
|
|
spine_snprintf(query, sizeof(query), "DROP USER '%s'@'%%'", username);
|
|
assert(db_insert(source, LOCAL, query));
|
|
spine_snprintf(query, sizeof(query), "DROP DATABASE %s", database);
|
|
assert(db_insert(source, LOCAL, query));
|
|
}
|
|
|
|
static void test_database_configuration(void) {
|
|
const char *hostname = getenv("SPINE_TEST_DB_HOST");
|
|
assert(hostname != NULL && hostname[0] != '\0');
|
|
config_defaults();
|
|
strncopy(set.database.host, hostname, sizeof(set.database.host));
|
|
STRNCOPY(set.database.user, "root");
|
|
STRNCOPY(set.database.database, "spine_regressions");
|
|
set.database.password[0] = '\0';
|
|
set.database.port = 3306;
|
|
set.poller.poller_id = 1;
|
|
set.hosts.start_host_id = -1;
|
|
set.hosts.end_host_id = -1;
|
|
set.poller.parent_fork = SPINE_PARENT;
|
|
MYSQL mysql;
|
|
db_connect(LOCAL, &mysql);
|
|
/* Restore the fixture input before testing configuration, including after a failed CLI run. */
|
|
assert(db_insert(&mysql, LOCAL, "REPLACE INTO poller (id,threads) VALUES (1,7)"));
|
|
test_database_version(&mysql);
|
|
assert(db_insert(&mysql, LOCAL, "DELETE FROM settings") == TRUE);
|
|
assert(db_insert(&mysql, LOCAL, "INSERT INTO settings (name,value) VALUES ('path_webroot','/srv/cacti'),('path_cactilog',''),('ping_timeout','650'),('script_timeout','2'),('php_servers','100'),('max_get_size','200'),('default_datechar','99')") == TRUE);
|
|
assert(db_insert(&mysql, LOCAL, "DELETE FROM poller_item") == TRUE);
|
|
assert(db_insert(&mysql, LOCAL, "INSERT INTO poller_item (local_data_id,host_id,action) VALUES (1,42,2)") == TRUE);
|
|
db_disconnect(&mysql);
|
|
read_config_options();
|
|
assert(set.cacti_version == 1232);
|
|
assert(strcmp(set.php.path_php_server, "/srv/cacti/script_server.php") == 0);
|
|
assert(strcmp(set.logging.path_logfile, "/srv/cacti/log/cacti.log") == 0);
|
|
assert(set.availability.ping_timeout == 650 && set.php.script_timeout == 5);
|
|
assert(set.php.php_servers == MAX_PHP_SERVERS && set.snmp.snmp_max_get_size == 128);
|
|
assert(set.logging.log_datetime_separator == GDC_DEFAULT);
|
|
assert(set.poller.threads == 7 && set.php.php_required);
|
|
db_connect(LOCAL, &mysql);
|
|
MYSQL_RES *result = db_query(&mysql, LOCAL, "SELECT value FROM settings WHERE name='spine_capabilities'");
|
|
assert(result != NULL && mysql_num_rows(result) == 1);
|
|
MYSQL_ROW row = mysql_fetch_row(result);
|
|
assert(row != NULL && row[0] != NULL && strstr(row[0], "authProtocols") != NULL);
|
|
db_free_result(result);
|
|
assert(db_insert(&mysql, LOCAL, "DELETE FROM poller_item") == TRUE);
|
|
assert(db_insert(&mysql, LOCAL, "UPDATE settings SET value='/tmp/configured.log' WHERE name='path_cactilog'") == TRUE);
|
|
db_disconnect(&mysql);
|
|
set.poller.threads_set = TRUE;
|
|
set.poller.threads = 3;
|
|
STRNCOPY(set.hosts.host_id_list, "42");
|
|
read_config_options();
|
|
assert(set.poller.threads == 3 && !set.php.php_required);
|
|
assert(strcmp(set.logging.path_logfile, "/tmp/configured.log") == 0);
|
|
set_option("ping_timeout", "777");
|
|
read_config_options();
|
|
assert(set.availability.ping_timeout == 777);
|
|
read_config_options();
|
|
assert(set.availability.ping_timeout == 777);
|
|
db_connect(LOCAL, &mysql);
|
|
test_settings_write_contracts(&mysql);
|
|
test_additional_database_contracts(&mysql);
|
|
test_poller_queries(&mysql);
|
|
test_collector_transfer(&mysql);
|
|
test_poll_pipeline(&mysql);
|
|
test_output_write_contracts(&mysql);
|
|
test_reindex_result_contracts(&mysql);
|
|
const char *reindex_agent = getenv("SPINE_TEST_SNMP_HOST");
|
|
if (reindex_agent != NULL && reindex_agent[0] != '\0') test_additional_reindex_contracts(&mysql);
|
|
test_cli_transfer_exit(&mysql);
|
|
db_disconnect(&mysql);
|
|
}
|
|
|
|
static void test_snmp_multi_responses(host_t *host) {
|
|
target_t items[4] = {0};
|
|
snmp_oids_t oids[4] = {0};
|
|
static const char *const names[] = {".1.3.6.1.2.1.1.6.0", "invalid-regression-oid", ".1.3.6.1.2.1.1.1.999", ".1.3.6.1.2.1.1.4.0"};
|
|
for (int index = 0; index < 4; index++) {
|
|
strncopy(oids[index].oid, names[index], sizeof(oids[index].oid));
|
|
oids[index].array_position = index;
|
|
items[index].local_data_id = 100 + index;
|
|
}
|
|
snmp_get_multi(host, items, oids, 4);
|
|
assert(!host->ignore_host);
|
|
assert(strstr(oids[0].result, "isolated-regression-agent") != NULL);
|
|
assert(strcmp(oids[1].result, "U") == 0);
|
|
/* v1 reports the missing instance as a PDU error and v2c as an inline
|
|
* exception varbind; neither is data, so both store U. */
|
|
assert(strcmp(oids[2].result, "U") == 0);
|
|
assert(strstr(oids[3].result, "regression") != NULL);
|
|
snmp_get_multi(NULL, NULL, NULL, 0); host_t missing_session = {0};
|
|
snmp_oids_t undefined[2] = {0};
|
|
STRNCOPY(undefined[0].result, "123");
|
|
STRNCOPY(undefined[1].result, "456");
|
|
snmp_get_multi(&missing_session, items, undefined, 2);
|
|
assert(missing_session.snmp.status == STAT_DESCRIP_ERROR);
|
|
assert(IS_UNDEFINED(undefined[0].result) && IS_UNDEFINED(undefined[1].result));
|
|
struct variable_list value = {0};
|
|
u_char text[] = "regression";
|
|
value.type = ASN_OCTET_STR;
|
|
value.val.string = text;
|
|
value.val_len = sizeof(text) - 1;
|
|
char formatted[128];
|
|
snmp_snprint_value(formatted, sizeof(formatted), NULL, 0, &value);
|
|
assert(strstr(formatted, "regression") != NULL);
|
|
struct { char before; char value; char after; } bounded = {'a', 'x', 'z'};
|
|
snmp_snprint_value(&bounded.value, 0, NULL, 0, &value);
|
|
assert(bounded.before == 'a' && bounded.value == 'x' && bounded.after == 'z');
|
|
snmp_snprint_value(&bounded.value, 1, NULL, 0, &value);
|
|
assert(bounded.before == 'a' && bounded.value == '\0' && bounded.after == 'z');
|
|
snmp_snprint_value(NULL, 0, NULL, 0, &value);
|
|
snmp_snprint_value(NULL, 1, NULL, 0, &value);
|
|
char insufficient[3] = "xx";
|
|
snmp_snprint_value(insufficient, 2, NULL, 0, &value);
|
|
assert(strcmp(insufficient, "U") == 0 && insufficient[2] == '\0');
|
|
|
|
}
|
|
|
|
static void test_snmp_scalar_responses(host_t *host) {
|
|
char *result = snmp_get(host, ".1.3.6.1.2.1.1.6.0");
|
|
assert(strstr(result, "isolated-regression-agent") != NULL && !host->ignore_host);
|
|
free(result);
|
|
/* A malformed configured OID must not poison later requests or leak a PDU. */
|
|
for (int count = 0; count < 50; count++) {
|
|
result = snmp_get(host, "invalid-regression-oid");
|
|
assert(strcmp(result, "U") == 0 && host->snmp.status == STAT_ERROR && !host->ignore_host);
|
|
free(result);
|
|
result = snmp_getnext(host, "invalid-regression-oid");
|
|
assert(strcmp(result, "U") == 0 && host->snmp.status == STAT_ERROR && !host->ignore_host);
|
|
free(result);
|
|
}
|
|
result = snmp_get_base(host, ".1.3.6.1.2.1.1.1.999", FALSE);
|
|
assert(!host->ignore_host);
|
|
if (host->snmp.profile.version == 2) assert(strcmp(result, "U") == 0);
|
|
else assert(strcmp(result, "") == 0);
|
|
free(result);
|
|
result = snmp_get(host, ".1.3.6.1.2.1.1.6.0");
|
|
assert(strstr(result, "isolated-regression-agent") != NULL && !host->ignore_host);
|
|
free(result);
|
|
if (host->snmp.profile.version == 2) {
|
|
result = snmp_get(host, ".1.3.6.1.2.1.1.1.999");
|
|
assert(strcmp(result, "U") == 0 && host->ignore_host);
|
|
free(result);
|
|
result = snmp_get(host, ".1.3.6.1.2.1.1.6.0");
|
|
assert(strcmp(result, "U") == 0);
|
|
free(result);
|
|
host->ignore_host = FALSE;
|
|
}
|
|
}
|
|
|
|
static void test_system_information(host_t *host) {
|
|
MYSQL mysql;
|
|
assert(mysql_init(&mysql) != NULL);
|
|
int previous_mibs = set.snmp.mibs;
|
|
set.snmp.mibs = FALSE;
|
|
STRNCOPY(host->system.snmp_sysLocation, "untouched");
|
|
get_system_information(host, &mysql, FALSE);
|
|
assert(strcmp(host->system.snmp_sysLocation, "untouched") == 0);
|
|
assert(host->system.snmp_sysUpTimeInstance > 0 && !host->ignore_host);
|
|
for (int explicit_update = 0; explicit_update <= 1; explicit_update++) {
|
|
set.snmp.mibs = !explicit_update;
|
|
get_system_information(host, &mysql, explicit_update);
|
|
assert(strcmp(host->system.snmp_sysLocation, "isolated-regression-agent") == 0);
|
|
assert(strcmp(host->system.snmp_sysContact, "regression") == 0);
|
|
assert(host->system.snmp_sysDescr[0] != '\0' && host->system.snmp_sysObjectID[0] != '\0');
|
|
assert(host->system.snmp_sysName[0] != '\0' && !host->ignore_host);
|
|
}
|
|
/* Missing sessions return allocated U responses; repeated short polls must free them. */
|
|
void *session = host->snmp.session;
|
|
int previous_ignore = host->ignore_host;
|
|
int previous_status = host->snmp.status;
|
|
host->snmp.session = NULL;
|
|
unsigned long long previous_uptime = host->system.snmp_sysUpTimeInstance;
|
|
set.snmp.mibs = FALSE;
|
|
for (int count = 0; count < 100; count++) get_system_information(host, &mysql, FALSE);
|
|
assert(host->system.snmp_sysUpTimeInstance == previous_uptime);
|
|
host->snmp.session = session;
|
|
host->ignore_host = previous_ignore;
|
|
host->snmp.status = previous_status;
|
|
set.snmp.mibs = previous_mibs;
|
|
mysql_close(&mysql);
|
|
}
|
|
|
|
static void test_snmp_multi_timeout(void) {
|
|
int previous_retries = set.snmp.snmp_retries;
|
|
set.snmp.snmp_retries = 1;
|
|
int silent = socket(AF_INET, SOCK_DGRAM, 0);
|
|
assert(silent >= 0);
|
|
struct sockaddr_in address = {0};
|
|
address.sin_family = AF_INET;
|
|
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
address.sin_port = 0;
|
|
assert(bind(silent, (struct sockaddr *)&address, sizeof(address)) == 0);
|
|
socklen_t address_length = sizeof(address);
|
|
assert(getsockname(silent, (struct sockaddr *)&address, &address_length) == 0);
|
|
assert(address.sin_family == AF_INET && ntohs(address.sin_port) > 0);
|
|
/* Keep the UDP endpoint bound, without receiving or answering requests.
|
|
* This produces a real transport timeout instead of ICMP port rejection. */
|
|
for (int version = 1; version <= 2; version++) {
|
|
host_t host = {0};
|
|
STRNCOPY(host.hostname, "127.0.0.1");
|
|
STRNCOPY(host.snmp.profile.community, "regression");
|
|
host.snmp.profile.version = version;
|
|
host.snmp.profile.port = ntohs(address.sin_port);
|
|
host.snmp.profile.timeout = 100;
|
|
host.snmp.session = snmp_host_init(&(snmp_connection_t){
|
|
.host_id = 1,
|
|
.hostname = host.hostname,
|
|
.snmp_version = host.snmp.profile.version,
|
|
.snmp_community = host.snmp.profile.community,
|
|
.snmp_username = "",
|
|
.snmp_password = "",
|
|
.snmp_auth_protocol = "SHA",
|
|
.snmp_priv_passphrase = "",
|
|
.snmp_priv_protocol = "[None]",
|
|
.snmp_context = "",
|
|
.snmp_engine_id = "",
|
|
.snmp_port = host.snmp.profile.port,
|
|
.snmp_timeout = host.snmp.profile.timeout,
|
|
});
|
|
assert(host.snmp.session != NULL);
|
|
const struct snmp_session *session = snmp_sess_session(host.snmp.session);
|
|
assert(session != NULL && session->timeout == 100000L && session->retries == 1);
|
|
const struct netsnmp_transport_s *transport = snmp_sess_transport(host.snmp.session);
|
|
assert(transport != NULL && transport->sock >= 0 && transport->sock != silent);
|
|
int session_fd = transport->sock;
|
|
target_t items[2] = {0};
|
|
snmp_oids_t oids[2] = {0};
|
|
STRNCOPY(oids[0].oid, ".1.3.6.1.2.1.1.3.0");
|
|
STRNCOPY(oids[1].oid, ".1.3.6.1.2.1.1.1.0");
|
|
for (int index = 0; index < 2; index++) {
|
|
oids[index].array_position = index;
|
|
items[index].local_data_id = 100 + index;
|
|
STRNCOPY(oids[index].result, "123");
|
|
}
|
|
snmp_get_multi(&host, items, oids, 2);
|
|
assert(host.snmp.status == STAT_TIMEOUT && host.ignore_host);
|
|
assert(IS_UNDEFINED(oids[0].result) && IS_UNDEFINED(oids[1].result));
|
|
assert(fcntl(session_fd, F_GETFD) >= 0);
|
|
snmp_host_cleanup(host.snmp.session);
|
|
host.snmp.session = NULL;
|
|
errno = 0;
|
|
assert(fcntl(session_fd, F_GETFD) == -1 && errno == EBADF);
|
|
}
|
|
/* Readiness proves requests reached our local fixture; do not drain them. */
|
|
assert(spine_wait_readable(silent, spine_monotonic_time() + 1) == 1);
|
|
assert(close(silent) == 0);
|
|
errno = 0;
|
|
assert(fcntl(silent, F_GETFD) == -1 && errno == EBADF);
|
|
set.snmp.snmp_retries = previous_retries;
|
|
puts("production local silent UDP SNMP multi timeout regressions passed");
|
|
}
|
|
|
|
static void test_snmp_agent(void) {
|
|
const char *address = getenv("SPINE_TEST_SNMP_HOST");
|
|
assert(address != NULL && address[0] != '\0');
|
|
snmp_spine_init();
|
|
set.snmp.snmp_retries = 0;
|
|
const int methods[] = {AVAIL_SNMP, AVAIL_SNMP_GET_SYSDESC, AVAIL_SNMP_GET_NEXT};
|
|
for (int version = 1; version <= 2; version++) {
|
|
host_t host = {0};
|
|
ping_t ping = {0};
|
|
strncopy(host.hostname, address, sizeof(host.hostname));
|
|
STRNCOPY(host.snmp.profile.community, "regression");
|
|
host.snmp.profile.version = version;
|
|
host.snmp.session = snmp_host_init(&(snmp_connection_t){
|
|
.host_id = 1,
|
|
.hostname = host.hostname,
|
|
.snmp_version = version,
|
|
.snmp_community = host.snmp.profile.community,
|
|
.snmp_username = "",
|
|
.snmp_password = "",
|
|
.snmp_auth_protocol = "SHA",
|
|
.snmp_priv_passphrase = "",
|
|
.snmp_priv_protocol = "[None]",
|
|
.snmp_context = "",
|
|
.snmp_engine_id = "",
|
|
.snmp_port = 1161,
|
|
.snmp_timeout = 500,
|
|
});
|
|
assert(host.snmp.session != NULL);
|
|
test_snmp_scalar_responses(&host);
|
|
test_snmp_multi_responses(&host);
|
|
test_system_information(&host);
|
|
assert(snmp_count(&host, ".1.3.6.1.2.1.1.6") == 1 && !host.ignore_host);
|
|
assert(snmp_count(&host, ".1.3.6.1.2.1.1.9999") == 0 && !host.ignore_host);
|
|
/* At the end of the entire MIB, v1 sends an agent error. The old
|
|
* walker repeated that same request forever. */
|
|
alarm(5);
|
|
double begin = spine_monotonic_time();
|
|
int terminal_count = snmp_count(&host, ".2.999");
|
|
alarm(0);
|
|
assert(terminal_count >= 0 && terminal_count <= 1);
|
|
assert(spine_monotonic_time() - begin < 2.0);
|
|
host.ignore_host = FALSE;
|
|
for (size_t index = 0; index < sizeof(methods) / sizeof(methods[0]); index++) {
|
|
host.availability.method = methods[index];
|
|
assert(ping_host(&host, &ping) == HOST_UP);
|
|
assert(strcmp(ping.snmp_response, "Device responded to SNMP") == 0);
|
|
assert(atof(ping.snmp_status) >= 0.0);
|
|
}
|
|
host.availability.ping_method = PING_TCP;
|
|
host.availability.port = -1;
|
|
host.availability.timeout = 100;
|
|
host.availability.method = AVAIL_SNMP_OR_PING;
|
|
assert(ping_host(&host, &ping) == HOST_UP);
|
|
assert(strcmp(ping.snmp_response, "Device responded to SNMP") == 0);
|
|
STRNCOPY(host.hostname, "localhost");
|
|
host.availability.method = AVAIL_SNMP_AND_PING;
|
|
assert(ping_host(&host, &ping) == HOST_UP);
|
|
snmp_host_cleanup(host.snmp.session);
|
|
}
|
|
test_snmp_multi_timeout();
|
|
snmp_spine_close();
|
|
}
|
|
|
|
static void test_host_status_transitions(void) {
|
|
config_t previous = set;
|
|
set.logging.log_level = POLLER_VERBOSITY_NONE;
|
|
set.availability.ping_failure_count = 2;
|
|
set.availability.ping_recovery_count = 2;
|
|
host_t host = {0};
|
|
ping_t ping = {0};
|
|
STRNCOPY(host.snmp.profile.community, "regression");
|
|
host.snmp.profile.version = 2;
|
|
host.state.status = HOST_UP;
|
|
host.statistics.min_time = 1000;
|
|
STRNCOPY(ping.ping_status, "4");
|
|
STRNCOPY(ping.snmp_status, "8");
|
|
STRNCOPY(ping.ping_response, "network unavailable");
|
|
STRNCOPY(ping.snmp_response, "SNMP unavailable");
|
|
update_host_status(HOST_DOWN, &host, &ping, AVAIL_SNMP_AND_PING);
|
|
assert(host.state.status == HOST_UP && host.state.status_event_count == 1 && host.state.status_fail_date[0] != '\0');
|
|
assert(strcmp(host.state.status_last_error, "SNMP unavailable, network unavailable") == 0);
|
|
update_host_status(HOST_DOWN, &host, &ping, AVAIL_SNMP_AND_PING);
|
|
assert(host.state.status == HOST_DOWN && host.state.status_event_count == 2);
|
|
update_host_status(HOST_UP, &host, &ping, AVAIL_SNMP_AND_PING);
|
|
assert(host.state.status == HOST_RECOVERING && host.state.status_event_count == 1 && host.state.status_rec_date[0] != '\0');
|
|
assert(host.statistics.cur_time == 6 && host.statistics.min_time == 6 && host.statistics.max_time == 6 && host.statistics.avg_time == 6);
|
|
update_host_status(HOST_DOWN, &host, &ping, AVAIL_SNMP);
|
|
assert(host.state.status == HOST_DOWN && host.state.status_event_count == 1);
|
|
update_host_status(HOST_UP, &host, &ping, AVAIL_SNMP);
|
|
update_host_status(HOST_UP, &host, &ping, AVAIL_PING);
|
|
assert(host.state.status == HOST_UP && host.state.status_event_count == 0);
|
|
assert(host.statistics.total_polls == 6 && host.statistics.failed_polls == 3 && host.statistics.availability == 50);
|
|
assert(host.statistics.cur_time == 4 && host.statistics.avg_time == 6 && host.statistics.min_time == 4 && host.statistics.max_time == 8);
|
|
const int methods[] = {AVAIL_NONE, AVAIL_SNMP, AVAIL_PING, AVAIL_SNMP_AND_PING, AVAIL_SNMP_OR_PING};
|
|
const double expected[] = {0, 8, 4, 6, 4};
|
|
for (size_t index = 0; index < sizeof(methods) / sizeof(methods[0]); index++) {
|
|
host_t sample = {0};
|
|
sample.statistics.min_time = 1000;
|
|
sample.snmp.profile.version = 3; /* v3 needs no community. */
|
|
update_host_status(HOST_UP, &sample, &ping, methods[index]);
|
|
assert(sample.state.status == HOST_UP && sample.statistics.cur_time == expected[index] && sample.statistics.avg_time == expected[index]);
|
|
}
|
|
host_t no_snmp = {0};
|
|
no_snmp.snmp.profile.version = 2;
|
|
update_host_status(HOST_UP, &no_snmp, &ping, AVAIL_SNMP);
|
|
assert(no_snmp.statistics.cur_time == 0);
|
|
update_host_status(HOST_DOWN, &no_snmp, &ping, AVAIL_SNMP);
|
|
assert(strcmp(no_snmp.state.status_last_error, "Device does not require SNMP") == 0);
|
|
set = previous;
|
|
}
|
|
|
|
static void test_availability_modes(void) {
|
|
host_t host = {0};
|
|
ping_t ping = {0};
|
|
STRNCOPY(host.hostname, "127.0.0.1");
|
|
STRNCOPY(host.snmp.profile.community, "regression");
|
|
host.snmp.profile.version = 2;
|
|
host.availability.ping_method = PING_TCP;
|
|
host.availability.port = -1;
|
|
host.availability.timeout = 100;
|
|
/* A failed network check must fall back to SNMP in OR mode. */
|
|
host.availability.method = AVAIL_SNMP_OR_PING;
|
|
assert(ping_host(&host, &ping) == HOST_DOWN);
|
|
assert(strcmp(ping.snmp_response, "Invalid SNMP Session") == 0);
|
|
STRNCOPY(ping.snmp_response, "untouched");
|
|
host.availability.method = AVAIL_SNMP_AND_PING;
|
|
assert(ping_host(&host, &ping) == HOST_DOWN);
|
|
assert(strcmp(ping.snmp_response, "untouched") == 0);
|
|
/* Preserve the existing localhost exemption, and OR short circuit. */
|
|
STRNCOPY(host.hostname, "localhost");
|
|
host.availability.method = AVAIL_SNMP_OR_PING;
|
|
assert(ping_host(&host, &ping) == HOST_UP);
|
|
assert(strcmp(ping.snmp_response, "untouched") == 0);
|
|
host.availability.method = AVAIL_SNMP_AND_PING;
|
|
assert(ping_host(&host, &ping) == HOST_DOWN);
|
|
assert(strcmp(ping.snmp_response, "Invalid SNMP Session") == 0);
|
|
host.availability.method = AVAIL_NONE;
|
|
assert(ping_host(&host, &ping) == HOST_UP);
|
|
host.availability.method = AVAIL_PING;
|
|
assert(ping_host(&host, &ping) == HOST_UP);
|
|
host.availability.method = AVAIL_STREAM;
|
|
assert(ping_host(&host, &ping) == HOST_DOWN);
|
|
host.availability.method = AVAIL_SNMP;
|
|
host.snmp.profile.community[0] = '\0';
|
|
assert(ping_host(&host, &ping) == HOST_UP);
|
|
host.snmp.profile.version = 3;
|
|
assert(ping_host(&host, &ping) == HOST_DOWN);
|
|
}
|
|
|
|
static void test_tcp_loopback(void) {
|
|
int server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
assert(server >= 0);
|
|
struct sockaddr_in address = {0};
|
|
assert(init_sockaddr(&address, "127.0.0.1", 0));
|
|
assert(bind(server, (struct sockaddr *)&address, sizeof(address)) == 0);
|
|
socklen_t length = sizeof(address);
|
|
assert(getsockname(server, (struct sockaddr *)&address, &length) == 0);
|
|
host_t host = {0};
|
|
STRNCOPY(host.hostname, "127.0.0.1");
|
|
host.availability.port = ntohs(address.sin_port);
|
|
host.availability.timeout = 100;
|
|
host.availability.retries = 0;
|
|
host.availability.ping_method = PING_TCP;
|
|
ping_t ping = {0};
|
|
/* Close the reservation so both Darwin and Linux return a refusal. */
|
|
assert(close(server) == 0);
|
|
assert(ping_tcp(&host, &ping) == HOST_DOWN);
|
|
host.availability.ping_method = PING_TCP_CLOSED;
|
|
assert(ping_tcp(&host, &ping) == HOST_UP);
|
|
assert(strcmp(ping.ping_response, "TCP: Device is Alive") == 0);
|
|
server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
assert(server >= 0);
|
|
address.sin_port = 0;
|
|
assert(bind(server, (struct sockaddr *)&address, sizeof(address)) == 0);
|
|
assert(getsockname(server, (struct sockaddr *)&address, &length) == 0);
|
|
host.availability.port = ntohs(address.sin_port);
|
|
assert(listen(server, 1) == 0);
|
|
host.availability.ping_method = PING_TCP;
|
|
assert(ping_tcp(&host, &ping) == HOST_UP);
|
|
int client = accept(server, NULL, NULL);
|
|
assert(client >= 0);
|
|
char byte;
|
|
assert(read(client, &byte, 1) == 0);
|
|
assert(close(client) == 0 && close(server) == 0);
|
|
host.availability.timeout = 0;
|
|
assert(ping_tcp(&host, &ping) == HOST_DOWN);
|
|
}
|
|
|
|
static void test_error_id_buffer_boundaries(void) {
|
|
config_t previous = set;
|
|
set.logging.log_destination = 0;
|
|
set.logging.log_level = POLLER_VERBOSITY_NONE;
|
|
char storage[DBL_BUFSIZE + 1] = {0};
|
|
storage[DBL_BUFSIZE] = 'G';
|
|
int size = 0;
|
|
int count = 0;
|
|
buffer_output_errors(storage, &size, &count, 1, 1, INT_MIN, FALSE);
|
|
buffer_output_errors(storage, &size, &count, 1, 1, 0, FALSE);
|
|
buffer_output_errors(storage, &size, &count, 1, 1, INT_MAX, FALSE);
|
|
assert(strcmp(storage, "-2147483648, 0, 2147483647") == 0);
|
|
assert(count == 3 && size == (int)strlen(storage));
|
|
assert(strpbrk(storage, "\r\n") == NULL && storage[DBL_BUFSIZE] == 'G');
|
|
/* Fill the caller-owned numeric list to its real boundary, then verify
|
|
* overflow starts a fresh record rather than overrunning storage. */
|
|
memset(storage, '7', DBL_BUFSIZE - 2);
|
|
storage[DBL_BUFSIZE - 2] = '\0';
|
|
size = DBL_BUFSIZE - 2;
|
|
count = 1;
|
|
buffer_output_errors(storage, &size, &count, 1, 1, INT_MAX, FALSE);
|
|
assert(strcmp(storage, "2147483647") == 0 && size == 10 && count == 1);
|
|
assert(storage[DBL_BUFSIZE] == 'G');
|
|
buffer_output_errors(storage, &size, &count, 1, 1, 0, TRUE);
|
|
assert(strcmp(storage, "2147483647") == 0 && size == 10 && count == 1);
|
|
set = previous;
|
|
puts("production numeric error-list boundary regressions passed");
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc == 3 && strcmp(argv[1], "--script-stream") == 0) return run_script_stream_fixture(argv[2]);
|
|
if (argc > 1 && strcmp(argv[1], "-q") == 0) return run_test_script_server(argc, argv);
|
|
extern int *debug_devices;
|
|
static int devices[100];
|
|
debug_devices = devices;
|
|
if (argc == 2 && strcmp(argv[1], "--raw-icmp") == 0) {
|
|
init_mutexes();
|
|
test_icmp_loopback();
|
|
puts("production ICMP loopback regression passed");
|
|
return 0;
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "--privilege-drop") == 0) {
|
|
test_privilege_contracts();
|
|
return 0;
|
|
}
|
|
if (argc == 3 && strcmp(argv[1], "--setuid-stress") == 0) {
|
|
test_setuid_stress(atoi(argv[2]));
|
|
return 0;
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "--setuid-icmp") == 0) {
|
|
test_setuid_icmp();
|
|
puts("production setuid ICMP regression passed");
|
|
return 0;
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "--icmp-no-capability") == 0) {
|
|
init_mutexes();
|
|
test_icmp_socket_failure();
|
|
puts("production ICMP socket-failure regression passed");
|
|
return 0;
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "--snmpv3-agent") == 0) {
|
|
init_mutexes();
|
|
test_snmpv3_agent_contracts();
|
|
return 0;
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "--snmp-agent") == 0) {
|
|
init_mutexes();
|
|
test_snmp_agent();
|
|
puts("production SNMP agent regressions passed");
|
|
return 0;
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "--database") == 0) {
|
|
init_mutexes();
|
|
test_database_configuration();
|
|
puts("production database configuration regressions passed");
|
|
return 0;
|
|
}
|
|
test_string_conversions();
|
|
test_result_count_range();
|
|
test_cli_option_shape();
|
|
test_concurrent_permits();
|
|
test_copy_bounds();
|
|
test_database_escape();
|
|
test_database_addresses();
|
|
test_database_option_failure();
|
|
test_regex_concurrency();
|
|
test_regex_storage();
|
|
test_formatting();
|
|
test_secret_clear();
|
|
test_log_boundary();
|
|
test_log_sanitization();
|
|
test_log_append_and_failures();
|
|
test_config_directives();
|
|
test_config_unknown_lines_not_echoed();
|
|
test_config_structure_bindings();
|
|
test_date_formats();
|
|
test_device_logging();
|
|
test_debug_device_bounds();
|
|
test_poll_result_formats();
|
|
test_hostnames();
|
|
test_udp_deadline();
|
|
test_tcp_loopback();
|
|
test_availability_modes();
|
|
test_icmp_reply_bounds();
|
|
test_snmp_initialization_failure();
|
|
test_snmp_security_protocols();
|
|
test_child_process();
|
|
alarm(10);
|
|
test_duplex_process();
|
|
test_write_process();
|
|
test_process_descriptor_aliases();
|
|
alarm(0);
|
|
test_php_response(4, true);
|
|
test_php_response(RESULTS_BUFFER - 1, true);
|
|
test_php_response(RESULTS_BUFFER, false);
|
|
test_php_partial_response_timeout();
|
|
init_mutexes();
|
|
test_php_command(4);
|
|
test_php_command(BUFSIZE - 3);
|
|
test_invalid_php_commands();
|
|
test_php_failure_logs_omit_command();
|
|
test_php_startup(argv[0]);
|
|
test_php_owned_shutdown();
|
|
test_host_status_transitions();
|
|
test_script_execution();
|
|
test_script_timeout_kills_descendants();
|
|
test_pclose_kills_descendants();
|
|
test_script_stream_contracts();
|
|
test_cli_alias_contracts();
|
|
test_privilege_contracts();
|
|
test_error_id_buffer_boundaries();
|
|
test_additional_contracts();
|
|
puts("production regression tests passed");
|
|
return 0;
|
|
}
|