mirror of
https://github.com/Cacti/spine.git
synced 2026-10-23 03:27:10 +09:00
refactor(core): add bounded native interfaces and polling contracts (#638)
Introduce bounded native interfaces and grouped runtime state, then link regression and fault contracts against the real poller implementations. The contracts capture CLI/configuration input, availability combinations, SNMP request/response ownership, script framing, reindexing, output persistence, worker completion and shutdown. This foundational change is larger because the shared interfaces and their existing production contracts must move together; later changes have separate review boundaries. Step 1/14 of the complete C17/native-correctness series. Base: develop. Earlier validation, before this review follow-up, in isolated copied snapshots on Linux arm64: - Clean GCC build and make check: 13 passed, 0 existing prerequisite skips, 0 failures. - ASan/UBSan: 12 passed, 1 existing privilege/allocator-interposition prerequisite skips, 0 failures; no sanitizer findings. - Production source/profile integrity verification against disposable MariaDB and loopback SNMP fixtures passed. - Release archive extracted into a separate source tree and built/tested out of tree; fuzz checks passed. Earlier local profiles retain source hashes for the copied validation tree; GitHub checks validate the published commit. No production database/device, host installation or live privilege change is part of validation. Existing prerequisite skips were retained; no test or finding was suppressed to obtain a pass. The worker contracts publish owned heap state and release it only after joining workers and restoring the runtime table. Coverage counters use atomic updates; cppcheck receives an explicit model of the fatal exit contract. Fixture images use public mirrors to avoid shared Docker Hub pull limits. Series navigation: review and merge in order. Every PR targets the preceding branch so its diff contains one step. 1. [refactor(core): add bounded native interfaces and polling contracts](https://github.com/Cacti/spine/pull/638) 2. [fix(privileges): drop root before input and retain owned ICMP sockets](https://github.com/Cacti/spine/pull/639) 3. [fix(poller): isolate worker failures and bound database reconnects](https://github.com/Cacti/spine/pull/640) 4. [fix(results): normalize output and unify SNMP batch persistence](https://github.com/Cacti/spine/pull/641) 5. [fix(runtime): bound probe retries and validate CLI and configuration](https://github.com/Cacti/spine/pull/642) 6. [build: select GNU C17 explicitly and probe hardening support](https://github.com/Cacti/spine/pull/643) 7. [fix(threads): size worker stacks and guard size arithmetic](https://github.com/Cacti/spine/pull/644) 8. [refactor(c17): simplify ownership guards and share test support](https://github.com/Cacti/spine/pull/645) 9. [refactor(core): extract configuration polling and runtime modules](https://github.com/Cacti/spine/pull/646) 10. [fix(boundaries): validate script slots and make cleanup portable](https://github.com/Cacti/spine/pull/647) 11. [refactor(layout): organize responsibilities and separate main from runtime](https://github.com/Cacti/spine/pull/648) 12. [docs(license): adopt SPDX notices and retain contributor credits](https://github.com/Cacti/spine/pull/649) 13. [test(runtime): measure all suites and cover protocol boundaries](https://github.com/Cacti/spine/pull/650) 14. [build(ci): validate connectors time width and native C17 modules](https://github.com/Cacti/spine/pull/651) Review follow-up (2026-10-09): Publish the decoded GET/GETNEXT status back to the host so availability checks observe exception and missing-response failures, while preserving the legacy transport-success behavior for agent PDU errors. Updated step 1 snapshot: isolated Linux arm64 build and make check passed (13 passed, no skips or failures). The restacked tip passes all five clang-tidy gate regression tests and real clang-tidy analysis (26 reviewed baseline findings, no new findings). Actionlint and matching DCO trailers pass. The setuid test also passes when run in the separate disposable root container. GitHub checks validate the updated published commits; earlier profile/archive/sanitizer evidence above belongs to the previous revisions. --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
1 parent
dd73131f58
commit
3065c6fc23
61 files changed
+12267
-7177
No files matched your search
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches: [develop, '1.2.x']
|
||||
pull_request:
|
||||
branches: [develop, '1.2.x']
|
||||
branches: [develop, '1.2.x', 'refactor/c17-series-*']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -166,6 +166,7 @@ jobs:
|
||||
--suppress=constVariablePointer \
|
||||
--suppress=redundantAssignment \
|
||||
--suppress=toomanyconfigs \
|
||||
--library=scripts/cppcheck/spine.cfg \
|
||||
./*.c ./*.h
|
||||
|
||||
shellcheck:
|
||||
@@ -187,6 +188,19 @@ jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
services:
|
||||
database:
|
||||
image: mirror.gcr.io/library/mariadb:10.11
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: '1'
|
||||
MARIADB_DATABASE: spine_regressions
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: >-
|
||||
--health-cmd="mariadb-admin ping --silent"
|
||||
--health-interval=5s
|
||||
--health-timeout=5s
|
||||
--health-retries=20
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
@@ -194,8 +208,8 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/build-spine
|
||||
with:
|
||||
extra-packages: gcovr python3-pip
|
||||
cflags: -g -O0 --coverage
|
||||
extra-packages: gcovr python3-pip mariadb-client snmpd libcap2-bin
|
||||
cflags: -g -O0 --coverage -fprofile-update=atomic
|
||||
ldflags: --coverage
|
||||
|
||||
- name: Build instrumented
|
||||
@@ -208,6 +222,32 @@ jobs:
|
||||
set -euo pipefail
|
||||
make check
|
||||
|
||||
- name: Cover production database and protocol scenarios
|
||||
env:
|
||||
SPINE_TEST_DB_HOST: 127.0.0.1
|
||||
SPINE_TEST_SNMP_HOST: 127.0.0.1
|
||||
SPINE_SNMPV3_EVIDENCE_DIR: snmpv3-evidence
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mariadb --protocol=TCP --host=127.0.0.1 --user=root spine_regressions < tests/fixtures/configuration.sql
|
||||
mariadb --protocol=TCP --host=127.0.0.1 --user=root spine_regressions < tests/fixtures/poller.sql
|
||||
snmpd -f -Lo -C -c tests/fixtures/snmpd.conf > coverage-snmp-agent.log 2>&1 &
|
||||
agent_pid=$!
|
||||
cleanup() {
|
||||
sudo setcap -r ./test_spine_regressions
|
||||
kill "$agent_pid"
|
||||
wait "$agent_pid" || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
sleep 1
|
||||
./test_spine_regressions --database
|
||||
./test_spine_regressions --snmp-agent
|
||||
./test_spine_faults --database
|
||||
tests/tools/run_snmpv3.sh
|
||||
./test_spine_regressions --icmp-no-capability
|
||||
sudo setcap cap_net_raw+ep ./test_spine_regressions
|
||||
./test_spine_regressions --raw-icmp
|
||||
|
||||
- name: Report coverage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Integration
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
branches: [develop, 'refactor/c17-series-*']
|
||||
paths: ['**.c', '**.h', 'Dockerfile', '.dockerignore', 'tests/integration/**', 'tests/test-harness.sh', 'tests/snmpv3/**', '.github/workflows/integration.yml']
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
name: Production regressions
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
regressions:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
services:
|
||||
database:
|
||||
image: mirror.gcr.io/library/mariadb:10.11
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: '1'
|
||||
MARIADB_DATABASE: spine_regressions
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: >-
|
||||
--health-cmd="mariadb-admin ping --silent"
|
||||
--health-interval=5s
|
||||
--health-timeout=5s
|
||||
--health-retries=20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
set -eu
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y autoconf automake libtool help2man libmariadb-dev mariadb-client libsnmp-dev snmpd libssl-dev libcap2-bin lcov
|
||||
- name: Build and test production code
|
||||
run: |
|
||||
set -eu
|
||||
mkdir -p config m4
|
||||
autoreconf -fi
|
||||
./configure
|
||||
make -j2 check
|
||||
- name: Verify database configuration loading
|
||||
env:
|
||||
SPINE_TEST_DB_HOST: 127.0.0.1
|
||||
run: |
|
||||
set -eu
|
||||
mariadb --protocol=TCP --host=127.0.0.1 --user=root spine_regressions < tests/fixtures/configuration.sql
|
||||
mariadb --protocol=TCP --host=127.0.0.1 --user=root spine_regressions < tests/fixtures/poller.sql
|
||||
./test_spine_regressions --database
|
||||
- name: Verify SNMP availability against local agent
|
||||
env:
|
||||
SPINE_TEST_SNMP_HOST: 127.0.0.1
|
||||
SPINE_TEST_DB_HOST: 127.0.0.1
|
||||
run: |
|
||||
set -eu
|
||||
snmpd -f -Lo -C -c tests/fixtures/snmpd.conf > snmp-agent.log 2>&1 &
|
||||
agent_pid=$!
|
||||
trap 'kill "$agent_pid"; wait "$agent_pid" || true' EXIT
|
||||
sleep 1
|
||||
./test_spine_regressions --snmp-agent
|
||||
./test_spine_regressions --database
|
||||
./test_spine_faults --database
|
||||
- name: Verify authenticated SNMPv3 against owned agent
|
||||
env:
|
||||
SPINE_SNMPV3_EVIDENCE_DIR: snmpv3-evidence
|
||||
run: tests/tools/run_snmpv3.sh
|
||||
- name: Verify ICMP against local loopback
|
||||
run: |
|
||||
set -eu
|
||||
./test_spine_regressions --icmp-no-capability
|
||||
sudo setcap cap_net_raw+ep ./test_spine_regressions
|
||||
./test_spine_regressions --raw-icmp
|
||||
- name: Measure every production translation unit
|
||||
env:
|
||||
SPINE_TEST_SNMP_HOST: 127.0.0.1
|
||||
SPINE_TEST_DB_HOST: 127.0.0.1
|
||||
run: |
|
||||
set -eu
|
||||
snmpd -f -Lo -C -c tests/fixtures/snmpd.conf > coverage-snmp-agent.log 2>&1 &
|
||||
agent_pid=$!
|
||||
trap 'kill "$agent_pid"; wait "$agent_pid" || true' EXIT
|
||||
sleep 1
|
||||
tests/tools/run_coverage.sh
|
||||
- name: Preserve coverage and source provenance
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: production-coverage-${{ github.sha }}
|
||||
path: coverage/
|
||||
if-no-files-found: error
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
schedule:
|
||||
- cron: "37 3 * * 6"
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
branches: [develop, 'refactor/c17-series-*']
|
||||
paths:
|
||||
- "configure.ac"
|
||||
- "Makefile.am"
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/src:ro" \
|
||||
--workdir /workspace \
|
||||
rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
|
||||
mirror.gcr.io/library/rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
|
||||
bash -euo pipefail -c '
|
||||
cp -a /src/. /workspace/
|
||||
dnf -y group install "Development Tools"
|
||||
|
||||
+2
-3
@@ -1,5 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM debian:bookworm-slim AS builder
|
||||
FROM mirror.gcr.io/library/debian:bookworm-slim AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
@@ -20,7 +19,7 @@ RUN autoreconf -fi \
|
||||
&& ./configure --prefix=/usr/local \
|
||||
&& make -j"$(nproc)" spine
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
FROM mirror.gcr.io/library/debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libmariadb3 \
|
||||
|
||||
+43
-7
@@ -22,11 +22,12 @@
|
||||
AUTOMAKE_OPTIONS = foreign
|
||||
ACLOCAL_AMFLAGS = -I m4
|
||||
|
||||
spine_SOURCES = sql.c spine.c util.c snmp.c src/core/locks.c poller.c \
|
||||
spine_shared_sources = sql.c util.c snmp.c src/core/locks.c poller.c \
|
||||
nft_popen.c php.c ping.c src/core/keywords.c error.c
|
||||
spine_SOURCES = $(spine_shared_sources) spine.c
|
||||
|
||||
configdir = $(sysconfdir)
|
||||
config_DATA = spine.conf.dist
|
||||
dist_config_DATA = spine.conf.dist
|
||||
|
||||
# Seed a live config on first install without ever overwriting an existing one.
|
||||
install-data-hook:
|
||||
@@ -46,12 +47,35 @@ uninstall-hook:
|
||||
bin_PROGRAMS = spine
|
||||
|
||||
man_MANS = spine.1
|
||||
DISTCLEANFILES = spine.1
|
||||
|
||||
check_PROGRAMS = test_spine_regressions
|
||||
dist_noinst_DATA = tests/fixtures/configuration.sql tests/fixtures/poller.sql tests/fixtures/snmpd.conf tests/fixtures/snmpv3-agent.conf tests/fixtures/snmpv3-users.conf
|
||||
TESTS = $(check_PROGRAMS)
|
||||
test_spine_regressions_SOURCES = $(spine_SOURCES) tests/test_spine_regressions.c tests/test_spine_contracts.c tests/test_spine_reindex_contracts.c tests/test_spine_snmpv3_contracts.c tests/test_spine_settings_contracts.c tests/test_spine_cli_contracts.c tests/test_spine_stream_contracts.c tests/test_spine_output_contracts.c
|
||||
# Link the real production translation units and globals, with a separate test
|
||||
# entry point. Only spine.c maps its own entry point in the test build.
|
||||
test_spine_regressions_CPPFLAGS = $(AM_CPPFLAGS) -I$(srcdir) -DSPINE_TEST_PROGRAM_ENTRY
|
||||
|
||||
if HAVE_LINKER_WRAP
|
||||
check_PROGRAMS += test_spine_faults
|
||||
# Reuse the real renamed spine object for its production globals, avoiding
|
||||
# another production main-renaming flag on the fault-test translation units.
|
||||
test_spine_faults_SOURCES = $(spine_shared_sources) tests/test_spine_faults.c
|
||||
test_spine_faults_CPPFLAGS = $(AM_CPPFLAGS) -I$(srcdir)
|
||||
test_spine_faults_LDADD = test_spine_regressions-spine.$(OBJEXT)
|
||||
test_spine_faults_LDFLAGS = $(AM_LDFLAGS) -Wl,--wrap=strftime -Wl,--wrap=mysql_query -Wl,--wrap=mysql_ping -Wl,--wrap=mysql_errno \
|
||||
-Wl,--wrap=pipe -Wl,--wrap=socketpair -Wl,--wrap=posix_spawn \
|
||||
-Wl,--wrap=malloc -Wl,--wrap=strdup \
|
||||
-Wl,--wrap=snmp_sess_open -Wl,--wrap=snmp_sess_close -Wl,--wrap=pthread_create
|
||||
endif
|
||||
|
||||
|
||||
# Headers are not referenced by any _SOURCES, so automake will not distribute
|
||||
# them unless they are listed. Without this the release tarball builds nothing.
|
||||
noinst_HEADERS = common.h error.h src/core/keywords.h src/core/locks.h \
|
||||
nft_popen.h php.h ping.h poller.h snmp.h spine.h spine_sem.h sql.h \
|
||||
uthash.h util.h
|
||||
nft_popen.h php.h ping.h poller.h snmp.h spine.h spine_sem.h spine_types.h \
|
||||
sql.h uthash.h util.h
|
||||
|
||||
EXTRA_DIST = spine.1 spine.conf.dist \
|
||||
tests/README.md \
|
||||
@@ -59,6 +83,18 @@ EXTRA_DIST = spine.1 spine.conf.dist \
|
||||
tests/integration/test_concurrency.sh \
|
||||
tests/integration/test_network_faults.sh
|
||||
|
||||
EXTRA_DIST += tests/tools/run_snmpv3.sh tests/tools/run_coverage.sh tests/tools/coverage_process_exit.h \
|
||||
tests/tools/verify_coverage.sh tests/tools/test_coverage_verifier.sh
|
||||
.PHONY: coverage-source-list coverage-test-source-list
|
||||
coverage-source-list:
|
||||
@printf '%s\n' $(spine_SOURCES)
|
||||
|
||||
coverage-test-source-list:
|
||||
@printf '%s\n' $(test_spine_regressions_SOURCES)
|
||||
|
||||
.PHONY: coverage-fault-source-list
|
||||
coverage-fault-source-list:
|
||||
@printf '%s\n' $(test_spine_faults_SOURCES)
|
||||
# Docker targets — require Dockerfile and Dockerfile.dev (from PR #401)
|
||||
.PHONY: docker docker-dev verify cppcheck
|
||||
|
||||
@@ -76,8 +112,6 @@ cppcheck: docker-dev
|
||||
|
||||
AM_CPPFLAGS = -I$(top_srcdir) -I$(top_srcdir)/src/core -I$(top_builddir)
|
||||
|
||||
check_PROGRAMS =
|
||||
|
||||
if HAVE_CMOCKA
|
||||
check_PROGRAMS += \
|
||||
tests/unit/test_util_strings \
|
||||
@@ -162,6 +196,8 @@ tests_unit_test_php_runtime_DEPENDENCIES = tests/unit/php_test_server$(EXEEXT)
|
||||
|
||||
TEST_EXTENSIONS = .sh
|
||||
SH_LOG_COMPILER = $(SHELL)
|
||||
TESTS = $(check_PROGRAMS) tests/regression/test_remote_push.sh tests/integration/test_alloc_failure.sh tests/regression/test_child_process_safety.sh
|
||||
TESTS += tests/regression/test_remote_push.sh tests/integration/test_alloc_failure.sh tests/regression/test_child_process_safety.sh
|
||||
TESTS_ENVIRONMENT = top_builddir='$(abs_top_builddir)'
|
||||
EXTRA_DIST += tests/regression/test_remote_push.sh tests/integration/test_alloc_failure.sh tests/regression/test_child_process_safety.sh
|
||||
|
||||
EXTRA_DIST += scripts/cppcheck/spine.cfg
|
||||
+995
-61
File diff suppressed because it is too large.
Load diff
@@ -134,11 +134,6 @@
|
||||
# include <priv.h>
|
||||
#endif
|
||||
|
||||
#undef PACKAGE_NAME
|
||||
#undef PACKAGE_VERSION
|
||||
#undef PACKAGE_BUGREPORT
|
||||
#undef PACKAGE_STRING
|
||||
#undef PACKAGE_TARNAME
|
||||
#include <net-snmp/net-snmp-config.h>
|
||||
#include <net-snmp/net-snmp-includes.h>
|
||||
#include <net-snmp/types.h>
|
||||
@@ -146,7 +141,6 @@
|
||||
#include <net-snmp/config_api.h>
|
||||
#include <net-snmp/library/snmpv3.h>
|
||||
#include <net-snmp/library/snmp_parse_args.h>
|
||||
//#include <net-snmp/mib_api.h>
|
||||
#include <net-snmp/utilities.h>
|
||||
|
||||
#include <net-snmp/library/snmp_api.h>
|
||||
|
||||
@@ -34,6 +34,22 @@ AM_INIT_AUTOMAKE([foreign subdir-objects])
|
||||
AM_SILENT_RULES([yes])
|
||||
AC_CONFIG_HEADERS(config/config.h)
|
||||
|
||||
# These generic package macros belong to the generated configuration header.
|
||||
# Leave them available for Net-SNMP to define; snmp_spine_init uses its
|
||||
# PACKAGE_VERSION when header/library version verification is enabled.
|
||||
# Continue each directive so config.status preserves the literal cleanup
|
||||
# instead of substituting another package definition into the header footer.
|
||||
AH_BOTTOM([#undef \
|
||||
PACKAGE_NAME
|
||||
#undef \
|
||||
PACKAGE_VERSION
|
||||
#undef \
|
||||
PACKAGE_BUGREPORT
|
||||
#undef \
|
||||
PACKAGE_STRING
|
||||
#undef \
|
||||
PACKAGE_TARNAME])
|
||||
|
||||
# static libraries
|
||||
AC_ARG_WITH(static,
|
||||
AS_HELP_STRING([--with-static],[Build using static libraries
|
||||
@@ -499,6 +515,23 @@ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
|
||||
],[AC_MSG_RESULT(no)
|
||||
])
|
||||
|
||||
spine_saved_ldflags=$LDFLAGS
|
||||
spine_linker_wrap=no
|
||||
case "$host_os" in
|
||||
linux*)
|
||||
LDFLAGS="$LDFLAGS -Wl,--wrap=strftime"
|
||||
AC_LINK_IFELSE([AC_LANG_PROGRAM([[
|
||||
#include <time.h>
|
||||
extern size_t __real_strftime(char *, size_t, const char *, const struct tm *);
|
||||
size_t __wrap_strftime(char *out, size_t size, const char *format, const struct tm *time) {
|
||||
return __real_strftime(out, size, format, time);
|
||||
}
|
||||
]], [[return 0;]])], [spine_linker_wrap=yes], [])
|
||||
;;
|
||||
*) ;;
|
||||
esac
|
||||
LDFLAGS=$spine_saved_ldflags
|
||||
AM_CONDITIONAL([HAVE_LINKER_WRAP], [test "$spine_linker_wrap" = yes])
|
||||
# cmocka is only used by the unit tests under "make check". Absence is not
|
||||
# fatal; the tests are dropped from check_PROGRAMS instead.
|
||||
have_cmocka=no
|
||||
|
||||
+17
-15
@@ -21,7 +21,8 @@
|
||||
# +-------------------------------------------------------------------------+
|
||||
|
||||
update_copyright() {
|
||||
local file=$1
|
||||
local original_file=$1
|
||||
local file=$original_file
|
||||
file=${file/$SCRIPT_BASE/}
|
||||
printf -v line "%60s" "$file"
|
||||
if [[ -z "$ERRORS_ONLY" ]]; then
|
||||
@@ -30,19 +31,19 @@ update_copyright() {
|
||||
fi
|
||||
|
||||
old_reg="20[0-9][0-9][ ]*-[ ]*20[0-9][0-9]"
|
||||
old_data=$(grep -c -e "$old_reg" "$1" 2>/dev/null)
|
||||
old_data=$(grep -c -e "$old_reg" "$original_file" 2>/dev/null)
|
||||
new_reg="2004-$YEAR"
|
||||
result=$?
|
||||
|
||||
|
||||
if [[ $old_data -eq 0 ]]; then
|
||||
old_reg="(Copyright.*) 20[0-9][0-9] "
|
||||
old_data=$(grep -c -e "$old_reg" "$1" 2>/dev/null)
|
||||
old_data=$(grep -c -e "$old_reg" "$original_file" 2>/dev/null)
|
||||
new_reg="\1 2004-$YEAR"
|
||||
result=$?
|
||||
|
||||
fi
|
||||
|
||||
if [[ $old_data -gt 0 ]]; then
|
||||
old_data=$(grep -e "$old_reg" "$1" 2>/dev/null)
|
||||
old_data=$(grep -e "$old_reg" "$original_file" 2>/dev/null)
|
||||
new_data=$(echo "$old_data" | sed -r s/"$old_reg"/"$new_reg"/g)
|
||||
if [[ "$old_data" == "$new_data" ]]; then
|
||||
if [[ -z "$ERRORS_ONLY" ]]; then
|
||||
@@ -50,16 +51,17 @@ update_copyright() {
|
||||
fi
|
||||
else
|
||||
echo "$line Updating Copyright Data"
|
||||
printf "%60s %s\n" "==============================" "===================="
|
||||
printf "%60s %s\n" "$old_data" "=>"
|
||||
printf "%60s %s\n" "$new_data" ""
|
||||
sed -i -r s/"$old_reg"/"$new_reg"/g "$1"
|
||||
printf "%60s %s\n" "==============================" "===================="
|
||||
printf '%60s %s\n' "==============================" "===================="
|
||||
printf '%60s %s\n' "$old_data" "=>"
|
||||
printf '%60s %s\n' "$new_data" ""
|
||||
sed -i -r s/"$old_reg"/"$new_reg"/g "$original_file"
|
||||
printf '%60s %s\n' "==============================" "===================="
|
||||
fi
|
||||
else
|
||||
echo "$line Copyright not found!"
|
||||
SCRIPT_ERR=1
|
||||
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
|
||||
@@ -71,11 +73,11 @@ for f in $BAD_FOLDERS; do
|
||||
SCRIPT_EXCLUSION="$SCRIPT_EXCLUSION -not -path ${SCRIPT_BASE}$f/\* "
|
||||
done
|
||||
|
||||
SCRIPT_ERR=0
|
||||
|
||||
YEAR=$(date +"%Y")
|
||||
EXT="" # "sh sql php js md conf c h ac dist"
|
||||
ERRORS_ONLY=1
|
||||
while [ -n "$1" ]; do
|
||||
while [[ -n "$1" ]]; do
|
||||
case $1 in
|
||||
"--help")
|
||||
echo "NOTE: Checks all Cacti pages for this years copyright"
|
||||
@@ -103,7 +105,7 @@ done
|
||||
SCRIPT_INCLUSION=
|
||||
SCRIPT_SEPARATOR=
|
||||
for ext in $EXT; do
|
||||
if [ -n "$SCRIPT_INCLUSION" ]; then
|
||||
if [[ -n "$SCRIPT_INCLUSION" ]]; then
|
||||
SCRIPT_SEPARATOR="-o "
|
||||
fi
|
||||
SCRIPT_INCLUSION="$SCRIPT_INCLUSION $SCRIPT_SEPARATOR-name \*.$ext"
|
||||
|
||||
@@ -108,7 +108,7 @@ static void spine_signal_handler(int spine_signal) {
|
||||
signal(spine_signal, SIG_DFL);
|
||||
}
|
||||
|
||||
set.exit_code = spine_signal;
|
||||
set.exit.exit_code = spine_signal;
|
||||
|
||||
switch (spine_signal) {
|
||||
case SIGABRT:
|
||||
|
||||
+30
-4
@@ -361,13 +361,21 @@ int nft_popen(const char * command, const char * type) {
|
||||
int cancel_state;
|
||||
extern char **environ;
|
||||
int retry_count = 0;
|
||||
/* nft_popen() reports why process creation failed through errno, as
|
||||
* popen() does; cleanup below must not overwrite it. */
|
||||
int failure_errno = 0;
|
||||
|
||||
if (command == NULL || type == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* On platforms where pipe() is bidirectional,
|
||||
* "r+" gives two-way communication.
|
||||
* "r+" gives two-way communication. Only the documented modes are
|
||||
* accepted; any other "+" mode is a caller error, not a duplex request.
|
||||
*/
|
||||
if (strchr(type, '+')) {
|
||||
if (strcmp(type, "r+") == 0) {
|
||||
twoway = 1;
|
||||
type = "r+";
|
||||
}else {
|
||||
twoway = 0;
|
||||
if ((*type != 'r' && *type != 'w') || type[1]) {
|
||||
@@ -376,8 +384,20 @@ int nft_popen(const char * command, const char * type) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!spine_open_pipe_cloexec(pdes))
|
||||
/* An ordinary pipe is one-way on Linux and macOS, so the duplex mode
|
||||
* needs a socket pair to be readable and writable at the parent end. */
|
||||
if (twoway) {
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, pdes) < 0)
|
||||
return -1;
|
||||
|
||||
if (spine_set_cloexec(pdes[0]) != 0 || spine_set_cloexec(pdes[1]) != 0) {
|
||||
(void)close(pdes[0]);
|
||||
(void)close(pdes[1]);
|
||||
return -1;
|
||||
}
|
||||
} else if (!spine_open_pipe_cloexec(pdes)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Disable thread cancellation from this point forward. */
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cancel_state);
|
||||
@@ -449,6 +469,7 @@ int nft_popen(const char * command, const char * type) {
|
||||
inherit_fd = spine_dup_cloexec(pdes[1]);
|
||||
|
||||
if (inherit_fd < 0) {
|
||||
failure_errno = errno;
|
||||
SPINE_LOG(("ERROR: Unable to duplicate the pipe for the child: %s", strerror(errno)));
|
||||
goto spawn_failed;
|
||||
}
|
||||
@@ -467,6 +488,7 @@ int nft_popen(const char * command, const char * type) {
|
||||
inherit_fd = spine_dup_cloexec(pdes[0]);
|
||||
|
||||
if (inherit_fd < 0) {
|
||||
failure_errno = errno;
|
||||
SPINE_LOG(("ERROR: Unable to duplicate the pipe for the child: %s", strerror(errno)));
|
||||
goto spawn_failed;
|
||||
}
|
||||
@@ -508,6 +530,7 @@ int nft_popen(const char * command, const char * type) {
|
||||
goto retry;
|
||||
}
|
||||
|
||||
failure_errno = spawn_err;
|
||||
SPINE_LOG(("ERROR: SCRIPT: posix_spawn failed: %s", strerror(spawn_err)));
|
||||
|
||||
spawn_failed:
|
||||
@@ -529,6 +552,9 @@ spawn_failed:
|
||||
free(command_copy);
|
||||
free(cur);
|
||||
pthread_setcancelstate(cancel_state, NULL);
|
||||
if (failure_errno != 0) {
|
||||
errno = failure_errno;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ char *php_cmd(const char *php_command, int php_process) {
|
||||
int retries = 0;
|
||||
|
||||
assert(php_command != 0);
|
||||
if (php_processes == NULL || php_process < 0 || php_process >= set.php_servers ||
|
||||
if (php_processes == NULL || php_process < 0 || php_process >= set.php.php_servers ||
|
||||
php_process >= MAX_PHP_SERVERS) {
|
||||
SPINE_LOG(("ERROR: SS[%i] PHP Script Server slot is unavailable", php_process));
|
||||
return php_undefined_result();
|
||||
@@ -245,15 +245,15 @@ int php_get_process(void) {
|
||||
int start_candidate;
|
||||
int server_count;
|
||||
|
||||
if (php_processes == NULL || set.php_servers <= 0) return -1;
|
||||
server_count = set.php_servers > MAX_PHP_SERVERS ? MAX_PHP_SERVERS : set.php_servers;
|
||||
if (php_processes == NULL || set.php.php_servers <= 0) return -1;
|
||||
server_count = set.php.php_servers > MAX_PHP_SERVERS ? MAX_PHP_SERVERS : set.php.php_servers;
|
||||
|
||||
/* LOCK_PHP protects only the round-robin cursor. A startup handshake can
|
||||
* wait for script_timeout, so it must never run under this process-global
|
||||
* lock. */
|
||||
thread_mutex_lock(LOCK_PHP);
|
||||
if (set.php_current_server >= server_count) set.php_current_server = 0;
|
||||
start_candidate = set.php_current_server++;
|
||||
if (set.php.php_current_server >= server_count) set.php.php_current_server = 0;
|
||||
start_candidate = set.php.php_current_server++;
|
||||
thread_mutex_unlock(LOCK_PHP);
|
||||
|
||||
/* Prefer any ready slot. Each snapshot uses the same per-slot mutex as
|
||||
@@ -352,6 +352,7 @@ static char *php_read_result(int php_process, char *command, int allow_restart)
|
||||
double remaining_usec = 0;
|
||||
char *result_string;
|
||||
int response_timeout;
|
||||
double read_deadline;
|
||||
|
||||
ssize_t i;
|
||||
char *cp;
|
||||
@@ -377,10 +378,14 @@ static char *php_read_result(int php_process, char *command, int allow_restart)
|
||||
/* Selection-path recovery runs this handshake while holding the slot lock.
|
||||
* Bound startup independently so a bad PHP configuration cannot pin every
|
||||
* selector for the full per-command timeout. */
|
||||
response_timeout = allow_restart || set.script_timeout < 2 ? set.script_timeout : 2;
|
||||
response_timeout = allow_restart || set.php.script_timeout < 2 ? set.php.script_timeout : 2;
|
||||
timeout.tv_sec = response_timeout;
|
||||
timeout.tv_usec = 0;
|
||||
|
||||
/* select() only says the first bytes arrived. A server that writes part of
|
||||
* a line and stalls must not hold this thread past the same deadline. */
|
||||
read_deadline = spine_monotonic_time() + response_timeout;
|
||||
|
||||
/* check to see which pipe talked and take action
|
||||
* should only be the READ pipe */
|
||||
retry:
|
||||
@@ -476,8 +481,24 @@ static char *php_read_result(int php_process, char *command, int allow_restart)
|
||||
}
|
||||
|
||||
size_t space = (size_t)RESULTS_BUFFER - 1 - used;
|
||||
|
||||
if (used > 0) {
|
||||
int ready = spine_wait_readable(php_processes[php_process].php_read_fd, read_deadline);
|
||||
|
||||
if (ready <= 0) {
|
||||
SPINE_LOG(("WARNING: SS[%i] The PHP Script Server sent a partial response and %s, Command[%s]", php_process, ready == 0 ? "timed out" : "failed", command));
|
||||
SET_UNDEFINED(result_string);
|
||||
read_ok = FALSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i = read(php_processes[php_process].php_read_fd, bptr, space);
|
||||
|
||||
if (i < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i <= 0) {
|
||||
SET_UNDEFINED(result_string);
|
||||
read_ok = FALSE;
|
||||
@@ -570,9 +591,16 @@ int php_init(int php_process) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* An out-of-range slot would index past php_processes. */
|
||||
if (php_processes == NULL || set.php.php_servers < 0 || set.php.php_servers > MAX_PHP_SERVERS ||
|
||||
(php_process != PHP_INIT && (php_process < 0 || php_process >= set.php.php_servers))) {
|
||||
SPINE_LOG(("ERROR: SS[%i] PHP Script Server slot is unavailable", php_process));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* special code to start all PHP Servers */
|
||||
if (php_process == PHP_INIT) {
|
||||
num_processes = set.php_servers;
|
||||
num_processes = set.php.php_servers;
|
||||
} else {
|
||||
num_processes = 1;
|
||||
}
|
||||
@@ -606,23 +634,23 @@ int php_init(int php_process) {
|
||||
|
||||
/* establish arguments for script server execution */
|
||||
if (set.cacti_version <= 1222) {
|
||||
argv[0] = set.path_php;
|
||||
argv[0] = set.php.path_php;
|
||||
argv[1] = arg_q;
|
||||
argv[2] = set.path_php_server;
|
||||
argv[2] = set.php.path_php_server;
|
||||
argv[3] = arg_spine;
|
||||
snprintf(poller_id, SMALL_BUFSIZE, "%d", set.poller_id);
|
||||
snprintf(poller_id, SMALL_BUFSIZE, "%d", set.poller.poller_id);
|
||||
argv[4] = poller_id;
|
||||
argv[5] = NULL;
|
||||
} else if (set.poller_id > 1) {
|
||||
argv[0] = set.path_php;
|
||||
} else if (set.poller.poller_id > 1) {
|
||||
argv[0] = set.php.path_php;
|
||||
argv[1] = arg_q;
|
||||
argv[2] = set.path_php_server;
|
||||
argv[2] = set.php.path_php_server;
|
||||
argv[3] = arg_environ_spine;
|
||||
|
||||
snprintf(poller_id, SMALL_BUFSIZE, "--poller=%d", set.poller_id);
|
||||
snprintf(poller_id, SMALL_BUFSIZE, "--poller=%d", set.poller.poller_id);
|
||||
argv[4] = poller_id;
|
||||
|
||||
if (set.mode == REMOTE_ONLINE) {
|
||||
if (set.poller.mode == REMOTE_ONLINE) {
|
||||
argv[5] = arg_mode_online;
|
||||
} else {
|
||||
argv[5] = arg_mode_offline;
|
||||
@@ -630,11 +658,11 @@ int php_init(int php_process) {
|
||||
|
||||
argv[6] = NULL;
|
||||
} else {
|
||||
argv[0] = set.path_php;
|
||||
argv[0] = set.php.path_php;
|
||||
argv[1] = arg_q;
|
||||
argv[2] = set.path_php_server;
|
||||
argv[2] = set.php.path_php_server;
|
||||
argv[3] = arg_environ_spine;
|
||||
snprintf(poller_id, SMALL_BUFSIZE, "--poller=%d", set.poller_id);
|
||||
snprintf(poller_id, SMALL_BUFSIZE, "--poller=%d", set.poller.poller_id);
|
||||
argv[4] = poller_id;
|
||||
|
||||
argv[5] = NULL;
|
||||
@@ -854,8 +882,14 @@ void php_close(int php_process) {
|
||||
int num_processes;
|
||||
int len;
|
||||
|
||||
/* An out-of-range slot would index past php_processes. */
|
||||
if (php_processes == NULL || set.php.php_servers < 0 || set.php.php_servers > MAX_PHP_SERVERS ||
|
||||
(php_process != PHP_INIT && (php_process < 0 || php_process >= set.php.php_servers))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (php_process == PHP_INIT) {
|
||||
num_processes = set.php_servers;
|
||||
num_processes = set.php.php_servers;
|
||||
} else {
|
||||
num_processes = 1;
|
||||
}
|
||||
@@ -881,6 +915,13 @@ void php_close(int php_process) {
|
||||
*/
|
||||
if (phpp->php_write_fd >= 0) {
|
||||
static const char quit[] = "quit\r\n";
|
||||
/* A hung server can leave the request pipe full; the quit message
|
||||
* is best effort and must not block shutdown. */
|
||||
int flags = fcntl(phpp->php_write_fd, F_GETFL);
|
||||
|
||||
if (flags >= 0) {
|
||||
(void)fcntl(phpp->php_write_fd, F_SETFL, flags | O_NONBLOCK);
|
||||
}
|
||||
|
||||
len = php_write_no_sigpipe(phpp->php_write_fd, quit, strlen(quit));
|
||||
|
||||
|
||||
@@ -163,12 +163,12 @@ extern spine_icmp_reply_t spine_icmp_classify_dgram_reply(const unsigned char *r
|
||||
|
||||
extern int ping_host(host_t *host, ping_t *ping);
|
||||
extern int ping_snmp(host_t *host, ping_t *ping);
|
||||
extern int ping_icmp(host_t *host, ping_t *ping);
|
||||
extern int ping_udp(host_t *host, ping_t *ping);
|
||||
extern int ping_tcp(host_t *host, ping_t *ping);
|
||||
extern name_t *get_namebyhost(char *hostname, name_t *name);
|
||||
extern void update_host_status(int status, host_t *host, ping_t *ping, int availability_method);
|
||||
extern int init_sockaddr(struct sockaddr_in *name, const char *hostname, unsigned short int port);
|
||||
extern int ping_icmp(const host_t *host, ping_t *ping);
|
||||
extern int ping_udp(const host_t *host, ping_t *ping);
|
||||
extern int ping_tcp(const host_t *host, ping_t *ping);
|
||||
extern name_t *get_namebyhost(const char *hostname, name_t *name);
|
||||
extern void update_host_status(int status, host_t *host, const ping_t *ping, int availability_method);
|
||||
extern int init_sockaddr(struct sockaddr_in *name, const char *hostname, int port);
|
||||
extern int get_address_type(host_t *host);
|
||||
extern unsigned short int get_checksum(void* buf, int len);
|
||||
|
||||
|
||||
@@ -33,16 +33,37 @@
|
||||
|
||||
#ifndef SPINE_POLLER_H
|
||||
#define SPINE_POLLER_H
|
||||
typedef struct {
|
||||
char items[BUFSIZE];
|
||||
char host[BIG_BUFSIZE];
|
||||
char reindex[BUFSIZE];
|
||||
char due_items[BUFSIZE];
|
||||
char schedule[BUFSIZE];
|
||||
char output[BUFSIZE];
|
||||
char agents[BUFSIZE];
|
||||
char due_agents[BUFSIZE];
|
||||
char boost_output[BUFSIZE];
|
||||
char suffix[BUFSIZE];
|
||||
} poller_queries_t;
|
||||
|
||||
extern void poller_prepare_queries(poller_queries_t *queries, int host_id, int host_thread, int host_data_ids);
|
||||
|
||||
extern void *child(void *arg);
|
||||
extern void child_cleanup(void *arg);
|
||||
extern void child_cleanup_thread(void *arg);
|
||||
extern void child_cleanup_script(void *arg);
|
||||
extern void poll_host(int device_counter, int host_id, int host_thread, int host_threads, int host_data_ids, const char *host_time, int *host_errors, double host_time_double);
|
||||
extern int poller_store_hex_result(char *result, size_t result_size, const char *hex, int *errors);
|
||||
extern void poll_host(const poller_thread_t *work, int *host_errors);
|
||||
extern char *exec_poll(host_t *current_host, char *command, int id, const char *type);
|
||||
extern void get_system_information(host_t *host, MYSQL *mysql, int system);
|
||||
extern int is_multipart_output(char *result);
|
||||
extern int is_multipart_output(const char *result);
|
||||
extern int validate_result(char *result);
|
||||
enum poll_result_status {
|
||||
POLL_RESULT_VALID,
|
||||
POLL_RESULT_UNDEFINED,
|
||||
POLL_RESULT_INVALID
|
||||
};
|
||||
extern enum poll_result_status normalize_poll_result(char *result, bool snmp);
|
||||
extern int format_poller_output_row(char *output, size_t output_size,
|
||||
int local_data_id, const char *escaped_rrd_name,
|
||||
const char *host_time, const char *escaped_result);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--
|
||||
Copyright (C) 2004-2026 The Cacti Group
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this library; if not, write to the Free Software Foundation,
|
||||
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
-->
|
||||
<!--
|
||||
cppcheck defines no compiler macros, so spine_types.h erases the
|
||||
__attribute__((noreturn)) on die() before cppcheck reads it. Without this
|
||||
the analyzer treats every "if (p == NULL) die(...)" as falling through.
|
||||
cppcheck matches library entries by argument count, hence the variadic.
|
||||
-->
|
||||
<def format="2">
|
||||
<function name="die">
|
||||
<noreturn>true</noreturn>
|
||||
<arg nr="1"/>
|
||||
<arg nr="variadic"/>
|
||||
</function>
|
||||
</def>
|
||||
@@ -22,6 +22,7 @@ cppcheck --enable=all --std=c11 --error-exitcode=1 \
|
||||
--suppress=unusedFunction \
|
||||
--suppress=checkersReport \
|
||||
--suppress=toomanyconfigs \
|
||||
--library=scripts/cppcheck/spine.cfg \
|
||||
-- "${source_files[@]}" 2>&1 | tee /tmp/cppcheck.txt
|
||||
|
||||
if [[ "${VERIFY_CPPCHECK_ONLY:-0}" == "1" ]]; then
|
||||
|
||||
@@ -37,15 +37,36 @@
|
||||
|
||||
extern void snmp_spine_init(void);
|
||||
extern void snmp_spine_close(void);
|
||||
extern void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_community, char *snmp_username, const char *snmp_password, char *snmp_auth_protocol, const char *snmp_priv_passphrase, char *snmp_priv_protocol, char *snmp_context, char *snmp_engine_id, int snmp_port, int snmp_timeout);
|
||||
/* Borrowed connection inputs; Net-SNMP clones the session before return. */
|
||||
typedef struct {
|
||||
int host_id;
|
||||
char *hostname;
|
||||
int snmp_version;
|
||||
char *snmp_community;
|
||||
char *snmp_username;
|
||||
const char *snmp_password;
|
||||
char *snmp_auth_protocol;
|
||||
const char *snmp_priv_passphrase;
|
||||
char *snmp_priv_protocol;
|
||||
char *snmp_context;
|
||||
char *snmp_engine_id;
|
||||
int snmp_port;
|
||||
int snmp_timeout;
|
||||
} snmp_connection_t;
|
||||
|
||||
extern void *snmp_host_init(const snmp_connection_t *options);
|
||||
extern void snmp_host_cleanup(void *snmp_session);
|
||||
extern char *snmp_get_base(host_t *current_host, const char *snmp_oid, bool should_fail);
|
||||
extern char *snmp_get_base(host_t *current_host, char *snmp_oid, bool should_fail);
|
||||
extern char *snmp_get(host_t *current_host, char *snmp_oid);
|
||||
extern char *snmp_get_allow_fail(host_t *current_host, char *snmp_oid);
|
||||
extern char *snmp_getnext(host_t *current_host, char *snmp_oid);
|
||||
extern int spine_snmpv3_protocol_is_set(const char *value);
|
||||
extern int spine_snmpv3_passphrase_is_set(const char *value);
|
||||
extern int spine_snmpv3_security_level(const char *auth_protocol, const char *auth_password,
|
||||
const char *priv_protocol, const char *priv_passphrase);
|
||||
extern int snmp_varbind_is_exception(const struct variable_list *vars);
|
||||
extern char *snmp_get(host_t *current_host, const char *snmp_oid);
|
||||
extern char *snmp_get_allow_fail(host_t *current_host, const char *snmp_oid);
|
||||
extern char *snmp_getnext(host_t *current_host, const char *snmp_oid);
|
||||
extern int snmp_count(host_t *current_host, const char *snmp_oid);
|
||||
extern void snmp_get_multi(host_t *current_host, target_t *poller_items, snmp_oids_t *snmp_oids, int num_oids);
|
||||
extern void snmp_snprint_value(char *obuf, size_t buf_len, const oid *objid, size_t objidlen, struct variable_list *variable);
|
||||
extern int snmp_count(host_t *current_host, char *snmp_oid);
|
||||
extern void snmp_get_multi(host_t *current_host, const target_t *poller_items, snmp_oids_t *snmp_oids, int num_oids);
|
||||
extern void snmp_snprint_value(char *obuf, size_t buf_len, const oid *objid, size_t objidlen, const struct variable_list *variable);
|
||||
|
||||
#endif /* SPINE_SNMP_H */
|
||||
@@ -34,585 +34,7 @@
|
||||
#ifndef _SPINE_H_
|
||||
#define _SPINE_H_
|
||||
|
||||
/* Defines */
|
||||
#ifndef FALSE
|
||||
#define FALSE 0
|
||||
#endif
|
||||
#ifndef TRUE
|
||||
#define TRUE 1
|
||||
#endif
|
||||
|
||||
#define LOCAL 0
|
||||
#define REMOTE 1
|
||||
|
||||
#define SPINE_NONE 0
|
||||
#define SPINE_IPV4 1
|
||||
#define SPINE_IPV6 2
|
||||
|
||||
#ifndef __GNUC__
|
||||
# define __attribute__(x) /* NOTHING */
|
||||
#endif
|
||||
|
||||
/* Windows does not support stderr. Therefore, don't use it. */
|
||||
#ifdef __CYGWIN__
|
||||
#define DISABLE_STDERR
|
||||
#endif
|
||||
|
||||
#ifdef HAS_EXECINFO_H
|
||||
#include <execinfo.h>
|
||||
#endif
|
||||
|
||||
/* if a host is legal, return TRUE */
|
||||
#define HOSTID_DEFINED(x) ((x) >= 0)
|
||||
|
||||
/* warning-suppression macros
|
||||
*
|
||||
* There are times when we cannot avoid using a parameter or variable which
|
||||
* is not used, and these correctly generate compiler warnings. But when we
|
||||
* *know* that the variable is actually intended to be unused, we can use one
|
||||
* of these macros inside the function to suppress it. This has the effect
|
||||
* of suppressing the warning (a good thing), plus documenting to the reader
|
||||
* that this is intentional.
|
||||
*
|
||||
* Both do the same thing - they're just for different semantics.
|
||||
*/
|
||||
|
||||
#define UNUSED_VARIABLE(p) (void)(p)
|
||||
#define UNUSED_PARAMETER(p) (void)(p)
|
||||
|
||||
/* logging macros
|
||||
*
|
||||
* These all perform conditional logging based on the current runtime logging
|
||||
* level, and it relies on a bit of tricky (but entirely portable) preprocessor
|
||||
* techniques.
|
||||
*
|
||||
* Standard C does not support variadic macros (macros with a variable number
|
||||
* of parameters), and though GNU C does, it's not at all portable. So we instead
|
||||
* rely on the fact that putting parens around something turn multiple params
|
||||
* into one:
|
||||
*
|
||||
* SPINE_LOG_DEBUG(("n=%d string=%s foo=%f", n, string, foo));
|
||||
*
|
||||
* This macros has *one* parameter:
|
||||
*
|
||||
* ("n=%d string=%s foo=%f", n, string, foo)
|
||||
*
|
||||
* and the parentheses are part of it. When we call this macro, we pass the
|
||||
* "single" parameter unadorned, so that
|
||||
*
|
||||
* spine_log args
|
||||
*
|
||||
* expands to
|
||||
*
|
||||
* spine_log ("n=%d string=%s foo=%f", n, string, foo)
|
||||
*
|
||||
* Voila: it's a normal printf-like call.
|
||||
*
|
||||
* The second part of this is the conditional test, and the obvious approach
|
||||
* of using an "if" statement is exceptionally bad form: there are all kinds
|
||||
* of pitfalls which arise in this case. Instead, we should try to use an
|
||||
* *expression*, which has none of these problems.
|
||||
*
|
||||
* The conditional tests are modelled after the assert() mechanism, which
|
||||
* checks the first parameter, and if it's true, it evaluates the second
|
||||
* parameter. If the test is not true, then the second part is *guaranteed*
|
||||
* not to be evaluated.
|
||||
*
|
||||
* The (void) prefix is to forestall compiler warnings about expressions
|
||||
* not being used.
|
||||
*/
|
||||
#define SPINE_LOG(format_and_args) (spine_log format_and_args)
|
||||
#define SPINE_LOG_LOW(format_and_args) (void)(set.log_level >= POLLER_VERBOSITY_LOW && spine_log format_and_args)
|
||||
#define SPINE_LOG_MEDIUM(format_and_args) (void)(set.log_level >= POLLER_VERBOSITY_MEDIUM && spine_log format_and_args)
|
||||
#define SPINE_LOG_HIGH(format_and_args) (void)(set.log_level >= POLLER_VERBOSITY_HIGH && spine_log format_and_args)
|
||||
#define SPINE_LOG_DEBUG(format_and_args) (void)(set.log_level >= POLLER_VERBOSITY_DEBUG && spine_log format_and_args)
|
||||
#define SPINE_LOG_DEVDBG(format_and_args) (void)(set.log_level >= POLLER_VERBOSITY_DEVDBG && spine_log format_and_args)
|
||||
|
||||
/* general constants */
|
||||
#define MAX_THREADS 100
|
||||
#define MAX_DEBUG_DEVICES 100
|
||||
#define TINY_BUFSIZE 16
|
||||
#define SMALL_BUFSIZE 256
|
||||
#define MEDIUM_BUFSIZE 512
|
||||
#define BUFSIZE 1024
|
||||
#define DBL_BUFSIZE 2048
|
||||
#define LRG_BUFSIZE 8096
|
||||
#define BIG_BUFSIZE 65535
|
||||
#define MEGA_BUFSIZE 1024000
|
||||
#define HUGE_BUFSIZE 2048000
|
||||
#define LOGSIZE 65535
|
||||
#define LRG_LOGSIZE 1024000
|
||||
#define BITSINBYTE 8
|
||||
#define THIRTYTWO 4294967295ul
|
||||
#define SIXTYFOUR 18446744073709551615ul
|
||||
#define STAT_DESCRIP_ERROR 99
|
||||
#define SPINE_PARENT 1
|
||||
#define SPINE_FORK 0
|
||||
|
||||
/* locations to search for the config file */
|
||||
#define CONFIG_PATHS 4
|
||||
#define CONFIG_PATH_1 ""
|
||||
#define CONFIG_PATH_2 "/etc/"
|
||||
#define CONFIG_PATH_3 "/etc/cacti/"
|
||||
#define CONFIG_PATH_4 "../etc/"
|
||||
|
||||
/* config file defaults */
|
||||
#define DEFAULT_CONF_FILE "spine.conf"
|
||||
#define DEFAULT_THREADS 5
|
||||
#define DEFAULT_DB_HOST "localhost"
|
||||
#define DEFAULT_DB_DB "cacti"
|
||||
#define DEFAULT_DB_USER "cactiuser"
|
||||
#define DEFAULT_DB_PASS "cactiuser"
|
||||
#define DEFAULT_DB_PORT 3306
|
||||
#define DEFAULT_DB_PREG 0
|
||||
#define DEFAULT_LOGFILE "/var/www/html/cacti/log/cacti.log"
|
||||
#define DEFAULT_TIMEOUT 294000000
|
||||
|
||||
/* threads constants */
|
||||
#define LOCK_SNMP 0
|
||||
#define LOCK_SETEUID 2
|
||||
#define LOCK_GHBN 3
|
||||
#define LOCK_POOL 4
|
||||
#define LOCK_PHP 6
|
||||
#define LOCK_PHP_PROC_0 7
|
||||
#define LOCK_PHP_PROC_1 8
|
||||
#define LOCK_PHP_PROC_2 9
|
||||
#define LOCK_PHP_PROC_3 10
|
||||
#define LOCK_PHP_PROC_4 11
|
||||
#define LOCK_PHP_PROC_5 12
|
||||
#define LOCK_PHP_PROC_6 13
|
||||
#define LOCK_PHP_PROC_7 14
|
||||
#define LOCK_PHP_PROC_8 15
|
||||
#define LOCK_PHP_PROC_9 16
|
||||
#define LOCK_PHP_PROC_10 17
|
||||
#define LOCK_PHP_PROC_11 18
|
||||
#define LOCK_PHP_PROC_12 19
|
||||
#define LOCK_PHP_PROC_13 20
|
||||
#define LOCK_PHP_PROC_14 21
|
||||
#define LOCK_THDET 40
|
||||
#define LOCK_HOST_TIME 41
|
||||
|
||||
#define LOCK_SNMP_O 0
|
||||
#define LOCK_SETEUID_O 2
|
||||
#define LOCK_GHBN_O 3
|
||||
#define LOCK_POOL_O 4
|
||||
#define LOCK_PHP_O 6
|
||||
#define LOCK_PHP_PROC_0_O 7
|
||||
#define LOCK_PHP_PROC_1_O 8
|
||||
#define LOCK_PHP_PROC_2_O 9
|
||||
#define LOCK_PHP_PROC_3_O 10
|
||||
#define LOCK_PHP_PROC_4_O 11
|
||||
#define LOCK_PHP_PROC_5_O 12
|
||||
#define LOCK_PHP_PROC_6_O 13
|
||||
#define LOCK_PHP_PROC_7_O 14
|
||||
#define LOCK_PHP_PROC_8_O 15
|
||||
#define LOCK_PHP_PROC_9_O 16
|
||||
#define LOCK_PHP_PROC_10_O 17
|
||||
#define LOCK_PHP_PROC_11_O 18
|
||||
#define LOCK_PHP_PROC_12_O 19
|
||||
#define LOCK_PHP_PROC_13_O 20
|
||||
#define LOCK_PHP_PROC_14_O 21
|
||||
#define LOCK_THDET_O 40
|
||||
#define LOCK_HOST_TIME_O 41
|
||||
|
||||
/* poller actions */
|
||||
#define POLLER_ACTION_SNMP 0
|
||||
#define POLLER_ACTION_SCRIPT 1
|
||||
#define POLLER_ACTION_PHP_SCRIPT_SERVER 2
|
||||
#define POLLER_ACTION_SNMP_COUNT 10
|
||||
#define POLLER_ACTION_SCRIPT_COUNT 11
|
||||
#define POLLER_ACTION_PHP_SCRIPT_SERVER_COUNT 12
|
||||
|
||||
/* reindex constants */
|
||||
#define POLLER_COMMAND_REINDEX 1
|
||||
|
||||
/* log destinations */
|
||||
#define LOGDEST_FILE 1
|
||||
#define LOGDEST_BOTH 2
|
||||
#define LOGDEST_SYSLOG 3
|
||||
#define LOGDEST_STDOUT 4
|
||||
|
||||
#define IS_LOGGING_TO_FILE() ((set.log_destination) == LOGDEST_FILE || (set.log_destination) == LOGDEST_BOTH)
|
||||
#define IS_LOGGING_TO_SYSLOG() ((set.log_destination) == LOGDEST_SYSLOG || (set.log_destination) == LOGDEST_BOTH)
|
||||
#define IS_LOGGING_TO_STDOUT() ((set.log_destination) == LOGDEST_STDOUT )
|
||||
|
||||
#define SPINE_FREE(s) do { if (s) { free((void *)s); s = NULL; } } while(0)
|
||||
|
||||
/* logging levels */
|
||||
#define POLLER_VERBOSITY_NONE 1
|
||||
#define POLLER_VERBOSITY_LOW 2
|
||||
#define POLLER_VERBOSITY_MEDIUM 3
|
||||
#define POLLER_VERBOSITY_HIGH 4
|
||||
#define POLLER_VERBOSITY_DEBUG 5
|
||||
#define POLLER_VERBOSITY_DEVDBG 6
|
||||
|
||||
/* logging separator constants */
|
||||
#define GDC_MIN 0
|
||||
#define GDC_HYPHEN 0
|
||||
#define GDC_SLASH 1
|
||||
#define GDC_DOT 2
|
||||
#define GDC_MAX 2
|
||||
#define GDC_DEFAULT 1
|
||||
|
||||
/* logging format constants */
|
||||
#define GD_FMT_SIZE 21
|
||||
#define GD_MIN 0
|
||||
#define GD_MO_D_Y 0
|
||||
#define GD_MN_D_Y 1
|
||||
#define GD_D_MO_Y 2
|
||||
#define GD_D_MN_Y 3
|
||||
#define GD_Y_MO_D 4
|
||||
#define GD_Y_MN_D 5
|
||||
#define GD_MAX 5
|
||||
#define GD_DEFAULT 5
|
||||
|
||||
/* host availability statistics */
|
||||
#define AVAIL_NONE 0
|
||||
#define AVAIL_SNMP_AND_PING 1
|
||||
#define AVAIL_SNMP 2
|
||||
#define AVAIL_PING 3
|
||||
#define AVAIL_SNMP_OR_PING 4
|
||||
#define AVAIL_SNMP_GET_SYSDESC 5
|
||||
#define AVAIL_SNMP_GET_NEXT 6
|
||||
#define AVAIL_STREAM 7
|
||||
|
||||
#define PING_ICMP 1
|
||||
#define PING_UDP 2
|
||||
#define PING_TCP 3
|
||||
#define PING_SNMP 4
|
||||
#define PING_TCP_CLOSED 5
|
||||
|
||||
#define HOST_UNKNOWN 0
|
||||
#define HOST_DOWN 1
|
||||
#define HOST_RECOVERING 2
|
||||
#define HOST_UP 3
|
||||
|
||||
/* required for ICMP and UDP ping */
|
||||
#define ICMP_ECHO 8
|
||||
#define ICMP_HDR_SIZE 8
|
||||
|
||||
/* required for PHP Script Server */
|
||||
#define MAX_PHP_SERVERS 15
|
||||
#define PHP_READY 0
|
||||
#define PHP_BUSY 1
|
||||
#define PHP_INIT 999
|
||||
#define PHP_ERROR 99
|
||||
|
||||
/* required for validation of script results */
|
||||
#define RESULT_INIT 0
|
||||
#define RESULT_ARGX 1
|
||||
#define RESULT_VALX 2
|
||||
#define RESULT_SEPARATOR 3
|
||||
#define RESULT_SPACE 4
|
||||
#define RESULT_ALPHA 5
|
||||
#define RESULT_DIGIT 6
|
||||
|
||||
/* snmp session status */
|
||||
#define SNMP_1 0
|
||||
#define SNMP_2c 1
|
||||
#define SNMP_3 3
|
||||
#define SNMP_NONE 4
|
||||
|
||||
/* Constants for remote polling */
|
||||
#define REMOTE_ONLINE 0
|
||||
#define REMOTE_OFFLINE 1
|
||||
#define REMOTE_RECOVERY 2
|
||||
|
||||
/* These are used to perform string matches, returning TRUE/VALUE values.
|
||||
* For strcmp() this is not really that useful, but the case-insensitive
|
||||
* one has slight portability issues. Better to abstract them here.
|
||||
*/
|
||||
#define STRMATCH(a,b) (strcmp((a),(b)) == 0)
|
||||
#define STRIMATCH(a,b) (strcasecmp((a),(b)) == 0)
|
||||
|
||||
/* When any kind of poller wants to set an undefined value; this particular
|
||||
* value used ('U') springs from the requirements of rrdupdate. We also
|
||||
* include the corresponding test macro which looks for the literal string
|
||||
* "U". This *could* use strcmp(), but this is more efficient.
|
||||
*/
|
||||
#define SET_UNDEFINED(buf) ( (buf)[0] = 'U', (buf)[1] = '\0' )
|
||||
#define IS_UNDEFINED(buf) ( (buf)[0] == 'U' && (buf)[1] == '\0' )
|
||||
|
||||
/*! Config Structure
|
||||
*
|
||||
* This structure holds Spine database configuration information and/or override values
|
||||
* obtained via either accessing the database or reading the runtime options. In addition,
|
||||
* it contains runtime status information.
|
||||
*
|
||||
*/
|
||||
typedef struct config_struct {
|
||||
/* stdout, stderr TTY protection */
|
||||
int stdout_notty;
|
||||
int stderr_notty;
|
||||
/* general configuration/runtime settings */
|
||||
int poller_id;
|
||||
int poller_interval;
|
||||
int parent_fork;
|
||||
int num_parent_processes;
|
||||
int script_timeout;
|
||||
int active_profiles;
|
||||
int total_snmp_ports;
|
||||
int threads;
|
||||
int threads_set;
|
||||
int logfile_processed;
|
||||
int boost_enabled;
|
||||
int boost_redirect;
|
||||
int cygwinshloc;
|
||||
/* debugging options */
|
||||
int snmponly;
|
||||
int SQL_readonly;
|
||||
/* host range to be poller with this spine process */
|
||||
int start_host_id;
|
||||
int end_host_id;
|
||||
char host_id_list[BIG_BUFSIZE];
|
||||
int has_device_0;
|
||||
int has_output_regex;
|
||||
/* database connection information */
|
||||
char db_host[BUFSIZE];
|
||||
char db_db[BUFSIZE];
|
||||
char db_user[BUFSIZE];
|
||||
char db_pass[BUFSIZE];
|
||||
int db_ssl;
|
||||
char db_ssl_key[BUFSIZE];
|
||||
char db_ssl_cert[BUFSIZE];
|
||||
char db_ssl_ca[BUFSIZE];
|
||||
int d_b;
|
||||
unsigned int db_port;
|
||||
char dbversion[BUFSIZE];
|
||||
int dbonupdate;
|
||||
int cacti_version;
|
||||
/* path information */
|
||||
char path_logfile[DBL_BUFSIZE];
|
||||
char path_php[BUFSIZE];
|
||||
char path_php_server[BUFSIZE];
|
||||
/* logging options */
|
||||
int log_level;
|
||||
int log_destination;
|
||||
int log_perror;
|
||||
int log_pwarn;
|
||||
int log_pstats;
|
||||
char selective_device_debug[LRG_BUFSIZE];
|
||||
int spine_log_level;
|
||||
int log_datetime_separator;
|
||||
int log_datetime_format;
|
||||
/* ping settings */
|
||||
int icmp_avail;
|
||||
int icmp_uses_caps;
|
||||
int availability_method;
|
||||
int ping_method;
|
||||
int ping_retries;
|
||||
int ping_timeout;
|
||||
int ping_failure_count;
|
||||
int ping_recovery_count;
|
||||
int ping_only;
|
||||
/* snmp options */
|
||||
int snmp_max_get_size;
|
||||
int snmp_retries;
|
||||
char snmp_clientaddr[BUFSIZE];
|
||||
int mibs;
|
||||
/* PHP Script Server Options */
|
||||
int php_required;
|
||||
int php_initialized;
|
||||
int php_servers;
|
||||
int php_current_server;
|
||||
/* Exit code if we need it */
|
||||
int exit_code;
|
||||
size_t exit_size;
|
||||
void* exit_stack[10];
|
||||
|
||||
/* Remote polling mode */
|
||||
int mode;
|
||||
/* remote database connection information */
|
||||
char rdb_host[BUFSIZE];
|
||||
char rdb_db[BUFSIZE];
|
||||
char rdb_user[BUFSIZE];
|
||||
char rdb_pass[BUFSIZE];
|
||||
int rdb_ssl;
|
||||
char rdb_ssl_key[BUFSIZE];
|
||||
char rdb_ssl_cert[BUFSIZE];
|
||||
char rdb_ssl_ca[BUFSIZE];
|
||||
unsigned int rdb_port;
|
||||
char rdbversion[BUFSIZE];
|
||||
int rdbonupdate;
|
||||
} config_t;
|
||||
|
||||
/*! Target Structure
|
||||
*
|
||||
* This structure holds the contents of the Poller Items table and the results
|
||||
* of each polling action.
|
||||
*
|
||||
*/
|
||||
typedef struct target_struct {
|
||||
int target_id;
|
||||
char result[RESULTS_BUFFER];
|
||||
int local_data_id;
|
||||
int action;
|
||||
char command[256];
|
||||
char hostname[250];
|
||||
char snmp_community[100];
|
||||
int snmp_version;
|
||||
char snmp_username[50];
|
||||
char snmp_password[50];
|
||||
char snmp_auth_protocol[7];
|
||||
char snmp_priv_passphrase[200];
|
||||
char snmp_priv_protocol[8];
|
||||
char snmp_context[65];
|
||||
char snmp_engine_id[30];
|
||||
int snmp_port;
|
||||
int snmp_timeout;
|
||||
int availability_method;
|
||||
int ping_method;
|
||||
int ping_port;
|
||||
int ping_timeout;
|
||||
int ping_retries;
|
||||
char rrd_name[30];
|
||||
char rrd_path[255];
|
||||
int rrd_num;
|
||||
char arg1[1024];
|
||||
char arg2[255];
|
||||
char arg3[255];
|
||||
char output_regex[255];
|
||||
} target_t;
|
||||
|
||||
/*! SNMP OID's Structure
|
||||
*
|
||||
* This structure holds SNMP get results temporarily while polling is taking place.
|
||||
*
|
||||
*/
|
||||
typedef struct snmp_oids {
|
||||
int array_position;
|
||||
char oid[1024];
|
||||
char result[RESULTS_BUFFER];
|
||||
} snmp_oids_t;
|
||||
|
||||
/*! Poller Structure
|
||||
*
|
||||
* This structure holds thread polling instructions.
|
||||
*
|
||||
*/
|
||||
typedef struct poller_thread {
|
||||
int device_counter;
|
||||
int host_id;
|
||||
int host_thread;
|
||||
int host_threads;
|
||||
int host_data_ids;
|
||||
int threads_complete;
|
||||
int complete;
|
||||
char host_time[40];
|
||||
double host_time_double;
|
||||
spine_sem_t *thread_init_sem;
|
||||
} poller_thread_t;
|
||||
|
||||
/*! PHP Script Server Structure
|
||||
*
|
||||
* This structure holds status and PID information for all the running
|
||||
* PHP Script Server processes.
|
||||
*
|
||||
*/
|
||||
typedef struct php_processes {
|
||||
int php_state;
|
||||
pid_t php_pid;
|
||||
int php_write_fd;
|
||||
int php_read_fd;
|
||||
} php_t;
|
||||
|
||||
/*! Host Structure
|
||||
*
|
||||
* This structure holds host information from the host table and is used throughout
|
||||
* the application.
|
||||
*
|
||||
*/
|
||||
typedef struct host_struct {
|
||||
int id;
|
||||
char hostname[250];
|
||||
char snmp_community[100];
|
||||
int snmp_version;
|
||||
char snmp_username[50];
|
||||
char snmp_password[50];
|
||||
char snmp_auth_protocol[7];
|
||||
char snmp_priv_passphrase[200];
|
||||
char snmp_priv_protocol[8];
|
||||
char snmp_context[65];
|
||||
char snmp_engine_id[30];
|
||||
int snmp_port;
|
||||
int snmp_timeout;
|
||||
int snmp_retries;
|
||||
char snmp_sysDescr[600];
|
||||
char snmp_sysObjectID[160];
|
||||
unsigned long long snmp_sysUpTimeInstance;
|
||||
char snmp_sysContact[300];
|
||||
char snmp_sysName[300];
|
||||
char snmp_sysLocation[600];
|
||||
int max_oids;
|
||||
int availability_method;
|
||||
int ping_method;
|
||||
int ping_port;
|
||||
int ping_timeout;
|
||||
int ping_retries;
|
||||
int status;
|
||||
int status_event_count;
|
||||
char status_fail_date[40];
|
||||
char status_rec_date[40];
|
||||
char status_last_error[BUFSIZE * 2 + 2];
|
||||
double min_time;
|
||||
double max_time;
|
||||
double cur_time;
|
||||
double avg_time;
|
||||
int total_polls;
|
||||
int failed_polls;
|
||||
double availability;
|
||||
int ignore_host;
|
||||
void *snmp_session;
|
||||
int snmp_status;
|
||||
} host_t;
|
||||
|
||||
/*! Host Reindex Structure
|
||||
*
|
||||
* This structure holds the results of the host re-index checks and values.
|
||||
*
|
||||
*/
|
||||
typedef struct host_reindex_struct {
|
||||
char op[4];
|
||||
char assert_value[100];
|
||||
char arg1[1024];
|
||||
int data_query_id;
|
||||
int action;
|
||||
} reindex_t;
|
||||
|
||||
/*! Ping Result Structure
|
||||
*
|
||||
* This structure holds the results of a host ping.
|
||||
*
|
||||
*/
|
||||
typedef struct ping_results {
|
||||
char hostname[BUFSIZE];
|
||||
char ping_status[50];
|
||||
char ping_response[BUFSIZE];
|
||||
char snmp_status[50];
|
||||
char snmp_response[BUFSIZE];
|
||||
} ping_t;
|
||||
|
||||
/*! Name Result Structure
|
||||
*
|
||||
* This structure holds the results of a name/port split
|
||||
*
|
||||
*/
|
||||
typedef struct name_port {
|
||||
// Method = 0 - default, 1 - tcp, 2 - udp
|
||||
char hostname[BUFSIZE];
|
||||
int method;
|
||||
int port;
|
||||
} name_t;
|
||||
|
||||
/*! MySQL Connection Pool Structure
|
||||
*
|
||||
* This structure holds the mysql connection pool object.
|
||||
*/
|
||||
typedef struct db_connection {
|
||||
int id;
|
||||
int free;
|
||||
MYSQL mysql;
|
||||
} pool_t;
|
||||
#include "spine_types.h"
|
||||
|
||||
/* Include all Standard Spine Headers */
|
||||
#include "poller.h"
|
||||
@@ -636,8 +58,8 @@ extern config_t set;
|
||||
extern php_t *php_processes;
|
||||
extern char start_datetime[20];
|
||||
extern char config_paths[CONFIG_PATHS][BUFSIZE];
|
||||
extern spine_sem_t available_threads;
|
||||
extern spine_sem_t available_scripts;
|
||||
extern spine_permits_t available_threads;
|
||||
extern spine_permits_t available_scripts;
|
||||
extern pool_t *db_pool_remote;
|
||||
extern pool_t *db_pool_local;
|
||||
|
||||
|
||||
+668
@@ -0,0 +1,668 @@
|
||||
/*
|
||||
ex: set tabstop=4 shiftwidth=4 autoindent:
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| This program is free software; you can redistribute it and/or |
|
||||
| modify it under the terms of the GNU Lesser General Public |
|
||||
| License as published by the Free Software Foundation; either |
|
||||
| version 2.1 of the License, or (at your option) any later version. |
|
||||
| |
|
||||
| This program is distributed in the hope that it will be useful, |
|
||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
| GNU Lesser General Public License for more details. |
|
||||
| |
|
||||
| You should have received a copy of the GNU Lesser General Public |
|
||||
| License along with this library; if not, write to the Free Software |
|
||||
| Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
|
||||
| 02110-1301, USA |
|
||||
| |
|
||||
+-------------------------------------------------------------------------+
|
||||
| spine: a backend data gatherer for cacti |
|
||||
+-------------------------------------------------------------------------+
|
||||
| This poller would not have been possible without: |
|
||||
| - Larry Adams (current development and enhancements) |
|
||||
| - Rivo Nurges (rrd support, mysql poller cache, misc functions) |
|
||||
| - RTG (core poller code, pthreads, snmp, autoconf examples) |
|
||||
| - Brady Alleman/Doug Warner (threading ideas, implementation details) |
|
||||
+-------------------------------------------------------------------------+
|
||||
| - Cacti - http://www.cacti.net/ |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
#ifndef SPINE_TYPES_H
|
||||
#define SPINE_TYPES_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
/* Defines */
|
||||
#ifndef FALSE
|
||||
#define FALSE 0
|
||||
#endif
|
||||
#ifndef TRUE
|
||||
#define TRUE 1
|
||||
#endif
|
||||
|
||||
#define LOCAL 0
|
||||
#define REMOTE 1
|
||||
|
||||
#define SPINE_NONE 0
|
||||
#define SPINE_IPV4 1
|
||||
#define SPINE_IPV6 2
|
||||
|
||||
#ifndef __GNUC__
|
||||
# define __attribute__(x) /* NOTHING */
|
||||
#endif
|
||||
|
||||
/* Windows does not support stderr. Therefore, don't use it. */
|
||||
#ifdef __CYGWIN__
|
||||
#define DISABLE_STDERR
|
||||
#endif
|
||||
|
||||
#ifdef HAS_EXECINFO_H
|
||||
#include <execinfo.h>
|
||||
#endif
|
||||
|
||||
/* if a host is legal, return TRUE */
|
||||
#define HOSTID_DEFINED(x) ((x) >= 0)
|
||||
|
||||
/* warning-suppression macros
|
||||
*
|
||||
* There are times when we cannot avoid using a parameter or variable which
|
||||
* is not used, and these correctly generate compiler warnings. But when we
|
||||
* *know* that the variable is actually intended to be unused, we can use one
|
||||
* of these macros inside the function to suppress it. This has the effect
|
||||
* of suppressing the warning (a good thing), plus documenting to the reader
|
||||
* that this is intentional.
|
||||
*
|
||||
* Both do the same thing - they're just for different semantics.
|
||||
*/
|
||||
|
||||
#define UNUSED_VARIABLE(p) (void)(p)
|
||||
#define UNUSED_PARAMETER(p) (void)(p)
|
||||
|
||||
/* logging macros
|
||||
*
|
||||
* These all perform conditional logging based on the current runtime logging
|
||||
* level, and it relies on a bit of tricky (but entirely portable) preprocessor
|
||||
* techniques.
|
||||
*
|
||||
* Standard C does not support variadic macros (macros with a variable number
|
||||
* of parameters), and though GNU C does, it's not at all portable. So we instead
|
||||
* rely on the fact that putting parens around something turn multiple params
|
||||
* into one:
|
||||
*
|
||||
* SPINE_LOG_DEBUG(("n=%d string=%s foo=%f", n, string, foo));
|
||||
*
|
||||
* This macros has *one* parameter:
|
||||
*
|
||||
* ("n=%d string=%s foo=%f", n, string, foo)
|
||||
*
|
||||
* and the parentheses are part of it. When we call this macro, we pass the
|
||||
* "single" parameter unadorned, so that
|
||||
*
|
||||
* spine_log args
|
||||
*
|
||||
* expands to
|
||||
*
|
||||
* spine_log ("n=%d string=%s foo=%f", n, string, foo)
|
||||
*
|
||||
* Voila: it's a normal printf-like call.
|
||||
*
|
||||
* The second part of this is the conditional test, and the obvious approach
|
||||
* of using an "if" statement is exceptionally bad form: there are all kinds
|
||||
* of pitfalls which arise in this case. Instead, we should try to use an
|
||||
* *expression*, which has none of these problems.
|
||||
*
|
||||
* The conditional tests are modelled after the assert() mechanism, which
|
||||
* checks the first parameter, and if it's true, it evaluates the second
|
||||
* parameter. If the test is not true, then the second part is *guaranteed*
|
||||
* not to be evaluated.
|
||||
*
|
||||
* The (void) prefix is to forestall compiler warnings about expressions
|
||||
* not being used.
|
||||
*/
|
||||
#define SPINE_LOG(format_and_args) (spine_log format_and_args)
|
||||
#define SPINE_LOG_LOW(format_and_args) (void)(set.logging.log_level >= POLLER_VERBOSITY_LOW && spine_log format_and_args)
|
||||
#define SPINE_LOG_MEDIUM(format_and_args) (void)(set.logging.log_level >= POLLER_VERBOSITY_MEDIUM && spine_log format_and_args)
|
||||
#define SPINE_LOG_HIGH(format_and_args) (void)(set.logging.log_level >= POLLER_VERBOSITY_HIGH && spine_log format_and_args)
|
||||
#define SPINE_LOG_DEBUG(format_and_args) (void)(set.logging.log_level >= POLLER_VERBOSITY_DEBUG && spine_log format_and_args)
|
||||
#define SPINE_LOG_DEVDBG(format_and_args) (void)(set.logging.log_level >= POLLER_VERBOSITY_DEVDBG && spine_log format_and_args)
|
||||
#define SPINE_LOG_DEVICE(host_id, verbosity, format_and_args) \
|
||||
(void)(spine_should_log_device(host_id, verbosity) && spine_log format_and_args)
|
||||
|
||||
/* general constants */
|
||||
#define MAX_THREADS 100
|
||||
#define MAX_DEBUG_DEVICES 100
|
||||
#define TINY_BUFSIZE 16
|
||||
#define SMALL_BUFSIZE 256
|
||||
#define MEDIUM_BUFSIZE 512
|
||||
#define BUFSIZE 1024
|
||||
#define DBL_BUFSIZE 2048
|
||||
#define LRG_BUFSIZE 8096
|
||||
#define BIG_BUFSIZE 65535
|
||||
#define MEGA_BUFSIZE 1024000
|
||||
#define HUGE_BUFSIZE 2048000
|
||||
#define LOGSIZE 65535
|
||||
#define LRG_LOGSIZE 1024000
|
||||
#define BITSINBYTE 8
|
||||
#define THIRTYTWO 4294967295ul
|
||||
#define SIXTYFOUR 18446744073709551615ul
|
||||
#define STAT_DESCRIP_ERROR 99
|
||||
#define SPINE_PARENT 1
|
||||
#define SPINE_FORK 0
|
||||
|
||||
/* locations to search for the config file */
|
||||
#define CONFIG_PATHS 4
|
||||
#define CONFIG_PATH_1 ""
|
||||
#define CONFIG_PATH_2 "/etc/"
|
||||
#define CONFIG_PATH_3 "/etc/cacti/"
|
||||
#define CONFIG_PATH_4 "../etc/"
|
||||
|
||||
/* config file defaults */
|
||||
#define DEFAULT_CONF_FILE "spine.conf"
|
||||
#define DEFAULT_THREADS 5
|
||||
#define DEFAULT_DB_HOST "localhost"
|
||||
#define DEFAULT_DB_DB "cacti"
|
||||
#define DEFAULT_DB_USER "cactiuser"
|
||||
#define DEFAULT_DB_PASS "cactiuser"
|
||||
#define DEFAULT_DB_PORT 3306
|
||||
#define DEFAULT_DB_PREG 0
|
||||
#define DEFAULT_LOGFILE "/var/www/html/cacti/log/cacti.log"
|
||||
#define DEFAULT_TIMEOUT 294000000
|
||||
|
||||
/* threads constants */
|
||||
#define LOCK_SNMP 0
|
||||
#define LOCK_SETEUID 2
|
||||
#define LOCK_GHBN 3
|
||||
#define LOCK_POOL 4
|
||||
#define LOCK_PHP 6
|
||||
#define LOCK_PHP_PROC_0 7
|
||||
#define LOCK_PHP_PROC_1 8
|
||||
#define LOCK_PHP_PROC_2 9
|
||||
#define LOCK_PHP_PROC_3 10
|
||||
#define LOCK_PHP_PROC_4 11
|
||||
#define LOCK_PHP_PROC_5 12
|
||||
#define LOCK_PHP_PROC_6 13
|
||||
#define LOCK_PHP_PROC_7 14
|
||||
#define LOCK_PHP_PROC_8 15
|
||||
#define LOCK_PHP_PROC_9 16
|
||||
#define LOCK_PHP_PROC_10 17
|
||||
#define LOCK_PHP_PROC_11 18
|
||||
#define LOCK_PHP_PROC_12 19
|
||||
#define LOCK_PHP_PROC_13 20
|
||||
#define LOCK_PHP_PROC_14 21
|
||||
#define LOCK_THDET 40
|
||||
#define LOCK_HOST_TIME 41
|
||||
|
||||
#define LOCK_SNMP_O 0
|
||||
#define LOCK_SETEUID_O 2
|
||||
#define LOCK_GHBN_O 3
|
||||
#define LOCK_POOL_O 4
|
||||
#define LOCK_PHP_O 6
|
||||
#define LOCK_PHP_PROC_0_O 7
|
||||
#define LOCK_PHP_PROC_1_O 8
|
||||
#define LOCK_PHP_PROC_2_O 9
|
||||
#define LOCK_PHP_PROC_3_O 10
|
||||
#define LOCK_PHP_PROC_4_O 11
|
||||
#define LOCK_PHP_PROC_5_O 12
|
||||
#define LOCK_PHP_PROC_6_O 13
|
||||
#define LOCK_PHP_PROC_7_O 14
|
||||
#define LOCK_PHP_PROC_8_O 15
|
||||
#define LOCK_PHP_PROC_9_O 16
|
||||
#define LOCK_PHP_PROC_10_O 17
|
||||
#define LOCK_PHP_PROC_11_O 18
|
||||
#define LOCK_PHP_PROC_12_O 19
|
||||
#define LOCK_PHP_PROC_13_O 20
|
||||
#define LOCK_PHP_PROC_14_O 21
|
||||
#define LOCK_THDET_O 40
|
||||
#define LOCK_HOST_TIME_O 41
|
||||
|
||||
/* poller actions */
|
||||
#define POLLER_ACTION_SNMP 0
|
||||
#define POLLER_ACTION_SCRIPT 1
|
||||
#define POLLER_ACTION_PHP_SCRIPT_SERVER 2
|
||||
#define POLLER_ACTION_SNMP_COUNT 10
|
||||
#define POLLER_ACTION_SCRIPT_COUNT 11
|
||||
#define POLLER_ACTION_PHP_SCRIPT_SERVER_COUNT 12
|
||||
|
||||
/* reindex constants */
|
||||
#define POLLER_COMMAND_REINDEX 1
|
||||
|
||||
/* log destinations */
|
||||
#define LOGDEST_FILE 1
|
||||
#define LOGDEST_BOTH 2
|
||||
#define LOGDEST_SYSLOG 3
|
||||
#define LOGDEST_STDOUT 4
|
||||
|
||||
#define IS_LOGGING_TO_FILE() ((set.logging.log_destination) == LOGDEST_FILE || (set.logging.log_destination) == LOGDEST_BOTH)
|
||||
#define IS_LOGGING_TO_SYSLOG() ((set.logging.log_destination) == LOGDEST_SYSLOG || (set.logging.log_destination) == LOGDEST_BOTH)
|
||||
#define IS_LOGGING_TO_STDOUT() ((set.logging.log_destination) == LOGDEST_STDOUT )
|
||||
|
||||
#define SPINE_FREE(s) do { if (s) { free((void *)s); s = NULL; } } while(0)
|
||||
|
||||
/* logging levels */
|
||||
#define POLLER_VERBOSITY_NONE 1
|
||||
#define POLLER_VERBOSITY_LOW 2
|
||||
#define POLLER_VERBOSITY_MEDIUM 3
|
||||
#define POLLER_VERBOSITY_HIGH 4
|
||||
#define POLLER_VERBOSITY_DEBUG 5
|
||||
#define POLLER_VERBOSITY_DEVDBG 6
|
||||
|
||||
/* logging separator constants */
|
||||
#define GDC_MIN 0
|
||||
#define GDC_HYPHEN 0
|
||||
#define GDC_SLASH 1
|
||||
#define GDC_DOT 2
|
||||
#define GDC_MAX 2
|
||||
#define GDC_DEFAULT 1
|
||||
|
||||
/* logging format constants */
|
||||
#define GD_FMT_SIZE 21
|
||||
#define GD_MIN 0
|
||||
#define GD_MO_D_Y 0
|
||||
#define GD_MN_D_Y 1
|
||||
#define GD_D_MO_Y 2
|
||||
#define GD_D_MN_Y 3
|
||||
#define GD_Y_MO_D 4
|
||||
#define GD_Y_MN_D 5
|
||||
#define GD_MAX 5
|
||||
#define GD_DEFAULT 5
|
||||
|
||||
/* host availability statistics */
|
||||
#define AVAIL_NONE 0
|
||||
#define AVAIL_SNMP_AND_PING 1
|
||||
#define AVAIL_SNMP 2
|
||||
#define AVAIL_PING 3
|
||||
#define AVAIL_SNMP_OR_PING 4
|
||||
#define AVAIL_SNMP_GET_SYSDESC 5
|
||||
#define AVAIL_SNMP_GET_NEXT 6
|
||||
#define AVAIL_STREAM 7
|
||||
|
||||
#define PING_ICMP 1
|
||||
#define PING_UDP 2
|
||||
#define PING_TCP 3
|
||||
#define PING_SNMP 4
|
||||
#define PING_TCP_CLOSED 5
|
||||
|
||||
#define HOST_UNKNOWN 0
|
||||
#define HOST_DOWN 1
|
||||
#define HOST_RECOVERING 2
|
||||
#define HOST_UP 3
|
||||
|
||||
/* required for ICMP and UDP ping */
|
||||
#define ICMP_ECHO 8
|
||||
#define ICMP_HDR_SIZE 8
|
||||
|
||||
/* required for PHP Script Server */
|
||||
#define MAX_PHP_SERVERS 15
|
||||
#define PHP_READY 0
|
||||
#define PHP_BUSY 1
|
||||
#define PHP_INIT 999
|
||||
#define PHP_ERROR 99
|
||||
|
||||
/* required for validation of script results */
|
||||
#define RESULT_INIT 0
|
||||
#define RESULT_ARGX 1
|
||||
#define RESULT_VALX 2
|
||||
#define RESULT_SEPARATOR 3
|
||||
#define RESULT_SPACE 4
|
||||
#define RESULT_ALPHA 5
|
||||
#define RESULT_DIGIT 6
|
||||
|
||||
/* snmp session status */
|
||||
#define SNMP_1 0
|
||||
#define SNMP_2c 1
|
||||
#define SNMP_3 3
|
||||
#define SNMP_NONE 4
|
||||
|
||||
/* Constants for remote polling */
|
||||
#define REMOTE_ONLINE 0
|
||||
#define REMOTE_OFFLINE 1
|
||||
#define REMOTE_RECOVERY 2
|
||||
|
||||
/* These are used to perform string matches, returning TRUE/VALUE values.
|
||||
* For strcmp() this is not really that useful, but the case-insensitive
|
||||
* one has slight portability issues. Better to abstract them here.
|
||||
*/
|
||||
#define STRMATCH(a,b) (strcmp((a),(b)) == 0)
|
||||
#define STRIMATCH(a,b) (strcasecmp((a),(b)) == 0)
|
||||
|
||||
/* When any kind of poller wants to set an undefined value; this particular
|
||||
* value used ('U') springs from the requirements of rrdupdate. We also
|
||||
* include the corresponding test macro which looks for the literal string
|
||||
* "U". This *could* use strcmp(), but this is more efficient.
|
||||
*/
|
||||
#define SET_UNDEFINED(buf) ( (buf)[0] = 'U', (buf)[1] = '\0' )
|
||||
#define IS_UNDEFINED(buf) ( (buf)[0] == 'U' && (buf)[1] == '\0' )
|
||||
|
||||
/*! Config Structure
|
||||
*
|
||||
* This structure holds Spine database configuration information and/or override values
|
||||
* obtained via either accessing the database or reading the runtime options. In addition,
|
||||
* it contains runtime status information.
|
||||
*
|
||||
*/
|
||||
/* Console configuration and runtime state. */
|
||||
typedef struct {
|
||||
int stdout_notty;
|
||||
int stderr_notty;
|
||||
} spine_console_config_t;
|
||||
|
||||
/* Poller configuration and runtime state. */
|
||||
typedef struct {
|
||||
int poller_id;
|
||||
int poller_interval;
|
||||
int parent_fork;
|
||||
int num_parent_processes;
|
||||
int active_profiles;
|
||||
int threads;
|
||||
int threads_set;
|
||||
int snmponly;
|
||||
int SQL_readonly;
|
||||
int mode;
|
||||
} spine_poller_config_t;
|
||||
|
||||
/* Hosts configuration and runtime state. */
|
||||
typedef struct {
|
||||
int start_host_id;
|
||||
int end_host_id;
|
||||
char host_id_list[BIG_BUFSIZE];
|
||||
int has_device_0;
|
||||
int has_output_regex;
|
||||
} spine_hosts_config_t;
|
||||
|
||||
/* Database configuration and runtime state. */
|
||||
typedef struct {
|
||||
char host[BUFSIZE];
|
||||
char database[BUFSIZE];
|
||||
char user[BUFSIZE];
|
||||
char password[BUFSIZE];
|
||||
int ssl;
|
||||
char ssl_key[BUFSIZE];
|
||||
char ssl_cert[BUFSIZE];
|
||||
char ssl_ca[BUFSIZE];
|
||||
unsigned int port;
|
||||
char version[BUFSIZE];
|
||||
int onupdate;
|
||||
} spine_database_config_t;
|
||||
|
||||
/* Php configuration and runtime state. */
|
||||
typedef struct {
|
||||
char path_php[BUFSIZE];
|
||||
char path_php_server[BUFSIZE];
|
||||
int script_timeout;
|
||||
int php_required;
|
||||
int php_initialized;
|
||||
int php_servers;
|
||||
int php_current_server;
|
||||
} spine_php_config_t;
|
||||
|
||||
/* Logging configuration and runtime state. */
|
||||
typedef struct {
|
||||
char path_logfile[DBL_BUFSIZE];
|
||||
int logfile_processed;
|
||||
int log_level;
|
||||
int log_destination;
|
||||
int log_perror;
|
||||
int log_pwarn;
|
||||
int log_pstats;
|
||||
char selective_device_debug[LRG_BUFSIZE];
|
||||
int spine_log_level;
|
||||
int log_datetime_separator;
|
||||
int log_datetime_format;
|
||||
} spine_logging_config_t;
|
||||
|
||||
/* Availability configuration and runtime state. */
|
||||
typedef struct {
|
||||
int icmp_avail;
|
||||
int icmp_uses_caps;
|
||||
int availability_method;
|
||||
int ping_method;
|
||||
int ping_retries;
|
||||
int ping_timeout;
|
||||
int ping_failure_count;
|
||||
int ping_recovery_count;
|
||||
int ping_only;
|
||||
} spine_availability_config_t;
|
||||
|
||||
/* Snmp configuration and runtime state. */
|
||||
typedef struct {
|
||||
int total_snmp_ports;
|
||||
int snmp_max_get_size;
|
||||
int snmp_retries;
|
||||
char snmp_clientaddr[BUFSIZE];
|
||||
int mibs;
|
||||
} spine_snmp_config_t;
|
||||
|
||||
/* Boost configuration and runtime state. */
|
||||
typedef struct {
|
||||
int boost_enabled;
|
||||
int boost_redirect;
|
||||
} spine_boost_config_t;
|
||||
|
||||
/* Exit configuration and runtime state. */
|
||||
typedef struct {
|
||||
int exit_code;
|
||||
size_t exit_size;
|
||||
void* exit_stack[10];
|
||||
} spine_exit_config_t;
|
||||
|
||||
/* Process-local aggregate. A full rebuild is required when its layout changes. */
|
||||
typedef struct config_struct {
|
||||
spine_console_config_t console;
|
||||
spine_poller_config_t poller;
|
||||
spine_hosts_config_t hosts;
|
||||
spine_database_config_t database;
|
||||
spine_database_config_t remote_database;
|
||||
spine_php_config_t php;
|
||||
spine_logging_config_t logging;
|
||||
spine_availability_config_t availability;
|
||||
spine_snmp_config_t snmp;
|
||||
spine_boost_config_t boost;
|
||||
spine_exit_config_t exit;
|
||||
int d_b;
|
||||
int cacti_version;
|
||||
int cygwinshloc;
|
||||
} config_t;
|
||||
|
||||
/*! Target Structure
|
||||
*
|
||||
* This structure holds the contents of the Poller Items table and the results
|
||||
* of each polling action.
|
||||
*
|
||||
*/
|
||||
/* Owned SNMP identity and transport options. The borrowed snmp_connection_t
|
||||
* adapter remains separate; these arrays are never external pointers. */
|
||||
typedef struct {
|
||||
char community[100];
|
||||
int version;
|
||||
char username[50];
|
||||
char password[50];
|
||||
char auth_protocol[7];
|
||||
char priv_passphrase[200];
|
||||
char priv_protocol[8];
|
||||
char context[65];
|
||||
char engine_id[30];
|
||||
int port;
|
||||
int timeout;
|
||||
} snmp_profile_t;
|
||||
|
||||
typedef struct {
|
||||
int method;
|
||||
int ping_method;
|
||||
int port;
|
||||
int timeout;
|
||||
int retries;
|
||||
} device_availability_t;
|
||||
|
||||
typedef struct target_struct {
|
||||
int target_id;
|
||||
char result[RESULTS_BUFFER];
|
||||
int local_data_id;
|
||||
int action;
|
||||
char command[256];
|
||||
char hostname[250];
|
||||
char rrd_name[30];
|
||||
char rrd_path[255];
|
||||
int rrd_num;
|
||||
char arg1[1024];
|
||||
char arg2[255];
|
||||
char arg3[255];
|
||||
char output_regex[255];
|
||||
snmp_profile_t snmp;
|
||||
device_availability_t availability;
|
||||
} target_t;
|
||||
|
||||
/*! SNMP OID's Structure
|
||||
*
|
||||
* This structure holds SNMP get results temporarily while polling is taking place.
|
||||
*
|
||||
*/
|
||||
typedef struct snmp_oids {
|
||||
int array_position;
|
||||
char oid[1024];
|
||||
char result[RESULTS_BUFFER];
|
||||
} snmp_oids_t;
|
||||
|
||||
/*! Poller Structure
|
||||
*
|
||||
* This structure holds thread polling instructions.
|
||||
*
|
||||
*/
|
||||
/* Process-local permits use a mutex because Darwin does not implement
|
||||
* unnamed POSIX semaphores. No caller needs a blocking semaphore wait. */
|
||||
typedef struct spine_permits {
|
||||
pthread_mutex_t mutex;
|
||||
int available;
|
||||
} spine_permits_t;
|
||||
|
||||
typedef struct poller_thread {
|
||||
int device_counter;
|
||||
int host_id;
|
||||
int host_thread;
|
||||
int host_threads;
|
||||
int host_data_ids;
|
||||
int threads_complete;
|
||||
int complete;
|
||||
int output_failed;
|
||||
char host_time[40];
|
||||
double host_time_double;
|
||||
spine_permits_t *thread_init_sem;
|
||||
} poller_thread_t;
|
||||
|
||||
/*! PHP Script Server Structure
|
||||
*
|
||||
* This structure holds status and PID information for all the running
|
||||
* PHP Script Server processes.
|
||||
*
|
||||
*/
|
||||
typedef struct php_processes {
|
||||
int php_state;
|
||||
pid_t php_pid;
|
||||
int php_write_fd;
|
||||
int php_read_fd;
|
||||
} php_t;
|
||||
|
||||
/*! Host Structure
|
||||
*
|
||||
* This structure holds host information from the host table and is used throughout
|
||||
* the application.
|
||||
*
|
||||
*/
|
||||
typedef struct {
|
||||
snmp_profile_t profile;
|
||||
int retries;
|
||||
int max_oids;
|
||||
void* session;
|
||||
int status;
|
||||
} host_snmp_t;
|
||||
|
||||
typedef struct {
|
||||
char snmp_sysDescr[600];
|
||||
char snmp_sysObjectID[160];
|
||||
unsigned long long snmp_sysUpTimeInstance;
|
||||
char snmp_sysContact[300];
|
||||
char snmp_sysName[300];
|
||||
char snmp_sysLocation[600];
|
||||
} host_system_t;
|
||||
|
||||
typedef struct {
|
||||
int status;
|
||||
int status_event_count;
|
||||
char status_fail_date[40];
|
||||
char status_rec_date[40];
|
||||
char status_last_error[BUFSIZE * 2 + 2];
|
||||
} host_state_t;
|
||||
|
||||
typedef struct {
|
||||
double min_time;
|
||||
double max_time;
|
||||
double cur_time;
|
||||
double avg_time;
|
||||
int total_polls;
|
||||
int failed_polls;
|
||||
double availability;
|
||||
} host_statistics_t;
|
||||
|
||||
typedef struct host_struct {
|
||||
int id;
|
||||
char hostname[250];
|
||||
int ignore_host;
|
||||
host_snmp_t snmp;
|
||||
host_system_t system;
|
||||
device_availability_t availability;
|
||||
host_state_t state;
|
||||
host_statistics_t statistics;
|
||||
} host_t;
|
||||
|
||||
/*! Host Reindex Structure
|
||||
*
|
||||
* This structure holds the results of the host re-index checks and values.
|
||||
*
|
||||
*/
|
||||
typedef struct host_reindex_struct {
|
||||
char op[4];
|
||||
char assert_value[100];
|
||||
char arg1[1024];
|
||||
int data_query_id;
|
||||
int action;
|
||||
} reindex_t;
|
||||
|
||||
/*! Ping Result Structure
|
||||
*
|
||||
* This structure holds the results of a host ping.
|
||||
*
|
||||
*/
|
||||
typedef struct ping_results {
|
||||
char hostname[BUFSIZE];
|
||||
char ping_status[50];
|
||||
char ping_response[BUFSIZE];
|
||||
char snmp_status[50];
|
||||
char snmp_response[BUFSIZE];
|
||||
} ping_t;
|
||||
|
||||
/*! Name Result Structure
|
||||
*
|
||||
* This structure holds the results of a name/port split
|
||||
*
|
||||
*/
|
||||
typedef struct name_port {
|
||||
// Method = 0 - default, 1 - tcp, 2 - udp
|
||||
char hostname[BUFSIZE];
|
||||
int method;
|
||||
int port;
|
||||
} name_t;
|
||||
|
||||
/*! MySQL Connection Pool Structure
|
||||
*
|
||||
* This structure holds the mysql connection pool object.
|
||||
*/
|
||||
typedef struct db_connection {
|
||||
int id;
|
||||
int free;
|
||||
MYSQL mysql;
|
||||
} pool_t;
|
||||
|
||||
#endif /* SPINE_TYPES_H */
|
||||
@@ -46,69 +46,48 @@
|
||||
* \return TRUE if successful, or FALSE if not.
|
||||
*
|
||||
*/
|
||||
int db_insert(MYSQL *mysql, int type, const char *query) {
|
||||
int error;
|
||||
int error_count = 0;
|
||||
char query_frag[LRG_BUFSIZE];
|
||||
|
||||
/* save a fragment just in case */
|
||||
memset(query_frag, 0, LRG_BUFSIZE);
|
||||
snprintf(query_frag, LRG_BUFSIZE, "%s", query);
|
||||
|
||||
/* show the sql query */
|
||||
SPINE_LOG_DEVDBG(("DEVDBG: SQL:%s", query_frag));
|
||||
|
||||
while(1) {
|
||||
if (set.SQL_readonly == FALSE) {
|
||||
if (mysql_query(mysql, query)) {
|
||||
error = mysql_errno(mysql);
|
||||
|
||||
if (error == 2013 || error == 2006) {
|
||||
if (errno != EINTR) {
|
||||
db_reconnect(mysql, type, error, "db_insert");
|
||||
|
||||
error_count++;
|
||||
|
||||
if (error_count > 30) {
|
||||
die("FATAL: Too many Reconnect Attempts!");
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
usleep(50000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ((error == 1213) || (error == 1205)) {
|
||||
usleep(50000);
|
||||
error_count++;
|
||||
|
||||
if (error_count > 30) {
|
||||
SPINE_LOG(("ERROR: Too many Lock/Deadlock errors occurred!, SQL Fragment:'%s'", query_frag));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
SPINE_LOG(("ERROR: SQL Failed! Error:'%i', Message:'%s', SQL Fragment:'%s'", error, mysql_error(mysql), query_frag));
|
||||
return FALSE;
|
||||
}
|
||||
} else {
|
||||
return TRUE;
|
||||
}
|
||||
} else {
|
||||
return TRUE;
|
||||
}
|
||||
static void retry_disconnected_query(MYSQL *mysql, int type, int error, const char *function, int *error_count) {
|
||||
if (errno == EINTR) {
|
||||
spine_sleep_usec(50000);
|
||||
return;
|
||||
}
|
||||
db_reconnect(mysql, type, error, function);
|
||||
if (++*error_count > 30) die("FATAL: Too many Reconnect Attempts!");
|
||||
}
|
||||
|
||||
int db_insert(MYSQL *mysql, int type, const char *query) {
|
||||
int error_count = 0;
|
||||
char query_frag[LRG_BUFSIZE];
|
||||
snprintf(query_frag, sizeof(query_frag), "%s", query);
|
||||
SPINE_LOG_DEVDBG(("DEVDBG: SQL:%s", query_frag));
|
||||
if (set.poller.SQL_readonly != FALSE) return TRUE;
|
||||
while (mysql_query(mysql, query) != 0) {
|
||||
int error = mysql_errno(mysql);
|
||||
if (error == 2013 || error == 2006) {
|
||||
retry_disconnected_query(mysql, type, error, "db_insert", &error_count);
|
||||
continue;
|
||||
}
|
||||
if (error == 1213 || error == 1205) {
|
||||
spine_sleep_usec(50000);
|
||||
if (++error_count > 30) {
|
||||
SPINE_LOG(("ERROR: Too many Lock/Deadlock errors occurred!, SQL Fragment:'%s'", query_frag));
|
||||
return FALSE;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
SPINE_LOG(("ERROR: SQL Failed! Error:'%i', Message:'%s', SQL Fragment:'%s'", error, mysql_error(mysql), query_frag));
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
int db_reconnect(MYSQL *mysql, int type, int error, const char *function) {
|
||||
unsigned long mysql_thread = 0;
|
||||
char query[100];
|
||||
int ping_status;
|
||||
|
||||
mysql_thread = mysql_thread_id(mysql);
|
||||
mysql_ping(mysql);
|
||||
ping_status = mysql_ping(mysql);
|
||||
|
||||
if (mysql_thread_id(mysql) != mysql_thread) {
|
||||
SPINE_LOG(("WARNING: Connection Broken in Function %s with Error %i. Reconnect via mysql_ping() successful.", function, error));
|
||||
@@ -126,6 +105,11 @@ int db_reconnect(MYSQL *mysql, int type, int error, const char *function) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* A live session on the same thread has nothing to reconnect. */
|
||||
if (ping_status == 0) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* mysql_ping() did not reconnect; do it explicitly */
|
||||
SPINE_LOG(("WARNING: Connection Broken in Function %s with Error %i. Attempting explicit reconnect.", function, error));
|
||||
|
||||
@@ -152,64 +136,29 @@ int db_reconnect(MYSQL *mysql, int type, int error, const char *function) {
|
||||
*
|
||||
*/
|
||||
MYSQL_RES *db_query(MYSQL *mysql, int type, const char *query) {
|
||||
MYSQL_RES *mysql_res = 0;
|
||||
|
||||
int error = 0;
|
||||
int error_count = 0;
|
||||
|
||||
char query_frag[LRG_BUFSIZE];
|
||||
|
||||
/* save a fragment just in case */
|
||||
memset(query_frag, 0, LRG_BUFSIZE);
|
||||
snprintf(query_frag, LRG_BUFSIZE, "%s", query);
|
||||
|
||||
/* show the sql query */
|
||||
int error_count = 0;
|
||||
char query_frag[LRG_BUFSIZE];
|
||||
snprintf(query_frag, sizeof(query_frag), "%s", query);
|
||||
SPINE_LOG_DEVDBG(("DEVDBG: SQL:%s", query_frag));
|
||||
|
||||
while (1) {
|
||||
if (mysql_query(mysql, query)) {
|
||||
error = mysql_errno(mysql);
|
||||
|
||||
if (error == 2013 || error == 2006) {
|
||||
if (errno != EINTR) {
|
||||
db_reconnect(mysql, type, error, "db_query");
|
||||
|
||||
error_count++;
|
||||
|
||||
if (error_count > 30) {
|
||||
die("FATAL: Too many Reconnect Attempts!");
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
usleep(50000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (error == 1213 || error == 1205) {
|
||||
usleep(50000);
|
||||
error_count++;
|
||||
|
||||
if (error_count > 30) {
|
||||
SPINE_LOG(("FATAL: Too many Lock/Deadlock errors occurred!, SQL Fragment:'%s'", query_frag));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
SPINE_LOG(("FATAL: Database Error:'%i', Message:'%s'", error, mysql_error(mysql)));
|
||||
SPINE_LOG(("ERROR: The Query Was:'%s'", query));
|
||||
while (mysql_query(mysql, query) != 0) {
|
||||
int error = mysql_errno(mysql);
|
||||
if (error == 2013 || error == 2006) {
|
||||
retry_disconnected_query(mysql, type, error, "db_query", &error_count);
|
||||
continue;
|
||||
}
|
||||
if (error == 1213 || error == 1205) {
|
||||
spine_sleep_usec(50000);
|
||||
if (++error_count > 30) {
|
||||
SPINE_LOG(("FATAL: Too many Lock/Deadlock errors occurred!, SQL Fragment:'%s'", query_frag));
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
mysql_res = mysql_store_result(mysql);
|
||||
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
SPINE_LOG(("FATAL: Database Error:'%i', Message:'%s'", error, mysql_error(mysql)));
|
||||
SPINE_LOG(("ERROR: The Query Was:'%s'", query));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return mysql_res;
|
||||
return mysql_store_result(mysql);
|
||||
}
|
||||
|
||||
/*! \fn void db_connect(char *database, MYSQL *mysql)
|
||||
@@ -222,58 +171,70 @@ MYSQL_RES *db_query(MYSQL *mysql, int type, const char *query) {
|
||||
* fails more than 20 times, the function will fail and Spine will terminate.
|
||||
*
|
||||
*/
|
||||
void db_address_init(db_address_t *address, const char *value, bool parse_socket) {
|
||||
address->storage = strdup(value);
|
||||
if (address->storage == NULL) die("ERROR: Fatal malloc error: database address!");
|
||||
address->hostname = address->storage;
|
||||
address->socket = NULL;
|
||||
if (!parse_socket) return;
|
||||
struct stat socket_stat;
|
||||
if (stat(address->storage, &socket_stat) == 0) {
|
||||
if (S_ISSOCK(socket_stat.st_mode)) {
|
||||
address->socket = address->storage;
|
||||
address->hostname = NULL;
|
||||
}
|
||||
return;
|
||||
}
|
||||
address->socket = strchr(address->storage, ':');
|
||||
if (address->socket != NULL) *address->socket++ = '\0';
|
||||
}
|
||||
|
||||
void db_address_release(db_address_t *address) {
|
||||
free(address->storage);
|
||||
address->storage = NULL;
|
||||
address->hostname = NULL;
|
||||
address->socket = NULL;
|
||||
}
|
||||
|
||||
void db_set_option(MYSQL *mysql, enum mysql_option option, const void *value, const char *description) {
|
||||
if (mysql_options(mysql, option, value) != 0) {
|
||||
set.exit.exit_code = EXIT_FAILURE;
|
||||
die("FATAL: MySQL options unable to set %s option", description);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HAS_MYSQL_OPT_SSL_KEY
|
||||
static void db_set_ssl_options(MYSQL *mysql, int type) {
|
||||
#ifdef HAS_MYSQL_OPT_SSL_VERIFY_SERVER_CERT
|
||||
int ssl_enabled = type == LOCAL ? set.database.ssl : set.remote_database.ssl;
|
||||
if (ssl_enabled == 0) {
|
||||
bool ssl_enforce = false;
|
||||
db_set_option(mysql, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, &ssl_enforce, "ssl disable");
|
||||
}
|
||||
#endif
|
||||
const char *key = type == REMOTE ? set.remote_database.ssl_key : set.database.ssl_key;
|
||||
const char *ca = type == REMOTE ? set.remote_database.ssl_ca : set.database.ssl_ca;
|
||||
const char *cert = type == REMOTE ? set.remote_database.ssl_cert : set.database.ssl_cert;
|
||||
if (key[0] != '\0') db_set_option(mysql, MYSQL_OPT_SSL_KEY, key, "ssl key");
|
||||
if (ca[0] != '\0') db_set_option(mysql, MYSQL_OPT_SSL_CA, ca, "ssl ca");
|
||||
if (cert[0] != '\0') db_set_option(mysql, MYSQL_OPT_SSL_CERT, cert, "ssl cert");
|
||||
}
|
||||
#endif
|
||||
|
||||
void db_connect(int type, MYSQL *mysql) {
|
||||
int tries;
|
||||
int attempts;
|
||||
int timeout;
|
||||
int rtimeout;
|
||||
int wtimeout;
|
||||
int options_error;
|
||||
int success;
|
||||
int error = 0;
|
||||
MYSQL *connect_error;
|
||||
char *hostname = NULL;
|
||||
char *socket = NULL;
|
||||
struct stat socket_stat;
|
||||
const MYSQL *connect_error;
|
||||
db_address_t address;
|
||||
static int connections = 0;
|
||||
#ifdef HAS_MYSQL_OPT_SSL_KEY
|
||||
char *ssl_key = NULL;
|
||||
char *ssl_ca = NULL;
|
||||
char *ssl_cert = NULL;
|
||||
#endif
|
||||
|
||||
/* see if the hostname variable is a file reference. If so,
|
||||
* and if it is a socket file, setup mysql to use it.
|
||||
*/
|
||||
if (set.poller_id > 1) {
|
||||
if (type == LOCAL) {
|
||||
STRDUP_OR_DIE(hostname, set.db_host, "db_host")
|
||||
|
||||
if (stat(hostname, &socket_stat) == 0) {
|
||||
if (socket_stat.st_mode & S_IFSOCK) {
|
||||
socket = strdup (set.db_host);
|
||||
free(hostname);
|
||||
hostname = NULL;
|
||||
}
|
||||
} else if ((socket = strstr(hostname,":"))) {
|
||||
*socket++ = 0x0;
|
||||
}
|
||||
} else {
|
||||
STRDUP_OR_DIE(hostname, set.rdb_host, "rdb_host")
|
||||
}
|
||||
} else {
|
||||
STRDUP_OR_DIE(hostname, set.db_host, "db_host")
|
||||
|
||||
if (stat(hostname, &socket_stat) == 0) {
|
||||
if (socket_stat.st_mode & S_IFSOCK) {
|
||||
socket = strdup (set.db_host);
|
||||
free(hostname);
|
||||
hostname = NULL;
|
||||
}
|
||||
} else if ((socket = strstr(hostname,":"))) {
|
||||
*socket++ = 0x0;
|
||||
}
|
||||
}
|
||||
bool local_address = set.poller.poller_id <= 1 || type == LOCAL;
|
||||
db_address_init(&address, local_address ? set.database.host : set.remote_database.host, local_address);
|
||||
|
||||
/* initialalize variables */
|
||||
tries = 2;
|
||||
@@ -284,69 +245,41 @@ void db_connect(int type, MYSQL *mysql) {
|
||||
attempts = 1;
|
||||
|
||||
if (mysql_init(mysql) == NULL) {
|
||||
db_address_release(&address);
|
||||
printf("FATAL: Database unable to allocate memory and therefore can not connect\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
MYSQL_SET_OPTION(MYSQL_OPT_READ_TIMEOUT, (int *)&rtimeout, "read timeout");
|
||||
MYSQL_SET_OPTION(MYSQL_OPT_WRITE_TIMEOUT, (int *)&wtimeout, "write timeout");
|
||||
MYSQL_SET_OPTION(MYSQL_OPT_CONNECT_TIMEOUT, (int *)&timeout, "general timeout");
|
||||
db_set_option(mysql, MYSQL_OPT_READ_TIMEOUT, &rtimeout, "read timeout");
|
||||
db_set_option(mysql, MYSQL_OPT_WRITE_TIMEOUT, &wtimeout, "write timeout");
|
||||
db_set_option(mysql, MYSQL_OPT_CONNECT_TIMEOUT, &timeout, "general timeout");
|
||||
|
||||
#ifdef HAS_MYSQL_OPT_RETRY_COUNT
|
||||
MYSQL_SET_OPTION(MYSQL_OPT_RETRY_COUNT, &tries, "retry count");
|
||||
db_set_option(mysql, MYSQL_OPT_RETRY_COUNT, &tries, "retry count");
|
||||
#endif
|
||||
|
||||
/* set SSL options if available */
|
||||
#ifdef HAS_MYSQL_OPT_SSL_KEY
|
||||
/* if the users has explicitly said to disable SSL, do that now */
|
||||
#ifdef HAS_MYSQL_OPT_SSL_VERIFY_SERVER_CERT
|
||||
if (type == LOCAL) {
|
||||
if (set.db_ssl == 0) {
|
||||
bool ssl_enforce = 0;
|
||||
MYSQL_SET_OPTION(MYSQL_OPT_SSL_VERIFY_SERVER_CERT, &ssl_enforce, "ssl disable");
|
||||
}
|
||||
} else {
|
||||
if (set.rdb_ssl == 0) {
|
||||
bool ssl_enforce = 0;
|
||||
MYSQL_SET_OPTION(MYSQL_OPT_SSL_VERIFY_SERVER_CERT, &ssl_enforce, "ssl disable");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (type == REMOTE) {
|
||||
STRDUP_OR_DIE(ssl_key, set.rdb_ssl_key, "rdb_ssl_key");
|
||||
STRDUP_OR_DIE(ssl_ca, set.rdb_ssl_ca, "rdb_ssl_ca");
|
||||
STRDUP_OR_DIE(ssl_cert, set.rdb_ssl_cert, "rdb_ssl_cert");
|
||||
} else {
|
||||
STRDUP_OR_DIE(ssl_key, set.db_ssl_key, "db_ssl_key");
|
||||
STRDUP_OR_DIE(ssl_ca, set.db_ssl_ca, "db_ssl_ca");
|
||||
STRDUP_OR_DIE(ssl_cert, set.db_ssl_cert, "db_ssl_cert");
|
||||
}
|
||||
|
||||
if (strlen(ssl_key)) MYSQL_SET_OPTION(MYSQL_OPT_SSL_KEY, ssl_key, "ssl key");
|
||||
if (strlen(ssl_ca)) MYSQL_SET_OPTION(MYSQL_OPT_SSL_CA, ssl_ca, "ssl ca");
|
||||
if (strlen(ssl_cert)) MYSQL_SET_OPTION(MYSQL_OPT_SSL_CERT, ssl_cert, "ssl cert");
|
||||
|
||||
db_set_ssl_options(mysql, type);
|
||||
#endif
|
||||
|
||||
while (tries > 0) {
|
||||
tries--;
|
||||
|
||||
if (set.poller_id > 1) {
|
||||
if (set.poller.poller_id > 1) {
|
||||
if (type == LOCAL) {
|
||||
connect_error = mysql_real_connect(mysql, hostname, set.db_user, set.db_pass, set.db_db, set.db_port, socket, 0);
|
||||
connect_error = mysql_real_connect(mysql, address.hostname, set.database.user, set.database.password, set.database.database, set.database.port, address.socket, 0);
|
||||
} else {
|
||||
connect_error = mysql_real_connect(mysql, hostname, set.rdb_user, set.rdb_pass, set.rdb_db, set.rdb_port, socket, 0);
|
||||
connect_error = mysql_real_connect(mysql, address.hostname, set.remote_database.user, set.remote_database.password, set.remote_database.database, set.remote_database.port, address.socket, 0);
|
||||
}
|
||||
} else {
|
||||
connect_error = mysql_real_connect(mysql, hostname, set.db_user, set.db_pass, set.db_db, set.db_port, socket, 0);
|
||||
connect_error = mysql_real_connect(mysql, address.hostname, set.database.user, set.database.password, set.database.database, set.database.port, address.socket, 0);
|
||||
}
|
||||
|
||||
if (!connect_error) {
|
||||
error = mysql_errno(mysql);
|
||||
|
||||
if ((error == 2002 || error == 2003 || error == 2006 || error == 2013) && errno == EINTR) {
|
||||
usleep(5000);
|
||||
spine_sleep_usec(5000);
|
||||
tries++;
|
||||
success = FALSE;
|
||||
} else if (error == 2002) {
|
||||
@@ -356,13 +289,12 @@ void db_connect(int type, MYSQL *mysql) {
|
||||
} else if (error != 1049 && error != 2005 && error != 1045) {
|
||||
printf("Database: Connection Failed: Error:'%d', Message:'%s'\n", error, mysql_error(mysql));
|
||||
success = FALSE;
|
||||
usleep(50000);
|
||||
spine_sleep_usec(50000);
|
||||
} else {
|
||||
tries = 0;
|
||||
success = FALSE;
|
||||
}
|
||||
} else {
|
||||
tries = 0;
|
||||
success = TRUE;
|
||||
break;
|
||||
}
|
||||
@@ -370,23 +302,9 @@ void db_connect(int type, MYSQL *mysql) {
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (hostname != NULL) {
|
||||
free(hostname);
|
||||
}
|
||||
db_address_release(&address);
|
||||
|
||||
#ifdef HAS_MYSQL_OPT_SSL_KEY
|
||||
if (ssl_key != NULL) {
|
||||
free(ssl_key);
|
||||
}
|
||||
|
||||
if (ssl_ca != NULL) {
|
||||
free(ssl_ca);
|
||||
}
|
||||
|
||||
if (ssl_cert != NULL) {
|
||||
free(ssl_cert);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!success){
|
||||
printf("FATAL: Connection Failed, Error:'%i', Message:'%s'\n", error, mysql_error(mysql));
|
||||
@@ -419,9 +337,9 @@ void db_create_connection_pool(int type) {
|
||||
int id;
|
||||
|
||||
if (type == LOCAL) {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Creating Local Connection Pool of %i threads.", set.threads));
|
||||
SPINE_LOG_DEBUG(("DEBUG: Creating Local Connection Pool of %i threads.", set.poller.threads));
|
||||
|
||||
for(id = 0; id < set.threads; id++) {
|
||||
for(id = 0; id < set.poller.threads; id++) {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Creating Local Connection %i.", id));
|
||||
|
||||
db_connect(type, &db_pool_local[id].mysql);
|
||||
@@ -438,9 +356,9 @@ void db_create_connection_pool(int type) {
|
||||
db_pool_local[id].id = id;
|
||||
}
|
||||
} else {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Creating Remote Connection Pool of %i threads.", set.threads));
|
||||
SPINE_LOG_DEBUG(("DEBUG: Creating Remote Connection Pool of %i threads.", set.poller.threads));
|
||||
|
||||
for(id = 0; id < set.threads; id++) {
|
||||
for(id = 0; id < set.poller.threads; id++) {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Creating Remote Connection %i.", id));
|
||||
|
||||
db_connect(type, &db_pool_remote[id].mysql);
|
||||
@@ -468,14 +386,14 @@ void db_close_connection_pool(int type) {
|
||||
int id;
|
||||
|
||||
if (type == LOCAL) {
|
||||
for(id = 0; id < set.threads; id++) {
|
||||
for(id = 0; id < set.poller.threads; id++) {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Closing Local Connection Pool ID %i", id));
|
||||
db_disconnect(&db_pool_local[id].mysql);
|
||||
}
|
||||
|
||||
free(db_pool_local);
|
||||
} else {
|
||||
for(id = 0; id < set.threads; id++) {
|
||||
for(id = 0; id < set.poller.threads; id++) {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Closing Remote Connection Pool ID %i", id));
|
||||
db_disconnect(&db_pool_remote[id].mysql);
|
||||
}
|
||||
@@ -496,7 +414,7 @@ pool_t *db_get_connection(int type) {
|
||||
|
||||
if (type == LOCAL) {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Traversing Local Connection Pool for free connection."));
|
||||
for (id = 0; id < set.threads; id++) {
|
||||
for (id = 0; id < set.poller.threads; id++) {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Checking Local Pool ID %i.", id));
|
||||
if (db_pool_local[id].free == TRUE) {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Allocating Local Pool ID %i.", id));
|
||||
@@ -507,7 +425,7 @@ pool_t *db_get_connection(int type) {
|
||||
}
|
||||
} else {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Traversing Remote Connection Pool for free connection."));
|
||||
for (id = 0; id < set.threads; id++) {
|
||||
for (id = 0; id < set.poller.threads; id++) {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Checking Remote Pool ID %i.", id));
|
||||
if (db_pool_remote[id].free == TRUE) {
|
||||
SPINE_LOG_DEBUG(("DEBUG: Allocating Remote Pool ID %i.", id));
|
||||
@@ -560,12 +478,12 @@ void db_release_connection(int type, int id) {
|
||||
* \return the number of characters added to the end of the character buffer
|
||||
*
|
||||
*/
|
||||
int append_hostrange(char *obuf, const char *colname) {
|
||||
if (HOSTID_DEFINED(set.start_host_id) && HOSTID_DEFINED(set.end_host_id)) {
|
||||
return snprintf(obuf, BUFSIZE, " AND %s BETWEEN %d AND %d",
|
||||
int append_hostrange(char *obuf, size_t capacity, const char *colname) {
|
||||
if (HOSTID_DEFINED(set.hosts.start_host_id) && HOSTID_DEFINED(set.hosts.end_host_id)) {
|
||||
return spine_snprintf(obuf, capacity, " AND %s BETWEEN %d AND %d",
|
||||
colname,
|
||||
set.start_host_id,
|
||||
set.end_host_id);
|
||||
set.hosts.start_host_id,
|
||||
set.hosts.end_host_id);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
@@ -584,40 +502,28 @@ int append_hostrange(char *obuf, const char *colname) {
|
||||
*
|
||||
*/
|
||||
void db_escape(MYSQL *mysql, char *output, int max_size, const char *input) {
|
||||
char *input_trimmed;
|
||||
size_t max_escaped_input_size;
|
||||
size_t trim_len;
|
||||
size_t input_len;
|
||||
size_t max_input;
|
||||
|
||||
if (input == NULL) return;
|
||||
if (max_size <= 0) return;
|
||||
if (input == NULL || output == NULL) return;
|
||||
|
||||
/* input_trimmed only ever needs to hold what can fit escaped into
|
||||
* output, so size it to the caller's max_size instead of a fixed
|
||||
* DBL_BUFSIZE. The previous fixed cap meant a larger max_size never
|
||||
* actually admitted a longer input, silently truncating it anyway. */
|
||||
if (!(input_trimmed = (char *) malloc((size_t) max_size))) {
|
||||
die("ERROR: Fatal malloc error: sql.c db_escape!");
|
||||
if (max_size <= 1) {
|
||||
if (max_size == 1) {
|
||||
*output = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
max_escaped_input_size = (strlen(input) * 2) + 1;
|
||||
|
||||
/* Escaping can double every byte, so input_trimmed may hold at most
|
||||
* (max_size - 1) / 2 characters and the escaped result still fits within
|
||||
* max_size, including the terminator. This is always >= 1 for any
|
||||
* max_size >= 1, so the terminator is always reserved: max_size of 1-3
|
||||
* previously drove this to 0 (an unterminated output) or -1 (converted
|
||||
* to a huge size_t that let snprintf overflow the allocation). */
|
||||
trim_len = ((size_t) max_size - 1) / 2 + 1;
|
||||
|
||||
if (max_escaped_input_size > (size_t) max_size) {
|
||||
snprintf(input_trimmed, trim_len, "%s", input);
|
||||
} else {
|
||||
snprintf(input_trimmed, (size_t) max_size, "%s", input);
|
||||
/* Escaping can double every byte and adds a NUL terminator. Derive the
|
||||
* input limit from the caller's destination rather than a fixed staging
|
||||
* buffer, so full RESULTS_BUFFER values survive in a 2N+1 destination. */
|
||||
max_input = ((size_t) max_size - 1) / 2;
|
||||
input_len = strlen(input);
|
||||
if (input_len > max_input) {
|
||||
input_len = max_input;
|
||||
}
|
||||
|
||||
mysql_real_escape_string(mysql, output, input_trimmed, strlen(input_trimmed));
|
||||
|
||||
free(input_trimmed);
|
||||
mysql_real_escape_string(mysql, output, input, (unsigned long) input_len);
|
||||
}
|
||||
|
||||
void db_free_result(MYSQL_RES *result) {
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
|
||||
#ifndef SPINE_SQL_H
|
||||
#define SPINE_SQL_H
|
||||
typedef struct db_address {
|
||||
char *storage;
|
||||
char *hostname;
|
||||
char *socket;
|
||||
} db_address_t;
|
||||
extern void db_address_init(db_address_t *address, const char *value, bool parse_socket);
|
||||
extern void db_address_release(db_address_t *address);
|
||||
|
||||
extern int db_insert(MYSQL *mysql, int type, const char *query);
|
||||
extern MYSQL_RES *db_query(MYSQL *mysql, int type, const char *query);
|
||||
extern void db_connect(int type, MYSQL *mysql);
|
||||
@@ -46,14 +54,8 @@ extern void db_release_connection(int type, int id);
|
||||
extern int db_reconnect(MYSQL *mysql, int type, int error, const char *location);
|
||||
extern int db_column_exists(MYSQL *mysql, int type, const char *table, const char *column);
|
||||
|
||||
extern int append_hostrange(char *obuf, const char *colname);
|
||||
extern int append_hostrange(char *obuf, size_t capacity, const char *colname);
|
||||
|
||||
#define MYSQL_SET_OPTION(opt, value, desc) \
|
||||
{\
|
||||
options_error = mysql_options(mysql, opt, value); \
|
||||
if (options_error < 0) {\
|
||||
die("FATAL: MySQL options unable to set %s option", desc);\
|
||||
}\
|
||||
}
|
||||
extern void db_set_option(MYSQL *mysql, enum mysql_option option, const void *value, const char *description);
|
||||
|
||||
#endif /* SPINE_SQL_H */
|
||||
+28
-25
@@ -72,32 +72,32 @@ DEFINE_SPINE_LOCK(php_proc_14)
|
||||
DEFINE_SPINE_LOCK(thdet)
|
||||
DEFINE_SPINE_LOCK(host_time)
|
||||
|
||||
void init_mutexes(void) {
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_SNMP_O), init_snmp_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_SETEUID_O), init_seteuid_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_GHBN_O), init_ghbn_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_POOL_O), init_pool_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_O), init_php_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_0_O), init_php_proc_0_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_1_O), init_php_proc_1_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_2_O), init_php_proc_2_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_3_O), init_php_proc_3_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_4_O), init_php_proc_4_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_5_O), init_php_proc_5_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_6_O), init_php_proc_6_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_7_O), init_php_proc_7_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_8_O), init_php_proc_8_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_9_O), init_php_proc_9_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_10_O), init_php_proc_10_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_11_O), init_php_proc_11_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_12_O), init_php_proc_12_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_13_O), init_php_proc_13_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_PHP_PROC_14_O), init_php_proc_14_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_THDET_O), init_thdet_lock);
|
||||
pthread_once((pthread_once_t*) get_attr(LOCK_HOST_TIME_O), init_host_time_lock);
|
||||
void init_mutexes() {
|
||||
pthread_once(get_attr(LOCK_SNMP_O), init_snmp_lock);
|
||||
pthread_once(get_attr(LOCK_SETEUID_O), init_seteuid_lock);
|
||||
pthread_once(get_attr(LOCK_GHBN_O), init_ghbn_lock);
|
||||
pthread_once(get_attr(LOCK_POOL_O), init_pool_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_O), init_php_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_0_O), init_php_proc_0_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_1_O), init_php_proc_1_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_2_O), init_php_proc_2_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_3_O), init_php_proc_3_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_4_O), init_php_proc_4_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_5_O), init_php_proc_5_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_6_O), init_php_proc_6_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_7_O), init_php_proc_7_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_8_O), init_php_proc_8_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_9_O), init_php_proc_9_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_10_O), init_php_proc_10_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_11_O), init_php_proc_11_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_12_O), init_php_proc_12_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_13_O), init_php_proc_13_lock);
|
||||
pthread_once(get_attr(LOCK_PHP_PROC_14_O), init_php_proc_14_lock);
|
||||
pthread_once(get_attr(LOCK_THDET_O), init_thdet_lock);
|
||||
pthread_once(get_attr(LOCK_HOST_TIME_O), init_host_time_lock);
|
||||
}
|
||||
|
||||
static const char *get_name(int lock) {
|
||||
const char *get_name(int lock) {
|
||||
switch (lock) {
|
||||
case LOCK_SNMP: return "snmp";
|
||||
case LOCK_SETEUID: return "seteuid";
|
||||
@@ -121,9 +121,9 @@ static const char *get_name(int lock) {
|
||||
case LOCK_PHP_PROC_14: return "php_proc_14";
|
||||
case LOCK_THDET: return "thdet";
|
||||
case LOCK_HOST_TIME: return "host_time";
|
||||
default: return "Unknown lock";
|
||||
}
|
||||
|
||||
return "Unknown lock";
|
||||
}
|
||||
|
||||
pthread_cond_t* get_cond(int lock) {
|
||||
@@ -152,6 +152,7 @@ pthread_cond_t* get_cond(int lock) {
|
||||
case LOCK_PHP_PROC_14: ret_val = &php_proc_14_cond; break;
|
||||
case LOCK_THDET: ret_val = &thdet_cond; break;
|
||||
case LOCK_HOST_TIME: ret_val = &host_time_cond; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
SPINE_LOG_DEVDBG(("LOCKS: [RET] Returning cond for %s", get_name(lock)));
|
||||
@@ -185,6 +186,7 @@ pthread_mutex_t* get_lock(int lock) {
|
||||
case LOCK_PHP_PROC_14: ret_val = &php_proc_14_lock; break;
|
||||
case LOCK_THDET: ret_val = &thdet_lock; break;
|
||||
case LOCK_HOST_TIME: ret_val = &host_time_lock; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
SPINE_LOG_DEVDBG(("LOCKS: [RET] Returning lock for %s", get_name(lock)));
|
||||
@@ -218,6 +220,7 @@ pthread_once_t* get_attr(int locko) {
|
||||
case LOCK_PHP_PROC_14_O: ret_val = &php_proc_14_lock_o; break;
|
||||
case LOCK_THDET_O: ret_val = &thdet_lock_o; break;
|
||||
case LOCK_HOST_TIME_O: ret_val = &host_time_lock_o; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
SPINE_LOG_DEVDBG(("LOCKS: [RET] Returning attr for %s", get_name(locko)));
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
#ifndef SPINE_LOCKS_H
|
||||
#define SPINE_LOCKS_H
|
||||
extern const char* get_name(int lock);
|
||||
extern void init_mutexes(void);
|
||||
extern void thread_mutex_lock(int mutex);
|
||||
extern void thread_mutex_unlock(int mutex);
|
||||
|
||||
Vendored
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| 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 General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| 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 General Public License for more details. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDTool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
| This code is designed, written, and maintained by the Cacti Group. See |
|
||||
| about.php and/or the AUTHORS file for specific developer information. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| http://www.cacti.net/ |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
-- Schema excerpt from Cacti cacti.sql; isolated regression fixtures only.
|
||||
CREATE TABLE settings (
|
||||
name varchar(255) NOT NULL default '',
|
||||
value varchar(4096) NOT NULL default '',
|
||||
PRIMARY KEY (name)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic;
|
||||
|
||||
CREATE TABLE `poller` (
|
||||
`id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`disabled` char(2) DEFAULT '',
|
||||
`name` varchar(30) DEFAULT NULL,
|
||||
`notes` varchar(1024) DEFAULT '',
|
||||
`status` int(10) unsigned NOT NULL DEFAULT '0',
|
||||
`timezone` varchar(40) DEFAULT '',
|
||||
`hostname` varchar(100) NOT NULL DEFAULT '',
|
||||
`dbdefault` varchar(20) NOT NULL DEFAULT '',
|
||||
`dbhost` varchar(64) NOT NULL DEFAULT '',
|
||||
`dbuser` varchar(20) NOT NULL DEFAULT '',
|
||||
`dbpass` varchar(64) NOT NULL DEFAULT '',
|
||||
`dbport` int(10) unsigned DEFAULT '3306',
|
||||
`dbretries` int(10) unsigned DEFAULT '2',
|
||||
`dbssl` char(3) DEFAULT '',
|
||||
`dbsslkey` varchar(255) DEFAULT NULL,
|
||||
`dbsslcert` varchar(255) DEFAULT NULL,
|
||||
`dbsslca` varchar(255) DEFAULT NULL,
|
||||
`total_time` double DEFAULT '0',
|
||||
`max_time` double DEFAULT NULL,
|
||||
`min_time` double DEFAULT NULL,
|
||||
`avg_time` double DEFAULT NULL,
|
||||
`total_polls` int(10) unsigned DEFAULT '0',
|
||||
`processes` int(10) unsigned DEFAULT '1',
|
||||
`threads` int(10) unsigned DEFAULT '1',
|
||||
`sync_interval` int(10) unsigned DEFAULT '7200',
|
||||
`snmp` mediumint(8) unsigned DEFAULT '0',
|
||||
`script` mediumint(8) unsigned DEFAULT '0',
|
||||
`server` mediumint(8) unsigned DEFAULT '0',
|
||||
`last_update` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
`last_status` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
`last_sync` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||
`requires_sync` char(2) DEFAULT '',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `name` (`name`),
|
||||
KEY `disabled` (`disabled`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic COMMENT='Pollers for Cacti';
|
||||
|
||||
CREATE TABLE poller_item (
|
||||
`local_data_id` int(10) unsigned NOT NULL default '0',
|
||||
`poller_id` int(10) unsigned NOT NULL default '1',
|
||||
`host_id` mediumint(8) unsigned NOT NULL default '0',
|
||||
`action` tinyint(3) unsigned NOT NULL default '1',
|
||||
`present` tinyint(3) unsigned NOT NULL DEFAULT '1',
|
||||
`last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`hostname` varchar(100) NOT NULL default '',
|
||||
`snmp_community` varchar(100) NOT NULL default '',
|
||||
`snmp_version` tinyint(3) unsigned NOT NULL default '0',
|
||||
`snmp_username` varchar(50) NOT NULL default '',
|
||||
`snmp_password` varchar(50) NOT NULL default '',
|
||||
`snmp_auth_protocol` char(6) NOT NULL default '',
|
||||
`snmp_priv_passphrase` varchar(200) NOT NULL default '',
|
||||
`snmp_priv_protocol` char(7) NOT NULL default '',
|
||||
`snmp_context` varchar(64) default '',
|
||||
`snmp_engine_id` varchar(64) default '',
|
||||
`snmp_port` mediumint(8) unsigned NOT NULL default '161',
|
||||
`snmp_timeout` mediumint(8) unsigned NOT NULL default '0',
|
||||
`rrd_name` varchar(19) NOT NULL default '',
|
||||
`rrd_path` varchar(255) NOT NULL default '',
|
||||
`rrd_num` tinyint(3) unsigned NOT NULL default '0',
|
||||
`rrd_step` mediumint(8) NOT NULL default '300',
|
||||
`rrd_next_step` mediumint(8) NOT NULL default '0',
|
||||
`arg1` TEXT,
|
||||
`arg2` varchar(255) default NULL,
|
||||
`arg3` varchar(255) default NULL,
|
||||
PRIMARY KEY (`local_data_id`,`rrd_name`),
|
||||
KEY `host_id` (`host_id`),
|
||||
KEY `action` (`action`),
|
||||
KEY `present` (`present`),
|
||||
KEY `poller_id_host_id` (`poller_id`,`host_id`),
|
||||
KEY `poller_id_rrd_next_step` (`poller_id`,`rrd_next_step`),
|
||||
KEY `poller_id_action` (`poller_id`,`action`),
|
||||
KEY `poller_id_last_updated` (`poller_id`, `last_updated`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic;
|
||||
|
||||
CREATE TABLE version (
|
||||
cacti char(20) default '',
|
||||
PRIMARY KEY (cacti)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic;
|
||||
INSERT INTO version VALUES ('1.2.32');
|
||||
INSERT INTO poller (id,threads) VALUES (1,7);
|
||||
Vendored
+153
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| 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 General Public License |
|
||||
| as published by the Free Software Foundation; either version 2 |
|
||||
| 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 General Public License for more details. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDTool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
| This code is designed, written, and maintained by the Cacti Group. See |
|
||||
| about.php and/or the AUTHORS file for specific developer information. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| http://www.cacti.net/ |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
-- Schema excerpt from Cacti develop cacti.sql; isolated poller regressions.
|
||||
CREATE TABLE `host` (
|
||||
id mediumint(8) unsigned NOT NULL auto_increment,
|
||||
poller_id int(10) unsigned NOT NULL default '1',
|
||||
site_id int(10) unsigned NOT NULL default '1',
|
||||
host_template_id mediumint(8) unsigned NOT NULL default '0',
|
||||
description varchar(150) NOT NULL default '',
|
||||
hostname varchar(100) default NULL,
|
||||
location varchar(40) default NULL,
|
||||
notes text,
|
||||
external_id varchar(40) default NULL,
|
||||
snmp_options tinyint(3) unsigned NOT NULL default '0',
|
||||
snmp_community varchar(100) default NULL,
|
||||
snmp_version tinyint(3) unsigned NOT NULL default '1',
|
||||
snmp_username varchar(50) default NULL,
|
||||
snmp_password varchar(50) default NULL,
|
||||
snmp_auth_protocol char(6) default '',
|
||||
snmp_priv_passphrase varchar(200) default '',
|
||||
snmp_priv_protocol char(7) default '',
|
||||
snmp_context varchar(64) default '',
|
||||
snmp_engine_id varchar(64) default '',
|
||||
snmp_port mediumint(8) unsigned NOT NULL default '161',
|
||||
snmp_timeout mediumint(8) unsigned NOT NULL default '500',
|
||||
snmp_retries tinyint(3) unsigned NOT NULL default '3',
|
||||
snmp_sysDescr varchar(300) NOT NULL default '',
|
||||
snmp_sysObjectID varchar(128) NOT NULL default '',
|
||||
snmp_sysUpTimeInstance bigint(20) unsigned NOT NULL default '0',
|
||||
snmp_sysContact varchar(300) NOT NULL default '',
|
||||
snmp_sysName varchar(300) NOT NULL default '',
|
||||
snmp_sysLocation varchar(300) NOT NULL default '',
|
||||
availability_method smallint(5) unsigned NOT NULL default '1',
|
||||
ping_method smallint(5) unsigned default '0',
|
||||
ping_port int(10) unsigned default '0',
|
||||
ping_timeout int(10) unsigned default '500',
|
||||
ping_retries int(10) unsigned default '2',
|
||||
max_oids int(10) unsigned default '10',
|
||||
bulk_walk_size int(11) default '-1',
|
||||
device_threads tinyint(3) unsigned NOT NULL default '1',
|
||||
deleted char(2) NOT NULL default '',
|
||||
disabled char(2) NOT NULL default '',
|
||||
graphs int(10) unsigned NOT NULL default '0',
|
||||
data_sources int(10) unsigned NOT NULL default '0',
|
||||
status tinyint(3) unsigned NOT NULL default '0',
|
||||
status_event_count mediumint(8) unsigned NOT NULL default '0',
|
||||
status_fail_date timestamp NOT NULL default '0000-00-00 00:00:00',
|
||||
status_rec_date timestamp NOT NULL default '0000-00-00 00:00:00',
|
||||
status_options_date timestamp NOT NULL default '0000-00-00 00:00:00',
|
||||
status_last_error varchar(255) default '',
|
||||
min_time decimal(10,5) default '9.99999',
|
||||
max_time decimal(10,5) default '0.00000',
|
||||
cur_time decimal(10,5) default '0.00000',
|
||||
avg_time decimal(10,5) default '0.00000',
|
||||
polling_time DOUBLE default '0',
|
||||
current_errors int(10) unsigned NOT NULL default '0',
|
||||
total_polls int(10) unsigned default '0',
|
||||
failed_polls int(10) unsigned default '0',
|
||||
availability decimal(8,5) NOT NULL default '100.00000',
|
||||
last_updated timestamp default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created timestamp default CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY poller_id_disabled (poller_id, disabled),
|
||||
KEY host_template_id (host_template_id),
|
||||
KEY external_id (external_id),
|
||||
KEY disabled (disabled),
|
||||
KEY status (status),
|
||||
KEY current_errors (current_errors),
|
||||
KEY site_id_location (site_id, location),
|
||||
KEY hostname (hostname),
|
||||
KEY poller_id_last_updated (poller_id, last_updated)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic;
|
||||
|
||||
CREATE TABLE `poller_reindex` (
|
||||
host_id mediumint(8) unsigned NOT NULL default '0',
|
||||
data_query_id mediumint(8) unsigned NOT NULL default '0',
|
||||
action tinyint(3) unsigned NOT NULL default '0',
|
||||
present tinyint(3) unsigned NOT NULL default '1',
|
||||
op char(1) NOT NULL default '',
|
||||
assert_value varchar(100) NOT NULL default '',
|
||||
arg1 varchar(255) NOT NULL default '',
|
||||
PRIMARY KEY (host_id, data_query_id, arg1(187)),
|
||||
KEY present (present)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic;
|
||||
|
||||
CREATE TABLE `poller_output` (
|
||||
local_data_id int(10) unsigned NOT NULL default '0',
|
||||
rrd_name varchar(19) NOT NULL default '',
|
||||
time timestamp NOT NULL default '0000-00-00 00:00:00',
|
||||
output varchar(512) NOT NULL default '',
|
||||
PRIMARY KEY (local_data_id, rrd_name, time) /*!50060 USING BTREE */
|
||||
) ENGINE=MEMORY;
|
||||
|
||||
CREATE TABLE `poller_output_boost` (
|
||||
`local_data_id` int(10) unsigned NOT NULL default '0',
|
||||
`rrd_name` varchar(19) NOT NULL default '',
|
||||
`time` timestamp NOT NULL default '0000-00-00 00:00:00',
|
||||
`output` varchar(512) NOT NULL,
|
||||
`last_updated` timestamp NOT NULL default current_timestamp,
|
||||
PRIMARY KEY USING BTREE (`local_data_id`, `time`, `rrd_name`),
|
||||
KEY `last_updated` (`last_updated`),
|
||||
KEY `time` (`time`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic;
|
||||
|
||||
CREATE TABLE `host_errors` (
|
||||
`host_id` mediumint(8) unsigned NOT NULL default 0,
|
||||
`poller_id` int(10) unsigned NOT NULL default 1,
|
||||
`errors` mediumint(8) unsigned NOT NULL default 0,
|
||||
`local_data_ids` text default NULL,
|
||||
PRIMARY KEY (`host_id`),
|
||||
KEY `poller_id` (`poller_id`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC COMMENT='Holds Device Error buffer for Spine';
|
||||
|
||||
CREATE TABLE `poller_command` (
|
||||
poller_id smallint(5) unsigned NOT NULL default '1',
|
||||
time timestamp NOT NULL default '0000-00-00 00:00:00',
|
||||
action tinyint(3) unsigned NOT NULL default '0',
|
||||
command varchar(191) NOT NULL default '',
|
||||
last_updated timestamp default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (poller_id, action, command),
|
||||
KEY poller_id_last_updated (poller_id, last_updated)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic;
|
||||
|
||||
CREATE TABLE `poller_time` (
|
||||
id bigint(20) unsigned NOT NULL auto_increment,
|
||||
pid int(10) unsigned NOT NULL default '0',
|
||||
poller_id int(10) unsigned NOT NULL default '1',
|
||||
start_time timestamp NOT NULL default '0000-00-00 00:00:00',
|
||||
end_time timestamp NOT NULL default '0000-00-00 00:00:00',
|
||||
PRIMARY KEY (id),
|
||||
KEY `poller_id_end_time` (`poller_id`, `end_time`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=Dynamic;
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
# Copyright (C) 2026 The Cacti Group
|
||||
# Licensed under the GNU Lesser General Public License, version 2.1 or later.
|
||||
# Isolated regression agent; never use this configuration in production.
|
||||
agentaddress udp:127.0.0.1:1161
|
||||
rocommunity regression 127.0.0.1
|
||||
sysLocation isolated-regression-agent
|
||||
sysContact regression
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
# Copyright (C) 2026 The Cacti Group
|
||||
# Licensed under the GNU Lesser General Public License, version 2.1 or later.
|
||||
# Isolated test agent. These usernames and passphrases are public test fixtures.
|
||||
agentaddress udp:127.0.0.1:1162
|
||||
rouser regression-v3-noauth noauth
|
||||
rouser regression-v3-auth auth
|
||||
rouser regression-v3-priv priv
|
||||
sysLocation isolated-regression-v3-agent
|
||||
sysContact regression
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
# Copyright (C) 2026 The Cacti Group
|
||||
# Licensed under the GNU Lesser General Public License, version 2.1 or later.
|
||||
# Copy into an owned temporary persistent snmpd.conf; never a system directory.
|
||||
# The authentication passphrase is the public RFC3414 Appendix A.3.2 vector.
|
||||
createUser regression-v3-noauth
|
||||
createUser regression-v3-auth SHA maplesyrup
|
||||
createUser regression-v3-priv SHA maplesyrup AES regression-privacy
|
||||
+3
-3
@@ -8,8 +8,8 @@
|
||||
#include "spine.h"
|
||||
#include "php.h"
|
||||
|
||||
spine_sem_t available_threads;
|
||||
spine_sem_t available_scripts;
|
||||
spine_permits_t available_threads;
|
||||
spine_permits_t available_scripts;
|
||||
double start_time;
|
||||
double total_time;
|
||||
config_t set;
|
||||
@@ -18,4 +18,4 @@ int *debug_devices;
|
||||
pool_t *db_pool_local;
|
||||
pool_t *db_pool_remote;
|
||||
php_t *php_processes;
|
||||
poller_thread_t *details;
|
||||
poller_thread_t **details;
|
||||
@@ -10,26 +10,27 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
body=$(awk '/^void poller_push_data_to_main\(void\) \{/{f=1} f{print} f&&/^\}/{exit}' util.c)
|
||||
poll_host_body=$(awk '/^void poll_host\(/{f=1} f{print} f&&/^\}/{exit}' poller.c)
|
||||
queries_body=$(awk '/^static void transfer_queries\(/{f=1} f{print} f&&/^\}/{exit}' util.c)
|
||||
table_body=$(awk '/^static bool transfer_table\(/{f=1} f{print} f&&/^\}/{exit}' util.c)
|
||||
prepare_body=$(awk '/^void poller_prepare_queries\(/{f=1} f{print} f&&/^\}/{exit}' poller.c)
|
||||
|
||||
[ -n "$body" ] || fail "could not find poller_push_data_to_main() in util.c"
|
||||
[ -n "$poll_host_body" ] || fail "could not find poll_host() in poller.c"
|
||||
[ -n "$queries_body" ] || fail "could not find transfer_queries() in util.c"
|
||||
[ -n "$table_body" ] || fail "could not find transfer_table() in util.c"
|
||||
[ -n "$prepare_body" ] || fail "could not find poller_prepare_queries() in poller.c"
|
||||
|
||||
printf '%s\n' "$body" | grep -q 'AS rs ON DUPLICATE KEY UPDATE' &&
|
||||
printf '%s\n' "$queries_body" | grep -q 'AS rs' &&
|
||||
fail "remote pushes must not use row-alias syntax selected from the local server"
|
||||
|
||||
printf '%s\n' "$body" | grep -q 'if (set.dbonupdate' &&
|
||||
printf '%s\n' "$queries_body" | grep -q 'onupdate' &&
|
||||
fail "remote pushes must not branch on the local server version"
|
||||
|
||||
awk '/^static void push_flush_batch/{f=1} f{print} f&&/^\}/{exit}' util.c |
|
||||
grep -q 'if (!spine_appendf' ||
|
||||
fail "overflowed remote push batches must not be sent"
|
||||
printf '%s\n' "$table_body" | grep -q 'length + suffix_length + 3 > HUGE_BUFSIZE - used' ||
|
||||
fail "remote push batches must flush before a row that would overflow them"
|
||||
|
||||
printf '%s\n' "$poll_host_body" | grep -q 'if (set\.dbonupdate' &&
|
||||
printf '%s\n' "$prepare_body" | grep -q 'onupdate' &&
|
||||
fail "poller_output must not select SQL syntax from the local server version"
|
||||
|
||||
printf '%s\n' "$poll_host_body" | grep -q 'output=VALUES(output)' ||
|
||||
printf '%s\n' "$prepare_body" | grep -q 'output=VALUES(output)' ||
|
||||
fail "poller_output must use the cross-vendor upsert form"
|
||||
|
||||
exit 0
|
||||
@@ -1,6 +1,6 @@
|
||||
# Single stage: the .gcda files land beside the objects, so /src must survive
|
||||
# into the running container.
|
||||
FROM debian:bookworm-slim
|
||||
FROM mirror.gcr.io/library/debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc make autoconf automake libtool pkg-config \
|
||||
libmariadb-dev libsnmp-dev libssl-dev gcovr \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
db:
|
||||
image: mysql:8.4
|
||||
image: mirror.gcr.io/library/mysql:8.4
|
||||
# The Cacti 1.x schema intentionally uses zero-date sentinel values.
|
||||
command: ["--sql-mode=NO_ENGINE_SUBSTITUTION"]
|
||||
environment:
|
||||
@@ -15,6 +15,6 @@ services:
|
||||
retries: 20
|
||||
|
||||
verify:
|
||||
image: mysql:8.4
|
||||
image: mirror.gcr.io/library/mysql:8.4
|
||||
environment:
|
||||
DB_CLIENT: mysql
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
|
||||
db:
|
||||
image: mariadb:11
|
||||
image: mirror.gcr.io/library/mariadb:11
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: root
|
||||
MARIADB_DATABASE: cacti
|
||||
@@ -51,7 +51,7 @@ services:
|
||||
# Asserts on the database spine wrote to. Without this the fixture only
|
||||
# proves spine exited 0, which a broken poll would also do.
|
||||
verify:
|
||||
image: mariadb:11
|
||||
image: mirror.gcr.io/library/mariadb:11
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:bookworm-slim
|
||||
FROM mirror.gcr.io/library/debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
iproute2 \
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
/* Exercise the built command-line entry point, without reaching a DB.
|
||||
* An explicit absent config keeps a misparsed --version from polling. */
|
||||
static void run_cli_contract(char *const arguments[], int expected, char *output, size_t capacity) {
|
||||
int descriptors[2];
|
||||
assert(pipe(descriptors) == 0);
|
||||
fflush(NULL);
|
||||
pid_t child = fork();
|
||||
assert(child >= 0);
|
||||
if (child == 0) {
|
||||
alarm(15);
|
||||
assert(close(descriptors[0]) == 0);
|
||||
assert(dup2(descriptors[1], STDOUT_FILENO) == STDOUT_FILENO);
|
||||
assert(dup2(descriptors[1], STDERR_FILENO) == STDERR_FILENO);
|
||||
assert(close(descriptors[1]) == 0);
|
||||
execv("./spine", arguments);
|
||||
_exit(127);
|
||||
}
|
||||
assert(close(descriptors[1]) == 0);
|
||||
size_t used = 0;
|
||||
for (;;) {
|
||||
assert(used < capacity - 1);
|
||||
ssize_t received = read(descriptors[0], output + used, capacity - used - 1);
|
||||
if (received < 0 && errno == EINTR) continue;
|
||||
assert(received >= 0);
|
||||
if (received == 0) break;
|
||||
used += (size_t)received;
|
||||
}
|
||||
output[used] = '\0';
|
||||
assert(close(descriptors[0]) == 0);
|
||||
int status;
|
||||
while (waitpid(child, &status, 0) < 0) assert(errno == EINTR);
|
||||
assert(WIFEXITED(status) && WEXITSTATUS(status) == expected);
|
||||
}
|
||||
|
||||
void test_cli_alias_contracts(void) {
|
||||
char output[8192];
|
||||
char version[8192];
|
||||
char help[8192];
|
||||
char *version_args[] = {"spine", "--version", NULL};
|
||||
run_cli_contract(version_args, EXIT_SUCCESS, version, sizeof(version));
|
||||
assert(strstr(version, "SPINE ") != NULL && strstr(version, "Usage:") == NULL);
|
||||
char *help_args[] = {"spine", "--help", NULL};
|
||||
run_cli_contract(help_args, EXIT_SUCCESS, help, sizeof(help));
|
||||
assert(strstr(help, "-h/--help") != NULL && strstr(help, "-H/--hostlist") != NULL);
|
||||
assert(strstr(help, "-p/--poller") != NULL && strstr(help, "-P/--pingonly") != NULL);
|
||||
char *short_help[] = {"spine", "-h", NULL};
|
||||
run_cli_contract(short_help, EXIT_SUCCESS, output, sizeof(output));
|
||||
assert(strcmp(help, output) == 0);
|
||||
char *value_forms[][6] = {
|
||||
{"spine", "-p", "17", "--version", NULL},
|
||||
{"spine", "-p=17", "--version", NULL},
|
||||
{"spine", "--poller=17", "--version", NULL},
|
||||
{"spine", "-H", "17,18", "--version", NULL},
|
||||
{"spine", "-H=17,18", "--version", NULL},
|
||||
{"spine", "--hostlist=17,18", "--version", NULL},
|
||||
{"spine", "-H", " 0 , 0017, 18 ", "--version", NULL},
|
||||
{"spine", "-H", "2147483647", "--version", NULL},
|
||||
{"spine", "-H", "17,17", "--version", NULL},
|
||||
};
|
||||
for (size_t index = 0; index < sizeof(value_forms) / sizeof(value_forms[0]); index++) {
|
||||
run_cli_contract(value_forms[index], EXIT_SUCCESS, output, sizeof(output));
|
||||
assert(strcmp(version, output) == 0);
|
||||
}
|
||||
char *ping_forms[][6] = {
|
||||
{"spine", "-C", "/nonexistent/spine-cli-contract.conf", "-P", "--version", NULL},
|
||||
{"spine", "-C", "/nonexistent/spine-cli-contract.conf", "--pingonly", "--version", NULL},
|
||||
};
|
||||
for (size_t index = 0; index < sizeof(ping_forms) / sizeof(ping_forms[0]); index++) {
|
||||
run_cli_contract(ping_forms[index], EXIT_SUCCESS, output, sizeof(output));
|
||||
assert(strcmp(version, output) == 0);
|
||||
}
|
||||
char *missing_poller[] = {"spine", "-p", NULL};
|
||||
run_cli_contract(missing_poller, EXIT_FAILURE, output, sizeof(output));
|
||||
assert(strstr(output, "ERROR: option -p requires a parameter") != NULL);
|
||||
char *missing_hostlist[] = {"spine", "-H", NULL};
|
||||
run_cli_contract(missing_hostlist, EXIT_FAILURE, output, sizeof(output));
|
||||
assert(strstr(output, "ERROR: option -H requires a parameter") != NULL);
|
||||
char invalid_lists[][48] = {"", " ", ",17", "17,", "17,,18", "17 18", "-1", "+1", "1.5", "1e2", "17x", "17;18", "2147483648", "999999999999999999999999999999999999"};
|
||||
for (size_t index = 0; index < sizeof(invalid_lists) / sizeof(invalid_lists[0]); index++) {
|
||||
char *arguments[] = {"spine", "-H", invalid_lists[index], "--version", NULL};
|
||||
run_cli_contract(arguments, EXIT_FAILURE, output, sizeof(output));
|
||||
assert(strstr(output, "ERROR: invalid or oversized host list") != NULL);
|
||||
}
|
||||
char *oversized = malloc(BIG_BUFSIZE + 1);
|
||||
assert(oversized != NULL);
|
||||
memset(oversized, '0', BIG_BUFSIZE);
|
||||
oversized[BIG_BUFSIZE] = '\0';
|
||||
char *oversized_args[] = {"spine", "-H", oversized, "--version", NULL};
|
||||
run_cli_contract(oversized_args, EXIT_FAILURE, output, sizeof(output));
|
||||
assert(strstr(output, "ERROR: invalid or oversized host list") != NULL);
|
||||
free(oversized);
|
||||
puts("production CLI alias regressions passed");
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* 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 <stdint.h>
|
||||
|
||||
static void test_keyword_roundtrips(void) {
|
||||
typedef struct { const char *word; int value; } keyword_case_t;
|
||||
static const keyword_case_t levels[] = {
|
||||
{"NONE", POLLER_VERBOSITY_NONE}, {"LOW", POLLER_VERBOSITY_LOW},
|
||||
{"MEDIUM", POLLER_VERBOSITY_MEDIUM}, {"HIGH", POLLER_VERBOSITY_HIGH},
|
||||
{"DEBUG", POLLER_VERBOSITY_DEBUG}
|
||||
};
|
||||
static const keyword_case_t destinations[] = {
|
||||
{"FILE", LOGDEST_FILE}, {"SYSLOG", LOGDEST_SYSLOG},
|
||||
{"BOTH", LOGDEST_BOTH}, {"STDOUT", LOGDEST_STDOUT}
|
||||
};
|
||||
static const keyword_case_t actions[] = {
|
||||
{"SNMP", POLLER_ACTION_SNMP}, {"SCRIPT", POLLER_ACTION_SCRIPT},
|
||||
{"PHPSCRIPT", POLLER_ACTION_PHP_SCRIPT_SERVER},
|
||||
{"SNMP_CT", POLLER_ACTION_SNMP_COUNT}, {"SCRIPT_CT", POLLER_ACTION_SCRIPT_COUNT},
|
||||
{"PHPSCRIPT_CT", POLLER_ACTION_PHP_SCRIPT_SERVER_COUNT}
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(levels)/sizeof(levels[0]); i++) {
|
||||
assert(parse_log_level(levels[i].word, -1) == levels[i].value);
|
||||
assert(strcmp(printable_log_level(levels[i].value), levels[i].word) == 0);
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(destinations)/sizeof(destinations[0]); i++) {
|
||||
assert(parse_logdest(destinations[i].word, -1) == destinations[i].value);
|
||||
assert(strcmp(printable_logdest(destinations[i].value), destinations[i].word) == 0);
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(actions)/sizeof(actions[0]); i++) {
|
||||
assert(parse_action(actions[i].word, -1) == actions[i].value);
|
||||
assert(strcmp(printable_action(actions[i].value), actions[i].word) == 0);
|
||||
}
|
||||
assert(parse_log_level("dEbUg", -1) == POLLER_VERBOSITY_DEBUG);
|
||||
assert(parse_logdest("sYsLoG", -1) == LOGDEST_SYSLOG);
|
||||
assert(parse_action("pHpScRiPt_Ct", -1) == POLLER_ACTION_PHP_SCRIPT_SERVER_COUNT);
|
||||
assert(parse_log_level("42", -1) == 42);
|
||||
assert(parse_logdest("007", -1) == 7);
|
||||
assert(parse_action("0", -1) == 0);
|
||||
assert(parse_log_level("", 97) == 97);
|
||||
assert(parse_log_level("unknown", 97) == 97);
|
||||
assert(parse_logdest("-1", 98) == 98);
|
||||
assert(parse_action(" 2", 99) == 99);
|
||||
assert(parse_action("2x", 99) == 99);
|
||||
assert(strcmp(printable_log_level(-1), "-unknown-") == 0);
|
||||
assert(strcmp(printable_logdest(-1), "-unknown-") == 0);
|
||||
assert(strcmp(printable_action(-1), "-unknown-") == 0);
|
||||
}
|
||||
|
||||
static void *try_busy_lock(void *argument) {
|
||||
int lock = *(int *)argument;
|
||||
int result = thread_mutex_trylock(lock);
|
||||
if (result == 0) thread_mutex_unlock(lock);
|
||||
return (void *)(intptr_t)result;
|
||||
}
|
||||
|
||||
static void test_lock_contracts(void) {
|
||||
static const int locks[] = {LOCK_SNMP, LOCK_SETEUID, LOCK_GHBN, LOCK_POOL, LOCK_PHP,
|
||||
LOCK_PHP_PROC_0, LOCK_PHP_PROC_1, LOCK_PHP_PROC_2, LOCK_PHP_PROC_3,
|
||||
LOCK_PHP_PROC_4, LOCK_PHP_PROC_5, LOCK_PHP_PROC_6, LOCK_PHP_PROC_7,
|
||||
LOCK_PHP_PROC_8, LOCK_PHP_PROC_9, LOCK_PHP_PROC_10, LOCK_PHP_PROC_11,
|
||||
LOCK_PHP_PROC_12, LOCK_PHP_PROC_13, LOCK_PHP_PROC_14, LOCK_THDET, LOCK_HOST_TIME};
|
||||
static const char *const names[] = {"snmp", "seteuid", "ghbn", "pool", "php",
|
||||
"php_proc_0", "php_proc_1", "php_proc_2", "php_proc_3", "php_proc_4",
|
||||
"php_proc_5", "php_proc_6", "php_proc_7", "php_proc_8", "php_proc_9",
|
||||
"php_proc_10", "php_proc_11", "php_proc_12", "php_proc_13", "php_proc_14", "thdet", "host_time"};
|
||||
init_mutexes();
|
||||
init_mutexes(); /* pthread_once must preserve the already-created locks. */
|
||||
for (size_t i = 0; i < sizeof(locks)/sizeof(locks[0]); i++) {
|
||||
assert(strcmp(get_name(locks[i]), names[i]) == 0);
|
||||
assert(get_lock(locks[i]) != NULL && get_cond(locks[i]) != NULL && get_attr(locks[i]) != NULL);
|
||||
assert(get_lock(locks[i]) == get_lock(locks[i]));
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
assert(get_lock(locks[i]) != get_lock(locks[j]));
|
||||
assert(get_cond(locks[i]) != get_cond(locks[j]));
|
||||
assert(get_attr(locks[i]) != get_attr(locks[j]));
|
||||
}
|
||||
assert(thread_mutex_trylock(locks[i]) == 0);
|
||||
pthread_t competitor;
|
||||
int lock = locks[i];
|
||||
assert(pthread_create(&competitor, NULL, try_busy_lock, &lock) == 0);
|
||||
void *result;
|
||||
assert(pthread_join(competitor, &result) == 0);
|
||||
assert((intptr_t)result == EBUSY);
|
||||
thread_mutex_unlock(locks[i]);
|
||||
assert(thread_mutex_trylock(locks[i]) == 0);
|
||||
thread_mutex_unlock(locks[i]);
|
||||
}
|
||||
assert(strcmp(get_name(-1), "Unknown lock") == 0);
|
||||
assert(get_lock(-1) == NULL && get_cond(-1) == NULL && get_attr(-1) == NULL);
|
||||
assert(get_lock(1) == NULL && get_cond(1) == NULL && get_attr(1) == NULL);
|
||||
}
|
||||
|
||||
static pthread_mutex_t distribution_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static int distribution[MAX_PHP_SERVERS];
|
||||
static void *next_php_process(void *unused) {
|
||||
(void)unused;
|
||||
for (int i = 0; i < 300; i++) {
|
||||
int process = php_get_process();
|
||||
assert(process >= 0 && process < 3);
|
||||
assert(pthread_mutex_lock(&distribution_mutex) == 0);
|
||||
distribution[process]++;
|
||||
assert(pthread_mutex_unlock(&distribution_mutex) == 0);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_php_roundrobin(void) {
|
||||
int previous_count = set.php.php_servers;
|
||||
int previous_current = set.php.php_current_server;
|
||||
/* php_get_process() hands out healthy slots, so every slot looks like a
|
||||
* live, ready server here. */
|
||||
php_t *previous_processes = php_processes;
|
||||
php_t healthy[MAX_PHP_SERVERS];
|
||||
php_processes_initialize(healthy, MAX_PHP_SERVERS);
|
||||
for (int i = 0; i < MAX_PHP_SERVERS; i++) {
|
||||
healthy[i].php_state = PHP_READY;
|
||||
healthy[i].php_pid = 2;
|
||||
healthy[i].php_read_fd = STDIN_FILENO;
|
||||
healthy[i].php_write_fd = STDOUT_FILENO;
|
||||
}
|
||||
php_processes = healthy;
|
||||
set.php.php_servers = MAX_PHP_SERVERS;
|
||||
set.php.php_current_server = 0;
|
||||
for (int i = 0; i < 2 * MAX_PHP_SERVERS; i++) assert(php_get_process() == i % MAX_PHP_SERVERS);
|
||||
set.php.php_servers = 1;
|
||||
set.php.php_current_server = MAX_PHP_SERVERS;
|
||||
assert(php_get_process() == 0 && php_get_process() == 0);
|
||||
set.php.php_servers = 3;
|
||||
set.php.php_current_server = 0;
|
||||
memset(distribution, 0, sizeof(distribution));
|
||||
pthread_t workers[4];
|
||||
for (size_t i = 0; i < 4; i++) assert(pthread_create(&workers[i], NULL, next_php_process, NULL) == 0);
|
||||
for (size_t i = 0; i < 4; i++) assert(pthread_join(workers[i], NULL) == 0);
|
||||
/* The cursor is fair, but a slot busy in another thread hands that caller
|
||||
* the next healthy one, so only the total and coverage are exact. */
|
||||
assert(distribution[0] + distribution[1] + distribution[2] == 1200);
|
||||
for (int i = 0; i < 3; i++) assert(distribution[i] > 0);
|
||||
assert(set.php.php_current_server == 3);
|
||||
set.php.php_servers = previous_count;
|
||||
set.php.php_current_server = previous_current;
|
||||
php_processes = previous_processes;
|
||||
}
|
||||
|
||||
static void test_legacy_ip_predicate(void) {
|
||||
/* This unused legacy helper recognizes characters, not IP syntax. */
|
||||
assert(is_ipaddress("127.0.0.1"));
|
||||
assert(is_ipaddress("::1"));
|
||||
assert(is_ipaddress(""));
|
||||
assert(is_ipaddress("999.999.999.999"));
|
||||
assert(is_ipaddress(".:"));
|
||||
assert(!is_ipaddress("example.org"));
|
||||
assert(!is_ipaddress("2001:db8::1"));
|
||||
assert(!is_ipaddress("127.0.0.1 "));
|
||||
assert(!is_ipaddress("127.0.0.1/8"));
|
||||
}
|
||||
|
||||
static void test_multipart_boundaries(void) {
|
||||
static const struct { const char *input; int expected; } cases[] = {
|
||||
{NULL, FALSE}, {"", FALSE}, {"123", FALSE}, {"hello", FALSE},
|
||||
{"a:1", TRUE}, {"a!1", TRUE}, {":", TRUE}, {"!", TRUE},
|
||||
{"a:1 b:2", TRUE}, {"a!1 b!2", TRUE}, {"a:1 b!2", TRUE},
|
||||
{"a:1 b:2", FALSE}, {" a:1", FALSE}, {"a:1 ", FALSE},
|
||||
{" a:1 b:2 ", FALSE}, {"a:1 b:2 c:3", TRUE},
|
||||
{"a::1 b:2", FALSE}, {"a::1", TRUE},
|
||||
{"a:1\tb:2", TRUE}, {"a:1\nb!2", TRUE},
|
||||
{"a:1\t b!2", TRUE}, {"a:1 b:2\n c!3", TRUE},
|
||||
{"a:1 \t b:2", FALSE}, {"a b", FALSE}
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) assert(is_multipart_output(cases[i].input) == cases[i].expected);
|
||||
}
|
||||
|
||||
static void test_fatal_signal_contracts(void) {
|
||||
static const struct { int signal; const char *message; } cases[] = {
|
||||
{SIGINT, "Console Operator"},
|
||||
{SIGSEGV, "Segmentation Fault"}, {SIGBUS, "Bus Error"},
|
||||
{SIGFPE, "Floating Point Exception"}, {SIGQUIT, "Keyboard Quit"},
|
||||
{SIGSYS, "Unhandled Exception"}, {SIGABRT, "Abort Signal"}
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||
int diagnostic[2];
|
||||
assert(pipe(diagnostic) == 0);
|
||||
pid_t child = fork();
|
||||
assert(child >= 0);
|
||||
if (child == 0) {
|
||||
alarm(5);
|
||||
close(diagnostic[0]);
|
||||
assert(dup2(diagnostic[1], STDERR_FILENO) == STDERR_FILENO);
|
||||
close(diagnostic[1]);
|
||||
assert(signal(cases[i].signal, SIG_DFL) != SIG_ERR);
|
||||
install_spine_signal_handler();
|
||||
assert(raise(cases[i].signal) == 0);
|
||||
assert(set.exit.exit_code == cases[i].signal);
|
||||
struct sigaction restored;
|
||||
assert(sigaction(cases[i].signal, NULL, &restored) == 0);
|
||||
assert(restored.sa_handler == SIG_DFL);
|
||||
uninstall_spine_signal_handler();
|
||||
_exit(0);
|
||||
}
|
||||
close(diagnostic[1]);
|
||||
char message[512] = {0};
|
||||
size_t used = 0;
|
||||
ssize_t count;
|
||||
while ((count = read(diagnostic[0], message + used, sizeof(message) - used - 1)) != 0) {
|
||||
if (count < 0 && errno == EINTR) continue;
|
||||
assert(count > 0);
|
||||
used += (size_t)count;
|
||||
assert(used < sizeof(message) - 1);
|
||||
}
|
||||
close(diagnostic[0]);
|
||||
int status;
|
||||
assert(waitpid(child, &status, 0) == child);
|
||||
assert(WIFEXITED(status));
|
||||
assert(WEXITSTATUS(status) == (cases[i].signal == SIGSEGV ? 1 : 0));
|
||||
assert(strstr(message, cases[i].message) != NULL);
|
||||
}
|
||||
/* A broken pipe is an ordinary write failure: it must neither terminate
|
||||
* Spine nor be reported as fatal, and the handler stays installed. */
|
||||
pid_t child = fork();
|
||||
assert(child >= 0);
|
||||
if (child == 0) {
|
||||
alarm(5);
|
||||
set.exit.exit_code = 0;
|
||||
install_spine_signal_handler();
|
||||
assert(raise(SIGPIPE) == 0 && raise(SIGPIPE) == 0);
|
||||
assert(set.exit.exit_code == 0);
|
||||
struct sigaction installed;
|
||||
assert(sigaction(SIGPIPE, NULL, &installed) == 0 && installed.sa_handler != SIG_DFL);
|
||||
uninstall_spine_signal_handler();
|
||||
assert(sigaction(SIGPIPE, NULL, &installed) == 0 && installed.sa_handler == SIG_DFL);
|
||||
_exit(0);
|
||||
}
|
||||
int status;
|
||||
assert(waitpid(child, &status, 0) == child);
|
||||
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
void test_additional_contracts(void) {
|
||||
test_keyword_roundtrips();
|
||||
test_lock_contracts();
|
||||
test_php_roundrobin();
|
||||
test_legacy_ip_predicate();
|
||||
test_multipart_boundaries();
|
||||
test_fatal_signal_contracts();
|
||||
puts("production additional contracts passed");
|
||||
}
|
||||
|
||||
void test_additional_database_contracts(MYSQL *mysql) {
|
||||
assert(db_column_exists(mysql, LOCAL, "host", "id") == TRUE);
|
||||
assert(db_column_exists(mysql, LOCAL, "host", "regression_missing_column") == FALSE);
|
||||
/* Only kill a connection created and owned by this test. */
|
||||
MYSQL victim;
|
||||
db_connect(LOCAL, &victim);
|
||||
unsigned long original = mysql_thread_id(&victim);
|
||||
char query[100];
|
||||
snprintf(query, sizeof(query), "KILL CONNECTION %lu", original);
|
||||
assert(mysql_query(mysql, query) == 0);
|
||||
assert(db_reconnect(&victim, LOCAL, 2006, "regression_owned_session") == TRUE);
|
||||
assert(mysql_thread_id(&victim) != original);
|
||||
assert(mysql_query(&victim, "SELECT 123") == 0);
|
||||
MYSQL_RES *result = mysql_store_result(&victim);
|
||||
assert(result != NULL);
|
||||
MYSQL_ROW row = mysql_fetch_row(result);
|
||||
assert(row != NULL && strcmp(row[0], "123") == 0);
|
||||
mysql_free_result(result);
|
||||
/* An already-healthy session does not count as a new reconnection. */
|
||||
assert(db_reconnect(&victim, LOCAL, 2006, "regression_healthy_session") == FALSE);
|
||||
/* Exercise query and mutation callers, not just explicit reconnect. */
|
||||
for (int insert = 0; insert < 2; insert++) {
|
||||
original = mysql_thread_id(&victim);
|
||||
snprintf(query, sizeof(query), "KILL CONNECTION %lu", original);
|
||||
assert(mysql_query(mysql, query) == 0);
|
||||
errno = 0;
|
||||
if (insert) assert(db_insert(&victim, LOCAL, "SET @spine_regression_retry=456"));
|
||||
else {
|
||||
result = db_query(&victim, LOCAL, "SELECT 456");
|
||||
assert(result != NULL);
|
||||
row = mysql_fetch_row(result);
|
||||
assert(row != NULL && row[0] != NULL && strcmp(row[0], "456") == 0);
|
||||
db_free_result(result);
|
||||
}
|
||||
assert(mysql_thread_id(&victim) != original);
|
||||
if (insert) {
|
||||
result = db_query(&victim, LOCAL, "SELECT @spine_regression_retry");
|
||||
assert(result != NULL);
|
||||
row = mysql_fetch_row(result);
|
||||
assert(row != NULL && row[0] != NULL && strcmp(row[0], "456") == 0);
|
||||
db_free_result(result);
|
||||
}
|
||||
}
|
||||
db_disconnect(&victim);
|
||||
}
|
||||
@@ -0,0 +1,712 @@
|
||||
/*
|
||||
* 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 <dirent.h>
|
||||
#include <spawn.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
/* GNU ld wraps the actual API boundary. No production test-only code is used. */
|
||||
extern size_t __real_strftime(char *, size_t, const char *, const struct tm *);
|
||||
extern int __real_mysql_query(MYSQL *, const char *);
|
||||
extern int __real_mysql_ping(MYSQL *);
|
||||
extern unsigned int __real_mysql_errno(MYSQL *);
|
||||
extern int __real_pipe(int [2]);
|
||||
extern int __real_socketpair(int, int, int, int [2]);
|
||||
extern int __real_posix_spawn(pid_t *, const char *, const posix_spawn_file_actions_t *,
|
||||
const posix_spawnattr_t *, char *const [], char *const []);
|
||||
extern void *__real_malloc(size_t);
|
||||
extern char *__real_strdup(const char *);
|
||||
extern void *__real_snmp_sess_open(netsnmp_session *);
|
||||
extern int __real_snmp_sess_close(void *);
|
||||
extern int __real_pthread_create(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *);
|
||||
extern int spine_program_main(int, char **);
|
||||
extern poller_thread_t **details;
|
||||
|
||||
static int format_failures;
|
||||
static int format_fault_calls;
|
||||
static MYSQL *query_fault_connection;
|
||||
static const char *query_fault_statement;
|
||||
static int query_fault_armed;
|
||||
static int observed_query_error;
|
||||
static int query_fault_calls;
|
||||
static int query_ping_calls;
|
||||
static MYSQL *interrupted_connection;
|
||||
static int interrupted_queries;
|
||||
static int interrupted_query_calls;
|
||||
static int pipe_failures;
|
||||
static int socketpair_failures;
|
||||
static int spawn_failures;
|
||||
static int spawn_failure_errno;
|
||||
static int spawn_fault_calls;
|
||||
static int allocation_failures;
|
||||
static int copy_failures;
|
||||
static const char *copy_failure_text;
|
||||
static bool account_snmp_sessions;
|
||||
static void *owned_snmp_session;
|
||||
static int session_opens;
|
||||
static int session_close_attempts;
|
||||
static int session_closes;
|
||||
static bool launch_case_active;
|
||||
static int launch_error;
|
||||
static int launch_failures;
|
||||
static int launch_expected_faults;
|
||||
static int launch_calls;
|
||||
static int launch_fault_calls;
|
||||
static int launch_checkpoints;
|
||||
static int launch_device_counter;
|
||||
static spine_permits_t *launch_startup;
|
||||
|
||||
int __wrap_pthread_create(pthread_t *thread, const pthread_attr_t *attributes,
|
||||
void *(*start)(void *), void *arg) {
|
||||
if (!launch_case_active || start != &child) return __real_pthread_create(thread, attributes, start, arg);
|
||||
const poller_thread_t *work = arg;
|
||||
assert(work != NULL && work->host_id == 902);
|
||||
launch_calls++;
|
||||
launch_device_counter = work->device_counter;
|
||||
launch_startup = work->thread_init_sem;
|
||||
if (launch_failures > 0) {
|
||||
launch_failures--;
|
||||
launch_fault_calls++;
|
||||
return launch_error;
|
||||
}
|
||||
return __real_pthread_create(thread, attributes, start, arg);
|
||||
}
|
||||
|
||||
static void inspect_worker_launch_completion(void) {
|
||||
assert(launch_startup != NULL && launch_calls > 0);
|
||||
assert(spine_permits_available(&available_threads) == 1);
|
||||
assert(spine_permits_available(launch_startup) == 1);
|
||||
assert(spine_permits_available(&available_scripts) == MAX_SIMULTANEOUS_SCRIPTS);
|
||||
assert(db_pool_local != NULL && db_pool_local[0].free);
|
||||
thread_mutex_lock(LOCK_THDET);
|
||||
const poller_thread_t *device = details[launch_device_counter];
|
||||
assert(device != NULL && device->host_id == 902);
|
||||
if (launch_error == EPERM) {
|
||||
assert(set.exit.exit_code == EXIT_FAILURE && !device->complete);
|
||||
assert(device->threads_complete == 0 && !device->output_failed);
|
||||
} else {
|
||||
assert(set.exit.exit_code == EXIT_SUCCESS && device->complete);
|
||||
assert(device->threads_complete == 1 && !device->output_failed);
|
||||
}
|
||||
thread_mutex_unlock(LOCK_THDET);
|
||||
launch_checkpoints++;
|
||||
}
|
||||
|
||||
static void verify_worker_launch_exit(void) {
|
||||
fflush(NULL);
|
||||
fprintf(stderr, "worker launch checkpoint: checkpoints=%i calls=%i faults=%i pending=%i exit=%i\n", launch_checkpoints, launch_calls, launch_fault_calls, launch_failures, set.exit.exit_code);
|
||||
assert(launch_checkpoints == 1 && launch_failures == 0);
|
||||
assert(launch_fault_calls == launch_expected_faults);
|
||||
assert(launch_calls == (launch_error == EAGAIN ? 2 : 1));
|
||||
puts("production worker launch completion checkpoint passed");
|
||||
}
|
||||
|
||||
void *__wrap_snmp_sess_open(netsnmp_session *session) {
|
||||
void *handle = __real_snmp_sess_open(session);
|
||||
if (account_snmp_sessions && handle != NULL) {
|
||||
assert(owned_snmp_session == NULL);
|
||||
owned_snmp_session = handle;
|
||||
session_opens++;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
int __wrap_snmp_sess_close(void *handle) {
|
||||
bool owned = account_snmp_sessions && handle == owned_snmp_session && handle != NULL;
|
||||
if (owned) session_close_attempts++;
|
||||
int result = __real_snmp_sess_close(handle);
|
||||
if (owned && result == 1) {
|
||||
session_closes++;
|
||||
owned_snmp_session = NULL;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t __wrap_strftime(char *output, size_t capacity, const char *format, const struct tm *time) {
|
||||
if (format_failures > 0) {
|
||||
format_failures--;
|
||||
format_fault_calls++;
|
||||
return 0; /* strftime documents zero when its destination is insufficient. */
|
||||
}
|
||||
return __real_strftime(output, capacity, format, time);
|
||||
}
|
||||
|
||||
int __wrap_mysql_query(MYSQL *mysql, const char *query) {
|
||||
static const char completion[] = "UPDATE poller_time SET end_time=NOW() WHERE poller_id=";
|
||||
/* The interrupted handle is never connected; it must not reach the client library. */
|
||||
if (mysql == interrupted_connection) {
|
||||
interrupted_query_calls++;
|
||||
if (interrupted_queries > 0) {
|
||||
interrupted_queries--;
|
||||
errno = EINTR;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (launch_case_active && strncmp(query, completion, sizeof(completion) - 1) == 0) {
|
||||
inspect_worker_launch_completion();
|
||||
}
|
||||
if (query_fault_armed && mysql == query_fault_connection && strcmp(query, query_fault_statement) == 0) {
|
||||
query_fault_armed = 0;
|
||||
bool reconnect = FALSE;
|
||||
assert(mysql_options(mysql, MYSQL_OPT_RECONNECT, &reconnect) == 0);
|
||||
int result = __real_mysql_query(mysql, query);
|
||||
observed_query_error = (int)mysql_errno(mysql);
|
||||
assert(result != 0 && (observed_query_error == 2006 || observed_query_error == 2013));
|
||||
reconnect = TRUE;
|
||||
assert(mysql_options(mysql, MYSQL_OPT_RECONNECT, &reconnect) == 0);
|
||||
assert((int)mysql_errno(mysql) == observed_query_error);
|
||||
query_fault_calls++;
|
||||
return result;
|
||||
}
|
||||
return __real_mysql_query(mysql, query);
|
||||
}
|
||||
|
||||
int __wrap_mysql_ping(MYSQL *mysql) {
|
||||
if (mysql == query_fault_connection) query_ping_calls++;
|
||||
return __real_mysql_ping(mysql);
|
||||
}
|
||||
|
||||
unsigned int __wrap_mysql_errno(MYSQL *mysql) {
|
||||
if (mysql == interrupted_connection) return 2013;
|
||||
return __real_mysql_errno(mysql);
|
||||
}
|
||||
|
||||
int __wrap_pipe(int descriptors[2]) {
|
||||
if (pipe_failures > 0) { pipe_failures--; errno = EMFILE; return -1; }
|
||||
return __real_pipe(descriptors);
|
||||
}
|
||||
|
||||
int __wrap_socketpair(int domain, int type, int protocol, int descriptors[2]) {
|
||||
if (socketpair_failures > 0) { socketpair_failures--; errno = EMFILE; return -1; }
|
||||
return __real_socketpair(domain, type, protocol, descriptors);
|
||||
}
|
||||
|
||||
/* Script children are created with posix_spawn(), which reports failure
|
||||
* through its return value rather than errno. */
|
||||
int __wrap_posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *actions,
|
||||
const posix_spawnattr_t *attributes, char *const arguments[], char *const environment[]) {
|
||||
if (spawn_failures > 0) { spawn_failures--; spawn_fault_calls++; return spawn_failure_errno; }
|
||||
return __real_posix_spawn(pid, path, actions, attributes, arguments, environment);
|
||||
}
|
||||
|
||||
void *__wrap_malloc(size_t size) {
|
||||
if (allocation_failures > 0) { allocation_failures--; errno = ENOMEM; return NULL; }
|
||||
return __real_malloc(size);
|
||||
}
|
||||
|
||||
char *__wrap_strdup(const char *text) {
|
||||
if (copy_failures > 0) { copy_failures--; errno = ENOMEM; return NULL; }
|
||||
if (copy_failure_text != NULL && strcmp(text, copy_failure_text) == 0) { errno = ENOMEM; return NULL; }
|
||||
return __real_strdup(text);
|
||||
}
|
||||
|
||||
static void test_logger_format_failure(void) {
|
||||
config_t previous = set;
|
||||
char filename[] = "spine-log-fault-XXXXXX";
|
||||
int log = mkstemp(filename);
|
||||
assert(log >= 0 && close(log) == 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 = FALSE;
|
||||
STRNCOPY(set.logging.path_logfile, filename);
|
||||
#ifdef DISABLE_STDERR
|
||||
const int diagnostic = STDOUT_FILENO;
|
||||
set.console.stdout_notty = FALSE;
|
||||
#else
|
||||
const int diagnostic = STDERR_FILENO;
|
||||
#endif
|
||||
int saved = dup(diagnostic);
|
||||
int capture[2];
|
||||
assert(saved >= 0 && pipe(capture) == 0);
|
||||
fflush(NULL);
|
||||
assert(dup2(capture[1], diagnostic) == diagnostic && close(capture[1]) == 0);
|
||||
format_failures = 1;
|
||||
format_fault_calls = 0;
|
||||
/* Date formatting failure reports a warning and preserves sink delivery. */
|
||||
assert(spine_log("regression date fallback") == TRUE);
|
||||
assert(format_failures == 0 && format_fault_calls == 1);
|
||||
fflush(NULL);
|
||||
assert(dup2(saved, diagnostic) == diagnostic && close(saved) == 0);
|
||||
char response[LOGSIZE * 2];
|
||||
ssize_t received = read(capture[0], response, sizeof(response) - 1);
|
||||
assert(received > 0 && (size_t)received < sizeof(response) && close(capture[0]) == 0);
|
||||
response[received] = '\0';
|
||||
assert(strstr(response, "ERROR: Could not get string from strftime()") != NULL);
|
||||
assert(strstr(response, "regression date fallback") != NULL);
|
||||
|
||||
set.console.stdout_notty = TRUE;
|
||||
set.console.stderr_notty = TRUE;
|
||||
saved = dup(diagnostic);
|
||||
assert(saved >= 0 && pipe(capture) == 0);
|
||||
assert(dup2(capture[1], diagnostic) == diagnostic && close(capture[1]) == 0);
|
||||
format_failures = 1;
|
||||
assert(spine_log("regression quiet date fallback") == TRUE);
|
||||
fflush(NULL);
|
||||
assert(dup2(saved, diagnostic) == diagnostic && close(saved) == 0);
|
||||
assert(read(capture[0], response, sizeof(response)) == 0 && close(capture[0]) == 0);
|
||||
assert(format_failures == 0 && format_fault_calls == 2);
|
||||
FILE *file = fopen(filename, "r");
|
||||
assert(file != NULL);
|
||||
size_t bytes = fread(response, 1, sizeof(response) - 1, file);
|
||||
assert(bytes > 0 && bytes < sizeof(response) && !ferror(file) && fclose(file) == 0);
|
||||
response[bytes] = '\0';
|
||||
assert(strstr(response, "regression date fallback") != NULL);
|
||||
assert(strstr(response, "regression quiet date fallback") != NULL);
|
||||
assert(strncmp(response, "SPINE: Poller[", strlen("SPINE: Poller[")) == 0);
|
||||
assert(unlink(filename) == 0);
|
||||
set = previous;
|
||||
puts("production logger date-format failure regressions passed");
|
||||
}
|
||||
|
||||
static unsigned int open_descriptor_count(void) {
|
||||
DIR *directory = opendir("/proc/self/fd");
|
||||
assert(directory != NULL);
|
||||
unsigned int count = 0;
|
||||
const struct dirent *entry;
|
||||
while ((entry = readdir(directory)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) count++;
|
||||
}
|
||||
assert(closedir(directory) == 0);
|
||||
return count;
|
||||
}
|
||||
|
||||
static void assert_cancel_state(int expected) {
|
||||
int previous;
|
||||
assert(pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &previous) == 0);
|
||||
assert(previous == expected);
|
||||
assert(pthread_setcancelstate(previous, NULL) == 0);
|
||||
}
|
||||
|
||||
static void test_child_setup_failures(unsigned int expected_descriptors) {
|
||||
/* Redirection and exec now happen inside posix_spawn(), out of reach of
|
||||
* the dup2()/execve() wrappers, so drive the failure through the shell:
|
||||
* a command that cannot execute must still be reaped with 127 and must not
|
||||
* leak a descriptor in either pipe mode. */
|
||||
for (int index = 0; index < 2; index++) {
|
||||
int descriptor = nft_popen("/spine-regression/nonexistent-command 2>/dev/null", index == 1 ? "r+" : "r");
|
||||
assert(descriptor >= 0);
|
||||
char bytes[3];
|
||||
assert(read(descriptor, bytes, sizeof(bytes)) == 0);
|
||||
int status = nft_pclose(descriptor);
|
||||
assert(status == -1 || (WIFEXITED(status) && WEXITSTATUS(status) == 127));
|
||||
assert(open_descriptor_count() == expected_descriptors);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_process_creation_failures(void) {
|
||||
unsigned int descriptors = open_descriptor_count();
|
||||
int unrelated[2];
|
||||
assert(pipe(unrelated) == 0);
|
||||
unsigned int with_unrelated = open_descriptor_count();
|
||||
assert(with_unrelated == descriptors + 2);
|
||||
pipe_failures = 1;
|
||||
assert(nft_popen("printf unused", "r") == -1 && errno == EMFILE);
|
||||
assert(pipe_failures == 0 && open_descriptor_count() == with_unrelated);
|
||||
socketpair_failures = 1;
|
||||
assert(nft_popen("printf unused", "r+") == -1 && errno == EMFILE);
|
||||
assert(socketpair_failures == 0 && open_descriptor_count() == with_unrelated);
|
||||
allocation_failures = 1;
|
||||
assert(nft_popen("printf unused", "r") == -1 && errno == ENOMEM);
|
||||
assert(allocation_failures == 0 && open_descriptor_count() == with_unrelated);
|
||||
assert_cancel_state(PTHREAD_CANCEL_ENABLE);
|
||||
copy_failures = 1;
|
||||
assert(nft_popen("printf unused", "r") == -1 && errno == ENOMEM);
|
||||
assert(copy_failures == 0 && open_descriptor_count() == with_unrelated);
|
||||
assert_cancel_state(PTHREAD_CANCEL_ENABLE);
|
||||
spawn_failures = 4;
|
||||
spawn_failure_errno = EAGAIN;
|
||||
spawn_fault_calls = 0;
|
||||
assert(nft_popen("printf unused", "r") == -1 && errno == EAGAIN);
|
||||
assert(spawn_failures == 0 && spawn_fault_calls == 4 && open_descriptor_count() == with_unrelated);
|
||||
assert_cancel_state(PTHREAD_CANCEL_ENABLE);
|
||||
spawn_failures = 1;
|
||||
spawn_failure_errno = EPERM;
|
||||
spawn_fault_calls = 0;
|
||||
assert(nft_popen("printf unused", "r") == -1 && errno == EPERM);
|
||||
assert(spawn_failures == 0 && spawn_fault_calls == 1 && open_descriptor_count() == with_unrelated);
|
||||
spawn_failures = 2;
|
||||
spawn_failure_errno = EAGAIN;
|
||||
spawn_fault_calls = 0;
|
||||
int descriptor = nft_popen("printf ok", "r");
|
||||
assert(descriptor >= 0 && spawn_failures == 0 && spawn_fault_calls == 2);
|
||||
char bytes[3] = {0};
|
||||
assert(read(descriptor, bytes, 2) == 2 && strcmp(bytes, "ok") == 0);
|
||||
int status = nft_pclose(descriptor);
|
||||
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
assert(open_descriptor_count() == with_unrelated);
|
||||
test_child_setup_failures(with_unrelated);
|
||||
assert(write(unrelated[1], "x", 1) == 1 && read(unrelated[0], bytes, 1) == 1 && bytes[0] == 'x');
|
||||
assert(close(unrelated[0]) == 0 && close(unrelated[1]) == 0);
|
||||
assert(open_descriptor_count() == descriptors);
|
||||
puts("production process creation failure regressions passed");
|
||||
}
|
||||
|
||||
static void kill_owned_connection(MYSQL *administrator, MYSQL *victim) {
|
||||
char query[100];
|
||||
spine_snprintf(query, sizeof(query), "KILL CONNECTION %lu", mysql_thread_id(victim));
|
||||
assert(mysql_query(administrator, query) == 0);
|
||||
}
|
||||
|
||||
static void arm_lost_connection(MYSQL *mysql, const char *statement) {
|
||||
query_fault_connection = mysql;
|
||||
query_fault_statement = statement;
|
||||
query_fault_armed = 1;
|
||||
query_fault_calls = 0;
|
||||
query_ping_calls = 0;
|
||||
observed_query_error = 0;
|
||||
}
|
||||
|
||||
/* A lost-connection error raised while a signal interrupted the call is
|
||||
* retried in place: db_insert() must neither give up nor ping for a reconnect. */
|
||||
static void test_interrupted_insert_retry(void) {
|
||||
MYSQL offline;
|
||||
assert(mysql_init(&offline) == &offline);
|
||||
set.poller.SQL_readonly = FALSE;
|
||||
interrupted_connection = &offline;
|
||||
interrupted_queries = 1;
|
||||
interrupted_query_calls = 0;
|
||||
query_fault_connection = &offline;
|
||||
query_ping_calls = 0;
|
||||
assert(db_insert(&offline, LOCAL, "INSERT INTO interrupted(value) VALUES(1)"));
|
||||
assert(interrupted_queries == 0 && interrupted_query_calls == 2 && query_ping_calls == 0);
|
||||
interrupted_connection = NULL;
|
||||
query_fault_connection = NULL;
|
||||
mysql_close(&offline);
|
||||
puts("production interrupted insert retry regressions passed");
|
||||
}
|
||||
|
||||
/* die() ends the process, so the copy failure runs in a child. The
|
||||
* authentication copy has already succeeded and must be released first. */
|
||||
static void test_snmpv3_privacy_copy_failure(void) {
|
||||
int output[2];
|
||||
assert(pipe(output) == 0);
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
pid_t child = fork();
|
||||
assert(child >= 0);
|
||||
if (child == 0) {
|
||||
if (dup2(output[1], STDERR_FILENO) != STDERR_FILENO) _exit(3);
|
||||
close(output[0]);
|
||||
close(output[1]);
|
||||
snmp_spine_init();
|
||||
copy_failure_text = "fault-privacy-passphrase";
|
||||
snmp_connection_t options = {
|
||||
.host_id = 47, .hostname = "127.0.0.1", .snmp_version = 3,
|
||||
.snmp_community = "", .snmp_username = "fault-v3-user",
|
||||
.snmp_password = "fault-auth-passphrase", .snmp_auth_protocol = "SHA",
|
||||
.snmp_priv_passphrase = "fault-privacy-passphrase", .snmp_priv_protocol = "AES",
|
||||
.snmp_context = "", .snmp_engine_id = "", .snmp_port = 1162,
|
||||
.snmp_timeout = 500
|
||||
};
|
||||
snmp_host_init(&options);
|
||||
_exit(4);
|
||||
}
|
||||
assert(close(output[1]) == 0);
|
||||
char message[BUFSIZE] = {0};
|
||||
size_t used = 0;
|
||||
ssize_t count;
|
||||
while (used < sizeof(message) - 1 && (count = read(output[0], message + used, sizeof(message) - 1 - used)) > 0) {
|
||||
used += (size_t)count;
|
||||
}
|
||||
assert(close(output[0]) == 0);
|
||||
int status;
|
||||
assert(waitpid(child, &status, 0) == child);
|
||||
assert(WIFEXITED(status) && WEXITSTATUS(status) == EXIT_FAILURE);
|
||||
assert(strstr(message, "ERROR: Fatal malloc error: SNMP privacy passphrase") != NULL);
|
||||
assert(strstr(message, "fault-auth-passphrase") == NULL);
|
||||
puts("production SNMPv3 privacy copy failure regressions passed");
|
||||
}
|
||||
|
||||
static void test_real_database_retry(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.SQL_readonly = FALSE;
|
||||
set.logging.log_destination = 0;
|
||||
set.console.stdout_notty = TRUE;
|
||||
set.console.stderr_notty = TRUE;
|
||||
MYSQL administrator;
|
||||
MYSQL victim;
|
||||
db_connect(LOCAL, &administrator);
|
||||
db_connect(LOCAL, &victim);
|
||||
unsigned long original = mysql_thread_id(&victim);
|
||||
kill_owned_connection(&administrator, &victim);
|
||||
const char *read_statement = "SELECT 123 AS recovered_value";
|
||||
arm_lost_connection(&victim, read_statement);
|
||||
MYSQL_RES *result = db_query(&victim, LOCAL, read_statement);
|
||||
assert(result != NULL && mysql_num_rows(result) == 1);
|
||||
MYSQL_ROW row = mysql_fetch_row(result);
|
||||
assert(row != NULL && strcmp(row[0], "123") == 0);
|
||||
db_free_result(result);
|
||||
assert(query_fault_calls == 1 && query_ping_calls == 1 && !query_fault_armed && observed_query_error != 0);
|
||||
assert(mysql_thread_id(&victim) != original);
|
||||
char table[100];
|
||||
spine_snprintf(table, sizeof(table), "spine_fault_%ld", (long)getpid());
|
||||
char query[BUFSIZE];
|
||||
spine_snprintf(query, sizeof(query), "CREATE TABLE %s (value INT NOT NULL) ENGINE=InnoDB", table);
|
||||
assert(db_insert(&administrator, LOCAL, query));
|
||||
original = mysql_thread_id(&victim);
|
||||
kill_owned_connection(&administrator, &victim);
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO %s(value) VALUES(456)", table);
|
||||
arm_lost_connection(&victim, query);
|
||||
assert(db_insert(&victim, LOCAL, query));
|
||||
assert(query_fault_calls == 1 && query_ping_calls == 1 && !query_fault_armed && observed_query_error != 0);
|
||||
assert(mysql_thread_id(&victim) != original);
|
||||
spine_snprintf(query, sizeof(query), "SELECT value FROM %s", table);
|
||||
result = db_query(&administrator, LOCAL, query);
|
||||
assert(result != NULL && mysql_num_rows(result) == 1);
|
||||
row = mysql_fetch_row(result);
|
||||
assert(row != NULL && strcmp(row[0], "456") == 0);
|
||||
db_free_result(result);
|
||||
spine_snprintf(query, sizeof(query), "DROP TABLE %s", table);
|
||||
assert(db_insert(&administrator, LOCAL, query));
|
||||
db_disconnect(&victim);
|
||||
db_disconnect(&administrator);
|
||||
query_fault_connection = NULL;
|
||||
query_fault_statement = NULL;
|
||||
puts("production real lost-connection retry regressions passed");
|
||||
}
|
||||
|
||||
static unsigned long long fault_database_count(MYSQL *mysql, const char *query) {
|
||||
MYSQL_RES *result = db_query(mysql, LOCAL, query);
|
||||
assert(result != NULL && mysql_num_rows(result) == 1);
|
||||
MYSQL_ROW row = mysql_fetch_row(result);
|
||||
assert(row != NULL && row[0] != NULL);
|
||||
unsigned long long count = strtoull(row[0], NULL, 10);
|
||||
db_free_result(result);
|
||||
return count;
|
||||
}
|
||||
|
||||
static void *run_ping_only_worker(void *argument) {
|
||||
assert(mysql_thread_init() == 0);
|
||||
int errors = -1;
|
||||
poll_host(argument, &errors);
|
||||
assert(errors == -1); /* Early ping-only return preserves this output. */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_ping_only_session_lifetime(void) {
|
||||
extern int *debug_devices;
|
||||
const char *agent = getenv("SPINE_TEST_SNMP_HOST");
|
||||
assert(agent != NULL && agent[0] != '\0');
|
||||
config_t previous = set;
|
||||
int *previous_debug_devices = debug_devices;
|
||||
int debug_fixture[100] = {0};
|
||||
debug_devices = debug_fixture;
|
||||
pool_t *previous_pool = db_pool_local;
|
||||
set.poller.threads = 1;
|
||||
set.poller.poller_id = 1;
|
||||
set.poller.mode = REMOTE_OFFLINE;
|
||||
set.availability.ping_only = TRUE;
|
||||
set.snmp.snmp_retries = 0;
|
||||
set.snmp.mibs = 0;
|
||||
MYSQL administrator;
|
||||
db_connect(LOCAL, &administrator);
|
||||
/* Refuse to replace pre-existing records, even in the isolated fixture. */
|
||||
assert(fault_database_count(&administrator, "SELECT COUNT(*) FROM host WHERE id=900") == 0);
|
||||
assert(fault_database_count(&administrator, "SELECT COUNT(*) FROM poller_item WHERE host_id=900") == 0);
|
||||
unsigned long long outputs = fault_database_count(&administrator, "SELECT COUNT(*) FROM poller_output");
|
||||
unsigned long long boosted = fault_database_count(&administrator, "SELECT COUNT(*) FROM poller_output_boost");
|
||||
db_pool_local = calloc(1, sizeof(*db_pool_local));
|
||||
assert(db_pool_local != NULL);
|
||||
db_pool_local[0].free = TRUE;
|
||||
db_connect(LOCAL, &db_pool_local[0].mysql);
|
||||
assert(spine_permits_init(&available_threads, 2) == 0);
|
||||
assert(spine_permits_init(&available_scripts, 3) == 0);
|
||||
char escaped_agent[BUFSIZE];
|
||||
db_escape(&administrator, escaped_agent, sizeof(escaped_agent), agent);
|
||||
snmp_spine_init();
|
||||
for (int with_session = 1; with_session >= 0; with_session--) {
|
||||
char query[LRG_BUFSIZE];
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO host(id,hostname,snmp_version,snmp_community,snmp_port,snmp_timeout,availability_method,status,total_polls,failed_polls,status_fail_date,status_rec_date,snmp_sysLocation) VALUES(900,'%s',2,'%s',1161,500,%i,%i,0,0,'2026-10-06 00:00:00','2026-10-06 00:00:00','ping-only-metadata')", escaped_agent, with_session ? "regression" : "", AVAIL_SNMP, HOST_UP);
|
||||
assert(db_insert(&administrator, LOCAL, query));
|
||||
poller_thread_t work = {0};
|
||||
work.host_id = 900;
|
||||
work.host_thread = 1;
|
||||
work.host_threads = 1;
|
||||
work.host_time_double = get_time_as_double();
|
||||
STRNCOPY(work.host_time, "1791244800");
|
||||
owned_snmp_session = NULL;
|
||||
session_opens = 0;
|
||||
session_close_attempts = 0;
|
||||
session_closes = 0;
|
||||
account_snmp_sessions = TRUE;
|
||||
pthread_t worker;
|
||||
assert(pthread_create(&worker, NULL, run_ping_only_worker, &work) == 0);
|
||||
assert(pthread_join(worker, NULL) == 0);
|
||||
account_snmp_sessions = FALSE;
|
||||
int observed_opens = session_opens;
|
||||
int observed_attempts = session_close_attempts;
|
||||
int observed_closes = session_closes;
|
||||
/* Preserve an old-code failure without leaking the task-owned fixture
|
||||
* handle: this real cleanup is excluded from production close counts. */
|
||||
if (owned_snmp_session != NULL) {
|
||||
assert(__real_snmp_sess_close(owned_snmp_session) == 1);
|
||||
owned_snmp_session = NULL;
|
||||
}
|
||||
assert(db_pool_local[0].free);
|
||||
assert(spine_permits_available(&available_threads) == 2);
|
||||
assert(spine_permits_available(&available_scripts) == 3);
|
||||
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM host WHERE id=900 AND status=%i AND total_polls=1 AND failed_polls=0 AND snmp_sysLocation='ping-only-metadata'", HOST_UP);
|
||||
assert(fault_database_count(&administrator, query) == 1);
|
||||
assert(fault_database_count(&administrator, "SELECT COUNT(*) FROM poller_output") == outputs);
|
||||
assert(fault_database_count(&administrator, "SELECT COUNT(*) FROM poller_output_boost") == boosted);
|
||||
assert(db_insert(&administrator, LOCAL, "DELETE FROM host WHERE id=900"));
|
||||
fprintf(stderr, "ping-only owned session accounting: session=%d opens=%d attempts=%d closes=%d\n", with_session, observed_opens, observed_attempts, observed_closes);
|
||||
assert(observed_opens == with_session);
|
||||
assert(observed_attempts == with_session && observed_closes == with_session);
|
||||
}
|
||||
snmp_spine_close();
|
||||
assert(spine_permits_destroy(&available_scripts) == 0);
|
||||
assert(spine_permits_destroy(&available_threads) == 0);
|
||||
db_close_connection_pool(LOCAL);
|
||||
db_pool_local = previous_pool;
|
||||
db_disconnect(&administrator);
|
||||
debug_devices = previous_debug_devices;
|
||||
set = previous;
|
||||
puts("production ping-only SNMP session ownership regressions passed");
|
||||
}
|
||||
|
||||
static int execute_worker_launch_case(const char *scenario, char *config) {
|
||||
assert(strcmp(scenario, "admitted") == 0 || strcmp(scenario, "rejected") == 0 || strcmp(scenario, "retry") == 0);
|
||||
launch_case_active = TRUE;
|
||||
if (strcmp(scenario, "rejected") == 0) launch_error = EPERM;
|
||||
else if (strcmp(scenario, "retry") == 0) launch_error = EAGAIN;
|
||||
launch_expected_faults = launch_error == 0 ? 0 : 1;
|
||||
launch_failures = launch_expected_faults;
|
||||
assert(atexit(verify_worker_launch_exit) == 0);
|
||||
alarm(15);
|
||||
char interval[] = "poller_interval:5";
|
||||
char profiles[] = "active_profiles:2";
|
||||
char boost[] = "boost_rrd_update_enable:0";
|
||||
char redirect[] = "boost_redirect:0";
|
||||
char *arguments[] = {"spine", "-C", "/nonexistent/spine-fault.conf", "--conf", config,
|
||||
"-p", "1", "-t", "1", "-H", "902", "-O", interval, "-O", profiles,
|
||||
"-O", boost, "-O", redirect, "-S", "-V", "2", NULL};
|
||||
return spine_program_main((int)(sizeof(arguments) / sizeof(arguments[0]) - 1), arguments);
|
||||
}
|
||||
|
||||
static pid_t run_worker_launch_case(const char *scenario, const char *config, int expected) {
|
||||
fflush(NULL);
|
||||
pid_t process = fork();
|
||||
assert(process >= 0);
|
||||
if (process == 0) {
|
||||
execl("./test_spine_faults", "test_spine_faults", "--worker-launch-case", scenario, config, NULL);
|
||||
_exit(127);
|
||||
}
|
||||
int status;
|
||||
assert(waitpid(process, &status, 0) == process);
|
||||
fprintf(stderr, "worker launch case=%s expected=%i status=%i\n", scenario, expected, status);
|
||||
assert(WIFEXITED(status) && WEXITSTATUS(status) == expected);
|
||||
return process;
|
||||
}
|
||||
|
||||
static void test_worker_launch_failures(void) {
|
||||
MYSQL mysql;
|
||||
db_connect(LOCAL, &mysql);
|
||||
/* Refuse any pre-existing spelling of this fixture identity. Grants are
|
||||
* limited to DML on the isolated fixture; no global administrative rights. */
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM mysql.user WHERE User='spine_owned_launch'") == 0);
|
||||
assert(db_insert(&mysql, LOCAL, "CREATE USER 'spine_owned_launch'@'%' IDENTIFIED BY 'regression-only'"));
|
||||
assert(db_insert(&mysql, LOCAL, "GRANT SELECT,INSERT,UPDATE,DELETE ON spine_regressions.* TO 'spine_owned_launch'@'%'"));
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM information_schema.SCHEMA_PRIVILEGES WHERE GRANTEE=CONCAT(CHAR(39),'spine_owned_launch',CHAR(39),'@',CHAR(39),'%',CHAR(39))") == 4);
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM information_schema.SCHEMA_PRIVILEGES WHERE GRANTEE=CONCAT(CHAR(39),'spine_owned_launch',CHAR(39),'@',CHAR(39),'%',CHAR(39)) AND TABLE_SCHEMA='spine_regressions' AND PRIVILEGE_TYPE IN('SELECT','INSERT','UPDATE','DELETE') AND IS_GRANTABLE='NO'") == 4);
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM information_schema.USER_PRIVILEGES WHERE GRANTEE=CONCAT(CHAR(39),'spine_owned_launch',CHAR(39),'@',CHAR(39),'%',CHAR(39)) AND PRIVILEGE_TYPE<>'USAGE'") == 0);
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM host WHERE id=902") == 0);
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=902 OR local_data_id=942001") == 0);
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=942001") == 0);
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=942001") == 0);
|
||||
/* Poller1 always includes device0 if present, regardless of hostlist. */
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=0 AND poller_id=1") == 0);
|
||||
assert(db_insert(&mysql, LOCAL, "INSERT INTO host(id,hostname,poller_id,disabled,device_threads,availability_method,snmp_version,status_fail_date,status_rec_date) VALUES(902,'127.0.0.1',1,'',1,0,0,'2026-10-06 00:00:00','2026-10-06 00:00:00')"));
|
||||
char query[BUFSIZE];
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO poller_item(local_data_id,host_id,poller_id,action,arg1,rrd_name,snmp_port,rrd_step,rrd_next_step) VALUES(942001,902,1,%i,'/usr/bin/printf 123','owned',161,300,0)", POLLER_ACTION_SCRIPT);
|
||||
assert(db_insert(&mysql, LOCAL, query));
|
||||
char directory[] = "/tmp/spine-worker-launch-XXXXXX";
|
||||
assert(mkdtemp(directory) != NULL);
|
||||
struct stat owned;
|
||||
assert(lstat(directory, &owned) == 0 && S_ISDIR(owned.st_mode));
|
||||
assert(owned.st_uid == geteuid() && (owned.st_mode & 0777) == 0700);
|
||||
char config[SMALL_BUFSIZE];
|
||||
char log_path[SMALL_BUFSIZE];
|
||||
spine_snprintf(config, sizeof(config), "%s/spine.conf", directory);
|
||||
spine_snprintf(log_path, sizeof(log_path), "%s/spine.log", directory);
|
||||
int descriptor = open(config, O_WRONLY | O_CREAT | O_EXCL, 0600);
|
||||
assert(descriptor >= 0);
|
||||
assert(fstat(descriptor, &owned) == 0 && S_ISREG(owned.st_mode));
|
||||
assert(owned.st_uid == geteuid() && (owned.st_mode & 0777) == 0600);
|
||||
FILE *file = fdopen(descriptor, "w");
|
||||
assert(file != NULL);
|
||||
/* Public fixture-only credentials follow the parser's two-token grammar.
|
||||
* A value-less DB_Pass line retains its configured default. */
|
||||
assert(fprintf(file, "DB_Host %s\nDB_Database spine_regressions\nDB_User spine_owned_launch\nDB_Pass regression-only\nDB_Port 3306\nCacti_Log %s\n", set.database.host, log_path) > 0);
|
||||
assert(fclose(file) == 0);
|
||||
const char *const scenarios[] = {"admitted", "rejected", "retry"};
|
||||
for (size_t index = 0; index < sizeof(scenarios) / sizeof(scenarios[0]); index++) {
|
||||
assert(db_insert(&mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id=942001"));
|
||||
assert(db_insert(&mysql, LOCAL, "UPDATE poller_item SET rrd_next_step=0 WHERE host_id=902 AND local_data_id=942001"));
|
||||
unsigned long long first_id = fault_database_count(&mysql, "SELECT COALESCE(MAX(id),0) FROM poller_time");
|
||||
bool rejected = index == 1;
|
||||
pid_t process = run_worker_launch_case(scenarios[index], config, rejected ? EXIT_FAILURE : EXIT_SUCCESS);
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=942001 AND output='123'") == (rejected ? 0 : 1));
|
||||
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_item WHERE host_id=902 AND local_data_id=942001 AND rrd_next_step=%i", rejected ? 0 : 295);
|
||||
assert(fault_database_count(&mysql, query) == 1);
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=942001") == 0);
|
||||
spine_snprintf(query, sizeof(query), "DELETE FROM poller_time WHERE poller_id=1 AND pid=%ld AND id>%llu", (long)process, first_id);
|
||||
assert(db_insert(&mysql, LOCAL, query));
|
||||
}
|
||||
assert(db_insert(&mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id=942001"));
|
||||
assert(db_insert(&mysql, LOCAL, "DELETE FROM poller_item WHERE host_id=902 AND local_data_id=942001"));
|
||||
assert(db_insert(&mysql, LOCAL, "DELETE FROM host WHERE id=902"));
|
||||
assert(db_insert(&mysql, LOCAL, "DROP USER 'spine_owned_launch'@'%'"));
|
||||
assert(fault_database_count(&mysql, "SELECT COUNT(*) FROM mysql.user WHERE User='spine_owned_launch'") == 0);
|
||||
assert(unlink(config) == 0);
|
||||
if (unlink(log_path) != 0) assert(errno == ENOENT);
|
||||
assert(rmdir(directory) == 0);
|
||||
db_disconnect(&mysql);
|
||||
puts("production actual main worker launch failure and retry regressions passed");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
/* Fresh exec enters production initialization exactly once, without
|
||||
* reinitializing the ordinary fault harness's inherited mutexes. */
|
||||
if (argc == 4 && strcmp(argv[1], "--worker-launch-case") == 0) {
|
||||
return execute_worker_launch_case(argv[2], argv[3]);
|
||||
}
|
||||
config_defaults();
|
||||
init_mutexes();
|
||||
alarm(20);
|
||||
test_logger_format_failure();
|
||||
test_process_creation_failures();
|
||||
test_interrupted_insert_retry();
|
||||
test_snmpv3_privacy_copy_failure();
|
||||
if (argc == 2 && strcmp(argv[1], "--database") == 0) {
|
||||
test_real_database_retry();
|
||||
test_ping_only_session_lifetime();
|
||||
test_worker_launch_failures();
|
||||
}
|
||||
else assert(argc == 1);
|
||||
alarm(0);
|
||||
puts("production linker fault regressions passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
/*
|
||||
* 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>
|
||||
|
||||
extern poller_thread_t **details;
|
||||
extern int *debug_devices;
|
||||
|
||||
static unsigned long long output_count(MYSQL *mysql, const char *query) {
|
||||
MYSQL_RES *result = db_query(mysql, LOCAL, query);
|
||||
assert(result != NULL && mysql_num_rows(result) == 1);
|
||||
MYSQL_ROW row = mysql_fetch_row(result);
|
||||
assert(row != NULL && row[0] != NULL);
|
||||
unsigned long long value = strtoull(row[0], NULL, 10);
|
||||
db_free_result(result);
|
||||
return value;
|
||||
}
|
||||
|
||||
static void assert_output_engine(MYSQL *mysql, const char *table, const char *engine) {
|
||||
char query[BUFSIZE];
|
||||
spine_snprintf(query, sizeof(query), "SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='%s'", table);
|
||||
MYSQL_RES *result = db_query(mysql, LOCAL, query);
|
||||
assert(result != NULL && mysql_num_rows(result) == 1);
|
||||
MYSQL_ROW row = mysql_fetch_row(result);
|
||||
assert(row != NULL && row[0] != NULL && STRIMATCH(row[0], engine));
|
||||
db_free_result(result);
|
||||
}
|
||||
|
||||
static void run_output_partition(const poller_thread_t *aggregate, int partition) {
|
||||
poller_thread_t *work = malloc(sizeof(*work));
|
||||
assert(work != NULL);
|
||||
*work = *aggregate;
|
||||
work->host_thread = partition;
|
||||
spine_permits_t startup;
|
||||
assert(spine_permits_init(&startup, 1) == 0);
|
||||
assert(spine_permits_try_acquire(&startup) == 0);
|
||||
work->thread_init_sem = &startup;
|
||||
assert(spine_permits_try_acquire(&available_threads) == 0);
|
||||
pthread_t worker;
|
||||
/* Production child owns/frees instructions and releases thread/startup
|
||||
* permits after poll_host, including output failures. */
|
||||
assert(pthread_create(&worker, NULL, child, work) == 0);
|
||||
assert(pthread_join(worker, NULL) == 0);
|
||||
assert(spine_permits_available(&startup) == 1);
|
||||
assert(spine_permits_destroy(&startup) == 0);
|
||||
assert(spine_permits_available(&available_threads) == 1);
|
||||
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
|
||||
}
|
||||
|
||||
/* A real bounded PHP-server producer: consume command lines and send numeric
|
||||
* samples. The production PHP transport and poll_host do all interpretation. */
|
||||
static void send_output_sample(int fd, const char *result, size_t length) {
|
||||
size_t sent = 0;
|
||||
while (sent < length) {
|
||||
ssize_t count = write(fd, result + sent, length - sent);
|
||||
if (count < 0 && errno == EINTR) continue;
|
||||
assert(count > 0);
|
||||
sent += (size_t)count;
|
||||
}
|
||||
}
|
||||
|
||||
static pid_t start_output_server(php_t *server, int payload) {
|
||||
int requests[2];
|
||||
int responses[2];
|
||||
assert(pipe(requests) == 0 && pipe(responses) == 0);
|
||||
fflush(NULL);
|
||||
pid_t child = fork();
|
||||
assert(child >= 0);
|
||||
if (child == 0) {
|
||||
alarm(180);
|
||||
assert(close(requests[1]) == 0 && close(responses[0]) == 0);
|
||||
FILE *input = fdopen(requests[0], "r");
|
||||
assert(input != NULL);
|
||||
char command[BUFSIZE];
|
||||
char result[512];
|
||||
assert(payload >= 3 && payload < (int)sizeof(result));
|
||||
memset(result, '0', payload - 3);
|
||||
memcpy(result + payload - 3, "123\n", 4);
|
||||
while (fgets(command, sizeof(command), input) != NULL) {
|
||||
send_output_sample(responses[1], result, (size_t)payload + 1);
|
||||
}
|
||||
assert(!ferror(input) && fclose(input) == 0 && close(responses[1]) == 0);
|
||||
_exit(0);
|
||||
}
|
||||
assert(close(requests[0]) == 0 && close(responses[1]) == 0);
|
||||
server->php_state = PHP_READY;
|
||||
server->php_pid = child;
|
||||
server->php_write_fd = requests[1];
|
||||
server->php_read_fd = responses[0];
|
||||
return child;
|
||||
}
|
||||
|
||||
static void reset_output_work(poller_thread_t *work, int rows, int partitions) {
|
||||
memset(work, 0, sizeof(*work));
|
||||
work->host_id = 901;
|
||||
work->host_threads = partitions;
|
||||
work->host_thread = partitions;
|
||||
work->host_data_ids = rows / partitions;
|
||||
work->host_time_double = get_time_as_double();
|
||||
STRNCOPY(work->host_time, "1791244800");
|
||||
set.exit.exit_code = EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
static void assert_sample(MYSQL *mysql, const char *table, int id, const char *expected) {
|
||||
char query[BUFSIZE];
|
||||
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM %s WHERE local_data_id=%i AND output=%s", table, id, expected);
|
||||
assert(output_count(mysql, query) == 1);
|
||||
}
|
||||
|
||||
static void assert_sample_at(MYSQL *mysql, const char *table, int id,
|
||||
const char *timestamp, const char *expected) {
|
||||
char query[BUFSIZE];
|
||||
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM %s WHERE local_data_id=%i AND time=FROM_UNIXTIME(%s) AND output='%s'", table, id, timestamp, expected);
|
||||
assert(output_count(mysql, query) == 1);
|
||||
}
|
||||
|
||||
static void test_output_recollection(MYSQL *mysql) {
|
||||
poller_thread_t **previous_details = details;
|
||||
char query[BUFSIZE];
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item WHERE host_id=901"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id BETWEEN 930001 AND 939999"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost WHERE local_data_id BETWEEN 930001 AND 939999"));
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO poller_item(local_data_id,host_id,poller_id,action,arg1,rrd_name,snmp_port,rrd_step,rrd_next_step) VALUES(930001,901,1,%i,'/usr/bin/printf 123','owned',161,300,0),(930002,901,1,%i,'/usr/bin/printf 123','owned',162,300,0)", POLLER_ACTION_SCRIPT, POLLER_ACTION_SCRIPT);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_output(local_data_id,rrd_name,time,output) VALUES(930001,'owned',FROM_UNIXTIME(1791244800),'old'),(930002,'owned',FROM_UNIXTIME(1791244800),'old')"));
|
||||
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_output_boost(local_data_id,rrd_name,time,output) VALUES(930001,'owned',FROM_UNIXTIME(1791244800),'old'),(930002,'owned',FROM_UNIXTIME(1791244800),'old')"));
|
||||
assert(db_insert(mysql, LOCAL, "CREATE TRIGGER spine_owned_output_reject BEFORE INSERT ON poller_output FOR EACH ROW BEGIN IF NEW.local_data_id=930002 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='owned output rejection'; END IF; END"));
|
||||
poller_thread_t *work = calloc(1, sizeof(*work));
|
||||
assert(work != NULL);
|
||||
reset_output_work(work, 2, 1);
|
||||
poller_thread_t **owned_details = calloc(1, sizeof(*owned_details));
|
||||
assert(owned_details != NULL);
|
||||
owned_details[0] = work;
|
||||
details = owned_details;
|
||||
run_output_partition(work, 1);
|
||||
assert(set.exit.exit_code == EXIT_FAILURE && work->output_failed && !work->complete);
|
||||
assert(work->threads_complete == 1);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=901 AND rrd_next_step=0") == 2);
|
||||
assert_sample_at(mysql, "poller_output", 930001, "1791244800", "123");
|
||||
assert_sample_at(mysql, "poller_output", 930002, "1791244800", "old");
|
||||
assert_sample_at(mysql, "poller_output_boost", 930001, "1791244800", "123");
|
||||
assert_sample_at(mysql, "poller_output_boost", 930002, "1791244800", "123");
|
||||
assert(db_insert(mysql, LOCAL, "DROP TRIGGER spine_owned_output_reject"));
|
||||
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET arg1='/usr/bin/printf 456' WHERE host_id=901"));
|
||||
/* A later poll recollects current values, rather than replaying failed
|
||||
* samples. Supply distinct work timestamps without depending on the clock. */
|
||||
reset_output_work(work, 2, 1);
|
||||
STRNCOPY(work->host_time, "1791244805");
|
||||
run_output_partition(work, 1);
|
||||
assert(set.exit.exit_code == EXIT_SUCCESS && !work->output_failed && work->complete);
|
||||
assert(work->threads_complete == 1);
|
||||
for (int id = 930001; id <= 930002; id++) {
|
||||
assert_sample_at(mysql, "poller_output", id, "1791244805", "456");
|
||||
assert_sample_at(mysql, "poller_output_boost", id, "1791244805", "456");
|
||||
assert_sample_at(mysql, "poller_output_boost", id, "1791244800", "123");
|
||||
}
|
||||
assert_sample_at(mysql, "poller_output", 930001, "1791244800", "123");
|
||||
assert_sample_at(mysql, "poller_output", 930002, "1791244800", "old");
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=901 AND rrd_next_step=295") == 2);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM host_errors WHERE host_id=901") == 0);
|
||||
assert_sample(mysql, "poller_output", 930000, "'unchanged'");
|
||||
assert_sample(mysql, "poller_output_boost", 930000, "'unchanged'");
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id BETWEEN 930000 AND 939999") == 5);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id BETWEEN 930000 AND 939999") == 5);
|
||||
details = previous_details;
|
||||
free(owned_details);
|
||||
free(work);
|
||||
puts("production output recollection retains incomplete historical samples passed");
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
poller_thread_t *work;
|
||||
int finished_fd;
|
||||
} controlled_output_worker_t;
|
||||
|
||||
static void output_worker_finished(void *arg) {
|
||||
const controlled_output_worker_t *worker = arg;
|
||||
/* This outer handler runs after production child cleanup frees instructions
|
||||
* and releases its permit, even though child exits through pthread_exit. */
|
||||
send_output_sample(worker->finished_fd, "F", 1);
|
||||
}
|
||||
|
||||
static void *controlled_output_child(void *arg) {
|
||||
const controlled_output_worker_t *worker = arg;
|
||||
pthread_cleanup_push(output_worker_finished, arg);
|
||||
child(worker->work);
|
||||
pthread_cleanup_pop(1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void output_descriptor_flags(int fd) {
|
||||
int flags = fcntl(fd, F_GETFD);
|
||||
assert(flags >= 0 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == 0);
|
||||
flags = fcntl(fd, F_GETFL);
|
||||
assert(flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0);
|
||||
}
|
||||
|
||||
static void await_output_byte(int fd, char expected, double deadline) {
|
||||
assert(spine_wait_readable(fd, deadline) == 1);
|
||||
char value;
|
||||
ssize_t count;
|
||||
do { count = read(fd, &value, 1); } while (count < 0 && errno == EINTR);
|
||||
assert(count == 1 && value == expected);
|
||||
}
|
||||
|
||||
static void close_output_signal(int ready, int release, const int finished[2], const char *ready_path, const char *release_path) {
|
||||
assert(close(ready) == 0 && close(release) == 0);
|
||||
assert(close(finished[0]) == 0 && close(finished[1]) == 0);
|
||||
assert(unlink(ready_path) == 0 && unlink(release_path) == 0);
|
||||
}
|
||||
|
||||
static void test_concurrent_output_failure(MYSQL *mysql) {
|
||||
pool_t *previous_pool = db_pool_local;
|
||||
poller_thread_t **previous_details = details;
|
||||
int previous_threads = set.poller.threads;
|
||||
int previous_timeout = set.php.script_timeout;
|
||||
assert(previous_threads == 1 && db_pool_local[0].free);
|
||||
assert(spine_permits_available(&available_threads) == 1);
|
||||
assert(spine_permits_available(&available_scripts) == 2);
|
||||
assert(spine_permits_destroy(&available_threads) == 0);
|
||||
assert(spine_permits_init(&available_threads, 2) == 0);
|
||||
set.poller.threads = 2;
|
||||
set.php.script_timeout = 30;
|
||||
db_pool_local = calloc(2, sizeof(*db_pool_local));
|
||||
assert(db_pool_local != NULL);
|
||||
db_create_connection_pool(LOCAL);
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item WHERE host_id=901"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id BETWEEN 930001 AND 939999"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost WHERE local_data_id BETWEEN 930001 AND 939999"));
|
||||
char directory[] = "/tmp/spine-output-overlap-XXXXXX";
|
||||
assert(mkdtemp(directory) != NULL);
|
||||
struct stat directory_status;
|
||||
assert(lstat(directory, &directory_status) == 0);
|
||||
assert(S_ISDIR(directory_status.st_mode));
|
||||
assert((directory_status.st_mode & 0777) == 0700 && directory_status.st_uid == geteuid());
|
||||
char ready_paths[2][SMALL_BUFSIZE];
|
||||
char release_paths[2][SMALL_BUFSIZE];
|
||||
int ready[2];
|
||||
int release[2];
|
||||
int finished[2][2];
|
||||
char query[LRG_BUFSIZE];
|
||||
for (int index = 0; index < 2; index++) {
|
||||
spine_snprintf(ready_paths[index], sizeof(ready_paths[index]), "%s/ready%i", directory, index);
|
||||
spine_snprintf(release_paths[index], sizeof(release_paths[index]), "%s/release%i", directory, index);
|
||||
assert(mkfifo(ready_paths[index], 0600) == 0 && mkfifo(release_paths[index], 0600) == 0);
|
||||
ready[index] = open(ready_paths[index], O_RDWR | O_NONBLOCK);
|
||||
release[index] = open(release_paths[index], O_RDWR | O_NONBLOCK);
|
||||
assert(ready[index] >= 0 && release[index] >= 0 && pipe(finished[index]) == 0);
|
||||
output_descriptor_flags(ready[index]);
|
||||
output_descriptor_flags(release[index]);
|
||||
output_descriptor_flags(finished[index][0]);
|
||||
output_descriptor_flags(finished[index][1]);
|
||||
/* Quote-free SQL string; the real shell producer signals selection then
|
||||
* blocks on its own gate before publishing a sample to Spine. */
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO poller_item(local_data_id,host_id,poller_id,action,arg1,rrd_name,snmp_port,rrd_step,rrd_next_step) VALUES(%i,901,1,%i,'/usr/bin/printf R > %s; read token < %s; /usr/bin/printf 123','owned',%i,300,0)", 930001 + index, POLLER_ACTION_SCRIPT, ready_paths[index], release_paths[index], 161 + index);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
}
|
||||
assert(db_insert(mysql, LOCAL, "CREATE TRIGGER spine_owned_output_reject BEFORE INSERT ON poller_output FOR EACH ROW BEGIN IF NEW.local_data_id=930002 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='owned output rejection'; END IF; END"));
|
||||
poller_thread_t *aggregate = calloc(1, sizeof(*aggregate));
|
||||
assert(aggregate != NULL);
|
||||
reset_output_work(aggregate, 2, 2);
|
||||
poller_thread_t **owned_details = calloc(1, sizeof(*owned_details));
|
||||
assert(owned_details != NULL);
|
||||
owned_details[0] = aggregate;
|
||||
details = owned_details;
|
||||
spine_permits_t startup;
|
||||
assert(spine_permits_init(&startup, 2) == 0);
|
||||
controlled_output_worker_t workers[2];
|
||||
pthread_t threads[2];
|
||||
for (int index = 0; index < 2; index++) {
|
||||
workers[index].work = malloc(sizeof(*workers[index].work));
|
||||
assert(workers[index].work != NULL);
|
||||
thread_mutex_lock(LOCK_THDET);
|
||||
*workers[index].work = *aggregate;
|
||||
thread_mutex_unlock(LOCK_THDET);
|
||||
workers[index].work->host_thread = index + 1;
|
||||
workers[index].work->thread_init_sem = &startup;
|
||||
workers[index].finished_fd = finished[index][1];
|
||||
assert(spine_permits_try_acquire(&startup) == 0);
|
||||
assert(spine_permits_try_acquire(&available_threads) == 0);
|
||||
assert(pthread_create(&threads[index], NULL, controlled_output_child, &workers[index]) == 0);
|
||||
}
|
||||
double deadline = spine_monotonic_time() + 10;
|
||||
await_output_byte(ready[0], 'R', deadline);
|
||||
await_output_byte(ready[1], 'R', deadline);
|
||||
assert(spine_permits_available(&startup) == 2);
|
||||
assert(spine_permits_available(&available_threads) == 0);
|
||||
assert(spine_permits_available(&available_scripts) == 0);
|
||||
thread_mutex_lock(LOCK_POOL);
|
||||
assert(!db_pool_local[0].free && !db_pool_local[1].free);
|
||||
thread_mutex_unlock(LOCK_POOL);
|
||||
/* Both real workers have selected their own partition and hold resources.
|
||||
* Allow only the rejected partition to finish first. */
|
||||
send_output_sample(release[1], "resume\n", 7);
|
||||
await_output_byte(finished[1][0], 'F', spine_monotonic_time() + 10);
|
||||
assert(pthread_join(threads[1], NULL) == 0);
|
||||
thread_mutex_lock(LOCK_THDET);
|
||||
assert(set.exit.exit_code == EXIT_FAILURE && aggregate->output_failed);
|
||||
assert(aggregate->threads_complete == 1 && !aggregate->complete);
|
||||
thread_mutex_unlock(LOCK_THDET);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=901 AND rrd_next_step=0") == 2);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id IN(930001,930002)") == 0);
|
||||
assert_sample_at(mysql, "poller_output_boost", 930002, "1791244800", "123");
|
||||
assert(spine_permits_available(&available_threads) == 1);
|
||||
assert(spine_permits_available(&available_scripts) == 1);
|
||||
thread_mutex_lock(LOCK_POOL);
|
||||
assert(db_pool_local[0].free + db_pool_local[1].free == 1);
|
||||
thread_mutex_unlock(LOCK_POOL);
|
||||
send_output_sample(release[0], "resume\n", 7);
|
||||
await_output_byte(finished[0][0], 'F', spine_monotonic_time() + 10);
|
||||
assert(pthread_join(threads[0], NULL) == 0);
|
||||
assert(set.exit.exit_code == EXIT_FAILURE && aggregate->output_failed && !aggregate->complete);
|
||||
assert(aggregate->threads_complete == 2);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=901 AND rrd_next_step=0") == 2);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=930002") == 0);
|
||||
assert_sample_at(mysql, "poller_output", 930001, "1791244800", "123");
|
||||
assert_sample_at(mysql, "poller_output_boost", 930001, "1791244800", "123");
|
||||
assert_sample_at(mysql, "poller_output_boost", 930002, "1791244800", "123");
|
||||
assert_sample(mysql, "poller_output", 930000, "'unchanged'");
|
||||
assert_sample(mysql, "poller_output_boost", 930000, "'unchanged'");
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM host_errors WHERE host_id=901") == 0);
|
||||
assert(spine_permits_available(&available_threads) == 2);
|
||||
assert(spine_permits_available(&available_scripts) == 2);
|
||||
assert(db_pool_local[0].free && db_pool_local[1].free);
|
||||
assert(db_insert(mysql, LOCAL, "DROP TRIGGER spine_owned_output_reject"));
|
||||
for (int index = 0; index < 2; index++) {
|
||||
close_output_signal(ready[index], release[index], finished[index], ready_paths[index], release_paths[index]);
|
||||
}
|
||||
assert(rmdir(directory) == 0);
|
||||
assert(spine_permits_destroy(&startup) == 0);
|
||||
assert(spine_permits_destroy(&available_threads) == 0);
|
||||
assert(spine_permits_init(&available_threads, 1) == 0);
|
||||
db_close_connection_pool(LOCAL);
|
||||
db_pool_local = previous_pool;
|
||||
details = previous_details;
|
||||
free(owned_details);
|
||||
free(aggregate);
|
||||
set.poller.threads = previous_threads;
|
||||
set.php.script_timeout = previous_timeout;
|
||||
puts("production simultaneous output failure ordering regressions passed");
|
||||
}
|
||||
|
||||
static void assert_remote_resources_and_local_output(MYSQL *mysql) {
|
||||
assert(db_pool_local[0].free && db_pool_remote[0].free);
|
||||
assert(spine_permits_available(&available_threads) == 1);
|
||||
assert(spine_permits_available(&available_scripts) == 2);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id BETWEEN 930001 AND 939999") == 0);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id BETWEEN 930001 AND 939999") == 0);
|
||||
assert_sample(mysql, "poller_output", 930000, "'unchanged'");
|
||||
assert_sample(mysql, "poller_output_boost", 930000, "'unchanged'");
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM host_errors WHERE host_id=901") == 0);
|
||||
}
|
||||
|
||||
static void assert_remote_samples(MYSQL *mysql, const char *timestamp, const char *sample) {
|
||||
for (int id = 930001; id <= 930002; id++) {
|
||||
assert_sample_at(mysql, "spine_owned_output_remote.poller_output", id, timestamp, sample);
|
||||
assert_sample_at(mysql, "spine_owned_output_remote.poller_output_boost", id, timestamp, sample);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_remote_output_destination(MYSQL *mysql) {
|
||||
pool_t *previous_remote_pool = db_pool_remote;
|
||||
poller_thread_t **previous_details = details;
|
||||
spine_database_config_t previous_remote_database = set.remote_database;
|
||||
int previous_id = set.poller.poller_id;
|
||||
int previous_mode = set.poller.mode;
|
||||
int previous_exit = set.exit.exit_code;
|
||||
assert(set.poller.threads == 1 && db_pool_local[0].free);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='spine_owned_output_remote'") == 0);
|
||||
assert(db_insert(mysql, LOCAL, "CREATE DATABASE spine_owned_output_remote"));
|
||||
/* Clone the actual fixture tables, preserving their keys, column defaults,
|
||||
* and different engines rather than inventing a remote schema. */
|
||||
assert(db_insert(mysql, LOCAL, "CREATE TABLE spine_owned_output_remote.poller_output LIKE poller_output"));
|
||||
assert(db_insert(mysql, LOCAL, "CREATE TABLE spine_owned_output_remote.poller_output_boost LIKE poller_output_boost"));
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='spine_owned_output_remote' AND TABLE_NAME='poller_output' AND ENGINE='MEMORY'") == 1);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='spine_owned_output_remote' AND TABLE_NAME='poller_output_boost' AND ENGINE='InnoDB'") == 1);
|
||||
set.remote_database = set.database;
|
||||
STRNCOPY(set.remote_database.database, "spine_owned_output_remote");
|
||||
set.poller.poller_id = 2;
|
||||
set.poller.mode = REMOTE_ONLINE;
|
||||
db_pool_remote = calloc(1, sizeof(*db_pool_remote));
|
||||
assert(db_pool_remote != NULL);
|
||||
db_create_connection_pool(REMOTE);
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item WHERE host_id=901"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id BETWEEN 930001 AND 939999"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost WHERE local_data_id BETWEEN 930001 AND 939999"));
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_reindex WHERE host_id=901") == 0);
|
||||
char query[BUFSIZE];
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO poller_item(local_data_id,host_id,poller_id,action,arg1,rrd_name,snmp_port,rrd_step,rrd_next_step) VALUES(930001,901,2,%i,'/usr/bin/printf 123','owned',161,300,0),(930002,901,2,%i,'/usr/bin/printf 123','owned',162,300,0)", POLLER_ACTION_SCRIPT, POLLER_ACTION_SCRIPT);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
poller_thread_t *work = calloc(1, sizeof(*work));
|
||||
assert(work != NULL);
|
||||
reset_output_work(work, 2, 1);
|
||||
poller_thread_t **owned_details = calloc(1, sizeof(*owned_details));
|
||||
assert(owned_details != NULL);
|
||||
owned_details[0] = work;
|
||||
details = owned_details;
|
||||
run_output_partition(work, 1);
|
||||
assert(set.exit.exit_code == EXIT_SUCCESS && work->complete && !work->output_failed);
|
||||
assert(work->threads_complete == 1);
|
||||
assert_remote_samples(mysql, "1791244800", "123");
|
||||
assert_remote_resources_and_local_output(mysql);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=901 AND poller_id=2 AND rrd_next_step=295") == 2);
|
||||
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET rrd_next_step=0,arg1='/usr/bin/printf 456' WHERE host_id=901 AND poller_id=2"));
|
||||
assert(db_insert(mysql, LOCAL, "CREATE TRIGGER spine_owned_output_remote.spine_owned_output_reject BEFORE INSERT ON spine_owned_output_remote.poller_output FOR EACH ROW BEGIN IF NEW.local_data_id=930002 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='owned remote output rejection'; END IF; END"));
|
||||
reset_output_work(work, 2, 1);
|
||||
STRNCOPY(work->host_time, "1791244805");
|
||||
run_output_partition(work, 1);
|
||||
assert(set.exit.exit_code == EXIT_FAILURE && work->output_failed && !work->complete);
|
||||
assert(work->threads_complete == 1);
|
||||
assert_remote_resources_and_local_output(mysql);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=901 AND poller_id=2 AND rrd_next_step=0") == 2);
|
||||
assert_remote_samples(mysql, "1791244800", "123");
|
||||
assert_sample_at(mysql, "spine_owned_output_remote.poller_output", 930001, "1791244805", "456");
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM spine_owned_output_remote.poller_output WHERE local_data_id=930002 AND time=FROM_UNIXTIME(1791244805)") == 0);
|
||||
assert_sample_at(mysql, "spine_owned_output_remote.poller_output_boost", 930001, "1791244805", "456");
|
||||
assert_sample_at(mysql, "spine_owned_output_remote.poller_output_boost", 930002, "1791244805", "456");
|
||||
assert(db_insert(mysql, LOCAL, "DROP TRIGGER spine_owned_output_remote.spine_owned_output_reject"));
|
||||
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET arg1='/usr/bin/printf 789' WHERE host_id=901 AND poller_id=2"));
|
||||
reset_output_work(work, 2, 1);
|
||||
STRNCOPY(work->host_time, "1791244810");
|
||||
run_output_partition(work, 1);
|
||||
assert(set.exit.exit_code == EXIT_SUCCESS && !work->output_failed && work->complete);
|
||||
assert(work->threads_complete == 1);
|
||||
assert_remote_resources_and_local_output(mysql);
|
||||
assert_remote_samples(mysql, "1791244810", "789");
|
||||
assert_remote_samples(mysql, "1791244800", "123");
|
||||
assert_sample_at(mysql, "spine_owned_output_remote.poller_output", 930001, "1791244805", "456");
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM spine_owned_output_remote.poller_output WHERE local_data_id=930002 AND time=FROM_UNIXTIME(1791244805)") == 0);
|
||||
assert_sample_at(mysql, "spine_owned_output_remote.poller_output_boost", 930001, "1791244805", "456");
|
||||
assert_sample_at(mysql, "spine_owned_output_remote.poller_output_boost", 930002, "1791244805", "456");
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM spine_owned_output_remote.poller_output") == 5);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM spine_owned_output_remote.poller_output_boost") == 6);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=901 AND poller_id=2 AND rrd_next_step=295") == 2);
|
||||
db_close_connection_pool(REMOTE);
|
||||
db_pool_remote = previous_remote_pool;
|
||||
assert(db_insert(mysql, LOCAL, "DROP DATABASE spine_owned_output_remote"));
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='spine_owned_output_remote'") == 0);
|
||||
details = previous_details;
|
||||
free(owned_details);
|
||||
free(work);
|
||||
set.remote_database = previous_remote_database;
|
||||
set.poller.poller_id = previous_id;
|
||||
set.poller.mode = previous_mode;
|
||||
set.exit.exit_code = previous_exit;
|
||||
puts("production remote output destination failure and recollection regressions passed");
|
||||
}
|
||||
|
||||
static void run_output_failure_scenario(MYSQL *mysql, int scenario, int payload) {
|
||||
poller_thread_t **previous_details = details;
|
||||
bool boundary = scenario >= 2;
|
||||
bool boost_failure = (scenario & 1) != 0;
|
||||
int rows = boundary ? MAX_MYSQL_BUF_SIZE / payload + 4 : 2;
|
||||
assert(rows < 9999);
|
||||
int partitions = scenario == 0 ? 2 : 1;
|
||||
char query[LRG_BUFSIZE];
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item WHERE host_id=901"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id BETWEEN 930001 AND 939999"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost WHERE local_data_id BETWEEN 930001 AND 939999"));
|
||||
for (int index = 1; index <= rows; index++) {
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO poller_item(local_data_id,host_id,poller_id,action,arg1,rrd_name,snmp_port,rrd_step,rrd_next_step) VALUES(%i,901,1,%i,'%s','owned',%i,300,0)", 930000 + index, boundary ? POLLER_ACTION_PHP_SCRIPT_SERVER : POLLER_ACTION_SCRIPT, boundary ? "owned numeric request" : "/usr/bin/printf 123", 160 + index);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO poller_output(local_data_id,rrd_name,time,output) VALUES(%i,'owned',FROM_UNIXTIME(1791244800),'old')", 930000 + index);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO poller_output_boost(local_data_id,rrd_name,time,output) VALUES(%i,'owned',FROM_UNIXTIME(1791244800),'old')", 930000 + index);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
}
|
||||
spine_snprintf(query, sizeof(query), "CREATE TRIGGER spine_owned_output_reject BEFORE INSERT ON %s FOR EACH ROW BEGIN IF NEW.local_data_id=930002 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='owned output rejection'; END IF; END", boost_failure ? "poller_output_boost" : "poller_output");
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
poller_thread_t *work = calloc(1, sizeof(*work));
|
||||
assert(work != NULL);
|
||||
reset_output_work(work, rows, partitions);
|
||||
poller_thread_t **owned_details = calloc(1, sizeof(*owned_details));
|
||||
assert(owned_details != NULL);
|
||||
owned_details[0] = work;
|
||||
details = owned_details;
|
||||
if (partitions == 2) {
|
||||
/* Failure finishes before the other partition even selects its rows. */
|
||||
run_output_partition(work, 2);
|
||||
fprintf(stderr, "output failure partition: exit=%i threads=%i complete=%i failed=%i\n", set.exit.exit_code, work->threads_complete, work->complete, work->output_failed);
|
||||
assert(work->threads_complete == 1 && !work->complete);
|
||||
assert(set.exit.exit_code == EXIT_FAILURE);
|
||||
}
|
||||
run_output_partition(work, 1);
|
||||
fprintf(stderr, "output persistence scenario=%i boundary=%i rows=%i exit=%i completed=%i\n", scenario, boundary, rows, set.exit.exit_code, work->complete);
|
||||
assert(set.exit.exit_code == EXIT_FAILURE);
|
||||
assert(work->output_failed && !work->complete && work->threads_complete == partitions);
|
||||
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_item WHERE host_id=901 AND rrd_next_step=0");
|
||||
assert(output_count(mysql, query) == (unsigned long long)rows);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM host_errors WHERE host_id=901") == 0);
|
||||
assert_sample(mysql, "poller_output", 930000, "'unchanged'");
|
||||
assert_sample(mysql, "poller_output_boost", 930000, "'unchanged'");
|
||||
const char *failed_table = boost_failure ? "poller_output_boost" : "poller_output";
|
||||
const char *other_table = boost_failure ? "poller_output" : "poller_output_boost";
|
||||
assert_sample(mysql, failed_table, 930002, "'old'");
|
||||
char expected[100];
|
||||
spine_snprintf(expected, sizeof(expected), "CONCAT(REPEAT('0',%i),'123')", boundary ? payload - 3 : 0);
|
||||
assert_sample(mysql, other_table, 930002, expected);
|
||||
if (boundary) {
|
||||
/* Sample bytes alone exceed the configured batch limit, so the
|
||||
* failure is in an early flush and later writes still execute. */
|
||||
assert((size_t)rows * (size_t)payload > MAX_MYSQL_BUF_SIZE);
|
||||
assert_sample(mysql, failed_table, 930000 + rows, expected);
|
||||
assert_sample(mysql, "poller_output", 930001, expected);
|
||||
}
|
||||
assert(db_insert(mysql, LOCAL, "DROP TRIGGER spine_owned_output_reject"));
|
||||
/* This controlled retry reuses its timestamp and resets completion/exit
|
||||
* state. It proves same-key upsert repair, including partial MEMORY rows;
|
||||
* a later CLI invocation recollects data with the current timestamp. */
|
||||
reset_output_work(work, rows, partitions);
|
||||
if (partitions == 2) run_output_partition(work, 2);
|
||||
run_output_partition(work, 1);
|
||||
assert(set.exit.exit_code == EXIT_SUCCESS && !work->output_failed && work->complete);
|
||||
assert(work->threads_complete == partitions);
|
||||
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output WHERE local_data_id BETWEEN 930001 AND %i AND output=%s", 930000 + rows, expected);
|
||||
assert(output_count(mysql, query) == (unsigned long long)rows);
|
||||
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id BETWEEN 930001 AND %i AND output=%s", 930000 + rows, expected);
|
||||
assert(output_count(mysql, query) == (unsigned long long)rows);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=901 AND rrd_next_step=295") == (unsigned long long)rows);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id BETWEEN 930000 AND 939999") == (unsigned long long)rows + 1);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id BETWEEN 930000 AND 939999") == (unsigned long long)rows + 1);
|
||||
details = previous_details;
|
||||
free(owned_details);
|
||||
free(work);
|
||||
}
|
||||
|
||||
void test_output_write_contracts(MYSQL *mysql) {
|
||||
config_t previous = set;
|
||||
pool_t *previous_pool = db_pool_local;
|
||||
poller_thread_t **previous_details = details;
|
||||
php_t *previous_servers = php_processes;
|
||||
int *previous_debug = debug_devices;
|
||||
static int owned_debug[100];
|
||||
debug_devices = owned_debug;
|
||||
assert_output_engine(mysql, "poller_output", "MEMORY");
|
||||
assert_output_engine(mysql, "poller_output_boost", "InnoDB");
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM host WHERE id=901") == 0);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=901 OR local_data_id BETWEEN 930000 AND 939999") == 0);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id BETWEEN 930000 AND 939999") == 0);
|
||||
assert(output_count(mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id BETWEEN 930000 AND 939999") == 0);
|
||||
set.poller.threads = 1;
|
||||
set.poller.poller_id = 1;
|
||||
set.poller.mode = REMOTE_OFFLINE;
|
||||
set.poller.active_profiles = 2;
|
||||
set.poller.poller_interval = 5;
|
||||
set.poller.SQL_readonly = FALSE;
|
||||
set.availability.ping_only = FALSE;
|
||||
set.snmp.total_snmp_ports = 0;
|
||||
set.snmp.mibs = FALSE;
|
||||
set.boost.boost_enabled = TRUE;
|
||||
set.boost.boost_redirect = TRUE;
|
||||
set.logging.spine_log_level = 0;
|
||||
set.logging.log_destination = 0;
|
||||
set.php.script_timeout = 5;
|
||||
set.php.php_servers = 1;
|
||||
set.php.php_current_server = 0;
|
||||
set.php.php_initialized = TRUE;
|
||||
set.php.php_required = TRUE;
|
||||
db_pool_local = calloc(1, sizeof(*db_pool_local));
|
||||
assert(db_pool_local != NULL);
|
||||
db_create_connection_pool(LOCAL);
|
||||
assert(spine_permits_init(&available_threads, 1) == 0);
|
||||
assert(spine_permits_init(&available_scripts, 2) == 0);
|
||||
php_t server = {0};
|
||||
int payload = RESULTS_BUFFER - 2 < 400 ? RESULTS_BUFFER - 2 : 400;
|
||||
assert(payload >= 3);
|
||||
pid_t producer = start_output_server(&server, payload);
|
||||
php_processes = &server;
|
||||
assert(db_insert(mysql, LOCAL, "INSERT INTO host(id,hostname,availability_method,snmp_version,status_fail_date,status_rec_date) VALUES(901,'127.0.0.1',0,0,'2026-10-06 00:00:00','2026-10-06 00:00:00')"));
|
||||
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_output(local_data_id,rrd_name,time,output) VALUES(930000,'sentinel',FROM_UNIXTIME(1791244800),'unchanged')"));
|
||||
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_output_boost(local_data_id,rrd_name,time,output) VALUES(930000,'sentinel',FROM_UNIXTIME(1791244800),'unchanged')"));
|
||||
for (int scenario = 0; scenario < 4; scenario++) {
|
||||
run_output_failure_scenario(mysql, scenario, payload);
|
||||
assert(details == previous_details);
|
||||
}
|
||||
test_output_recollection(mysql);
|
||||
test_concurrent_output_failure(mysql);
|
||||
test_remote_output_destination(mysql);
|
||||
assert(close(server.php_write_fd) == 0);
|
||||
int status;
|
||||
assert(waitpid(producer, &status, 0) == producer);
|
||||
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
assert(close(server.php_read_fd) == 0);
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item WHERE host_id=901"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM host WHERE id=901"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id BETWEEN 930000 AND 939999"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost WHERE local_data_id BETWEEN 930000 AND 939999"));
|
||||
assert(spine_permits_destroy(&available_scripts) == 0);
|
||||
assert(spine_permits_destroy(&available_threads) == 0);
|
||||
db_close_connection_pool(LOCAL);
|
||||
db_pool_local = previous_pool;
|
||||
details = previous_details;
|
||||
php_processes = previous_servers;
|
||||
debug_devices = previous_debug;
|
||||
set = previous;
|
||||
puts("production output write failure and retry regressions passed");
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
extern poller_thread_t **details;
|
||||
|
||||
typedef struct {
|
||||
poller_thread_t thread;
|
||||
int errors;
|
||||
} reindex_test_work_t;
|
||||
|
||||
static unsigned long reindex_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 count = strtoul(row[0], NULL, 10);
|
||||
db_free_result(result);
|
||||
return count;
|
||||
}
|
||||
|
||||
static void *run_reindex_worker(void *argument) {
|
||||
reindex_test_work_t *work = argument;
|
||||
assert(mysql_thread_init() == 0);
|
||||
poll_host(&work->thread, &work->errors);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void run_reindex_case(MYSQL *mysql, int action, const char *argument,
|
||||
const char *op, const char *expected, const char *stored, int commands) {
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex WHERE host_id=45"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command WHERE command='45:7' OR command='45:8'"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id=801"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost WHERE local_data_id=801"));
|
||||
char query[LRG_BUFSIZE];
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO poller_reindex(host_id,data_query_id,action,op,assert_value,arg1) VALUES (45,7,%d,'%s','%s','%s')", action, op, expected, argument);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
reindex_test_work_t *work = calloc(1, sizeof(*work));
|
||||
assert(work != NULL);
|
||||
work->thread.host_id = 45;
|
||||
work->thread.host_thread = 1;
|
||||
work->thread.host_threads = 1;
|
||||
work->thread.host_data_ids = 1;
|
||||
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;
|
||||
poller_thread_t **prior_details = details;
|
||||
details = owned_details;
|
||||
pthread_t worker;
|
||||
assert(pthread_create(&worker, NULL, run_reindex_worker, work) == 0);
|
||||
assert(pthread_join(worker, NULL) == 0);
|
||||
assert(work->thread.complete && work->thread.threads_complete == 1 && work->errors == 0);
|
||||
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
|
||||
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_command WHERE poller_id=1 AND action=1 AND command='45:7'") == (unsigned long)commands);
|
||||
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_reindex WHERE host_id=45 AND data_query_id=7 AND assert_value='%s'", stored);
|
||||
assert(reindex_count(mysql, query) == 1);
|
||||
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=801 AND output='123'") == 1);
|
||||
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=801 AND output='123'") == 1);
|
||||
assert(reindex_count(mysql, "SELECT COUNT(*) FROM host_errors WHERE host_id=45") == 0);
|
||||
details = prior_details;
|
||||
free(owned_details);
|
||||
free(work);
|
||||
}
|
||||
|
||||
static void run_uptime_cache_case(MYSQL *mysql) {
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex WHERE host_id=45"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command WHERE command='45:7' OR command='45:8'"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id=801"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost WHERE local_data_id=801"));
|
||||
/* Different query IDs prevent the first assertion's shortcut skipping row2. */
|
||||
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_reindex(host_id,data_query_id,action,op,assert_value,arg1) VALUES (45,7,0,'>','0','.1.3.6.1.2.1.1.3.0'),(45,8,0,'>','0','.1.3.6.1.6.3.10.2.1.3.0')"));
|
||||
reindex_test_work_t *work = calloc(1, sizeof(*work));
|
||||
assert(work != NULL);
|
||||
work->thread.host_id = 45;
|
||||
work->thread.host_thread = 1;
|
||||
work->thread.host_threads = 1;
|
||||
work->thread.host_data_ids = 1;
|
||||
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;
|
||||
poller_thread_t **prior_details = details;
|
||||
details = owned_details;
|
||||
pthread_t worker;
|
||||
assert(pthread_create(&worker, NULL, run_reindex_worker, work) == 0);
|
||||
assert(pthread_join(worker, NULL) == 0);
|
||||
assert(work->thread.complete && work->thread.threads_complete == 1 && work->errors == 0);
|
||||
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
|
||||
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_command WHERE poller_id=1 AND action=1 AND command IN ('45:7','45:8')") == 2);
|
||||
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_reindex WHERE host_id=45 AND assert_value REGEXP '^[0-9]+$' AND CAST(assert_value AS UNSIGNED)>0") == 2);
|
||||
/* A failed assertion on an uptime OID means the counters reset, so the
|
||||
* cycle's samples are discarded as a spike. */
|
||||
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=801 AND output='U'") == 1);
|
||||
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=801 AND output='U'") == 1);
|
||||
assert(reindex_count(mysql, "SELECT COUNT(*) FROM host_errors WHERE host_id=45") == 0);
|
||||
details = prior_details;
|
||||
free(owned_details);
|
||||
free(work);
|
||||
}
|
||||
|
||||
void test_additional_reindex_contracts(MYSQL *mysql) {
|
||||
const char *agent = getenv("SPINE_TEST_SNMP_HOST");
|
||||
assert(agent != NULL && agent[0] != '\0');
|
||||
config_t previous_config = set;
|
||||
pool_t *previous_pool = db_pool_local;
|
||||
poller_thread_t **previous_details = details;
|
||||
php_t *previous_processes = php_processes;
|
||||
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;
|
||||
set.snmp.mibs = FALSE;
|
||||
set.availability.ping_recovery_count = 1;
|
||||
set.availability.ping_failure_count = 1;
|
||||
set.php.php_servers = 1;
|
||||
set.php.php_current_server = 0;
|
||||
set.cacti_version = 1232;
|
||||
set.poller.mode = REMOTE_ONLINE;
|
||||
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);
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM host_errors WHERE host_id=45"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM host WHERE id=45"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item WHERE local_data_id=801"));
|
||||
char escaped_agent[BUFSIZE];
|
||||
db_escape(mysql, escaped_agent, sizeof(escaped_agent), agent);
|
||||
char query[LRG_BUFSIZE];
|
||||
spine_snprintf(query, sizeof(query), "INSERT INTO host(id,hostname,availability_method,snmp_version,snmp_community,snmp_port,snmp_timeout,status_fail_date,status_rec_date,max_oids) VALUES (45,'%s',%d,2,'regression',1161,500,'2026-10-05 00:00:00','2026-10-05 00:00:00',5)", escaped_agent, AVAIL_NONE);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
assert(db_insert(mysql, LOCAL, "INSERT INTO poller_item(local_data_id,host_id,poller_id,action,arg1,rrd_name) VALUES(801,45,1,1,'/usr/bin/printf 123','reindex_valid')"));
|
||||
snmp_spine_init();
|
||||
run_reindex_case(mysql, POLLER_ACTION_SNMP, ".1.3.6.1.2.1.1.6.0", "=", "isolated-regression-agent", "isolated-regression-agent", 0);
|
||||
run_reindex_case(mysql, POLLER_ACTION_SNMP, ".1.3.6.1.2.1.1.6.0", "=", "previous", "isolated-regression-agent", 1);
|
||||
run_reindex_case(mysql, POLLER_ACTION_SNMP, "invalid-regression-oid", "=", "previous", "previous", 0);
|
||||
run_reindex_case(mysql, POLLER_ACTION_SNMP_COUNT, ".1.3.6.1.2.1.1.6", "=", "0", "1", 1);
|
||||
run_uptime_cache_case(mysql);
|
||||
assert(db_insert(mysql, LOCAL, "UPDATE host SET snmp_community='' WHERE id=45"));
|
||||
run_reindex_case(mysql, POLLER_ACTION_SNMP, ".1.3.6.1.2.1.1.6.0", "=", "previous", "previous", 0);
|
||||
|
||||
php_t processes[1] = {0};
|
||||
php_processes = processes;
|
||||
char *executable = realpath("./test_spine_regressions", NULL);
|
||||
assert(executable != NULL);
|
||||
strncopy(set.php.path_php, executable, sizeof(set.php.path_php));
|
||||
free(executable);
|
||||
STRNCOPY(set.php.path_php_server, "regression-server");
|
||||
assert(php_init(PHP_INIT));
|
||||
run_reindex_case(mysql, POLLER_ACTION_PHP_SCRIPT_SERVER, "regression request", "=", "7", "7", 0);
|
||||
run_reindex_case(mysql, POLLER_ACTION_PHP_SCRIPT_SERVER, "regression request", "=", "6", "7", 1);
|
||||
run_reindex_case(mysql, POLLER_ACTION_PHP_SCRIPT_SERVER_COUNT, "regression request", "=", "2", "1", 1);
|
||||
php_close(PHP_INIT);
|
||||
assert(processes[0].php_pid == -1 && processes[0].php_read_fd == -1 && processes[0].php_write_fd == -1);
|
||||
php_processes = previous_processes;
|
||||
assert(spine_permits_destroy(&available_scripts) == 0);
|
||||
db_close_connection_pool(LOCAL);
|
||||
db_pool_local = previous_pool;
|
||||
details = previous_details;
|
||||
set = previous_config;
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex WHERE host_id=45"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command WHERE command='45:7' OR command='45:8'"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id=801"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost WHERE local_data_id=801"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item WHERE local_data_id=801"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM host_errors WHERE host_id=45"));
|
||||
assert(db_insert(mysql, LOCAL, "DELETE FROM host WHERE id=45"));
|
||||
puts("production live SNMP and PHP reindex contracts passed");
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
extern int putsetting(MYSQL *, int, const char *, const char *);
|
||||
|
||||
static void assert_saved_setting(MYSQL *mysql, const char *name, const char *expected) {
|
||||
char query[BUFSIZE];
|
||||
spine_snprintf(query, sizeof(query), "SELECT value FROM settings WHERE name='%s'", name);
|
||||
MYSQL_RES *result = db_query(mysql, LOCAL, query);
|
||||
assert(result != NULL && mysql_num_rows(result) == 1);
|
||||
MYSQL_ROW row = mysql_fetch_row(result);
|
||||
assert(row != NULL && row[0] != NULL && strcmp(row[0], expected) == 0);
|
||||
db_free_result(result);
|
||||
}
|
||||
|
||||
void test_settings_write_contracts(MYSQL *mysql) {
|
||||
config_t previous = set;
|
||||
set.database.onupdate = 0; /* MariaDB's supported INSERT ... VALUES upsert dialect. */
|
||||
set.poller.SQL_readonly = FALSE;
|
||||
char name[100];
|
||||
char trigger[100];
|
||||
spine_snprintf(name, sizeof(name), "spine_settings_fault_%ld", (long)getpid());
|
||||
spine_snprintf(trigger, sizeof(trigger), "spine_settings_reject_%ld", (long)getpid());
|
||||
char query[LRG_BUFSIZE];
|
||||
/* This owned key is ASCII letters/digits/underscores; reader escaping is
|
||||
* a separate contract from the writer's result being verified here. */
|
||||
spine_snprintf(query, sizeof(query), "DELETE FROM settings WHERE name='%s'", name);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
assert(putsetting(mysql, LOCAL, name, "first value") == TRUE);
|
||||
assert_saved_setting(mysql, name, "first value");
|
||||
assert(putsetting(mysql, LOCAL, name, "multibyte café 🌵") == TRUE);
|
||||
assert_saved_setting(mysql, name, "multibyte café 🌵");
|
||||
assert(putsetting(mysql, LOCAL, name, "multibyte café 🌵") == TRUE);
|
||||
assert_saved_setting(mysql, name, "multibyte café 🌵");
|
||||
spine_snprintf(query, sizeof(query), "CREATE TRIGGER %s BEFORE INSERT ON settings FOR EACH ROW BEGIN IF NEW.name='%s' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='owned settings failure'; END IF; END", trigger, name);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
assert(putsetting(mysql, LOCAL, name, "failed replacement") == FALSE);
|
||||
assert_saved_setting(mysql, name, "multibyte café 🌵");
|
||||
spine_snprintf(query, sizeof(query), "DROP TRIGGER %s", trigger);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
assert(putsetting(mysql, LOCAL, name, "retried replacement") == TRUE);
|
||||
assert_saved_setting(mysql, name, "retried replacement");
|
||||
spine_snprintf(query, sizeof(query), "DELETE FROM settings WHERE name='%s'", name);
|
||||
assert(db_insert(mysql, LOCAL, query));
|
||||
set = previous;
|
||||
puts("production settings write outcome regressions passed");
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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 <limits.h>
|
||||
|
||||
/* Independent SHA-1 Ku oracle from RFC3414 Appendix A.3.2, not a duplicate
|
||||
* implementation or a second call to the production key-generation API:
|
||||
* https://www.rfc-editor.org/rfc/rfc3414.html#appendix-A.3.2 */
|
||||
static const unsigned char maplesyrup_sha_key[] = {
|
||||
0x9f, 0xb5, 0xcc, 0x03, 0x81, 0x49, 0x7b, 0x37, 0x93, 0x52,
|
||||
0x89, 0x39, 0xff, 0x78, 0x8d, 0x5d, 0x79, 0x14, 0x52, 0x11
|
||||
};
|
||||
|
||||
static snmp_connection_t session_options(char *hostname, char *username) {
|
||||
return (snmp_connection_t){
|
||||
.host_id = 46, .hostname = hostname, .snmp_version = 3,
|
||||
.snmp_community = "", .snmp_username = username,
|
||||
.snmp_password = "maplesyrup", .snmp_auth_protocol = "SHA",
|
||||
.snmp_priv_passphrase = "", .snmp_priv_protocol = "[None]",
|
||||
.snmp_context = "", .snmp_engine_id = "", .snmp_port = 1162,
|
||||
.snmp_timeout = 500
|
||||
};
|
||||
}
|
||||
|
||||
void test_additional_snmp_session_boundaries(void) {
|
||||
config_t previous = set;
|
||||
set.snmp.snmp_retries = 0;
|
||||
/* Runs inside the default suite's single Net-SNMP init/shutdown lifecycle. */
|
||||
snmp_connection_t options = session_options("127.0.0.1", "regression-v3-vector-auth");
|
||||
void *handle = snmp_host_init(&options);
|
||||
assert(handle != NULL);
|
||||
const struct snmp_session *session = snmp_sess_session(handle);
|
||||
assert(session != NULL && session->securityLevel == SNMP_SEC_LEVEL_AUTHNOPRIV);
|
||||
assert(session->securityAuthKeyLen == sizeof(maplesyrup_sha_key));
|
||||
assert(memcmp(session->securityAuthKey, maplesyrup_sha_key, sizeof(maplesyrup_sha_key)) == 0);
|
||||
assert(session->securityPrivKeyLen == 0);
|
||||
snmp_host_cleanup(handle);
|
||||
options.snmp_username = "regression-v3-vector-priv";
|
||||
options.snmp_priv_protocol = "AES";
|
||||
options.snmp_priv_passphrase = "maplesyrup";
|
||||
handle = snmp_host_init(&options);
|
||||
assert(handle != NULL);
|
||||
session = snmp_sess_session(handle);
|
||||
assert(session != NULL && session->securityLevel == SNMP_SEC_LEVEL_AUTHPRIV);
|
||||
assert(session->securityAuthKeyLen == sizeof(maplesyrup_sha_key));
|
||||
assert(session->securityPrivKeyLen == sizeof(maplesyrup_sha_key));
|
||||
assert(memcmp(session->securityAuthKey, maplesyrup_sha_key, sizeof(maplesyrup_sha_key)) == 0);
|
||||
assert(memcmp(session->securityPrivKey, maplesyrup_sha_key, sizeof(maplesyrup_sha_key)) == 0);
|
||||
snmp_host_cleanup(handle);
|
||||
options.snmp_priv_protocol = "[None]";
|
||||
options.snmp_priv_passphrase = "";
|
||||
options.snmp_password = "short";
|
||||
assert(snmp_host_init(&options) == NULL);
|
||||
options.snmp_username = "regression-v3-vector-noauth";
|
||||
options.snmp_password = "";
|
||||
handle = snmp_host_init(&options);
|
||||
assert(handle != NULL);
|
||||
session = snmp_sess_session(handle);
|
||||
assert(session != NULL && session->securityLevel == SNMP_SEC_LEVEL_NOAUTH);
|
||||
assert(session->securityAuthKeyLen == 0 && session->securityPrivKeyLen == 0);
|
||||
snmp_host_cleanup(handle);
|
||||
|
||||
options.snmp_version = 2;
|
||||
options.snmp_community = "regression";
|
||||
const int milliseconds[] = {1, 500, INT_MAX / 1000, INT_MAX / 1000 + 1, INT_MAX};
|
||||
for (size_t index = 0; index < sizeof(milliseconds)/sizeof(milliseconds[0]); index++) {
|
||||
options.snmp_timeout = milliseconds[index];
|
||||
handle = snmp_host_init(&options);
|
||||
#if LONG_MAX / 1000L < INT_MAX
|
||||
if (milliseconds[index] > LONG_MAX / 1000L) {
|
||||
assert(handle == NULL);
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
assert(handle != NULL);
|
||||
session = snmp_sess_session(handle);
|
||||
assert(session != NULL && session->timeout == (long)milliseconds[index] * 1000L);
|
||||
snmp_host_cleanup(handle);
|
||||
}
|
||||
options.snmp_timeout = 0;
|
||||
assert(snmp_host_init(&options) == NULL);
|
||||
options.snmp_timeout = -1;
|
||||
assert(snmp_host_init(&options) == NULL);
|
||||
options.snmp_timeout = INT_MIN;
|
||||
assert(snmp_host_init(&options) == NULL);
|
||||
set = previous;
|
||||
puts("production SNMPv3 key and timeout boundary regressions passed");
|
||||
}
|
||||
|
||||
static void assert_v3_response(const snmp_connection_t *options, bool admitted, bool access_denied, int level) {
|
||||
/* Net-SNMP has a process-global USM user cache. Isolate every credential
|
||||
* case so a previous admitted session cannot satisfy a later rejected one. */
|
||||
pid_t child = fork();
|
||||
assert(child >= 0);
|
||||
if (child == 0) {
|
||||
alarm(4);
|
||||
host_t host = {0};
|
||||
host.id = options->host_id;
|
||||
host.snmp.session = snmp_host_init(options);
|
||||
assert(host.snmp.session != NULL);
|
||||
const struct snmp_session *session = snmp_sess_session(host.snmp.session);
|
||||
assert(session != NULL && session->securityLevel == level);
|
||||
char *response = snmp_get(&host, ".1.3.6.1.2.1.1.6.0");
|
||||
assert(response != NULL);
|
||||
if (admitted) {
|
||||
assert(strcmp(response, "isolated-regression-v3-agent") == 0);
|
||||
assert(!host.ignore_host);
|
||||
} else if (access_denied) {
|
||||
/* Authenticated VACM refusal is a transport-success agent error.
|
||||
* Preserve the existing empty-data availability contract. */
|
||||
assert(response[0] == '\0');
|
||||
assert(host.snmp.status == STAT_SUCCESS && !host.ignore_host);
|
||||
} else {
|
||||
assert(IS_UNDEFINED(response));
|
||||
assert(host.ignore_host);
|
||||
}
|
||||
free(response);
|
||||
snmp_host_cleanup(host.snmp.session);
|
||||
snmp_spine_close();
|
||||
_exit(0);
|
||||
}
|
||||
int status;
|
||||
assert(waitpid(child, &status, 0) == child);
|
||||
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
void test_snmpv3_agent_contracts(void) {
|
||||
char *agent = getenv("SPINE_TEST_SNMPV3_HOST");
|
||||
assert(agent != NULL && agent[0] != '\0');
|
||||
config_t previous = set;
|
||||
set.snmp.snmp_retries = 0;
|
||||
snmp_spine_init();
|
||||
snmp_connection_t options = session_options(agent, "regression-v3-noauth");
|
||||
options.snmp_password = "";
|
||||
assert_v3_response(&options, TRUE, FALSE, SNMP_SEC_LEVEL_NOAUTH);
|
||||
options.snmp_username = "regression-v3-auth";
|
||||
options.snmp_password = "maplesyrup";
|
||||
assert_v3_response(&options, TRUE, FALSE, SNMP_SEC_LEVEL_AUTHNOPRIV);
|
||||
options.snmp_password = "regression-wrong-auth";
|
||||
assert_v3_response(&options, FALSE, FALSE, SNMP_SEC_LEVEL_AUTHNOPRIV);
|
||||
options.snmp_username = "regression-v3-priv";
|
||||
options.snmp_password = "maplesyrup";
|
||||
options.snmp_priv_protocol = "AES";
|
||||
options.snmp_priv_passphrase = "regression-privacy";
|
||||
assert_v3_response(&options, TRUE, FALSE, SNMP_SEC_LEVEL_AUTHPRIV);
|
||||
options.snmp_priv_passphrase = "regression-wrong-privacy";
|
||||
assert_v3_response(&options, FALSE, FALSE, SNMP_SEC_LEVEL_AUTHPRIV);
|
||||
options.snmp_priv_protocol = "[None]";
|
||||
options.snmp_priv_passphrase = "";
|
||||
assert_v3_response(&options, FALSE, TRUE, SNMP_SEC_LEVEL_AUTHNOPRIV); /* VACM requires privacy. */
|
||||
snmp_spine_close();
|
||||
set = previous;
|
||||
puts("production live SNMPv3 authentication and privacy regressions passed");
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
/* Dispatch before regression initialization: this is a real child producer,
|
||||
* not a replacement implementation of exec_poll or its pipe reader. */
|
||||
int run_script_stream_fixture(const char *scenario) {
|
||||
const char *bytes;
|
||||
int status = 0;
|
||||
if (strcmp(scenario, "empty") == 0) bytes = "";
|
||||
else if (strcmp(scenario, "partial") == 0) bytes = "partial-7";
|
||||
else if (strcmp(scenario, "lines") == 0) bytes = "7\n8\n";
|
||||
else if (strcmp(scenario, "failure-output") == 0) { bytes = "9"; status = 23; }
|
||||
else if (strcmp(scenario, "failure-empty") == 0) { bytes = ""; status = 23; }
|
||||
else return 99;
|
||||
size_t length = strlen(bytes);
|
||||
assert(length <= _POSIX_PIPE_BUF);
|
||||
if (length != 0) {
|
||||
ssize_t sent;
|
||||
do { sent = write(STDOUT_FILENO, bytes, length); } while (sent < 0 && errno == EINTR);
|
||||
assert(sent == (ssize_t)length);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
static void assert_stream_result(host_t *host, const char *scenario, const char *type, const char *expected) {
|
||||
char command[BUFSIZE];
|
||||
spine_snprintf(command, sizeof(command), "./test_spine_regressions --script-stream %s", scenario);
|
||||
char *result = exec_poll(host, command, 991, type);
|
||||
assert(result != NULL && strcmp(result, expected) == 0);
|
||||
free(result);
|
||||
assert(spine_permits_available(&available_scripts) == 1);
|
||||
}
|
||||
|
||||
void test_script_stream_contracts(void) {
|
||||
int previous_timeout = set.php.script_timeout;
|
||||
set.php.script_timeout = 5;
|
||||
assert(spine_permits_init(&available_scripts, 1) == 0);
|
||||
host_t host = {0};
|
||||
host.id = 991;
|
||||
STRNCOPY(host.hostname, "owned-stream-fixture");
|
||||
assert_stream_result(&host, "empty", "DS", "U");
|
||||
assert_stream_result(&host, "empty", "DQ", "U");
|
||||
assert_stream_result(&host, "partial", "DS", "partial-7");
|
||||
assert_stream_result(&host, "lines", "DS", "7\n8\n");
|
||||
/* Legacy script results are determined by bytes, not child exit status.
|
||||
* Preserve this contract while verifying the pipe backend exposes status. */
|
||||
assert_stream_result(&host, "failure-output", "DS", "9");
|
||||
assert_stream_result(&host, "failure-empty", "DQ", "U");
|
||||
assert(spine_permits_destroy(&available_scripts) == 0);
|
||||
set.php.script_timeout = previous_timeout;
|
||||
|
||||
int descriptor = nft_popen("./test_spine_regressions --script-stream failure-output", "r");
|
||||
assert(descriptor >= 0);
|
||||
pid_t child = nft_pchild(descriptor);
|
||||
assert(child > 0);
|
||||
assert(spine_wait_readable(descriptor, spine_monotonic_time() + 5) == 1);
|
||||
char output[8] = {0};
|
||||
ssize_t received;
|
||||
do { received = read(descriptor, output, sizeof(output)); } while (received < 0 && errno == EINTR);
|
||||
assert(received == 1 && output[0] == '9');
|
||||
do { received = read(descriptor, output, sizeof(output)); } while (received < 0 && errno == EINTR);
|
||||
assert(received == 0);
|
||||
int status = nft_pclose(descriptor);
|
||||
assert(WIFEXITED(status) && WEXITSTATUS(status) == 23);
|
||||
assert(nft_pchild(descriptor) == -1 && errno == EBADF);
|
||||
assert(nft_pclose(descriptor) == -1 && errno == EBADF);
|
||||
assert(waitpid(child, &status, WNOHANG) == -1 && errno == ECHILD);
|
||||
puts("production script stream regressions passed");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* Copyright (C) 2026 The Cacti Group
|
||||
* Licensed under the GNU Lesser General Public License, version 2.1 or later. */
|
||||
#ifndef SPINE_COVERAGE_PROCESS_EXIT_H
|
||||
#define SPINE_COVERAGE_PROCESS_EXIT_H
|
||||
/* Instrumentation only: flush owned subprocess counters before POSIX _exit.
|
||||
* Preserve _exit's descriptor and stdio behavior; do not replace it with exit. */
|
||||
#include <unistd.h>
|
||||
extern void __gcov_dump(void);
|
||||
static inline __attribute__((noreturn)) void spine_coverage_process_exit(int status) {
|
||||
__gcov_dump();
|
||||
_exit(status);
|
||||
}
|
||||
#define _exit(status) spine_coverage_process_exit(status)
|
||||
|
||||
#endif
|
||||
Executable
+182
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (C) 2026 The Cacti Group
|
||||
# Licensed under the GNU Lesser General Public License, version 2.1 or later.
|
||||
# Run only against isolated regression services; database tests mutate fixtures.
|
||||
set -euo pipefail
|
||||
: "${SPINE_TEST_DB_HOST:?Set the isolated regression database host}"
|
||||
: "${SPINE_TEST_SNMP_HOST:?Set the isolated regression SNMP host}"
|
||||
coverage_base="${SPINE_COVERAGE_DIRECTORY:-coverage}"
|
||||
mkdir -p "$coverage_base"
|
||||
coverage_base="$(cd "$coverage_base" && pwd -P)"
|
||||
source_dir="$(pwd -P)"
|
||||
coverage_dir="$(mktemp -d "$coverage_base/run.XXXXXX")"
|
||||
printf '%s\n' incomplete > "$coverage_dir/status.txt"
|
||||
printf '%s\n' "$coverage_dir" > "$coverage_base/latest-run.txt"
|
||||
coverage_complete=0
|
||||
capability_added=0
|
||||
finish() {
|
||||
status=$?
|
||||
if [[ "$capability_added" -eq 1 ]]; then
|
||||
if [[ "$(id -u)" -eq 0 ]]; then setcap -r ./test_spine_regressions; else sudo setcap -r ./test_spine_regressions; fi
|
||||
fi
|
||||
if [[ "$coverage_complete" -eq 1 ]] && [[ "$status" -eq 0 ]]; then
|
||||
printf '%s\n' success > "$coverage_dir/status.txt"
|
||||
else
|
||||
printf '%s\n' incomplete > "$coverage_dir/status.txt"
|
||||
fi
|
||||
}
|
||||
trap finish EXIT
|
||||
for tool in gcc gcov lcov sha256sum tar; do command -v "$tool" >/dev/null; done
|
||||
# Exclude only the owned evidence directory, never an untested production unit.
|
||||
coverage_find_path='/not-in-source-tree'
|
||||
[[ "$coverage_base" != "$source_dir" ]]
|
||||
case "$coverage_base" in
|
||||
"$source_dir"/*) coverage_find_path="./${coverage_base#"$source_dir"/}";;
|
||||
*) ;;
|
||||
esac
|
||||
find . -path "$coverage_find_path" -prune -o -type f \( -name '*.c' -o -name '*.h' -o -name 'Makefile.am' -o -name 'configure.ac' -o -name 'copyright_year.sh' -o -path './tests/fixtures/*' -o -path './tests/tools/*' -o -path './.github/workflows/*.yml' \) -not -path './.git/*' -not -path './config/config.h' -print0 | sort -z > "$coverage_dir/source-inputs.nul"
|
||||
xargs -0 sha256sum < "$coverage_dir/source-inputs.nul" > "$coverage_dir/source.sha256"
|
||||
tar -czf "$coverage_dir/source-inputs.tar.gz" --null -T "$coverage_dir/source-inputs.nul"
|
||||
# Autoreconf replaces this tracked template. Preserve its original bytes and
|
||||
# hash the generated template actually used by configure separately below.
|
||||
cp Makefile.in "$coverage_dir/source-Makefile.in"
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
git rev-parse HEAD > "$coverage_dir/revision.txt"
|
||||
git status --porcelain > "$coverage_dir/worktree-status.txt"
|
||||
else
|
||||
: "${SPINE_SOURCE_REVISION:?Set the exact source revision for a copied build tree}"
|
||||
printf '%s\n' "$SPINE_SOURCE_REVISION" > "$coverage_dir/revision.txt"
|
||||
printf '%s\n' 'Copied build tree: source.sha256 is authoritative; repository status unavailable.' > "$coverage_dir/worktree-status.txt"
|
||||
fi
|
||||
gcc --version > "$coverage_dir/compiler.txt"
|
||||
gcc -dumpmachine >> "$coverage_dir/compiler.txt"
|
||||
if command -v dpkg-query >/dev/null; then
|
||||
dpkg-query -W gcc libc6-dev libmariadb-dev libsnmp-dev libssl-dev snmpd lcov autoconf automake libtool > "$coverage_dir/dependencies.txt"
|
||||
else
|
||||
printf '%s\n' 'Package version inventory unavailable: dpkg-query is not installed.' > "$coverage_dir/dependencies.txt"
|
||||
fi
|
||||
gcov --version > "$coverage_dir/gcov.txt"
|
||||
lcov --version > "$coverage_dir/lcov.txt"
|
||||
mkdir -p config m4
|
||||
autoreconf -fi > "$coverage_dir/build.log" 2>&1
|
||||
./configure CC=gcc CFLAGS="-std=gnu11 -g -O0 -UNDEBUG --coverage -fprofile-update=atomic -include $source_dir/tests/tools/coverage_process_exit.h" LDFLAGS='--coverage' >> "$coverage_dir/build.log" 2>&1
|
||||
make --no-print-directory coverage-source-list > "$coverage_dir/production-sources.txt"
|
||||
make --no-print-directory coverage-test-source-list > "$coverage_dir/test-sources.txt"
|
||||
make --no-print-directory coverage-fault-source-list > "$coverage_dir/fault-sources.txt"
|
||||
mapfile -t production_sources < "$coverage_dir/production-sources.txt"
|
||||
mapfile -t test_sources < "$coverage_dir/test-sources.txt"
|
||||
mapfile -t fault_sources < "$coverage_dir/fault-sources.txt"
|
||||
[[ "${#production_sources[@]}" -gt 0 ]]
|
||||
[[ "${#test_sources[@]}" -gt "${#production_sources[@]}" ]]
|
||||
[[ "${#fault_sources[@]}" -gt 0 ]]
|
||||
# subdir-objects places each object beside its source, so a producer is the
|
||||
# source directory plus the per-target object prefix and the unit name.
|
||||
producer_path() {
|
||||
local directory
|
||||
directory="$(dirname "$1")"
|
||||
if [[ "$directory" = . ]]; then printf '%s%s' "$2" "$(basename "${1%.c}")"
|
||||
else printf '%s/%s%s' "$directory" "$2" "$(basename "${1%.c}")"; fi
|
||||
}
|
||||
producer_units=()
|
||||
for source in "${production_sources[@]}"; do
|
||||
case "$source" in *.c) ;; *) printf 'Unsupported production source: %s\n' "$source" >&2; exit 1;; esac
|
||||
[[ -s "$source" ]]
|
||||
producer_units+=("$(producer_path "$source" "")")
|
||||
grep -Fxq "$source" "$coverage_dir/test-sources.txt"
|
||||
# The fault binary reuses the registered renamed spine globals object.
|
||||
if [[ "$source" != spine.c ]]; then grep -Fxq "$source" "$coverage_dir/fault-sources.txt"; fi
|
||||
done
|
||||
for source in "${test_sources[@]}"; do
|
||||
case "$source" in *.c) ;; *) printf 'Unsupported test source: %s\n' "$source" >&2; exit 1;; esac
|
||||
[[ -s "$source" ]]
|
||||
producer_units+=("$(producer_path "$source" test_spine_regressions-)")
|
||||
done
|
||||
for source in "${fault_sources[@]}"; do
|
||||
case "$source" in *.c) ;; *) printf 'Unsupported fault source: %s\n' "$source" >&2; exit 1;; esac
|
||||
[[ -s "$source" ]]
|
||||
producer_units+=("$(producer_path "$source" test_spine_faults-)")
|
||||
done
|
||||
printf '%s\n' "${producer_units[@]}" > "$coverage_dir/producers.txt"
|
||||
[[ "$(sort -u "$coverage_dir/producers.txt" | wc -l)" -eq "${#producer_units[@]}" ]]
|
||||
make clean >> "$coverage_dir/build.log" 2>&1
|
||||
find . -path "$coverage_find_path" -prune -o -name '*.gcda' -type f -exec rm -f {} +
|
||||
make -j2 spine test_spine_regressions test_spine_faults >> "$coverage_dir/build.log" 2>&1
|
||||
sha256sum config/config.h Makefile.in > "$coverage_dir/generated-config.sha256"
|
||||
cp config/config.h "$coverage_dir/generated-config.h"
|
||||
cp Makefile.in "$coverage_dir/generated-Makefile.in"
|
||||
sha256sum spine test_spine_regressions test_spine_faults > "$coverage_dir/binaries.sha256"
|
||||
cp config.log "$coverage_dir/config.log"
|
||||
notes=()
|
||||
for producer in "${producer_units[@]}"; do [[ -s "$producer.gcno" ]]; notes+=("$producer.gcno"); done
|
||||
tests/tools/verify_coverage.sh --producers "$source_dir" "$coverage_dir/producers.txt" gcno
|
||||
sha256sum "${notes[@]}" > "$coverage_dir/notes.sha256"
|
||||
./test_spine_regressions > "$coverage_dir/default.log" 2>&1
|
||||
grep -Fq 'production regression tests passed' "$coverage_dir/default.log"
|
||||
grep -Fq 'production SNMPv3 key and timeout boundary regressions passed' "$coverage_dir/default.log"
|
||||
grep -Fq 'production CLI alias regressions passed' "$coverage_dir/default.log"
|
||||
grep -Fq 'production script stream regressions passed' "$coverage_dir/default.log"
|
||||
grep -Fq 'production config structure bindings passed' "$coverage_dir/default.log"
|
||||
grep -Fq 'production numeric error-list boundary regressions passed' "$coverage_dir/default.log"
|
||||
grep -Fq 'production additional contracts passed' "$coverage_dir/default.log"
|
||||
./test_spine_regressions --database > "$coverage_dir/database.log" 2>&1
|
||||
grep -Fq 'production settings write outcome regressions passed' "$coverage_dir/database.log"
|
||||
grep -Fq 'production database configuration regressions passed' "$coverage_dir/database.log"
|
||||
grep -Fq 'production output write failure and retry regressions passed' "$coverage_dir/database.log"
|
||||
grep -Fq 'production output recollection retains incomplete historical samples passed' "$coverage_dir/database.log"
|
||||
grep -Fq 'production simultaneous output failure ordering regressions passed' "$coverage_dir/database.log"
|
||||
grep -Fq 'production remote output destination failure and recollection regressions passed' "$coverage_dir/database.log"
|
||||
grep -Fq 'production nullable SNMP profile handoff passed' "$coverage_dir/database.log"
|
||||
grep -Fq 'production live SNMP and PHP reindex contracts passed' "$coverage_dir/database.log"
|
||||
./test_spine_regressions --snmp-agent > "$coverage_dir/snmp.log" 2>&1
|
||||
grep -Fq 'production SNMP agent regressions passed' "$coverage_dir/snmp.log"
|
||||
grep -Fq 'production local silent UDP SNMP multi timeout regressions passed' "$coverage_dir/snmp.log"
|
||||
SPINE_SNMPV3_EVIDENCE_DIR="$coverage_dir" tests/tools/run_snmpv3.sh > "$coverage_dir/snmpv3.log" 2>&1
|
||||
grep -Fq 'production live SNMPv3 authentication and privacy regressions passed' "$coverage_dir/snmpv3.log"
|
||||
./test_spine_faults > "$coverage_dir/fault-default.log" 2>&1
|
||||
grep -Fq 'production logger date-format failure regressions passed' "$coverage_dir/fault-default.log"
|
||||
grep -Fq 'production process creation failure regressions passed' "$coverage_dir/fault-default.log"
|
||||
grep -Fq 'production linker fault regressions passed' "$coverage_dir/fault-default.log"
|
||||
./test_spine_faults --database > "$coverage_dir/fault-database.log" 2>&1
|
||||
grep -Fq 'production real lost-connection retry regressions passed' "$coverage_dir/fault-database.log"
|
||||
grep -Fq 'production ping-only SNMP session ownership regressions passed' "$coverage_dir/fault-database.log"
|
||||
grep -Fq 'production actual main worker launch failure and retry regressions passed' "$coverage_dir/fault-database.log"
|
||||
grep -Fq 'production linker fault regressions passed' "$coverage_dir/fault-database.log"
|
||||
profiles=()
|
||||
for producer in "${producer_units[@]}"; do [[ -s "$producer.gcda" ]]; profiles+=("$producer.gcda"); done
|
||||
tests/tools/verify_coverage.sh --producers "$source_dir" "$coverage_dir/producers.txt" gcda
|
||||
if [[ "$(id -u)" -eq 0 ]]; then
|
||||
chmod a+rw "${profiles[@]}"
|
||||
setpriv --reuid=65534 --regid=65534 --clear-groups ./test_spine_regressions --icmp-no-capability > "$coverage_dir/icmp-denied.log" 2>&1
|
||||
setcap cap_net_raw+ep ./test_spine_regressions
|
||||
capability_added=1
|
||||
setpriv --reuid=65534 --regid=65534 --clear-groups ./test_spine_regressions --raw-icmp > "$coverage_dir/icmp-capability.log" 2>&1
|
||||
else
|
||||
./test_spine_regressions --icmp-no-capability > "$coverage_dir/icmp-denied.log" 2>&1
|
||||
sudo setcap cap_net_raw+ep ./test_spine_regressions
|
||||
capability_added=1
|
||||
./test_spine_regressions --raw-icmp > "$coverage_dir/icmp-capability.log" 2>&1
|
||||
fi
|
||||
grep -Fq 'production ICMP socket-failure regression passed' "$coverage_dir/icmp-denied.log"
|
||||
grep -Fq 'production ICMP loopback regression passed' "$coverage_dir/icmp-capability.log"
|
||||
lcov_args=()
|
||||
for source in "${production_sources[@]}"; do lcov_args+=(--include "$source_dir/$source"); done
|
||||
lcov --capture --directory . --output-file "$coverage_dir/production.info" --rc branch_coverage=1 "${lcov_args[@]}" > "$coverage_dir/capture.log" 2>&1
|
||||
tests/tools/test_coverage_verifier.sh > "$coverage_dir/verifier-self-test.log"
|
||||
tests/tools/verify_coverage.sh "$coverage_dir/production.info" "$source_dir" "$coverage_dir/production-sources.txt"
|
||||
lcov --summary "$coverage_dir/production.info" --rc branch_coverage=1 > "$coverage_dir/summary.txt" 2>&1
|
||||
# Bind every producer and scenario to this immutable tested source snapshot.
|
||||
sha256sum "${profiles[@]}" > "$coverage_dir/profiles.sha256"
|
||||
mkdir -p "$coverage_dir/profiles" "$coverage_dir/bin"
|
||||
cp --parents -- "${notes[@]}" "${profiles[@]}" "$coverage_dir/profiles/"
|
||||
cp -- spine test_spine_regressions test_spine_faults "$coverage_dir/bin/"
|
||||
(cd "$coverage_dir" && find profiles bin -type f -print0 | sort -z | xargs -0 sha256sum) > "$coverage_dir/saved-producers.sha256"
|
||||
{
|
||||
sha256sum --check "$coverage_dir/source.sha256"
|
||||
sha256sum --check "$coverage_dir/generated-config.sha256"
|
||||
sha256sum --check "$coverage_dir/binaries.sha256"
|
||||
sha256sum --check "$coverage_dir/notes.sha256"
|
||||
} > "$coverage_dir/source-verification.log"
|
||||
printf '%s\n' default numeric-error-boundaries config-bindings script-streams cli-aliases additional-contracts database settings-write-outcome output-write-failure-retry output-recollection-new-timestamp simultaneous-output-failure-ordering remote-output-destination-failure-recollection output-sql-batch-boundary nullable-snmp-profile live-reindex snmp local-silent-udp-snmp-multi-timeout snmpv3-key-timeout snmpv3-live fault-default fault-logger fault-process fault-database-retry fault-ping-only-session worker-launch-admitted worker-launch-rejected worker-launch-eagain-retry icmp-denied icmp-capability > "$coverage_dir/scenarios.txt"
|
||||
(cd "$coverage_dir" && sha256sum default.log database.log snmp.log snmpv3.log snmpv3-agent.log fault-default.log fault-database.log icmp-denied.log icmp-capability.log scenarios.txt production.info summary.txt source.sha256 generated-config.sha256 binaries.sha256 notes.sha256 profiles.sha256 production-sources.txt test-sources.txt fault-sources.txt producers.txt revision.txt worktree-status.txt compiler.txt dependencies.txt gcov.txt lcov.txt source-Makefile.in generated-Makefile.in generated-config.h source-inputs.nul source-inputs.tar.gz saved-producers.sha256) > "$coverage_dir/evidence.sha256"
|
||||
cat "$coverage_dir/summary.txt"
|
||||
coverage_complete=1
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (C) 2026 The Cacti Group
|
||||
# Licensed under the GNU Lesser General Public License, version 2.1 or later.
|
||||
# Run only against the owned local agent and public regression credentials.
|
||||
set -euo pipefail
|
||||
: "${SPINE_SNMPV3_EVIDENCE_DIR:?Set the owned agent evidence directory}"
|
||||
command -v snmpd >/dev/null
|
||||
mkdir -p "$SPINE_SNMPV3_EVIDENCE_DIR"
|
||||
snmpv3_persistent="$(mktemp -d "${TMPDIR:-/tmp}/spine-snmpv3.XXXXXX")"
|
||||
snmpv3_agent_pid=''
|
||||
cleanup_snmpv3() {
|
||||
status=$?
|
||||
if [[ -n "$snmpv3_agent_pid" ]]; then
|
||||
kill "$snmpv3_agent_pid" 2>/dev/null || true
|
||||
wait "$snmpv3_agent_pid" || true
|
||||
fi
|
||||
cp "$snmpv3_persistent/agent.log" "$SPINE_SNMPV3_EVIDENCE_DIR/snmpv3-agent.log" || status=1
|
||||
rm -rf -- "$snmpv3_persistent"
|
||||
exit "$status"
|
||||
}
|
||||
trap cleanup_snmpv3 EXIT
|
||||
cp tests/fixtures/snmpv3-users.conf "$snmpv3_persistent/snmpd.conf"
|
||||
SNMP_PERSISTENT_DIR="$snmpv3_persistent" snmpd -f -Lo -C -c "$snmpv3_persistent/snmpd.conf,tests/fixtures/snmpv3-agent.conf" > "$snmpv3_persistent/agent.log" 2>&1 &
|
||||
snmpv3_agent_pid=$!
|
||||
sleep 1
|
||||
kill -0 "$snmpv3_agent_pid"
|
||||
SPINE_TEST_SNMPV3_HOST=127.0.0.1 ./test_spine_regressions --snmpv3-agent
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (C) 2026 The Cacti Group
|
||||
# Licensed under the GNU Lesser General Public License, version 2.1 or later.
|
||||
set -euo pipefail
|
||||
owned_dir="$(mktemp -d "${TMPDIR:-/tmp}/spine-coverage-verifier.XXXXXX")"
|
||||
trap 'rm -rf "$owned_dir"' EXIT
|
||||
verifier="$(dirname "$0")/verify_coverage.sh"
|
||||
printf '%s\n' one.c two.c > "$owned_dir/sources.txt"
|
||||
for source in one.c two.c; do
|
||||
printf 'SF:/source/%s\nLF:2\nLH:1\nBRF:2\nBRH:1\nFNF:1\nFNH:1\nend_of_record\n' "$source" >> "$owned_dir/complete.info"
|
||||
done
|
||||
"$verifier" "$owned_dir/complete.info" /source "$owned_dir/sources.txt"
|
||||
reject() {
|
||||
local profile="$1"
|
||||
if "$verifier" "$profile" /source "$owned_dir/sources.txt" > /dev/null 2>&1; then
|
||||
printf 'Verifier incorrectly admitted %s\n' "$profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
reject "$owned_dir/absent.info"
|
||||
sed '/SF:\/source\/one.c/,/end_of_record/d' "$owned_dir/complete.info" > "$owned_dir/omitted.info"
|
||||
reject "$owned_dir/omitted.info"
|
||||
sed -e 's/BRF:2/BRF:0/' -e 's/BRH:1/BRH:0/' "$owned_dir/complete.info" > "$owned_dir/no-branches.info"
|
||||
reject "$owned_dir/no-branches.info"
|
||||
sed -e 's/LH:1/LH:0/' -e 's/BRH:1/BRH:0/' "$owned_dir/complete.info" > "$owned_dir/unexecuted.info"
|
||||
reject "$owned_dir/unexecuted.info"
|
||||
{ printf 'SF:/source/one.c\n'; sed '/SF:\/source\/one.c/,/end_of_record/d' "$owned_dir/complete.info"; } > "$owned_dir/borrowed-metrics.info"
|
||||
reject "$owned_dir/borrowed-metrics.info"
|
||||
sed '$d' "$owned_dir/complete.info" > "$owned_dir/dangling.info"
|
||||
reject "$owned_dir/dangling.info"
|
||||
cat "$owned_dir/complete.info" "$owned_dir/complete.info" > "$owned_dir/duplicate.info"
|
||||
reject "$owned_dir/duplicate.info"
|
||||
sed '/^LH:/d' "$owned_dir/complete.info" > "$owned_dir/missing-metric.info"
|
||||
reject "$owned_dir/missing-metric.info"
|
||||
sed 's/LH:1/LH:3/' "$owned_dir/complete.info" > "$owned_dir/overcount.info"
|
||||
reject "$owned_dir/overcount.info"
|
||||
printf '%s\n' one two > "$owned_dir/producers.txt"
|
||||
printf 'fixture\n' > "$owned_dir/one.gcno"
|
||||
printf 'fixture\n' > "$owned_dir/two.gcno"
|
||||
"$verifier" --producers "$owned_dir" "$owned_dir/producers.txt" gcno
|
||||
if "$verifier" --producers "$owned_dir" "$owned_dir/producers.txt" gcda; then exit 1; fi
|
||||
printf '%s\n' one missing > "$owned_dir/missing-producer.txt"
|
||||
if "$verifier" --producers "$owned_dir" "$owned_dir/missing-producer.txt" gcno; then exit 1; fi
|
||||
printf '%s\n' one one > "$owned_dir/duplicate-producer.txt"
|
||||
if "$verifier" --producers "$owned_dir" "$owned_dir/duplicate-producer.txt" gcno; then exit 1; fi
|
||||
printf '%s\n' zero > "$owned_dir/zero-producer.txt"
|
||||
: > "$owned_dir/zero.gcno"
|
||||
if "$verifier" --producers "$owned_dir" "$owned_dir/zero-producer.txt" gcno; then exit 1; fi
|
||||
printf 'Coverage verifier rejects absent, omitted, branchless, unexecuted, malformed, dangling, duplicate and inconsistent reports\n'
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (C) 2026 The Cacti Group
|
||||
# Licensed under the GNU Lesser General Public License, version 2.1 or later.
|
||||
set -euo pipefail
|
||||
if [[ "${1:-}" = '--producers' ]]; then
|
||||
: "${2:?Usage: verify_coverage.sh --producers BUILD_DIRECTORY MANIFEST gcno|gcda}"
|
||||
: "${3:?Usage: verify_coverage.sh --producers BUILD_DIRECTORY MANIFEST gcno|gcda}"
|
||||
: "${4:?Usage: verify_coverage.sh --producers BUILD_DIRECTORY MANIFEST gcno|gcda}"
|
||||
case "$4" in gcno|gcda) ;; *) exit 1;; esac
|
||||
[[ -s "$3" ]]
|
||||
[[ -z "$(sort "$3" | uniq -d)" ]]
|
||||
while IFS= read -r producer; do
|
||||
case "$producer" in ''|/*|*..*|*[!a-zA-Z0-9_/-]*) exit 1;; *) ;; esac
|
||||
[[ -s "$2/$producer.$4" ]]
|
||||
done < "$3"
|
||||
exit 0
|
||||
fi
|
||||
: "${1:?Usage: verify_coverage.sh PROFILE SOURCE_DIRECTORY PRODUCTION_MANIFEST}"
|
||||
: "${2:?Usage: verify_coverage.sh PROFILE SOURCE_DIRECTORY PRODUCTION_MANIFEST}"
|
||||
: "${3:?Usage: verify_coverage.sh PROFILE SOURCE_DIRECTORY PRODUCTION_MANIFEST}"
|
||||
[[ -s "$1" ]]
|
||||
[[ -s "$3" ]]
|
||||
awk -F: -v source_dir="$2" '
|
||||
function fail(message) { print message > "/dev/stderr"; failed=1; exit 1 }
|
||||
FNR==NR {
|
||||
if ($0=="" || $0 !~ /\.c$/) fail("Invalid production source manifest")
|
||||
identity="SF:" source_dir "/" $0
|
||||
if (identity in expected) fail("Duplicate production source in manifest")
|
||||
expected[identity]=1
|
||||
next
|
||||
}
|
||||
/^SF:/ {
|
||||
if (open_record) fail("Previous coverage record was not terminated")
|
||||
identity=$0
|
||||
if (!(identity in expected)) fail("Unexpected coverage source: " identity)
|
||||
if (identity in produced) fail("Duplicate coverage source: " identity)
|
||||
open_record=1
|
||||
delete fields
|
||||
next
|
||||
}
|
||||
/^(LF|LH|BRF|BRH|FNF|FNH):/ {
|
||||
if (!open_record) fail("Coverage metrics outside a source record")
|
||||
if (NF!=2 || $2 !~ /^[0-9]+$/ || ($1 in fields)) fail("Invalid or duplicate coverage metric")
|
||||
fields[$1]=$2+0
|
||||
next
|
||||
}
|
||||
/^end_of_record$/ {
|
||||
if (!open_record) fail("Coverage terminator without a source record")
|
||||
if (!("LF" in fields) || !("LH" in fields) || !("BRF" in fields) || !("BRH" in fields) || !("FNF" in fields) || !("FNH" in fields)) fail("Incomplete coverage metrics")
|
||||
if (fields["LF"]<=0 || fields["FNF"]<=0 || fields["LH"]>fields["LF"] || fields["BRH"]>fields["BRF"] || fields["FNH"]>fields["FNF"]) fail("Inconsistent coverage metrics")
|
||||
total_lines_hit+=fields["LH"]
|
||||
total_branches_hit+=fields["BRH"]
|
||||
produced[identity]=1
|
||||
open_record=0
|
||||
next
|
||||
}
|
||||
END {
|
||||
if (failed) exit 1
|
||||
if (open_record) fail("Unterminated final coverage record")
|
||||
for (identity in expected) if (!(identity in produced)) fail("Absent production coverage: " identity)
|
||||
if (total_lines_hit<=0 || total_branches_hit<=0) fail("No measured line/branch execution")
|
||||
}
|
||||
' "$3" "$1"
|
||||
+16
-14
@@ -424,6 +424,8 @@ static void build_ip_icmp(unsigned char *buf, size_t len, uint16_t id, uint16_t
|
||||
iph = (struct ip *) buf;
|
||||
iph->ip_hl = sizeof(struct ip) / 4;
|
||||
iph->ip_v = 4;
|
||||
/* a raw ICMP socket only delivers ICMP datagrams; the classifier checks it */
|
||||
iph->ip_p = IPPROTO_ICMP;
|
||||
|
||||
pkt = (struct icmp *) (buf + sizeof(struct ip));
|
||||
pkt->icmp_type = type;
|
||||
@@ -542,10 +544,10 @@ static void test_config_defaults_populates_the_set(void **state) {
|
||||
memset(&set, 0, sizeof set);
|
||||
config_defaults();
|
||||
|
||||
assert_int_equal(set.threads, DEFAULT_THREADS);
|
||||
assert_int_equal(set.db_port, DEFAULT_DB_PORT);
|
||||
assert_string_equal(set.db_host, DEFAULT_DB_HOST);
|
||||
assert_string_equal(set.db_db, DEFAULT_DB_DB);
|
||||
assert_int_equal(set.poller.threads, DEFAULT_THREADS);
|
||||
assert_int_equal(set.database.port, DEFAULT_DB_PORT);
|
||||
assert_string_equal(set.database.host, DEFAULT_DB_HOST);
|
||||
assert_string_equal(set.database.database, DEFAULT_DB_DB);
|
||||
}
|
||||
|
||||
static void test_read_spine_config_rejects_a_missing_file(void **state) {
|
||||
@@ -572,10 +574,10 @@ static void test_read_spine_config_reads_settings(void **state) {
|
||||
config_defaults();
|
||||
assert_int_equal(read_spine_config(path), 0);
|
||||
|
||||
assert_string_equal(set.db_host, "testhost");
|
||||
assert_string_equal(set.db_db, "testdb");
|
||||
assert_string_equal(set.db_user, "testuser");
|
||||
assert_int_equal(set.db_port, 3399);
|
||||
assert_string_equal(set.database.host, "testhost");
|
||||
assert_string_equal(set.database.database, "testdb");
|
||||
assert_string_equal(set.database.user, "testuser");
|
||||
assert_int_equal(set.database.port, 3399);
|
||||
|
||||
remove(path);
|
||||
}
|
||||
@@ -602,15 +604,15 @@ static void test_set_date_format_clamps_an_out_of_range_format(void **state) {
|
||||
(void) state;
|
||||
|
||||
config_defaults();
|
||||
set.log_datetime_format = GD_MAX + 10;
|
||||
set.log_datetime_separator = GDC_MAX + 10;
|
||||
set.logging.log_datetime_format = GD_MAX + 10;
|
||||
set.logging.log_datetime_separator = GDC_MAX + 10;
|
||||
|
||||
set_date_format();
|
||||
fmt = get_date_format();
|
||||
|
||||
assert_non_null(fmt);
|
||||
assert_int_equal(set.log_datetime_format, GD_DEFAULT);
|
||||
assert_int_equal(set.log_datetime_separator, GDC_DEFAULT);
|
||||
assert_int_equal(set.logging.log_datetime_format, GD_DEFAULT);
|
||||
assert_int_equal(set.logging.log_datetime_separator, GDC_DEFAULT);
|
||||
}
|
||||
|
||||
static void test_get_date_format_covers_each_supported_format(void **state) {
|
||||
@@ -623,8 +625,8 @@ static void test_get_date_format_covers_each_supported_format(void **state) {
|
||||
|
||||
for (fmt_value = GD_MIN; fmt_value <= GD_MAX; fmt_value++) {
|
||||
for (sep_value = GDC_MIN; sep_value <= GDC_MAX; sep_value++) {
|
||||
set.log_datetime_format = fmt_value;
|
||||
set.log_datetime_separator = sep_value;
|
||||
set.logging.log_datetime_format = fmt_value;
|
||||
set.logging.log_datetime_separator = sep_value;
|
||||
|
||||
set_date_format();
|
||||
fmt = get_date_format();
|
||||
|
||||
@@ -170,11 +170,11 @@ static int php_setup(void **state) {
|
||||
assert_non_null(php_processes);
|
||||
php_processes_initialize(php_processes, MAX_PHP_SERVERS);
|
||||
|
||||
set.php_servers = 2;
|
||||
set.script_timeout = 1;
|
||||
set.php.php_servers = 2;
|
||||
set.php.script_timeout = 1;
|
||||
set.cacti_version = 1300;
|
||||
set.poller_id = 1;
|
||||
set.log_destination = LOGDEST_STDOUT;
|
||||
set.poller.poller_id = 1;
|
||||
set.logging.log_destination = LOGDEST_STDOUT;
|
||||
block_shell_spawn = FALSE;
|
||||
block_php_spawn = FALSE;
|
||||
shell_spawn_reached = FALSE;
|
||||
@@ -192,8 +192,8 @@ static int php_setup(void **state) {
|
||||
track_write = FALSE;
|
||||
write_calls = 0;
|
||||
restore_nofile = FALSE;
|
||||
snprintf(set.path_php, sizeof(set.path_php), "%s", PHP_TEST_SERVER_PATH);
|
||||
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "normal");
|
||||
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", PHP_TEST_SERVER_PATH);
|
||||
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "normal");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ static void test_round_robin_wraps_at_server_count(void **state) {
|
||||
php_processes[1].php_pid = 12;
|
||||
php_processes[1].php_read_fd = 12;
|
||||
php_processes[1].php_write_fd = 13;
|
||||
set.php_current_server = set.php_servers;
|
||||
set.php.php_current_server = set.php.php_servers;
|
||||
assert_int_equal(php_get_process(), 0);
|
||||
assert_int_equal(php_get_process(), 1);
|
||||
assert_int_equal(php_get_process(), 0);
|
||||
@@ -257,7 +257,7 @@ static void test_process_slot_initialization_invalidates_zero_descriptors(void *
|
||||
|
||||
static void test_round_robin_rejects_failed_slots(void **state) {
|
||||
(void) state;
|
||||
snprintf(set.path_php, sizeof(set.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
assert_int_equal(php_get_process(), -1);
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ static void test_round_robin_returns_a_healthy_slot_when_all_are_contended(void
|
||||
int i;
|
||||
(void) state;
|
||||
|
||||
for (i = 0; i < set.php_servers; i++) {
|
||||
for (i = 0; i < set.php.php_servers; i++) {
|
||||
php_processes[i].php_state = PHP_READY;
|
||||
php_processes[i].php_pid = 100 + i;
|
||||
php_processes[i].php_read_fd = 20 + (i * 2);
|
||||
@@ -273,10 +273,10 @@ static void test_round_robin_returns_a_healthy_slot_when_all_are_contended(void
|
||||
thread_mutex_lock(LOCK_PHP_PROC_0 + i);
|
||||
}
|
||||
|
||||
set.php_current_server = 0;
|
||||
set.php.php_current_server = 0;
|
||||
assert_int_equal(php_get_process(), 0);
|
||||
|
||||
for (i = 0; i < set.php_servers; i++) {
|
||||
for (i = 0; i < set.php.php_servers; i++) {
|
||||
thread_mutex_unlock(LOCK_PHP_PROC_0 + i);
|
||||
php_processes[i].php_pid = -1;
|
||||
php_processes[i].php_read_fd = -1;
|
||||
@@ -309,7 +309,7 @@ static void test_dead_slot_is_recovered_while_another_slot_is_contended(void **s
|
||||
php_processes[0].php_pid = 100;
|
||||
php_processes[0].php_read_fd = 20;
|
||||
php_processes[0].php_write_fd = 21;
|
||||
set.php_current_server = 0;
|
||||
set.php.php_current_server = 0;
|
||||
assert_int_equal(pthread_create(&holder, NULL, hold_php_slot, &held_slot), 0);
|
||||
pthread_mutex_lock(&spawn_barrier_mutex);
|
||||
while (!shell_spawn_reached) {
|
||||
@@ -341,8 +341,8 @@ static void test_failed_recovery_falls_back_to_the_contended_slot(void **state)
|
||||
php_processes[0].php_pid = 100;
|
||||
php_processes[0].php_read_fd = 20;
|
||||
php_processes[0].php_write_fd = 21;
|
||||
set.php_current_server = 0;
|
||||
snprintf(set.path_php, sizeof(set.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
set.php.php_current_server = 0;
|
||||
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
assert_int_equal(pthread_create(&holder, NULL, hold_php_slot, &held_slot), 0);
|
||||
pthread_mutex_lock(&spawn_barrier_mutex);
|
||||
while (!shell_spawn_reached) {
|
||||
@@ -506,7 +506,7 @@ static void test_legacy_cacti_version_uses_positional_poller_id(void **state) {
|
||||
|
||||
static void test_remote_poller_id_uses_environ_argument(void **state) {
|
||||
(void) state;
|
||||
set.poller_id = 2;
|
||||
set.poller.poller_id = 2;
|
||||
assert_int_equal(php_init(0), TRUE);
|
||||
assert_int_equal(php_processes[0].php_state, PHP_READY);
|
||||
php_close(0);
|
||||
@@ -514,7 +514,7 @@ static void test_remote_poller_id_uses_environ_argument(void **state) {
|
||||
|
||||
static void test_php_child_restores_sigpipe_default(void **state) {
|
||||
(void) state;
|
||||
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "check-sigpipe");
|
||||
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "check-sigpipe");
|
||||
assert_int_equal(php_init(0), TRUE);
|
||||
assert_int_equal(php_processes[0].php_state, PHP_READY);
|
||||
}
|
||||
@@ -629,7 +629,7 @@ static void test_php_spawn_does_not_inherit_an_nft_collision_descriptor(void **s
|
||||
|
||||
static void test_init_marks_an_unexpected_handshake_busy(void **state) {
|
||||
(void) state;
|
||||
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "bad-start");
|
||||
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "bad-start");
|
||||
assert_int_equal(php_init(0), TRUE);
|
||||
assert_int_equal(php_processes[0].php_state, PHP_BUSY);
|
||||
}
|
||||
@@ -640,7 +640,7 @@ static void test_busy_handshake_is_recovered_on_the_next_poll(void **state) {
|
||||
pid_t failed_pid;
|
||||
|
||||
(void) state;
|
||||
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "bad-start");
|
||||
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "bad-start");
|
||||
assert_int_equal(php_init(0), TRUE);
|
||||
assert_int_equal(php_processes[0].php_state, PHP_BUSY);
|
||||
failed_pid = php_processes[0].php_pid;
|
||||
@@ -648,7 +648,7 @@ static void test_busy_handshake_is_recovered_on_the_next_poll(void **state) {
|
||||
assert_true(php_processes[0].php_read_fd >= 0);
|
||||
assert_true(php_processes[0].php_write_fd >= 0);
|
||||
|
||||
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "normal");
|
||||
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "normal");
|
||||
process = php_get_process();
|
||||
assert_int_equal(process, 0);
|
||||
assert_true(php_processes[0].php_pid > 1);
|
||||
@@ -692,7 +692,7 @@ static void test_recovery_and_command_share_the_slot_lock(void **state) {
|
||||
pthread_t command_thread;
|
||||
(void) state;
|
||||
|
||||
set.php_servers = 1;
|
||||
set.php.php_servers = 1;
|
||||
block_php_spawn = TRUE;
|
||||
command_finished = FALSE;
|
||||
assert_int_equal(pthread_create(&recovery_thread, NULL, get_php_process_thread, &recovery), 0);
|
||||
@@ -728,9 +728,9 @@ static void test_recovery_attempts_only_one_failed_slot_per_call(void **state) {
|
||||
double elapsed;
|
||||
(void) state;
|
||||
|
||||
set.php_servers = 3;
|
||||
set.script_timeout = 1;
|
||||
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "silent");
|
||||
set.php.php_servers = 3;
|
||||
set.php.script_timeout = 1;
|
||||
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "silent");
|
||||
started = get_time_as_double();
|
||||
assert_int_equal(php_get_process(), -1);
|
||||
elapsed = get_time_as_double() - started;
|
||||
@@ -740,7 +740,7 @@ static void test_recovery_attempts_only_one_failed_slot_per_call(void **state) {
|
||||
|
||||
static void test_child_exit_during_startup_fails_closed_without_restart(void **state) {
|
||||
(void) state;
|
||||
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "exit-before-start");
|
||||
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "exit-before-start");
|
||||
assert_int_equal(php_init(0), TRUE);
|
||||
assert_int_equal(php_processes[0].php_state, PHP_BUSY);
|
||||
}
|
||||
@@ -751,7 +751,7 @@ static void test_php_init_later_failure_preserves_earlier_server(void **state) {
|
||||
int first_write_fd;
|
||||
(void) state;
|
||||
|
||||
set.php_servers = 2;
|
||||
set.php.php_servers = 2;
|
||||
fail_php_spawn_call = 2;
|
||||
assert_int_equal(php_init(PHP_INIT), FALSE);
|
||||
|
||||
@@ -846,8 +846,8 @@ static void test_cloexec_pipe_failure_releases_both_descriptors(void **state) {
|
||||
|
||||
static void test_init_timeout_does_not_recurse(void **state) {
|
||||
(void) state;
|
||||
set.script_timeout = 0;
|
||||
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "silent");
|
||||
set.php.script_timeout = 0;
|
||||
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "silent");
|
||||
assert_int_equal(php_init(0), TRUE);
|
||||
assert_int_equal(php_processes[0].php_state, PHP_BUSY);
|
||||
}
|
||||
@@ -857,7 +857,7 @@ static void test_spawn_failure_releases_every_resource(void **state) {
|
||||
/* calloc-like zeroes reproduce the process-wide initialization that used to
|
||||
* let a failed slot masquerade as stdin. php_init() must replace them. */
|
||||
memset(&php_processes[0], 0, sizeof(php_processes[0]));
|
||||
snprintf(set.path_php, sizeof(set.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
assert_int_equal(php_init(0), FALSE);
|
||||
assert_int_equal(php_processes[0].php_pid, -1);
|
||||
assert_int_equal(php_processes[0].php_read_fd, -1);
|
||||
@@ -931,7 +931,7 @@ static void test_readpipe_rejects_fd_at_fd_setsize(void **state) {
|
||||
oversized_fd = duplicate_at_fdsetsize(pdes[0]);
|
||||
close(pdes[0]);
|
||||
close(pdes[1]);
|
||||
snprintf(set.path_php, sizeof(set.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
php_processes[0].php_state = PHP_READY;
|
||||
php_processes[0].php_read_fd = oversized_fd;
|
||||
result = php_readpipe(0, command);
|
||||
@@ -985,8 +985,8 @@ static void test_command_retires_fd_at_fd_setsize(void **state) {
|
||||
pause();
|
||||
_exit(0);
|
||||
}
|
||||
set.php_servers = 1;
|
||||
snprintf(set.path_php, sizeof(set.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
set.php.php_servers = 1;
|
||||
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
php_processes[0].php_state = PHP_READY;
|
||||
php_processes[0].php_read_fd = oversized_fd;
|
||||
php_processes[0].php_write_fd = pdes[1];
|
||||
@@ -1050,7 +1050,7 @@ static void test_failed_write_stops_when_restart_handshake_is_not_ready(void **s
|
||||
|
||||
(void) state;
|
||||
prepare_broken_ready_slot();
|
||||
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "bad-start");
|
||||
snprintf(set.php.path_php_server, sizeof(set.php.path_php_server), "%s", "bad-start");
|
||||
result = php_cmd("poll 9", 0);
|
||||
track_write = FALSE;
|
||||
assert_non_null(result);
|
||||
@@ -1095,7 +1095,7 @@ static void test_command_gives_up_after_three_failed_writes(void **state) {
|
||||
}
|
||||
php_processes[0].php_read_fd = pdes[0];
|
||||
php_processes[0].php_write_fd = pdes[1];
|
||||
snprintf(set.path_php, sizeof(set.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
snprintf(set.php.path_php, sizeof(set.php.path_php), "%s", "/does/not/exist/spine-php-test");
|
||||
assert_int_equal(sigaction(SIGPIPE, NULL, &saved_sigpipe), 0);
|
||||
memset(&default_sigpipe, 0, sizeof(default_sigpipe));
|
||||
default_sigpipe.sa_handler = SIG_DFL;
|
||||
|
||||
+11
-11
@@ -248,11 +248,11 @@ static void make_host(host_t *host, const char *addr) {
|
||||
memset(host, 0, sizeof(*host));
|
||||
host->id = 1;
|
||||
snprintf(host->hostname, sizeof(host->hostname), "%s", addr);
|
||||
host->ping_timeout = 400;
|
||||
host->ping_retries = 1;
|
||||
host->ping_port = 33439;
|
||||
host->availability_method = AVAIL_PING;
|
||||
host->ping_method = PING_ICMP;
|
||||
host->availability.timeout = 400;
|
||||
host->availability.retries = 1;
|
||||
host->availability.port = 33439;
|
||||
host->availability.method = AVAIL_PING;
|
||||
host->availability.ping_method = PING_ICMP;
|
||||
}
|
||||
|
||||
static int ping_reset(void **state) {
|
||||
@@ -263,8 +263,8 @@ static int ping_reset(void **state) {
|
||||
/* is_debug_device() walks this global unguarded and ping_icmp() calls it */
|
||||
memset(pi_debug_table, 0, sizeof(pi_debug_table));
|
||||
debug_devices = pi_debug_table;
|
||||
set.ping_timeout = 400;
|
||||
set.ping_retries = 1;
|
||||
set.availability.ping_timeout = 400;
|
||||
set.availability.ping_retries = 1;
|
||||
use_controlled_socket = 0;
|
||||
controlled_socket_fd = -1;
|
||||
controlled_socket_type = 0;
|
||||
@@ -275,7 +275,7 @@ static int ping_reset(void **state) {
|
||||
packet_size = ICMP_HDR_SIZE + strlen("cacti-monitoring-system");
|
||||
packet_allocation = NULL;
|
||||
packet_released = 0;
|
||||
set.icmp_uses_caps = FALSE;
|
||||
set.availability.icmp_uses_caps = FALSE;
|
||||
controlled_pair[0] = -1;
|
||||
controlled_pair[1] = -1;
|
||||
resolver_mode = 0;
|
||||
@@ -377,8 +377,8 @@ static void test_timeout_releases_packet_and_socket(void **state) {
|
||||
use_owned_controlled_socket();
|
||||
track_packet = 1;
|
||||
make_host(&host, "127.0.0.1");
|
||||
host.ping_timeout = 1;
|
||||
host.ping_retries = 0;
|
||||
host.availability.timeout = 1;
|
||||
host.availability.retries = 0;
|
||||
memset(&ping, 0, sizeof(ping));
|
||||
|
||||
assert_int_equal(ping_icmp(&host, &ping), HOST_DOWN);
|
||||
@@ -424,7 +424,7 @@ static void test_cached_capability_path_releases_resources(void **state) {
|
||||
(void) state;
|
||||
use_controlled_socket = 1;
|
||||
controlled_socket_fd = FD_SETSIZE;
|
||||
set.icmp_uses_caps = TRUE;
|
||||
set.availability.icmp_uses_caps = TRUE;
|
||||
make_host(&host, "127.0.0.1");
|
||||
memset(&ping, 0, sizeof(ping));
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ static int session_reset(void **state) {
|
||||
(void) state;
|
||||
config_defaults();
|
||||
init_mutexes();
|
||||
set.snmp_retries = 1;
|
||||
set.snmp.snmp_retries = 1;
|
||||
captured_security_level = -1;
|
||||
captured_auth_key_len = 0;
|
||||
captured_priv_key_len = 0;
|
||||
@@ -133,8 +133,14 @@ static int level_for(char *auth_protocol, char *auth_password,
|
||||
char eid[] = "";
|
||||
void *sessp;
|
||||
|
||||
sessp = snmp_host_init(1, host, 3, NULL, user, auth_password, auth_protocol,
|
||||
priv_passphrase, priv_protocol, ctx, eid, 161, 500);
|
||||
const snmp_connection_t options = {
|
||||
.host_id = 1, .hostname = host, .snmp_version = 3, .snmp_community = NULL,
|
||||
.snmp_username = user, .snmp_password = auth_password, .snmp_auth_protocol = auth_protocol,
|
||||
.snmp_priv_passphrase = priv_passphrase, .snmp_priv_protocol = priv_protocol,
|
||||
.snmp_context = ctx, .snmp_engine_id = eid, .snmp_port = 161, .snmp_timeout = 500
|
||||
};
|
||||
|
||||
sessp = snmp_host_init(&options);
|
||||
|
||||
if (sessp == NULL) {
|
||||
return -1;
|
||||
|
||||
@@ -45,13 +45,19 @@ static char *item_inserts[8];
|
||||
static int host_insert_count;
|
||||
static int item_insert_count;
|
||||
static int total_insert_count;
|
||||
static int host_insert_failures;
|
||||
|
||||
#define mysql_num_rows test_mysql_num_rows
|
||||
#define mysql_get_server_version test_mysql_get_server_version
|
||||
#define mysql_fetch_row test_mysql_fetch_row
|
||||
#define mysql_num_fields test_mysql_num_fields
|
||||
#define mysql_errno test_mysql_errno
|
||||
|
||||
my_ulonglong test_mysql_num_rows(MYSQL_RES *res) { (void) res; return (my_ulonglong) rows_to_report; }
|
||||
unsigned long test_mysql_get_server_version(MYSQL *mysql) { (void) mysql; return 80020; }
|
||||
/* The remote push checks the column count against its transfer plan. */
|
||||
unsigned int test_mysql_num_fields(MYSQL_RES *res) { (void) res; return fake_query_kind == FAKE_QUERY_ITEMS ? 5 : 21; }
|
||||
unsigned int test_mysql_errno(MYSQL *mysql) { (void) mysql; return 0; }
|
||||
|
||||
MYSQL_ROW test_mysql_fetch_row(MYSQL_RES *res) {
|
||||
static char *cells[21];
|
||||
@@ -129,13 +135,18 @@ int db_insert(MYSQL *mysql, int type, const char *query) {
|
||||
assert_true(host_insert_count < 8);
|
||||
host_inserts[host_insert_count++] = strdup(query);
|
||||
assert_non_null(host_inserts[host_insert_count - 1]);
|
||||
if (host_insert_failures > 0) {
|
||||
host_insert_failures--;
|
||||
return 0;
|
||||
}
|
||||
} else if (strncmp(query, "INSERT INTO poller_item ", strlen("INSERT INTO poller_item ")) == 0) {
|
||||
assert_true(item_insert_count < 8);
|
||||
item_inserts[item_insert_count++] = strdup(query);
|
||||
assert_non_null(item_inserts[item_insert_count - 1]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
/* TRUE: the remote push stops on the first failed statement. */
|
||||
return 1;
|
||||
}
|
||||
void db_escape(MYSQL *mysql, char *output, int max_size, const char *input) {
|
||||
size_t in = 0;
|
||||
@@ -152,7 +163,7 @@ void db_escape(MYSQL *mysql, char *output, int max_size, const char *input) {
|
||||
}
|
||||
output[out] = '\0';
|
||||
}
|
||||
int append_hostrange(char *obuf, const char *colname) { return 0; }
|
||||
int append_hostrange(char *obuf, size_t capacity, const char *colname) { return 0; }
|
||||
int parse_logdest(const char *res, int default_dest) { return 0; }
|
||||
const char *printable_logdest(int dest) { return ""; }
|
||||
void php_close(int php_process) {}
|
||||
@@ -162,10 +173,12 @@ void php_close(int php_process) {}
|
||||
#undef mysql_num_rows
|
||||
#undef mysql_get_server_version
|
||||
#undef mysql_fetch_row
|
||||
#undef mysql_num_fields
|
||||
#undef mysql_errno
|
||||
|
||||
static char *build(int sep_code, int fmt_code) {
|
||||
set.log_datetime_separator = sep_code;
|
||||
set.log_datetime_format = fmt_code;
|
||||
set.logging.log_datetime_separator = sep_code;
|
||||
set.logging.log_datetime_format = fmt_code;
|
||||
set_date_format();
|
||||
|
||||
return get_date_format();
|
||||
@@ -221,8 +234,8 @@ static void test_out_of_range_codes_clamp_to_defaults(void **state) {
|
||||
|
||||
from_default = build(GDC_MAX + 7, GD_MAX + 7);
|
||||
assert_string_equal(from_default, expected);
|
||||
assert_int_equal(set.log_datetime_separator, GDC_DEFAULT);
|
||||
assert_int_equal(set.log_datetime_format, GD_DEFAULT);
|
||||
assert_int_equal(set.logging.log_datetime_separator, GDC_DEFAULT);
|
||||
assert_int_equal(set.logging.log_datetime_format, GD_DEFAULT);
|
||||
|
||||
from_default = build(GDC_MIN - 3, GD_MIN - 3);
|
||||
assert_string_equal(from_default, expected);
|
||||
@@ -369,33 +382,12 @@ static int count_occurrences(const char *haystack, const char *needle) {
|
||||
return count;
|
||||
}
|
||||
|
||||
static void test_push_flush_drops_suffix_overflow(void **state) {
|
||||
char *sqlbuf;
|
||||
char *cursor;
|
||||
MYSQL mysql;
|
||||
int before;
|
||||
|
||||
(void) state;
|
||||
sqlbuf = malloc(HUGE_BUFSIZE);
|
||||
assert_non_null(sqlbuf);
|
||||
memset(sqlbuf, 'x', HUGE_BUFSIZE - 1);
|
||||
sqlbuf[HUGE_BUFSIZE - 1] = '\0';
|
||||
cursor = sqlbuf + HUGE_BUFSIZE - 1;
|
||||
before = total_insert_count;
|
||||
|
||||
push_flush_batch(&mysql, sqlbuf, &cursor, " suffix");
|
||||
|
||||
assert_int_equal(total_insert_count, before);
|
||||
assert_ptr_equal(cursor, sqlbuf + HUGE_BUFSIZE - 1);
|
||||
free(sqlbuf);
|
||||
}
|
||||
|
||||
static void test_remote_push_keeps_batch_boundary_rows(void **state) {
|
||||
int i;
|
||||
|
||||
(void) state;
|
||||
memset(&set, 0, sizeof(set));
|
||||
set.poller_id = 2;
|
||||
set.poller.poller_id = 2;
|
||||
row_is_null = 0;
|
||||
fake_query_kind = FAKE_QUERY_DEFAULT;
|
||||
byte_boundary_mode = 0;
|
||||
@@ -406,14 +398,14 @@ static void test_remote_push_keeps_batch_boundary_rows(void **state) {
|
||||
|
||||
assert_int_equal(host_insert_count, 2);
|
||||
assert_int_equal(item_insert_count, 2);
|
||||
assert_int_equal(count_occurrences(host_inserts[0], " (500, "), 1);
|
||||
assert_int_equal(count_occurrences(host_inserts[0], " (501, "), 0);
|
||||
assert_int_equal(count_occurrences(host_inserts[1], " (500, "), 0);
|
||||
assert_int_equal(count_occurrences(host_inserts[1], " (501, "), 1);
|
||||
assert_int_equal(count_occurrences(item_inserts[0], " (10000, "), 1);
|
||||
assert_int_equal(count_occurrences(item_inserts[0], " (10001, "), 0);
|
||||
assert_int_equal(count_occurrences(item_inserts[1], " (10000, "), 0);
|
||||
assert_int_equal(count_occurrences(item_inserts[1], " (10001, "), 1);
|
||||
assert_int_equal(count_occurrences(host_inserts[0], " ('500', "), 1);
|
||||
assert_int_equal(count_occurrences(host_inserts[0], " ('501', "), 0);
|
||||
assert_int_equal(count_occurrences(host_inserts[1], " ('500', "), 0);
|
||||
assert_int_equal(count_occurrences(host_inserts[1], " ('501', "), 1);
|
||||
assert_int_equal(count_occurrences(item_inserts[0], " ('10000', "), 1);
|
||||
assert_int_equal(count_occurrences(item_inserts[0], " ('10001', "), 0);
|
||||
assert_int_equal(count_occurrences(item_inserts[1], " ('10000', "), 0);
|
||||
assert_int_equal(count_occurrences(item_inserts[1], " ('10001', "), 1);
|
||||
assert_null(strstr(host_inserts[0], " AS rs "));
|
||||
assert_non_null(strstr(host_inserts[0], "VALUES(snmp_sysDescr)"));
|
||||
assert_null(strstr(item_inserts[0], " AS rs "));
|
||||
@@ -433,7 +425,7 @@ static void test_remote_push_flushes_wide_rows_before_overflow(void **state) {
|
||||
|
||||
(void) state;
|
||||
memset(&set, 0, sizeof(set));
|
||||
set.poller_id = 2;
|
||||
set.poller.poller_id = 2;
|
||||
row_is_null = 0;
|
||||
fake_query_kind = FAKE_QUERY_DEFAULT;
|
||||
byte_boundary_mode = 1;
|
||||
@@ -458,6 +450,33 @@ static void test_remote_push_flushes_wide_rows_before_overflow(void **state) {
|
||||
fake_query_kind = FAKE_QUERY_DEFAULT;
|
||||
}
|
||||
|
||||
/* A failed batch must end the push: the row that triggered the flush is not
|
||||
* queued for a later batch, and poller items are never attempted. */
|
||||
static void test_remote_push_stops_on_failed_batch(void **state) {
|
||||
int before;
|
||||
|
||||
(void) state;
|
||||
memset(&set, 0, sizeof(set));
|
||||
set.poller.poller_id = 2;
|
||||
row_is_null = 0;
|
||||
fake_query_kind = FAKE_QUERY_DEFAULT;
|
||||
byte_boundary_mode = 0;
|
||||
host_insert_count = 0;
|
||||
item_insert_count = 0;
|
||||
host_insert_failures = 1;
|
||||
before = frees_seen;
|
||||
|
||||
assert_false(poller_transfer_status(&fake_mysql, &fake_mysql));
|
||||
|
||||
assert_int_equal(host_insert_failures, 0);
|
||||
assert_int_equal(host_insert_count, 1);
|
||||
assert_int_equal(item_insert_count, 0);
|
||||
assert_int_equal(frees_seen - before, 1);
|
||||
assert_int_equal(count_occurrences(host_inserts[0], " ('501', "), 0);
|
||||
free(host_inserts[0]);
|
||||
host_inserts[0] = NULL;
|
||||
fake_query_kind = FAKE_QUERY_DEFAULT;
|
||||
}
|
||||
|
||||
/* ---- issue#565: spine_log() appends the newline without overrunning ---- */
|
||||
|
||||
@@ -467,11 +486,11 @@ static void route_log_to_a_file(void) {
|
||||
snprintf(log_path, sizeof(log_path), "/tmp/spine_log_test_%d.log", (int) getpid());
|
||||
unlink(log_path);
|
||||
|
||||
set.log_destination = LOGDEST_FILE;
|
||||
set.log_level = POLLER_VERBOSITY_DEBUG;
|
||||
set.logfile_processed = TRUE;
|
||||
set.poller_id = 1;
|
||||
snprintf(set.path_logfile, sizeof(set.path_logfile), "%s", log_path);
|
||||
set.logging.log_destination = LOGDEST_FILE;
|
||||
set.logging.log_level = POLLER_VERBOSITY_DEBUG;
|
||||
set.logging.logfile_processed = TRUE;
|
||||
set.poller.poller_id = 1;
|
||||
snprintf(set.logging.path_logfile, sizeof(set.logging.path_logfile), "%s", log_path);
|
||||
}
|
||||
|
||||
static char *read_log(size_t *len) {
|
||||
@@ -531,7 +550,9 @@ static void test_spine_log_survives_a_full_line(void **state) {
|
||||
out = read_log(&n);
|
||||
assert_non_null(out);
|
||||
assert_int_equal(n, LOGSIZE - 1);
|
||||
assert_int_equal(out[n - 1], 'y');
|
||||
/* The message is shortened so the record still ends its line. */
|
||||
assert_int_equal(out[n - 1], '\n');
|
||||
assert_int_equal(out[n - 2], 'y');
|
||||
|
||||
free(big);
|
||||
unlink(log_path);
|
||||
@@ -571,7 +592,7 @@ int main(void) {
|
||||
cmocka_unit_test(test_success_path_frees_once),
|
||||
cmocka_unit_test(test_remote_push_keeps_batch_boundary_rows),
|
||||
cmocka_unit_test(test_remote_push_flushes_wide_rows_before_overflow),
|
||||
cmocka_unit_test(test_push_flush_drops_suffix_overflow),
|
||||
cmocka_unit_test(test_remote_push_stops_on_failed_batch),
|
||||
cmocka_unit_test(test_spine_log_appends_a_newline),
|
||||
cmocka_unit_test(test_spine_log_survives_a_full_line),
|
||||
cmocka_unit_test(test_spine_log_does_not_double_an_existing_newline),
|
||||
|
||||
@@ -23,7 +23,7 @@ MYSQL_RES *db_query(MYSQL *mysql, int type, const char *query) { return NULL; }
|
||||
void db_free_result(MYSQL_RES *result) {}
|
||||
int db_insert(MYSQL *mysql, int type, const char *query) { return 0; }
|
||||
void db_escape(MYSQL *mysql, char *output, int max_size, const char *input) {}
|
||||
int append_hostrange(char *obuf, const char *colname) { return 0; }
|
||||
int append_hostrange(char *obuf, size_t capacity, const char *colname) { return 0; }
|
||||
int parse_logdest(const char *res, int default_dest) { return 0; }
|
||||
const char *printable_logdest(int dest) { return ""; }
|
||||
void php_close(int php_process) {}
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
extern void read_config_options(void);
|
||||
extern int read_spine_config(const char *file);
|
||||
extern void config_defaults(void);
|
||||
extern bool poller_transfer_status(MYSQL *source, MYSQL *destination);
|
||||
extern void poller_push_data_to_main(void);
|
||||
|
||||
/* cacti logging function */
|
||||
extern int spine_log(const char *format, ...)
|
||||
@@ -57,17 +59,23 @@ extern int is_hexadecimal(const char * str, const short ignore_special);
|
||||
|
||||
/* determine if a device is a debug device */
|
||||
extern int is_debug_device(int device_id);
|
||||
extern void parse_debug_devices(char *device_list, int *devices, size_t capacity);
|
||||
extern bool spine_should_log_device(int host_id, int verbosity);
|
||||
|
||||
/* string and file functions */
|
||||
extern char *add_slashes(const char *string);
|
||||
extern int file_exists(const char *filename);
|
||||
extern char *strip_alpha(char *string);
|
||||
extern char *strncopy(char *dst, const char *src, size_t n);
|
||||
extern int spine_snprintf(char *output, size_t capacity, const char *format, ...)
|
||||
__attribute__((format(printf, 3, 4)));
|
||||
extern void spine_clear_sensitive(void *buffer, size_t length);
|
||||
extern void spine_sanitize_log_message(char *message);
|
||||
extern char *trim(char *str);
|
||||
extern char *rtrim(char *str);
|
||||
extern char *ltrim(char *str);
|
||||
extern char *reverse(char *str);
|
||||
extern int strpos(const char *haystack, const char *needle);
|
||||
extern int strpos(const char *haystack, const char *needle) ;
|
||||
extern int char_count(const char *str, int chr);
|
||||
|
||||
/* convert a delimited hexadecimal value without conflating overflow with 0 */
|
||||
@@ -77,7 +85,7 @@ int hex2dec(const char *str, unsigned long long *result);
|
||||
#define MAX_MATCHES 5
|
||||
#define REGEX_NUMBER "([-+]*)([0-9]*)([.][0-9]+)"
|
||||
#define CAPABILITY_PROTOCOL_LIST_MAX 480
|
||||
const char *regex_replace(const char *exp, const char *value);
|
||||
char *regex_replace(const char *exp, char *value);
|
||||
int format_spine_capabilities(char *output, size_t output_size,
|
||||
const char *auth_protocols, const char *priv_protocols);
|
||||
|
||||
@@ -118,6 +126,19 @@ extern double start_time;
|
||||
/* the version of Cacti as a decimal */
|
||||
int get_cacti_version(MYSQL *psql, int mode);
|
||||
|
||||
extern void spine_sleep_usec(unsigned int microseconds);
|
||||
extern double spine_monotonic_time(void);
|
||||
|
||||
extern int spine_count_to_int(unsigned long long count);
|
||||
|
||||
extern int spine_wait_readable(int fd, double deadline);
|
||||
extern int spine_wait_writable(int fd, double deadline);
|
||||
|
||||
extern int spine_permits_init(spine_permits_t *permits, int count);
|
||||
extern int spine_permits_destroy(spine_permits_t *permits);
|
||||
extern int spine_permits_try_acquire(spine_permits_t *permits);
|
||||
extern int spine_permits_release(spine_permits_t *permits);
|
||||
extern int spine_permits_available(spine_permits_t *permits);
|
||||
/*! \fn int spine_appendf(char **cursor, size_t *remaining, const char *fmt, ...)
|
||||
* \brief append to a bounded buffer without walking off the end
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user