fix(results): [develop] normalize output and unify SNMP batch persistence (#641)

Classify numeric, undefined, multipart and Cacti octet results through
one parser, normalize undefined agent values before persistence and
flush every SNMP batch consistently. Validate whole non-negative
uptimes, retain unknown reindex baselines and free script results
through their owning pointer. The mocked poll_host suite checks
session-profile boundaries, spike handling, output_regex input and
completion writes.

Originally step 4/14 of the C17/native-correctness series; its preceding
steps have merged. The current base is develop.

Earlier validation, before this review follow-up, in isolated copied
snapshots on Linux arm64:

- Clean GCC build and make check: 15 passed, 1 existing prerequisite
skips, 0 failures.
- ASan/UBSan: 14 passed, 2 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.



Review follow-up (2026-10-09): Restacked on the corrected preceding
branches. The SNMP status fix is introduced in #638, the
reindex/completion fixes in #640, and focused regression coverage in
#650.

Restacked tip validation: isolated Linux arm64 build and make check
passed (18 passed, 1 setuid prerequisite skip, no failures).
GitHub checks validate the updated published commits; earlier
profile/archive/sanitizer evidence above belongs to the previous
revisions.

## Current merge dependencies

Current base: `develop`. Immediate dependency: `develop` (earlier steps
already merged). Next open PR:
[#644](https://github.com/Cacti/spine/pull/644).

Merge order: #641 → #644 → #654 → #646 → #648 → #649 → #650 → #651 →
#655 → #658 → #659.

This repository uses squash-only merges and deletes merged branches.
Record the old parent SHA, squash one PR at a time, then restack and
retarget dependents before merging the next PR. Re-run merge validation
and checks on the resulting heads; an ordinary clean merge does not
replace squash/restack validation.

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
Thomas Vincent authored and GitHub committed 2026-10-12 18:39:39 -04:00
1 parent 898d8c692c
commit 9be5f8ad2e
16 files changed
+1508 -422

No files matched your search

+1 -1
View File
@@ -132,7 +132,7 @@ jobs:
./bootstrap ./bootstrap
./configure --enable-warnings --with-snmp="$net_snmp_prefix" ./configure --enable-warnings --with-snmp="$net_snmp_prefix"
make -j"$(sysctl -n hw.ncpu)" make -j"$(sysctl -n hw.ncpu)"
make check make check || { cat test-suite.log; exit 1; }
cppcheck: cppcheck:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
+21 -1
View File
@@ -122,6 +122,7 @@ check_PROGRAMS += \
tests/unit/test_build_fixes \ tests/unit/test_build_fixes \
tests/unit/test_safety_fixes \ tests/unit/test_safety_fixes \
tests/unit/test_linked \ tests/unit/test_linked \
tests/unit/test_classify_result \
tests/unit/test_util_paths \ tests/unit/test_util_paths \
tests/unit/test_snmpv3_session \ tests/unit/test_snmpv3_session \
tests/unit/test_ping_icmp tests/unit/test_ping_icmp
@@ -133,7 +134,7 @@ noinst_PROGRAMS = tests/unit/php_test_server
tests_unit_php_test_server_SOURCES = tests/unit/php_test_server.c tests_unit_php_test_server_SOURCES = tests/unit/php_test_server.c
if HAVE_LD_WRAP if HAVE_LD_WRAP
check_PROGRAMS += tests/unit/test_php_runtime check_PROGRAMS += tests/unit/test_php_runtime tests/unit/test_poll_host
endif endif
endif endif
@@ -171,6 +172,11 @@ tests_unit_test_linked_LDFLAGS = -Wl,--wrap=waitpid
tests_unit_test_linked_CPPFLAGS += -DSPINE_TEST_WRAP_WAITPID tests_unit_test_linked_CPPFLAGS += -DSPINE_TEST_WRAP_WAITPID
endif endif
tests_unit_test_classify_result_SOURCES = tests/unit/test_classify_result.c tests/fuzz/stubs.c \
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
tests_unit_test_classify_result_LDADD = $(CMOCKA_LIBS) $(LIBS)
# includes util.c, so it needs the same libraries spine links against # includes util.c, so it needs the same libraries spine links against
tests_unit_test_util_paths_SOURCES = tests/unit/test_util_paths.c tests_unit_test_util_paths_SOURCES = tests/unit/test_util_paths.c
tests_unit_test_util_paths_LDADD = $(CMOCKA_LIBS) $(LIBS) tests_unit_test_util_paths_LDADD = $(CMOCKA_LIBS) $(LIBS)
@@ -185,6 +191,20 @@ tests_unit_test_ping_icmp_SOURCES = tests/unit/test_ping_icmp.c tests/fuzz/stubs
src/core/keywords.c error.c src/core/keywords.c error.c
tests_unit_test_ping_icmp_LDADD = $(CMOCKA_LIBS) $(LIBS) tests_unit_test_ping_icmp_LDADD = $(CMOCKA_LIBS) $(LIBS)
# poll_host() against an in-memory database and SNMP agent: the wrapped
# calls are the ones poller.o makes into sql.o, snmp.o, ping.o and libmysql.
tests_unit_test_poll_host_SOURCES = tests/unit/test_poll_host.c tests/fuzz/stubs.c \
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
tests_unit_test_poll_host_LDADD = $(CMOCKA_LIBS) $(LIBS)
tests_unit_test_poll_host_LDFLAGS = \
-Wl,--wrap=db_query -Wl,--wrap=db_insert -Wl,--wrap=db_escape \
-Wl,--wrap=db_get_connection -Wl,--wrap=db_release_connection \
-Wl,--wrap=db_free_result -Wl,--wrap=mysql_num_rows -Wl,--wrap=mysql_fetch_row \
-Wl,--wrap=snmp_host_init -Wl,--wrap=snmp_host_cleanup -Wl,--wrap=snmp_get_base \
-Wl,--wrap=snmp_get -Wl,--wrap=snmp_get_allow_fail -Wl,--wrap=snmp_get_multi \
-Wl,--wrap=ping_host
tests_unit_test_php_runtime_SOURCES = tests/unit/test_php_runtime.c tests/fuzz/stubs.c \ tests_unit_test_php_runtime_SOURCES = tests/unit/test_php_runtime.c tests/fuzz/stubs.c \
sql.c util.c snmp.c src/core/locks.c poller.c nft_popen.c php.c ping.c \ 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 src/core/keywords.c error.c
+92 -6
View File
@@ -120,13 +120,14 @@ check_PROGRAMS = test_spine_regressions$(EXEEXT) $(am__EXEEXT_1) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_build_fixes \ @HAVE_CMOCKA_TRUE@ tests/unit/test_build_fixes \
@HAVE_CMOCKA_TRUE@ tests/unit/test_safety_fixes \ @HAVE_CMOCKA_TRUE@ tests/unit/test_safety_fixes \
@HAVE_CMOCKA_TRUE@ tests/unit/test_linked \ @HAVE_CMOCKA_TRUE@ tests/unit/test_linked \
@HAVE_CMOCKA_TRUE@ tests/unit/test_classify_result \
@HAVE_CMOCKA_TRUE@ tests/unit/test_util_paths \ @HAVE_CMOCKA_TRUE@ tests/unit/test_util_paths \
@HAVE_CMOCKA_TRUE@ tests/unit/test_snmpv3_session \ @HAVE_CMOCKA_TRUE@ tests/unit/test_snmpv3_session \
@HAVE_CMOCKA_TRUE@ tests/unit/test_ping_icmp @HAVE_CMOCKA_TRUE@ tests/unit/test_ping_icmp
@HAVE_CMOCKA_TRUE@noinst_PROGRAMS = \ @HAVE_CMOCKA_TRUE@noinst_PROGRAMS = \
@HAVE_CMOCKA_TRUE@ tests/unit/php_test_server$(EXEEXT) @HAVE_CMOCKA_TRUE@ tests/unit/php_test_server$(EXEEXT)
@HAVE_CMOCKA_TRUE@@HAVE_LD_WRAP_TRUE@am__append_3 = tests/unit/test_php_runtime @HAVE_CMOCKA_TRUE@@HAVE_LD_WRAP_TRUE@am__append_3 = tests/unit/test_php_runtime tests/unit/test_poll_host
@HAVE_LD_WRAP_TRUE@am__append_4 = -DSPINE_TEST_WRAP_WAITPID @HAVE_LD_WRAP_TRUE@am__append_4 = -DSPINE_TEST_WRAP_WAITPID
subdir = . subdir = .
ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
@@ -153,10 +154,12 @@ am__installdirs = "$(DESTDIR)$(bindir)" "$(DESTDIR)$(man1dir)" \
@HAVE_CMOCKA_TRUE@ tests/unit/test_build_fixes$(EXEEXT) \ @HAVE_CMOCKA_TRUE@ tests/unit/test_build_fixes$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_safety_fixes$(EXEEXT) \ @HAVE_CMOCKA_TRUE@ tests/unit/test_safety_fixes$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_linked$(EXEEXT) \ @HAVE_CMOCKA_TRUE@ tests/unit/test_linked$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_classify_result$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_util_paths$(EXEEXT) \ @HAVE_CMOCKA_TRUE@ tests/unit/test_util_paths$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_snmpv3_session$(EXEEXT) \ @HAVE_CMOCKA_TRUE@ tests/unit/test_snmpv3_session$(EXEEXT) \
@HAVE_CMOCKA_TRUE@ tests/unit/test_ping_icmp$(EXEEXT) @HAVE_CMOCKA_TRUE@ tests/unit/test_ping_icmp$(EXEEXT)
@HAVE_CMOCKA_TRUE@@HAVE_LD_WRAP_TRUE@am__EXEEXT_3 = tests/unit/test_php_runtime$(EXEEXT) @HAVE_CMOCKA_TRUE@@HAVE_LD_WRAP_TRUE@am__EXEEXT_3 = tests/unit/test_php_runtime$(EXEEXT) \
@HAVE_CMOCKA_TRUE@@HAVE_LD_WRAP_TRUE@ tests/unit/test_poll_host$(EXEEXT)
PROGRAMS = $(bin_PROGRAMS) $(noinst_PROGRAMS) PROGRAMS = $(bin_PROGRAMS) $(noinst_PROGRAMS)
am__dirstamp = $(am__leading_dot)dirstamp am__dirstamp = $(am__leading_dot)dirstamp
am__objects_1 = sql.$(OBJEXT) util.$(OBJEXT) snmp.$(OBJEXT) \ am__objects_1 = sql.$(OBJEXT) util.$(OBJEXT) snmp.$(OBJEXT) \
@@ -230,6 +233,16 @@ tests_unit_test_build_fixes_OBJECTS = \
$(am_tests_unit_test_build_fixes_OBJECTS) $(am_tests_unit_test_build_fixes_OBJECTS)
am__DEPENDENCIES_1 = am__DEPENDENCIES_1 =
tests_unit_test_build_fixes_DEPENDENCIES = $(am__DEPENDENCIES_1) tests_unit_test_build_fixes_DEPENDENCIES = $(am__DEPENDENCIES_1)
am_tests_unit_test_classify_result_OBJECTS = \
tests/unit/test_classify_result.$(OBJEXT) \
tests/fuzz/stubs.$(OBJEXT) sql.$(OBJEXT) util.$(OBJEXT) \
snmp.$(OBJEXT) src/core/locks.$(OBJEXT) poller.$(OBJEXT) \
nft_popen.$(OBJEXT) php.$(OBJEXT) ping.$(OBJEXT) \
src/core/keywords.$(OBJEXT) error.$(OBJEXT)
tests_unit_test_classify_result_OBJECTS = \
$(am_tests_unit_test_classify_result_OBJECTS)
tests_unit_test_classify_result_DEPENDENCIES = $(am__DEPENDENCIES_1) \
$(am__DEPENDENCIES_1)
am_tests_unit_test_linked_OBJECTS = \ am_tests_unit_test_linked_OBJECTS = \
tests/unit/test_linked-test_linked.$(OBJEXT) \ tests/unit/test_linked-test_linked.$(OBJEXT) \
tests/fuzz/unit_test_linked-stubs.$(OBJEXT) \ tests/fuzz/unit_test_linked-stubs.$(OBJEXT) \
@@ -278,6 +291,20 @@ tests_unit_test_ping_icmp_OBJECTS = \
$(am_tests_unit_test_ping_icmp_OBJECTS) $(am_tests_unit_test_ping_icmp_OBJECTS)
tests_unit_test_ping_icmp_DEPENDENCIES = $(am__DEPENDENCIES_1) \ tests_unit_test_ping_icmp_DEPENDENCIES = $(am__DEPENDENCIES_1) \
$(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1)
am_tests_unit_test_poll_host_OBJECTS = \
tests/unit/test_poll_host.$(OBJEXT) tests/fuzz/stubs.$(OBJEXT) \
sql.$(OBJEXT) util.$(OBJEXT) snmp.$(OBJEXT) \
src/core/locks.$(OBJEXT) poller.$(OBJEXT) nft_popen.$(OBJEXT) \
php.$(OBJEXT) ping.$(OBJEXT) src/core/keywords.$(OBJEXT) \
error.$(OBJEXT)
tests_unit_test_poll_host_OBJECTS = \
$(am_tests_unit_test_poll_host_OBJECTS)
tests_unit_test_poll_host_DEPENDENCIES = $(am__DEPENDENCIES_1) \
$(am__DEPENDENCIES_1)
tests_unit_test_poll_host_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \
$(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \
$(AM_CFLAGS) $(CFLAGS) $(tests_unit_test_poll_host_LDFLAGS) \
$(LDFLAGS) -o $@
am_tests_unit_test_safety_fixes_OBJECTS = \ am_tests_unit_test_safety_fixes_OBJECTS = \
tests/unit/test_safety_fixes.$(OBJEXT) tests/unit/test_safety_fixes.$(OBJEXT)
tests_unit_test_safety_fixes_OBJECTS = \ tests_unit_test_safety_fixes_OBJECTS = \
@@ -381,9 +408,11 @@ am__depfiles_remade = ./$(DEPDIR)/error.Po ./$(DEPDIR)/nft_popen.Po \
tests/fuzz/$(DEPDIR)/unit_test_php_runtime-stubs.Po \ tests/fuzz/$(DEPDIR)/unit_test_php_runtime-stubs.Po \
tests/unit/$(DEPDIR)/php_test_server.Po \ tests/unit/$(DEPDIR)/php_test_server.Po \
tests/unit/$(DEPDIR)/test_build_fixes.Po \ tests/unit/$(DEPDIR)/test_build_fixes.Po \
tests/unit/$(DEPDIR)/test_classify_result.Po \
tests/unit/$(DEPDIR)/test_linked-test_linked.Po \ tests/unit/$(DEPDIR)/test_linked-test_linked.Po \
tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po \ tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po \
tests/unit/$(DEPDIR)/test_ping_icmp.Po \ tests/unit/$(DEPDIR)/test_ping_icmp.Po \
tests/unit/$(DEPDIR)/test_poll_host.Po \
tests/unit/$(DEPDIR)/test_safety_fixes.Po \ tests/unit/$(DEPDIR)/test_safety_fixes.Po \
tests/unit/$(DEPDIR)/test_snmpv3_session.Po \ tests/unit/$(DEPDIR)/test_snmpv3_session.Po \
tests/unit/$(DEPDIR)/test_util_paths.Po \ tests/unit/$(DEPDIR)/test_util_paths.Po \
@@ -411,9 +440,11 @@ SOURCES = $(spine_SOURCES) $(test_spine_faults_SOURCES) \
$(test_spine_regressions_SOURCES) \ $(test_spine_regressions_SOURCES) \
$(tests_unit_php_test_server_SOURCES) \ $(tests_unit_php_test_server_SOURCES) \
$(tests_unit_test_build_fixes_SOURCES) \ $(tests_unit_test_build_fixes_SOURCES) \
$(tests_unit_test_classify_result_SOURCES) \
$(tests_unit_test_linked_SOURCES) \ $(tests_unit_test_linked_SOURCES) \
$(tests_unit_test_php_runtime_SOURCES) \ $(tests_unit_test_php_runtime_SOURCES) \
$(tests_unit_test_ping_icmp_SOURCES) \ $(tests_unit_test_ping_icmp_SOURCES) \
$(tests_unit_test_poll_host_SOURCES) \
$(tests_unit_test_safety_fixes_SOURCES) \ $(tests_unit_test_safety_fixes_SOURCES) \
$(tests_unit_test_snmpv3_session_SOURCES) \ $(tests_unit_test_snmpv3_session_SOURCES) \
$(tests_unit_test_util_paths_SOURCES) \ $(tests_unit_test_util_paths_SOURCES) \
@@ -422,9 +453,11 @@ DIST_SOURCES = $(spine_SOURCES) $(am__test_spine_faults_SOURCES_DIST) \
$(test_spine_regressions_SOURCES) \ $(test_spine_regressions_SOURCES) \
$(am__tests_unit_php_test_server_SOURCES_DIST) \ $(am__tests_unit_php_test_server_SOURCES_DIST) \
$(tests_unit_test_build_fixes_SOURCES) \ $(tests_unit_test_build_fixes_SOURCES) \
$(tests_unit_test_classify_result_SOURCES) \
$(tests_unit_test_linked_SOURCES) \ $(tests_unit_test_linked_SOURCES) \
$(tests_unit_test_php_runtime_SOURCES) \ $(tests_unit_test_php_runtime_SOURCES) \
$(tests_unit_test_ping_icmp_SOURCES) \ $(tests_unit_test_ping_icmp_SOURCES) \
$(tests_unit_test_poll_host_SOURCES) \
$(tests_unit_test_safety_fixes_SOURCES) \ $(tests_unit_test_safety_fixes_SOURCES) \
$(tests_unit_test_snmpv3_session_SOURCES) \ $(tests_unit_test_snmpv3_session_SOURCES) \
$(tests_unit_test_util_paths_SOURCES) \ $(tests_unit_test_util_paths_SOURCES) \
@@ -895,6 +928,11 @@ tests_unit_test_linked_LDADD = $(CMOCKA_LIBS) $(LIBS)
# needs to find locks.h there. # needs to find locks.h there.
tests_unit_test_linked_CPPFLAGS = $(AM_CPPFLAGS) $(am__append_4) tests_unit_test_linked_CPPFLAGS = $(AM_CPPFLAGS) $(am__append_4)
@HAVE_LD_WRAP_TRUE@tests_unit_test_linked_LDFLAGS = -Wl,--wrap=waitpid @HAVE_LD_WRAP_TRUE@tests_unit_test_linked_LDFLAGS = -Wl,--wrap=waitpid
tests_unit_test_classify_result_SOURCES = tests/unit/test_classify_result.c tests/fuzz/stubs.c \
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
tests_unit_test_classify_result_LDADD = $(CMOCKA_LIBS) $(LIBS)
# includes util.c, so it needs the same libraries spine links against # includes util.c, so it needs the same libraries spine links against
tests_unit_test_util_paths_SOURCES = tests/unit/test_util_paths.c tests_unit_test_util_paths_SOURCES = tests/unit/test_util_paths.c
@@ -909,6 +947,22 @@ tests_unit_test_ping_icmp_SOURCES = tests/unit/test_ping_icmp.c tests/fuzz/stubs
src/core/keywords.c error.c src/core/keywords.c error.c
tests_unit_test_ping_icmp_LDADD = $(CMOCKA_LIBS) $(LIBS) tests_unit_test_ping_icmp_LDADD = $(CMOCKA_LIBS) $(LIBS)
# poll_host() against an in-memory database and SNMP agent: the wrapped
# calls are the ones poller.o makes into sql.o, snmp.o, ping.o and libmysql.
tests_unit_test_poll_host_SOURCES = tests/unit/test_poll_host.c tests/fuzz/stubs.c \
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
tests_unit_test_poll_host_LDADD = $(CMOCKA_LIBS) $(LIBS)
tests_unit_test_poll_host_LDFLAGS = \
-Wl,--wrap=db_query -Wl,--wrap=db_insert -Wl,--wrap=db_escape \
-Wl,--wrap=db_get_connection -Wl,--wrap=db_release_connection \
-Wl,--wrap=db_free_result -Wl,--wrap=mysql_num_rows -Wl,--wrap=mysql_fetch_row \
-Wl,--wrap=snmp_host_init -Wl,--wrap=snmp_host_cleanup -Wl,--wrap=snmp_get_base \
-Wl,--wrap=snmp_get -Wl,--wrap=snmp_get_allow_fail -Wl,--wrap=snmp_get_multi \
-Wl,--wrap=ping_host
tests_unit_test_php_runtime_SOURCES = tests/unit/test_php_runtime.c tests/fuzz/stubs.c \ tests_unit_test_php_runtime_SOURCES = tests/unit/test_php_runtime.c tests/fuzz/stubs.c \
sql.c util.c snmp.c src/core/locks.c poller.c nft_popen.c php.c ping.c \ 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 src/core/keywords.c error.c
@@ -1120,8 +1174,7 @@ tests/unit/test_build_fixes.$(OBJEXT): tests/unit/$(am__dirstamp) \
tests/unit/test_build_fixes$(EXEEXT): $(tests_unit_test_build_fixes_OBJECTS) $(tests_unit_test_build_fixes_DEPENDENCIES) $(EXTRA_tests_unit_test_build_fixes_DEPENDENCIES) tests/unit/$(am__dirstamp) tests/unit/test_build_fixes$(EXEEXT): $(tests_unit_test_build_fixes_OBJECTS) $(tests_unit_test_build_fixes_DEPENDENCIES) $(EXTRA_tests_unit_test_build_fixes_DEPENDENCIES) tests/unit/$(am__dirstamp)
@rm -f tests/unit/test_build_fixes$(EXEEXT) @rm -f tests/unit/test_build_fixes$(EXEEXT)
$(AM_V_CCLD)$(LINK) $(tests_unit_test_build_fixes_OBJECTS) $(tests_unit_test_build_fixes_LDADD) $(LIBS) $(AM_V_CCLD)$(LINK) $(tests_unit_test_build_fixes_OBJECTS) $(tests_unit_test_build_fixes_LDADD) $(LIBS)
tests/unit/test_linked-test_linked.$(OBJEXT): \ tests/unit/test_classify_result.$(OBJEXT): tests/unit/$(am__dirstamp) \
tests/unit/$(am__dirstamp) \
tests/unit/$(DEPDIR)/$(am__dirstamp) tests/unit/$(DEPDIR)/$(am__dirstamp)
tests/fuzz/$(am__dirstamp): tests/fuzz/$(am__dirstamp):
@$(MKDIR_P) tests/fuzz @$(MKDIR_P) tests/fuzz
@@ -1129,6 +1182,15 @@ tests/fuzz/$(am__dirstamp):
tests/fuzz/$(DEPDIR)/$(am__dirstamp): tests/fuzz/$(DEPDIR)/$(am__dirstamp):
@$(MKDIR_P) tests/fuzz/$(DEPDIR) @$(MKDIR_P) tests/fuzz/$(DEPDIR)
@: > tests/fuzz/$(DEPDIR)/$(am__dirstamp) @: > tests/fuzz/$(DEPDIR)/$(am__dirstamp)
tests/fuzz/stubs.$(OBJEXT): tests/fuzz/$(am__dirstamp) \
tests/fuzz/$(DEPDIR)/$(am__dirstamp)
tests/unit/test_classify_result$(EXEEXT): $(tests_unit_test_classify_result_OBJECTS) $(tests_unit_test_classify_result_DEPENDENCIES) $(EXTRA_tests_unit_test_classify_result_DEPENDENCIES) tests/unit/$(am__dirstamp)
@rm -f tests/unit/test_classify_result$(EXEEXT)
$(AM_V_CCLD)$(LINK) $(tests_unit_test_classify_result_OBJECTS) $(tests_unit_test_classify_result_LDADD) $(LIBS)
tests/unit/test_linked-test_linked.$(OBJEXT): \
tests/unit/$(am__dirstamp) \
tests/unit/$(DEPDIR)/$(am__dirstamp)
tests/fuzz/unit_test_linked-stubs.$(OBJEXT): \ tests/fuzz/unit_test_linked-stubs.$(OBJEXT): \
tests/fuzz/$(am__dirstamp) \ tests/fuzz/$(am__dirstamp) \
tests/fuzz/$(DEPDIR)/$(am__dirstamp) tests/fuzz/$(DEPDIR)/$(am__dirstamp)
@@ -1156,12 +1218,16 @@ tests/unit/test_php_runtime$(EXEEXT): $(tests_unit_test_php_runtime_OBJECTS) $(t
$(AM_V_CCLD)$(tests_unit_test_php_runtime_LINK) $(tests_unit_test_php_runtime_OBJECTS) $(tests_unit_test_php_runtime_LDADD) $(LIBS) $(AM_V_CCLD)$(tests_unit_test_php_runtime_LINK) $(tests_unit_test_php_runtime_OBJECTS) $(tests_unit_test_php_runtime_LDADD) $(LIBS)
tests/unit/test_ping_icmp.$(OBJEXT): tests/unit/$(am__dirstamp) \ tests/unit/test_ping_icmp.$(OBJEXT): tests/unit/$(am__dirstamp) \
tests/unit/$(DEPDIR)/$(am__dirstamp) tests/unit/$(DEPDIR)/$(am__dirstamp)
tests/fuzz/stubs.$(OBJEXT): tests/fuzz/$(am__dirstamp) \
tests/fuzz/$(DEPDIR)/$(am__dirstamp)
tests/unit/test_ping_icmp$(EXEEXT): $(tests_unit_test_ping_icmp_OBJECTS) $(tests_unit_test_ping_icmp_DEPENDENCIES) $(EXTRA_tests_unit_test_ping_icmp_DEPENDENCIES) tests/unit/$(am__dirstamp) tests/unit/test_ping_icmp$(EXEEXT): $(tests_unit_test_ping_icmp_OBJECTS) $(tests_unit_test_ping_icmp_DEPENDENCIES) $(EXTRA_tests_unit_test_ping_icmp_DEPENDENCIES) tests/unit/$(am__dirstamp)
@rm -f tests/unit/test_ping_icmp$(EXEEXT) @rm -f tests/unit/test_ping_icmp$(EXEEXT)
$(AM_V_CCLD)$(LINK) $(tests_unit_test_ping_icmp_OBJECTS) $(tests_unit_test_ping_icmp_LDADD) $(LIBS) $(AM_V_CCLD)$(LINK) $(tests_unit_test_ping_icmp_OBJECTS) $(tests_unit_test_ping_icmp_LDADD) $(LIBS)
tests/unit/test_poll_host.$(OBJEXT): tests/unit/$(am__dirstamp) \
tests/unit/$(DEPDIR)/$(am__dirstamp)
tests/unit/test_poll_host$(EXEEXT): $(tests_unit_test_poll_host_OBJECTS) $(tests_unit_test_poll_host_DEPENDENCIES) $(EXTRA_tests_unit_test_poll_host_DEPENDENCIES) tests/unit/$(am__dirstamp)
@rm -f tests/unit/test_poll_host$(EXEEXT)
$(AM_V_CCLD)$(tests_unit_test_poll_host_LINK) $(tests_unit_test_poll_host_OBJECTS) $(tests_unit_test_poll_host_LDADD) $(LIBS)
tests/unit/test_safety_fixes.$(OBJEXT): tests/unit/$(am__dirstamp) \ tests/unit/test_safety_fixes.$(OBJEXT): tests/unit/$(am__dirstamp) \
tests/unit/$(DEPDIR)/$(am__dirstamp) tests/unit/$(DEPDIR)/$(am__dirstamp)
@@ -1264,9 +1330,11 @@ distclean-compile:
@AMDEP_TRUE@@am__include@ @am__quote@tests/fuzz/$(DEPDIR)/unit_test_php_runtime-stubs.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@tests/fuzz/$(DEPDIR)/unit_test_php_runtime-stubs.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/php_test_server.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/php_test_server.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_build_fixes.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_build_fixes.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_classify_result.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_linked-test_linked.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_linked-test_linked.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_ping_icmp.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_ping_icmp.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_poll_host.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_safety_fixes.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_safety_fixes.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_snmpv3_session.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_snmpv3_session.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_util_paths.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@tests/unit/$(DEPDIR)/test_util_paths.Po@am__quote@ # am--include-marker
@@ -2388,6 +2456,13 @@ tests/unit/test_linked.log: tests/unit/test_linked$(EXEEXT)
--log-file $$b.log --trs-file $$b.trs \ --log-file $$b.log --trs-file $$b.trs \
$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
"$$tst" $(AM_TESTS_FD_REDIRECT) "$$tst" $(AM_TESTS_FD_REDIRECT)
tests/unit/test_classify_result.log: tests/unit/test_classify_result$(EXEEXT)
@p='tests/unit/test_classify_result$(EXEEXT)'; \
b='tests/unit/test_classify_result'; \
$(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
--log-file $$b.log --trs-file $$b.trs \
$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
"$$tst" $(AM_TESTS_FD_REDIRECT)
tests/unit/test_util_paths.log: tests/unit/test_util_paths$(EXEEXT) tests/unit/test_util_paths.log: tests/unit/test_util_paths$(EXEEXT)
@p='tests/unit/test_util_paths$(EXEEXT)'; \ @p='tests/unit/test_util_paths$(EXEEXT)'; \
b='tests/unit/test_util_paths'; \ b='tests/unit/test_util_paths'; \
@@ -2416,6 +2491,13 @@ tests/unit/test_php_runtime.log: tests/unit/test_php_runtime$(EXEEXT)
--log-file $$b.log --trs-file $$b.trs \ --log-file $$b.log --trs-file $$b.trs \
$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
"$$tst" $(AM_TESTS_FD_REDIRECT) "$$tst" $(AM_TESTS_FD_REDIRECT)
tests/unit/test_poll_host.log: tests/unit/test_poll_host$(EXEEXT)
@p='tests/unit/test_poll_host$(EXEEXT)'; \
b='tests/unit/test_poll_host'; \
$(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \
--log-file $$b.log --trs-file $$b.trs \
$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
"$$tst" $(AM_TESTS_FD_REDIRECT)
.sh.log: .sh.log:
@p='$<'; \ @p='$<'; \
$(am__set_b); \ $(am__set_b); \
@@ -2727,9 +2809,11 @@ distclean: distclean-am
-rm -f tests/fuzz/$(DEPDIR)/unit_test_php_runtime-stubs.Po -rm -f tests/fuzz/$(DEPDIR)/unit_test_php_runtime-stubs.Po
-rm -f tests/unit/$(DEPDIR)/php_test_server.Po -rm -f tests/unit/$(DEPDIR)/php_test_server.Po
-rm -f tests/unit/$(DEPDIR)/test_build_fixes.Po -rm -f tests/unit/$(DEPDIR)/test_build_fixes.Po
-rm -f tests/unit/$(DEPDIR)/test_classify_result.Po
-rm -f tests/unit/$(DEPDIR)/test_linked-test_linked.Po -rm -f tests/unit/$(DEPDIR)/test_linked-test_linked.Po
-rm -f tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po -rm -f tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po
-rm -f tests/unit/$(DEPDIR)/test_ping_icmp.Po -rm -f tests/unit/$(DEPDIR)/test_ping_icmp.Po
-rm -f tests/unit/$(DEPDIR)/test_poll_host.Po
-rm -f tests/unit/$(DEPDIR)/test_safety_fixes.Po -rm -f tests/unit/$(DEPDIR)/test_safety_fixes.Po
-rm -f tests/unit/$(DEPDIR)/test_snmpv3_session.Po -rm -f tests/unit/$(DEPDIR)/test_snmpv3_session.Po
-rm -f tests/unit/$(DEPDIR)/test_util_paths.Po -rm -f tests/unit/$(DEPDIR)/test_util_paths.Po
@@ -2849,9 +2933,11 @@ maintainer-clean: maintainer-clean-am
-rm -f tests/fuzz/$(DEPDIR)/unit_test_php_runtime-stubs.Po -rm -f tests/fuzz/$(DEPDIR)/unit_test_php_runtime-stubs.Po
-rm -f tests/unit/$(DEPDIR)/php_test_server.Po -rm -f tests/unit/$(DEPDIR)/php_test_server.Po
-rm -f tests/unit/$(DEPDIR)/test_build_fixes.Po -rm -f tests/unit/$(DEPDIR)/test_build_fixes.Po
-rm -f tests/unit/$(DEPDIR)/test_classify_result.Po
-rm -f tests/unit/$(DEPDIR)/test_linked-test_linked.Po -rm -f tests/unit/$(DEPDIR)/test_linked-test_linked.Po
-rm -f tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po -rm -f tests/unit/$(DEPDIR)/test_php_runtime-test_php_runtime.Po
-rm -f tests/unit/$(DEPDIR)/test_ping_icmp.Po -rm -f tests/unit/$(DEPDIR)/test_ping_icmp.Po
-rm -f tests/unit/$(DEPDIR)/test_poll_host.Po
-rm -f tests/unit/$(DEPDIR)/test_safety_fixes.Po -rm -f tests/unit/$(DEPDIR)/test_safety_fixes.Po
-rm -f tests/unit/$(DEPDIR)/test_snmpv3_session.Po -rm -f tests/unit/$(DEPDIR)/test_snmpv3_session.Po
-rm -f tests/unit/$(DEPDIR)/test_util_paths.Po -rm -f tests/unit/$(DEPDIR)/test_util_paths.Po
+91 -158
View File
@@ -135,62 +135,19 @@ void *child(void *arg) {
* This function is core to Spine. It takes a host_id and polls it, first * This function is core to Spine. It takes a host_id and polls it, first
* checking reachability and any required data-query reindexing. * checking reachability and any required data-query reindexing.
*/ */
/*! \fn int poller_store_hex_result(char *result, size_t result_size, const char *hex, int *errors)
* \brief convert a hexadecimal poll result and account for rejected values
*
* result must name at least two writable bytes so failures can be represented
* by the normal undefined marker. Invalid output arguments are rejected
* without modifying memory whose writable extent cannot be established.
*/
int poller_store_hex_result(char *result, size_t result_size, const char *hex, int *errors) {
unsigned long long value;
int written;
if (result == NULL || result_size < 2 || hex == NULL) {
if (errors != NULL) {
(*errors)++;
}
return FALSE;
}
if (!hex2dec(hex, &value)) {
/* An over-wide or malformed OctetString is unusable poll data. */
SET_UNDEFINED(result);
if (errors != NULL) {
(*errors)++;
}
return FALSE;
}
written = snprintf(result, result_size, "%llu", value);
if (written < 0 || (size_t) written >= result_size) {
SET_UNDEFINED(result);
if (errors != NULL) {
(*errors)++;
}
return FALSE;
}
return TRUE;
}
enum poll_result_status normalize_poll_result(char *result, bool snmp) { enum poll_result_status normalize_poll_result(char *result, bool snmp) {
classified_result_t classified;
bool unknown_marker;
if (IS_UNDEFINED(result)) return POLL_RESULT_UNDEFINED; if (IS_UNDEFINED(result)) return POLL_RESULT_UNDEFINED;
if (is_numeric(result) || is_multipart_output(snmp ? result : trim(result))) { unknown_marker = snmp && (STRIMATCH(result, "U") || STRIMATCH(result, "Nan"));
if (classify_result(result, &classified) != RESULT_UNKNOWN) {
strncopy(result, classified.text, RESULTS_BUFFER);
return POLL_RESULT_VALID; return POLL_RESULT_VALID;
} }
if (is_hexadecimal(result, TRUE)) { /* An agent's "u" or "NaN" is still unknown; store the U RRDtool expects. */
/* Values wider than 64 bits become U and count as collection errors. */ SET_UNDEFINED(result);
return poller_store_hex_result(result, RESULTS_BUFFER, result, NULL) ? POLL_RESULT_VALID : POLL_RESULT_INVALID; return unknown_marker ? POLL_RESULT_UNDEFINED : POLL_RESULT_INVALID;
}
if (snmp && (STRIMATCH(result, "U") || STRIMATCH(result, "Nan"))) {
return POLL_RESULT_UNDEFINED;
}
/* trim a non-numeric prefix or suffix, then validate below */
char normalized[RESULTS_BUFFER];
snprintf(normalized, sizeof(normalized), "%s", strip_alpha(result));
strncopy(result, normalized, RESULTS_BUFFER);
return validate_result(result) ? POLL_RESULT_VALID : POLL_RESULT_INVALID;
} }
typedef struct poll_error_context { typedef struct poll_error_context {
@@ -609,7 +566,11 @@ static bool evaluate_reindex_assertion(const reindex_evaluation_t *evaluation, c
/* Advancing the stored value without a queued reindex would lose the /* Advancing the stored value without a queued reindex would lose the
* reindex for good: the next poll compares against the new value. */ * reindex for good: the next poll compares against the new value. */
bool queued = !failed || queue_reindex(evaluation, reindex); bool queued = !failed || queue_reindex(evaluation, reindex);
if (queued && (failed || STRMATCH(reindex->op, ">") || STRMATCH(reindex->op, "<"))) update_reindex_value(evaluation, reindex, value); /* Keep the last good baseline. U compares as 0, so storing it would hide
* the next reboot under "<" and fire a false reindex under ">". */
if (queued && !unavailable && (failed || STRMATCH(reindex->op, ">") || STRMATCH(reindex->op, "<"))) {
update_reindex_value(evaluation, reindex, value);
}
/* A failed uptime assertion means the counters reset, so the sample is a spike. */ /* A failed uptime assertion means the counters reset, so the sample is a spike. */
if (failed && (STRMATCH(reindex->op, "<") || if (failed && (STRMATCH(reindex->op, "<") ||
STRMATCH(reindex->arg1, ".1.3.6.1.2.1.1.3.0") || STRMATCH(reindex->arg1, ".1.3.6.1.2.1.1.3.0") ||
@@ -619,6 +580,25 @@ static bool evaluate_reindex_assertion(const reindex_evaluation_t *evaluation, c
return failed; return failed;
} }
/* An uptime is a non-negative integer; anything else cannot be compared. */
static bool parse_uptime(const char *text, unsigned long long *ticks) {
unsigned long long value = 0;
unsigned int digit;
if (text == NULL || ticks == NULL) return FALSE;
while (isspace((unsigned char)*text)) text++;
if (*text < '0' || *text > '9') return FALSE;
while (*text >= '0' && *text <= '9') {
digit = (unsigned int)(*text++ - '0');
if (value > (ULLONG_MAX - digit) / 10) return FALSE;
value = value * 10 + digit;
}
while (isspace((unsigned char)*text)) text++;
if (*text != '\0') return FALSE;
*ticks = value;
return TRUE;
}
static char *poll_reindex_snmp(host_t *host, reindex_t *reindex, int host_thread, char *sysUptime, bool *unavailable) { static char *poll_reindex_snmp(host_t *host, reindex_t *reindex, int host_thread, char *sysUptime, bool *unavailable) {
if (host->snmp.session == NULL) { if (host->snmp.session == NULL) {
*unavailable = TRUE; *unavailable = TRUE;
@@ -638,6 +618,9 @@ static char *poll_reindex_snmp(host_t *host, reindex_t *reindex, int host_thread
snprintf(poll_result, BUFSIZE, "%s", sysUptime); snprintf(poll_result, BUFSIZE, "%s", sysUptime);
} else if (strstr(reindex->arg1, ".1.3.6.1.2.1.1.3.0") || } else if (strstr(reindex->arg1, ".1.3.6.1.2.1.1.3.0") ||
strstr(reindex->arg1, ".1.3.6.1.6.3.10.2.1.3.0")) { strstr(reindex->arg1, ".1.3.6.1.6.3.10.2.1.3.0")) {
unsigned long long ticks;
bool uptime_use_engine_oid;
// Ensure uptime is empty to start with // Ensure uptime is empty to start with
sysUptime[0] = '\0'; sysUptime[0] = '\0';
@@ -649,10 +632,10 @@ static char *poll_reindex_snmp(host_t *host, reindex_t *reindex, int host_thread
engine OID is not present with numeric data. */ engine OID is not present with numeric data. */
poll_result = snmp_get_base(host, ".1.3.6.1.6.3.10.2.1.3.0", false); poll_result = snmp_get_base(host, ".1.3.6.1.6.3.10.2.1.3.0", false);
bool uptime_use_engine_oid = (poll_result != NULL && is_numeric(poll_result)); uptime_use_engine_oid = parse_uptime(poll_result, &ticks) && ticks <= ULLONG_MAX / 100;
if (uptime_use_engine_oid) { if (uptime_use_engine_oid) {
snprintf(sysUptime, BUFSIZE, "%lld", atoll(poll_result) * 100); snprintf(sysUptime, BUFSIZE, "%llu", ticks * 100);
} }
SPINE_FREE(poll_result); SPINE_FREE(poll_result);
@@ -663,23 +646,25 @@ static char *poll_reindex_snmp(host_t *host, reindex_t *reindex, int host_thread
// Engine OID unavailable, fall back to the legacy sysUpTime OID // Engine OID unavailable, fall back to the legacy sysUpTime OID
poll_result = snmp_get(host, ".1.3.6.1.2.1.1.3.0"); poll_result = snmp_get(host, ".1.3.6.1.2.1.1.3.0");
if (poll_result && is_numeric(poll_result)) { if (parse_uptime(poll_result, &ticks)) {
snprintf(sysUptime, BUFSIZE, "%s", poll_result); snprintf(sysUptime, BUFSIZE, "%llu", ticks);
} }
SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] HT[%i] DQ[%i] Legacy Uptime Result: %s, Is Numeric: %d", host->id, host_thread, reindex->data_query_id, poll_result != NULL ? poll_result : "U", is_numeric(poll_result) )); SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] HT[%i] DQ[%i] Legacy Uptime Result: %s, Is Numeric: %d", host->id, host_thread, reindex->data_query_id, poll_result != NULL ? poll_result : "U", sysUptime[0] != '\0'));
SPINE_FREE(poll_result); SPINE_FREE(poll_result);
} }
// Use the primed uptime to repopulate the poll_result // Use the primed uptime to repopulate the poll_result
// This ensures whichever response was valid gets used // This ensures whichever response was valid gets used
poll_result = strdup(sysUptime); /* With neither OID readable there is nothing to compare. An empty
* value reads as 0, which looks like a reboot and queues a reindex. */
poll_result = strdup(sysUptime[0] != '\0' ? sysUptime : "U");
if (poll_result == NULL) { if (poll_result == NULL) {
die("ERROR: Fatal malloc error: poller.c uptime result"); die("ERROR: Fatal malloc error: poller.c uptime result");
} }
SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] HT[%i] DQ[%i] Extended Uptime Result: %s, Is Numeric: %d", host->id, host_thread, reindex->data_query_id, poll_result, is_numeric(poll_result) )); SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] HT[%i] DQ[%i] Extended Uptime Result: %s, Is Numeric: %d", host->id, host_thread, reindex->data_query_id, poll_result, sysUptime[0] != '\0'));
} else { } else {
poll_result = snmp_get(host, reindex->arg1); poll_result = snmp_get(host, reindex->arg1);
} }
@@ -688,6 +673,17 @@ static char *poll_reindex_snmp(host_t *host, reindex_t *reindex, int host_thread
return poll_result; return poll_result;
} }
/* trim() can return a pointer past the start of the allocation. Move the
* trimmed text to the front so the caller still frees what malloc returned. */
static char *trim_owned(char *owned) {
char *trimmed;
if (owned == NULL) return NULL;
trimmed = trim(owned);
if (trimmed != owned) memmove(owned, trimmed, strlen(trimmed) + 1);
return owned;
}
static char *poll_reindex_action(host_t *host, reindex_t *reindex, int host_thread, char *sysUptime, bool *unavailable) { static char *poll_reindex_action(host_t *host, reindex_t *reindex, int host_thread, char *sysUptime, bool *unavailable) {
char *poll_result = NULL; char *poll_result = NULL;
int php_process; int php_process;
@@ -704,7 +700,7 @@ static char *poll_reindex_action(host_t *host, reindex_t *reindex, int host_thre
break; break;
} }
poll_result = trim(exec_poll(host, reindex->arg1, reindex->data_query_id, "DQ")); poll_result = trim_owned(exec_poll(host, reindex->arg1, reindex->data_query_id, "DQ"));
php_command_script(reindex->arg1, script, sizeof(script)); php_command_script(reindex->arg1, script, sizeof(script));
SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] HT[%i] DQ[%i] RECACHE CMD: %s, output: %s", host->id, host_thread, reindex->data_query_id, script, poll_result)); SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] HT[%i] DQ[%i] RECACHE CMD: %s, output: %s", host->id, host_thread, reindex->data_query_id, script, poll_result));
@@ -713,7 +709,7 @@ static char *poll_reindex_action(host_t *host, reindex_t *reindex, int host_thre
case POLLER_ACTION_PHP_SCRIPT_SERVER: /* script (php script server) */ case POLLER_ACTION_PHP_SCRIPT_SERVER: /* script (php script server) */
php_process = php_get_process(); php_process = php_get_process();
poll_result = trim(php_cmd(reindex->arg1, php_process)); poll_result = trim_owned(php_cmd(reindex->arg1, php_process));
php_command_script(reindex->arg1, script, sizeof(script)); php_command_script(reindex->arg1, script, sizeof(script));
SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] HT[%i] DQ[%i] RECACHE SERVER: %s, output: %s", host->id, host_thread, reindex->data_query_id, script, poll_result)); SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] HT[%i] DQ[%i] RECACHE SERVER: %s, output: %s", host->id, host_thread, reindex->data_query_id, script, poll_result));
@@ -1053,18 +1049,12 @@ static poll_output_buffers_t write_poll_results(MYSQL *mysql, MYSQL *mysqlr,
return (poll_output_buffers_t){query3, query12, failed}; return (poll_output_buffers_t){query3, query12, failed};
} }
/* The key holds a whole profile. Narrower copies truncated long communities,
* which then never matched their own item and forced a session per OID. */
typedef struct { typedef struct {
int initialized; int initialized;
int version; char hostname[sizeof(((target_t *)0)->hostname)];
int port; snmp_profile_t profile;
char community[50];
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];
} snmp_item_key_t; } snmp_item_key_t;
typedef struct { typedef struct {
@@ -1076,17 +1066,8 @@ typedef struct {
} snmp_poll_batch_t; } snmp_poll_batch_t;
static void remember_snmp_item(snmp_item_key_t *key, const target_t *item) { static void remember_snmp_item(snmp_item_key_t *key, const target_t *item) {
key->port = item->snmp.port; key->profile = item->snmp;
key->version = item->snmp.version; STRNCOPY(key->hostname, item->hostname);
STRNCOPY(key->community, item->snmp.community);
STRNCOPY(key->username, item->snmp.username);
STRNCOPY(key->password, item->snmp.password);
STRNCOPY(key->auth_protocol, item->snmp.auth_protocol);
STRNCOPY(key->priv_passphrase, item->snmp.priv_passphrase);
STRNCOPY(key->priv_protocol, item->snmp.priv_protocol);
STRNCOPY(key->context, item->snmp.context);
STRNCOPY(key->engine_id, item->snmp.engine_id);
} }
static void *open_snmp_item(const host_t *host, target_t *item) { static void *open_snmp_item(const host_t *host, target_t *item) {
@@ -1108,20 +1089,24 @@ static void *open_snmp_item(const host_t *host, target_t *item) {
} }
static bool snmp_item_changed(const snmp_item_key_t *key, const target_t *item) { static bool snmp_item_changed(const snmp_item_key_t *key, const target_t *item) {
return (key->port != item->snmp.port) || return !STRMATCH(key->hostname, item->hostname) ||
(key->version != item->snmp.version) || (key->profile.port != item->snmp.port) ||
(key->profile.timeout != item->snmp.timeout) ||
(key->profile.version != item->snmp.version) ||
(item->snmp.version < 3 && (item->snmp.version < 3 &&
(!STRMATCH(key->community, item->snmp.community))) || (!STRMATCH(key->profile.community, item->snmp.community))) ||
(item->snmp.version > 2 && (item->snmp.version > 2 &&
((!STRMATCH(key->username, item->snmp.username)) || ((!STRMATCH(key->profile.username, item->snmp.username)) ||
(!STRMATCH(key->password, item->snmp.password)) || (!STRMATCH(key->profile.password, item->snmp.password)) ||
(!STRMATCH(key->auth_protocol, item->snmp.auth_protocol)) || (!STRMATCH(key->profile.auth_protocol, item->snmp.auth_protocol)) ||
(!STRMATCH(key->priv_passphrase, item->snmp.priv_passphrase)) || (!STRMATCH(key->profile.priv_passphrase, item->snmp.priv_passphrase)) ||
(!STRMATCH(key->priv_protocol, item->snmp.priv_protocol)) || (!STRMATCH(key->profile.priv_protocol, item->snmp.priv_protocol)) ||
(!STRMATCH(key->context, item->snmp.context)) || (!STRMATCH(key->profile.context, item->snmp.context)) ||
(!STRMATCH(key->engine_id, item->snmp.engine_id)))); (!STRMATCH(key->profile.engine_id, item->snmp.engine_id))));
} }
/* The batch-full, credential-change and final flushes all come through here,
* so every item gets the same regex, validation and spike kill. */
static void flush_snmp_batch(host_t *host, snmp_poll_batch_t *batch, static void flush_snmp_batch(host_t *host, snmp_poll_batch_t *batch,
double thread_start, bool spike_kill) { double thread_start, bool spike_kill) {
if (batch->count <= 0) return; if (batch->count <= 0) return;
@@ -1144,8 +1129,7 @@ static void poll_snmp_item(host_t *host, snmp_poll_batch_t *batch, int position,
return; return;
} }
if (snmp_item_changed(&batch->key, item)) { if (snmp_item_changed(&batch->key, item)) {
/* Credential-switch flushes preserve the legacy no-discard policy. */ flush_snmp_batch(host, batch, thread_start, spike_kill);
flush_snmp_batch(host, batch, thread_start, FALSE);
if (host->snmp.session != NULL) { if (host->snmp.session != NULL) {
snmp_host_cleanup(host->snmp.session); snmp_host_cleanup(host->snmp.session);
host->snmp.session = NULL; host->snmp.session = NULL;
@@ -1237,11 +1221,7 @@ static int collect_poll_items(host_t *host, snmp_poll_batch_t *batch, int num_ro
} }
/* process last multi-get request if applicable */ /* process last multi-get request if applicable */
if (batch->count > 0) { flush_snmp_batch(host, batch, thread_start, spike_kill);
snmp_get_multi(host, batch->items, batch->oids, batch->count);
store_snmp_results(host, batch->items, batch->oids, batch->count, batch->errors, thread_start, spike_kill);
}
return rows_processed; return rows_processed;
} }
@@ -1437,6 +1417,9 @@ void poll_host(const poller_thread_t *work, int *host_errors) {
if (error_string == NULL || buf_size == NULL || buf_errors == NULL) { if (error_string == NULL || buf_size == NULL || buf_errors == NULL) {
die("ERROR: Fatal malloc error: poller error buffer!"); die("ERROR: Fatal malloc error: poller error buffer!");
} }
/* Reindex assertion failures count errors without naming a data source,
* so the list can reach the host_errors INSERT before anything is added. */
error_string[0] = '\0';
*buf_size = 0; *buf_size = 0;
*buf_errors = 0; *buf_errors = 0;
const poll_error_context_t error_context = { const poll_error_context_t error_context = {
@@ -1631,39 +1614,16 @@ void buffer_output_errors(char *error_string, int *buf_size, int *buf_errors, in
} }
} }
/*! \fn int is_multipart_output(const char *result)
* \brief validates the output syntax is a valid name value pair syntax
* \param result the value to be checked for legality
*
* This function will poll a specific host using the script pointed to by
* the command variable.
*
* \return TRUE if the result is valid, otherwise FALSE.
*
*/
int is_multipart_output(const char *result) {
if (result == NULL) return FALSE;
if (strchr(result, ':') == NULL && strchr(result, '!') == NULL) return FALSE;
if (strchr(result, ' ') == NULL) return TRUE;
size_t space_cnt = 0;
size_t delim_cnt = 0;
for (const char *cursor = result; *cursor != '\0'; cursor++) {
if (*cursor == ':' || *cursor == '!') delim_cnt++;
else if (*cursor == ' ') space_cnt++;
}
return space_cnt + 1 == delim_cnt;
}
static void poll_system_uptime(host_t *host) { static void poll_system_uptime(host_t *host) {
char *poll_result; char *poll_result;
unsigned long long ticks;
// Get the legacy system uptime instance first // Get the legacy system uptime instance first
SPINE_LOG_DEVDBG(("DEVDBG: Device[%d] poll_result = snmp_get_allow_fail(host, '.1.3.6.1.2.1.1.3.0');", host->id)); SPINE_LOG_DEVDBG(("DEVDBG: Device[%d] poll_result = snmp_get_allow_fail(host, '.1.3.6.1.2.1.1.3.0');", host->id));
poll_result = snmp_get_allow_fail(host, ".1.3.6.1.2.1.1.3.0"); poll_result = snmp_get_allow_fail(host, ".1.3.6.1.2.1.1.3.0");
SPINE_LOG_DEVDBG(("DEVDGB: Device[%d] poll_result = snmp_get_allow_fail(host, '.1.3.6.1.2.1.1.3.0'); [complete]", host->id)); SPINE_LOG_DEVDBG(("DEVDGB: Device[%d] poll_result = snmp_get_allow_fail(host, '.1.3.6.1.2.1.1.3.0'); [complete]", host->id));
if (poll_result && is_numeric(poll_result)) { if (parse_uptime(poll_result, &ticks)) {
host->system.snmp_sysUpTimeInstance = atoll(poll_result); host->system.snmp_sysUpTimeInstance = ticks;
SPINE_FREE(poll_result); SPINE_FREE(poll_result);
// Attempt to get the more modern version // Attempt to get the more modern version
@@ -1671,8 +1631,8 @@ static void poll_system_uptime(host_t *host) {
poll_result = snmp_get_allow_fail(host, ".1.3.6.1.6.3.10.2.1.3.0"); poll_result = snmp_get_allow_fail(host, ".1.3.6.1.6.3.10.2.1.3.0");
SPINE_LOG_DEVDBG(("DEVDGB: Device[%d] poll_result = snmp_get_allow_fail(host, '.1.3.6.1.6.3.10.2.1.3.0'); [complete]", host->id)); SPINE_LOG_DEVDBG(("DEVDGB: Device[%d] poll_result = snmp_get_allow_fail(host, '.1.3.6.1.6.3.10.2.1.3.0'); [complete]", host->id));
if (poll_result && is_numeric(poll_result)) { if (parse_uptime(poll_result, &ticks) && ticks <= ULLONG_MAX / 100) {
host->system.snmp_sysUpTimeInstance = atoll(poll_result) * 100; host->system.snmp_sysUpTimeInstance = ticks * 100;
} }
} }
SPINE_FREE(poll_result); SPINE_FREE(poll_result);
@@ -1701,33 +1661,6 @@ void get_system_information(host_t *host, MYSQL *mysql, int system) {
} }
} }
/*! \fn int validate_result(char *result)
* \brief validates the output from the polling action is valid
* \param result the value to be checked for legality
*
* This function will poll a specific host using the script pointed to by
* the command variable.
*
* \return TRUE if the result is valid, otherwise FALSE.
*
*/
int validate_result(char *result) {
/* check the easy cases first */
if (result) {
if (is_numeric(result)) {
return TRUE;
} else {
if (is_multipart_output(trim(result))) {
return TRUE;
} else {
return FALSE;
}
}
}
return FALSE;
}
static int acquire_script_permit(const host_t *host) { static int acquire_script_permit(const host_t *host) {
if (set.php.script_timeout <= 0) return EINVAL; if (set.php.script_timeout <= 0) return EINVAL;
/* Preserve the existing retry budget without signed multiplication overflow. */ /* Preserve the existing retry budget without signed multiplication overflow. */
@@ -1784,9 +1717,9 @@ static bool read_script_result(const script_result_context_t *context, int fd, d
SET_UNDEFINED(result); SET_UNDEFINED(result);
return FALSE; return FALSE;
} }
/* Preserve the single-read response contract; later output is ignored. */ /* Use the full capacity to detect truncation without waiting for EOF. */
ssize_t bytes = read(fd, result, RESULTS_BUFFER - 1); ssize_t bytes = read(fd, result, RESULTS_BUFFER);
if (bytes > 0 && bytes < RESULTS_BUFFER) { if (bytes > 0 && bytes < RESULTS_BUFFER && memchr(result, '\0', (size_t)bytes) == NULL) {
result[bytes] = '\0'; result[bytes] = '\0';
} else { } else {
char script[SMALL_BUFSIZE]; char script[SMALL_BUFSIZE];
-3
View File
@@ -52,12 +52,9 @@ extern void *child(void *arg);
extern void child_cleanup(void *arg); extern void child_cleanup(void *arg);
extern void child_cleanup_thread(void *arg); extern void child_cleanup_thread(void *arg);
extern void child_cleanup_script(void *arg); extern void child_cleanup_script(void *arg);
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 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 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 void get_system_information(host_t *host, MYSQL *mysql, int system);
extern int is_multipart_output(const char *result);
extern int validate_result(char *result);
enum poll_result_status { enum poll_result_status {
POLL_RESULT_VALID, POLL_RESULT_VALID,
POLL_RESULT_UNDEFINED, POLL_RESULT_UNDEFINED,
+3
View File
@@ -3,5 +3,8 @@
# Isolated regression agent; never use this configuration in production. # Isolated regression agent; never use this configuration in production.
agentaddress udp:127.0.0.1:1161 agentaddress udp:127.0.0.1:1161
rocommunity regression 127.0.0.1 rocommunity regression 127.0.0.1
rocommunity regression-community-wider-than-the-old-fifty-byte-batch-key 127.0.0.1
sysLocation isolated-regression-agent sysLocation isolated-regression-agent
sysContact regression sysContact regression
# Octets that normalize to 266, for output_regex ordering.
sysName 00 00 01 0A
+5 -2
View File
@@ -47,8 +47,11 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
(void)format_poller_output_row(row, sizeof(row), 1, (void)format_poller_output_row(row, sizeof(row), 1,
"fuzz", "1700000000", result); "fuzz", "1700000000", result);
(void)is_multipart_output(value); classified_result_t classified;
(void)validate_result(value); if (classify_result(value, &classified) == RESULT_UNKNOWN &&
!IS_UNDEFINED(classified.text)) {
abort();
}
return 0; return 0;
} }
+6 -3
View File
@@ -174,14 +174,17 @@ static void test_multipart_boundaries(void) {
{NULL, FALSE}, {"", FALSE}, {"123", FALSE}, {"hello", FALSE}, {NULL, FALSE}, {"", FALSE}, {"123", FALSE}, {"hello", FALSE},
{"a:1", TRUE}, {"a!1", TRUE}, {":", TRUE}, {"!", TRUE}, {"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", 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", TRUE}, {"a:1 ", TRUE},
{" a:1 b:2 ", FALSE}, {"a:1 b:2 c:3", TRUE}, {" a:1 b:2 ", TRUE}, {"a:1 b:2 c:3", TRUE},
{"a::1 b:2", FALSE}, {"a::1", TRUE}, {"a::1 b:2", FALSE}, {"a::1", TRUE},
{"a:1\tb:2", TRUE}, {"a:1\nb!2", 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", TRUE}, {"a:1 b:2\n c!3", TRUE},
{"a:1 \t b:2", FALSE}, {"a b", FALSE} {"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); for (size_t i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) {
classified_result_t out;
assert((classify_result(cases[i].input, &out) == RESULT_MULTIPART) == cases[i].expected);
}
} }
static void test_fatal_signal_contracts(void) { static void test_fatal_signal_contracts(void) {
+98
View File
@@ -1298,6 +1298,103 @@ static void test_ping_only_session_lifetime(void) {
puts("production ping-only SNMP session ownership regressions passed"); puts("production ping-only SNMP session ownership regressions passed");
} }
static void *run_batch_worker(void *argument) {
assert(mysql_thread_init() == 0);
int errors = 0;
poll_host(argument, &errors);
assert(errors == 0);
return NULL;
}
/* The batch key once held 49 bytes of a 99-byte community, so a longer
* community never matched its own key and every item reopened the session. */
static void test_snmp_batch_key_width(void) {
extern int *debug_devices;
static const char community[] = "regression-community-wider-than-the-old-fifty-byte-batch-key";
const char *agent = getenv("SPINE_TEST_SNMP_HOST");
assert(agent != NULL && agent[0] != '\0');
assert(sizeof(community) > 50);
config_t previous = set;
int *previous_debug_devices = debug_devices;
poller_thread_t **previous_details = details;
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.poller.poller_interval = 0;
set.poller.active_profiles = 1;
set.availability.ping_only = FALSE;
set.boost.boost_enabled = FALSE;
set.boost.boost_redirect = FALSE;
set.logging.spine_log_level = 0;
set.snmp.snmp_retries = 0;
set.snmp.mibs = 0;
MYSQL administrator;
db_connect(LOCAL, &administrator);
assert(fault_database_count(&administrator, "SELECT COUNT(*) FROM host WHERE id=904") == 0);
assert(fault_database_count(&administrator, "SELECT COUNT(*) FROM poller_item WHERE host_id=904") == 0);
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_scripts, 3) == 0);
char escaped_agent[BUFSIZE];
db_escape(&administrator, escaped_agent, sizeof(escaped_agent), agent);
char query[LRG_BUFSIZE];
spine_snprintf(query, sizeof(query), "INSERT INTO host(id,hostname,snmp_version,snmp_community,snmp_port,snmp_timeout,availability_method,max_oids,status_fail_date,status_rec_date) VALUES(904,'%s',2,'%s',1161,500,%i,10,'2026-10-06 00:00:00','2026-10-06 00:00:00')", escaped_agent, community, AVAIL_NONE);
assert(db_insert(&administrator, LOCAL, query));
spine_snprintf(query, sizeof(query), "INSERT INTO poller_item(local_data_id,host_id,poller_id,action,hostname,snmp_community,snmp_version,snmp_port,snmp_timeout,arg1,rrd_name) VALUES"
"(941,904,1,0,'%s','%s',2,1161,500,'.1.3.6.1.2.1.1.3.0','a'),"
"(942,904,1,0,'%s','%s',2,1161,500,'.1.3.6.1.2.1.1.3.0','b'),"
"(943,904,1,0,'%s','%s',2,1161,500,'.1.3.6.1.2.1.1.3.0','c')",
escaped_agent, community, escaped_agent, community, escaped_agent, community);
assert(db_insert(&administrator, LOCAL, query));
assert(db_insert(&administrator, LOCAL, "DELETE FROM poller_output WHERE local_data_id IN (941,942,943)"));
snmp_spine_init();
poller_thread_t work = {0};
work.host_id = 904;
work.host_thread = 1;
work.host_threads = 1;
work.host_data_ids = 3;
work.host_time_double = get_time_as_double();
STRNCOPY(work.host_time, "1791244800");
poller_thread_t *device = &work;
details = &device;
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_batch_worker, &work) == 0);
assert(pthread_join(worker, NULL) == 0);
account_snmp_sessions = FALSE;
int observed_opens = session_opens;
if (owned_snmp_session != NULL) {
assert(__real_snmp_sess_close(owned_snmp_session) == 1);
owned_snmp_session = NULL;
}
fprintf(stderr, "long community batch: opens=%d closes=%d\n", observed_opens, session_closes);
assert(work.complete && db_pool_local[0].free);
assert(fault_database_count(&administrator, "SELECT COUNT(*) FROM poller_output WHERE local_data_id IN (941,942,943) AND output REGEXP '^[0-9]+$'") == 3);
/* One session for the device checks, one for the single batch. */
assert(observed_opens == 2 && session_closes == 2);
assert(db_insert(&administrator, LOCAL, "DELETE FROM poller_output WHERE local_data_id IN (941,942,943)"));
assert(db_insert(&administrator, LOCAL, "DELETE FROM poller_item WHERE host_id=904"));
assert(db_insert(&administrator, LOCAL, "DELETE FROM host WHERE id=904"));
snmp_spine_close();
assert(spine_permits_destroy(&available_scripts) == 0);
db_close_connection_pool(LOCAL);
db_pool_local = previous_pool;
db_disconnect(&administrator);
details = previous_details;
debug_devices = previous_debug_devices;
set = previous;
puts("production long-community SNMP batching regressions passed");
}
static int execute_worker_launch_case(const char *scenario, char *config) { static int execute_worker_launch_case(const char *scenario, char *config) {
assert(strcmp(scenario, "admitted") == 0 || strcmp(scenario, "rejected") == 0 || strcmp(scenario, "retry") == 0); assert(strcmp(scenario, "admitted") == 0 || strcmp(scenario, "rejected") == 0 || strcmp(scenario, "retry") == 0);
launch_case_active = TRUE; launch_case_active = TRUE;
@@ -1424,6 +1521,7 @@ int main(int argc, char **argv) {
if (argc == 2 && strcmp(argv[1], "--database") == 0) { if (argc == 2 && strcmp(argv[1], "--database") == 0) {
test_real_database_retry(); test_real_database_retry();
test_ping_only_session_lifetime(); test_ping_only_session_lifetime();
test_snmp_batch_key_width();
test_worker_launch_failures(); test_worker_launch_failures();
} }
else assert(argc == 1); else assert(argc == 1);
+112 -22
View File
@@ -29,6 +29,7 @@ extern void test_script_stream_contracts(void);
extern void test_additional_database_contracts(MYSQL *mysql); extern void test_additional_database_contracts(MYSQL *mysql);
extern void test_settings_write_contracts(MYSQL *mysql); extern void test_settings_write_contracts(MYSQL *mysql);
extern void test_additional_reindex_contracts(MYSQL *mysql); extern void test_additional_reindex_contracts(MYSQL *mysql);
extern void test_reindex_result_contracts(MYSQL *mysql);
extern void test_additional_snmp_session_boundaries(void); extern void test_additional_snmp_session_boundaries(void);
extern void test_snmpv3_agent_contracts(void); extern void test_snmpv3_agent_contracts(void);
@@ -56,15 +57,11 @@ static void test_string_conversions(void) {
assert(strcmp(reverse(empty), "") == 0); assert(strcmp(reverse(empty), "") == 0);
assert(strcmp(reverse(one), "x") == 0); assert(strcmp(reverse(one), "x") == 0);
assert(strcmp(reverse(word), "dcba") == 0); assert(strcmp(reverse(word), "dcba") == 0);
char hex[] = "\"FF ff\t\""; classified_result_t hex;
char maximum[] = "FFFFFFFFFFFFFFFF"; assert(classify_result("\"FF ff\t\"", &hex) == RESULT_HEX_COUNTER && hex.value.counter == 65535);
char overflow[] = "10000000000000000"; assert(classify_result("FF:FF:FF:FF FF-FF-FF-FF", &hex) == RESULT_HEX_COUNTER && hex.value.counter == ULLONG_MAX);
char invalid[] = "not hex"; assert(classify_result("01 00 00 00 00 00 00 00 00", &hex) == RESULT_UNKNOWN && IS_UNDEFINED(hex.text));
unsigned long long value = 0; assert(classify_result("not hex", &hex) == RESULT_UNKNOWN);
assert(hex2dec(hex, &value) && value == 65535);
assert(hex2dec(maximum, &value) && value == ULLONG_MAX);
assert(!hex2dec(overflow, &value));
assert(!hex2dec(invalid, &value) && !hex2dec(NULL, &value));
char input[BUFSIZE * 2]; char input[BUFSIZE * 2];
memset(input, '\\', sizeof(input) - 1); memset(input, '\\', sizeof(input) - 1);
input[sizeof(input) - 1] = '\0'; input[sizeof(input) - 1] = '\0';
@@ -668,7 +665,16 @@ static void test_poll_result_formats(void) {
{"a:1 b!2", "a:1 b!2", POLL_RESULT_VALID}, {"a:1 b!2", "a:1 b!2", POLL_RESULT_VALID},
{"a!1", "a!1", POLL_RESULT_VALID}, {"a!1", "a!1", POLL_RESULT_VALID},
{"U", "U", POLL_RESULT_UNDEFINED}, {"U", "U", POLL_RESULT_UNDEFINED},
{"nonsense", "", POLL_RESULT_INVALID} {"nonsense", "U", POLL_RESULT_INVALID},
/* Padding is not data: " 42" once parsed as the octets 0x42. */
{" 42", "42", POLL_RESULT_VALID},
{"42\n", "42", POLL_RESULT_VALID},
{" 10\n", "10", POLL_RESULT_VALID},
{"\"42\"", "42", POLL_RESULT_VALID},
{"1F FF FF FF FF FF FF F1", "2305843009213693937", POLL_RESULT_VALID},
{"0x1A", "U", POLL_RESULT_INVALID},
{"---", "U", POLL_RESULT_INVALID},
{" ", "U", POLL_RESULT_INVALID}
}; };
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
for (int snmp = 0; snmp <= 1; snmp++) { for (int snmp = 0; snmp <= 1; snmp++) {
@@ -678,10 +684,17 @@ static void test_poll_result_formats(void) {
assert(strcmp(result, cases[i].output) == 0); assert(strcmp(result, cases[i].output) == 0);
} }
} }
char undefined[RESULTS_BUFFER] = "u"; /* An agent's "u" or "NaN" is unknown, and poller_output stores it as U. */
assert(normalize_poll_result(undefined, true) == POLL_RESULT_UNDEFINED); static const char *const unknown[] = {"u", "NaN", "nan"};
assert(strcmp(undefined, "u") == 0); for (size_t i = 0; i < sizeof(unknown) / sizeof(unknown[0]); i++) {
assert(normalize_poll_result(undefined, false) == POLL_RESULT_INVALID); char undefined[RESULTS_BUFFER];
strncopy(undefined, unknown[i], sizeof(undefined));
assert(normalize_poll_result(undefined, true) == POLL_RESULT_UNDEFINED);
assert(strcmp(undefined, "U") == 0);
strncopy(undefined, unknown[i], sizeof(undefined));
assert(normalize_poll_result(undefined, false) == POLL_RESULT_INVALID);
assert(strcmp(undefined, "U") == 0);
}
} }
static void test_hostnames(void) { static void test_hostnames(void) {
@@ -1105,11 +1118,18 @@ static void test_script_execution(void) {
for (int excess = 0; excess <= 1; excess++) { for (int excess = 0; excess <= 1; excess++) {
spine_snprintf(output_command, sizeof(output_command), "/usr/bin/printf '%%%ds' x", RESULTS_BUFFER - 1 + excess); spine_snprintf(output_command, sizeof(output_command), "/usr/bin/printf '%%%ds' x", RESULTS_BUFFER - 1 + excess);
result = exec_poll(&host, output_command, 1, "DS"); result = exec_poll(&host, output_command, 1, "DS");
assert(strlen(result) == RESULTS_BUFFER - 1); if (excess) assert(strcmp(result, "U") == 0);
assert(result[RESULTS_BUFFER - 1] == '\0'); else {
assert(strlen(result) == RESULTS_BUFFER - 1);
assert(result[RESULTS_BUFFER - 1] == '\0');
}
free(result); free(result);
assert(spine_permits_available(&available_scripts) == 1); assert(spine_permits_available(&available_scripts) == 1);
} }
char embedded_nul[] = "/usr/bin/printf '42\\000junk'";
result = exec_poll(&host, embedded_nul, 1, "DS");
assert(strcmp(result, "U") == 0);
free(result);
char empty[] = "/usr/bin/printf ''"; char empty[] = "/usr/bin/printf ''";
result = exec_poll(&host, empty, 1, "DQ"); result = exec_poll(&host, empty, 1, "DQ");
assert(strcmp(result, "U") == 0); assert(strcmp(result, "U") == 0);
@@ -1813,7 +1833,7 @@ static void test_reindex_pipeline(MYSQL *mysql, test_poll_work_t *work) {
{"<", "0", "/usr/bin/printf 123", POLLER_ACTION_SCRIPT, 0, "123", "123"}, {"<", "0", "/usr/bin/printf 123", POLLER_ACTION_SCRIPT, 0, "123", "123"},
{"=", "123", "/usr/bin/printf 'a\\nb\\n'", POLLER_ACTION_SCRIPT_COUNT, 1, "2", "123"}, {"=", "123", "/usr/bin/printf 'a\\nb\\n'", POLLER_ACTION_SCRIPT_COUNT, 1, "2", "123"},
{"=", "123", "/usr/bin/printf U", POLLER_ACTION_SCRIPT, 0, "123", "123"}, {"=", "123", "/usr/bin/printf U", POLLER_ACTION_SCRIPT, 0, "123", "123"},
{"<", "124", "/usr/bin/printf U", POLLER_ACTION_SCRIPT, 0, "U", "123"}, {"<", "124", "/usr/bin/printf U", POLLER_ACTION_SCRIPT, 0, "124", "123"},
{"<", "124", "unknown", 99, 0, "124", "123"} {"<", "124", "unknown", 99, 0, "124", "123"}
}; };
for (int level = 0; level <= 2; level++) { for (int level = 0; level <= 2; level++) {
@@ -1987,10 +2007,11 @@ static void prepare_nullable_snmp_profile(MYSQL *mysql) {
assert(database_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=44 AND snmp_context IS NULL AND snmp_engine_id IS NULL AND arg2 IS NULL AND arg3 IS NULL") == 2); assert(database_count(mysql, "SELECT COUNT(*) FROM poller_item WHERE host_id=44 AND snmp_context IS NULL AND snmp_engine_id IS NULL AND arg2 IS NULL AND arg3 IS NULL") == 2);
} }
static void assert_snmp_pipeline_samples(MYSQL *mysql, bool spike, bool change_version) { static void assert_snmp_pipeline_samples(MYSQL *mysql, bool spike) {
char query[LRG_BUFSIZE]; char query[LRG_BUFSIZE];
bool first_discarded = spike && !change_version; /* A restarted agent invalidates every sample of the cycle, including
const char *first_check = first_discarded ? "output='U'" : "output REGEXP '^[0-9]+$' AND CAST(output AS UNSIGNED)>0"; * those flushed early because the next item's credentials differ. */
const char *first_check = spike ? "output='U'" : "output REGEXP '^[0-9]+$' AND CAST(output AS UNSIGNED)>0";
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output WHERE local_data_id=601 AND %s", first_check); spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output WHERE local_data_id=601 AND %s", first_check);
assert(database_count(mysql, query) == 1); assert(database_count(mysql, query) == 1);
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=601 AND %s", first_check); spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=601 AND %s", first_check);
@@ -2032,7 +2053,7 @@ static void test_snmp_item_pipeline(MYSQL *mysql, test_poll_work_t *work, const
assert(work->thread.complete && work->thread.threads_complete == 1); assert(work->thread.complete && work->thread.threads_complete == 1);
assert(work->errors == (spike ? 0 : 1)); assert(work->errors == (spike ? 0 : 1));
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2); assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
assert_snmp_pipeline_samples(mysql, spike, change_version); assert_snmp_pipeline_samples(mysql, spike);
if (scenario == 8) puts("production nullable SNMP profile handoff passed"); if (scenario == 8) puts("production nullable SNMP profile handoff passed");
} }
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex")); assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
@@ -2040,6 +2061,71 @@ static void test_snmp_item_pipeline(MYSQL *mysql, test_poll_work_t *work, const
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET snmp_context='',snmp_engine_id='' WHERE host_id=44")); assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET snmp_context='',snmp_engine_id='' WHERE host_id=44"));
} }
/* Each item leaves through a different batch flush: 601 through the
* batch-full flush (max_oids=1) or the credential-change flush (602 switches
* to v1), and 602 through the final flush. All three must apply the same
* output_regex and spike kill. */
static void test_snmp_flush_contracts(MYSQL *mysql, test_poll_work_t *work, const char *agent) {
char escaped_agent[BUFSIZE];
char query[LRG_BUFSIZE];
int previous_regex = set.hosts.has_output_regex;
db_escape(mysql, escaped_agent, sizeof(escaped_agent), agent);
assert(db_insert(mysql, LOCAL, "ALTER TABLE poller_item ADD COLUMN output_regex varchar(255) NOT NULL DEFAULT ''"));
set.hosts.has_output_regex = TRUE;
for (int scenario = 0; scenario < 4; scenario++) {
bool full_batch = (scenario & 1) != 0;
bool spike = (scenario & 2) != 0;
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost"));
spine_snprintf(query, sizeof(query), "UPDATE host SET max_oids=%d WHERE id=44", full_batch ? 1 : 5);
assert(db_insert(mysql, LOCAL, query));
/* The leading digit of sysUpTime: a visible, still numeric rewrite. */
spine_snprintf(query, sizeof(query), "UPDATE poller_item SET action=0,hostname='%s',snmp_community='regression',snmp_version=2,snmp_port=1161,snmp_timeout=500,arg1='.1.3.6.1.2.1.1.3.0',output_regex='^[0-9]' WHERE host_id=44", escaped_agent);
assert(db_insert(mysql, LOCAL, query));
if (!full_batch) assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET snmp_version=1 WHERE local_data_id=602"));
if (spike) assert(db_insert(mysql, LOCAL, "INSERT INTO poller_reindex(host_id,data_query_id,action,op,assert_value,arg1) VALUES (44,7,1,'<','124','/usr/bin/printf 123')"));
work->thread.complete = FALSE;
work->thread.threads_complete = 0;
work->errors = 0;
pthread_t worker;
assert(pthread_create(&worker, NULL, test_poll_worker, work) == 0);
assert(pthread_join(worker, NULL) == 0);
assert(work->thread.complete && work->thread.threads_complete == 1);
const char *expected = spike ? "output='U'" : "output REGEXP '^[0-9]$'";
for (int item = 601; item <= 602; item++) {
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output WHERE local_data_id=%d AND %s", item, expected);
if (database_count(mysql, query) != 1) {
fprintf(stderr, "flush contract: scenario=%d full_batch=%d spike=%d item=%d expected %s\n", scenario, full_batch, spike, item, expected);
assert(0);
}
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=%d AND %s", item, expected);
assert(database_count(mysql, query) == 1);
}
}
/* Existing regexes were written against the normalized value: a
* Hex-STRING becomes 266 first, then the regex keeps "266", not "00". */
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost"));
assert(db_insert(mysql, LOCAL, "UPDATE poller_item SET snmp_version=2,arg1='.1.3.6.1.2.1.1.5.0',output_regex='^[0-9][0-9]*' WHERE host_id=44"));
work->thread.complete = FALSE;
work->thread.threads_complete = 0;
work->errors = 0;
pthread_t regex_worker;
assert(pthread_create(&regex_worker, NULL, test_poll_worker, work) == 0);
assert(pthread_join(regex_worker, NULL) == 0);
if (database_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id IN (601,602) AND output='266'") != 2) {
fprintf(stderr, "flush contract: output_regex did not see the normalized Hex-STRING\n");
assert(0);
}
set.hosts.has_output_regex = previous_regex;
assert(db_insert(mysql, LOCAL, "ALTER TABLE poller_item DROP COLUMN output_regex"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex"));
puts("production SNMP batch flush contracts passed");
}
static void assert_polled_host_statistics(MYSQL *mysql, int host_id) { static void assert_polled_host_statistics(MYSQL *mysql, int host_id) {
if (host_id == 44) { if (host_id == 44) {
assert(database_count(mysql, "SELECT COUNT(*) FROM host WHERE id=44 AND total_polls=1 AND failed_polls=0 AND status_last_error='' AND snmp_sysUpTimeInstance>0 AND snmp_sysDescr!='' AND snmp_sysObjectID!='' AND snmp_sysContact='regression' AND snmp_sysName!='' AND snmp_sysLocation='isolated-regression-agent'") == 1); assert(database_count(mysql, "SELECT COUNT(*) FROM host WHERE id=44 AND total_polls=1 AND failed_polls=0 AND status_last_error='' AND snmp_sysUpTimeInstance>0 AND snmp_sysDescr!='' AND snmp_sysObjectID!='' AND snmp_sysContact='regression' AND snmp_sysName!='' AND snmp_sysLocation='isolated-regression-agent'") == 1);
@@ -2123,7 +2209,10 @@ static void test_poll_pipeline(MYSQL *mysql) {
spine_snprintf(query, sizeof(query), "SELECT errors FROM host_errors WHERE host_id=%d", host_id); spine_snprintf(query, sizeof(query), "SELECT errors FROM host_errors WHERE host_id=%d", host_id);
assert(database_count(mysql, query) == 1); assert(database_count(mysql, query) == 1);
assert_polled_host_statistics(mysql, host_id); assert_polled_host_statistics(mysql, host_id);
if (host_id == 44) test_snmp_item_pipeline(mysql, work, agent); if (host_id == 44) {
test_snmp_item_pipeline(mysql, work, agent);
test_snmp_flush_contracts(mysql, work, agent);
}
if (host_id == 43) { if (host_id == 43) {
test_reindex_pipeline(mysql, work); test_reindex_pipeline(mysql, work);
test_reindex_query_shortcut(mysql, work); test_reindex_query_shortcut(mysql, work);
@@ -2315,6 +2404,7 @@ static void test_database_configuration(void) {
test_collector_transfer(&mysql); test_collector_transfer(&mysql);
test_poll_pipeline(&mysql); test_poll_pipeline(&mysql);
test_output_write_contracts(&mysql); test_output_write_contracts(&mysql);
test_reindex_result_contracts(&mysql);
const char *reindex_agent = getenv("SPINE_TEST_SNMP_HOST"); const char *reindex_agent = getenv("SPINE_TEST_SNMP_HOST");
if (reindex_agent != NULL && reindex_agent[0] != '\0') test_additional_reindex_contracts(&mysql); if (reindex_agent != NULL && reindex_agent[0] != '\0') test_additional_reindex_contracts(&mysql);
test_cli_transfer_exit(&mysql); test_cli_transfer_exit(&mysql);
+158
View File
@@ -13,6 +13,9 @@
*/ */
#include "common.h" #include "common.h"
#include "spine.h" #include "spine.h"
#if defined(__GLIBC__)
#include <malloc.h>
#endif
extern poller_thread_t **details; extern poller_thread_t **details;
@@ -187,3 +190,158 @@ void test_additional_reindex_contracts(MYSQL *mysql) {
assert(db_insert(mysql, LOCAL, "DELETE FROM host WHERE id=45")); assert(db_insert(mysql, LOCAL, "DELETE FROM host WHERE id=45"));
puts("production live SNMP and PHP reindex contracts passed"); puts("production live SNMP and PHP reindex contracts passed");
} }
static int poll_reindex_result(MYSQL *mysql, int action, const char *argument,
const char *op, const char *expected) {
char escaped_argument[BUFSIZE];
char query[LRG_BUFSIZE];
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex WHERE host_id=46"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command WHERE command='46:7'"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id=811"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost WHERE local_data_id=811"));
assert(db_insert(mysql, LOCAL, "DELETE FROM host_errors WHERE host_id=46"));
db_escape(mysql, escaped_argument, sizeof(escaped_argument), argument);
spine_snprintf(query, sizeof(query), "INSERT INTO poller_reindex(host_id,data_query_id,action,op,assert_value,arg1) VALUES (46,7,%d,'%s','%s','%s')", action, op, expected, escaped_argument);
assert(db_insert(mysql, LOCAL, query));
reindex_test_work_t *work = calloc(1, sizeof(*work));
assert(work != NULL);
work->thread.host_id = 46;
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);
details = prior_details;
free(owned_details);
assert(work->thread.complete && work->thread.threads_complete == 1);
assert(db_pool_local[0].free && spine_permits_available(&available_scripts) == 2);
int errors = work->errors;
free(work);
return errors;
}
/* Runs against the database alone: the SNMP host points at a port with no
* agent, which is the failure the uptime case needs. */
void test_reindex_result_contracts(MYSQL *mysql) {
config_t previous_config = set;
pool_t *previous_pool = db_pool_local;
set.poller.threads = 1;
set.poller.poller_id = 1;
set.poller.poller_interval = 0;
set.poller.active_profiles = 1;
set.poller.mode = REMOTE_OFFLINE;
set.boost.boost_enabled = FALSE;
set.boost.boost_redirect = FALSE;
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.snmp.snmp_retries = 0;
db_pool_local = calloc(1, sizeof(*db_pool_local));
assert(db_pool_local != NULL);
db_create_connection_pool(LOCAL);
assert(spine_permits_init(&available_scripts, 2) == 0);
assert(db_insert(mysql, LOCAL, "DELETE FROM host WHERE id=46"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item WHERE local_data_id=811"));
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 (46,'127.0.0.1',%d,2,'regression',1,200,'2026-10-05 00:00:00','2026-10-05 00:00:00',5)", 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(811,46,1,1,'/usr/bin/printf 123','reindex_result')"));
snmp_spine_init();
/* Trimmed script output must be freed through the pointer malloc returned. */
assert(poll_reindex_result(mysql, POLLER_ACTION_SCRIPT, "/usr/bin/printf ' 123'", "=", "123") == 0);
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_command WHERE command='46:7'") == 0);
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_output WHERE local_data_id=811 AND output='123'") == 1);
assert(poll_reindex_result(mysql, POLLER_ACTION_SCRIPT, "/usr/bin/printf '\"123\"\\n'", "=", "122") == 0);
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_command WHERE command='46:7'") == 1);
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_reindex WHERE host_id=46 AND assert_value='123'") == 1);
/* A failed assertion counts an error with no data source behind it. The
* error list it reports must be empty, not uninitialized heap. */
set.logging.spine_log_level = 1;
#if defined(__GLIBC__)
assert(mallopt(M_PERTURB, 0x5a) == 1);
#endif
int errors = poll_reindex_result(mysql, POLLER_ACTION_SCRIPT, "/usr/bin/printf 123", "=", "122");
#if defined(__GLIBC__)
assert(mallopt(M_PERTURB, 0) == 1);
#endif
set.logging.spine_log_level = 0;
assert(errors == 1);
assert(reindex_count(mysql, "SELECT COUNT(*) FROM host_errors WHERE host_id=46 AND errors=1 AND local_data_ids=''") == 1);
/* An uptime that cannot be read is unknown. It must not queue a reindex
* or replace the last good assertion. */
poll_reindex_result(mysql, POLLER_ACTION_SNMP, ".1.3.6.1.2.1.1.3.0", "<", "124");
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_command WHERE command='46:7'") == 0);
assert(reindex_count(mysql, "SELECT COUNT(*) FROM poller_reindex WHERE host_id=46 AND assert_value='124'") == 1);
/* Good baseline, unreadable cycle, then a reboot. Keeping U as the
* baseline would compare as 0 and the reboot would never fire. */
char sample_path[] = "/tmp/spine-reindex-sample-XXXXXX";
int sample = mkstemp(sample_path);
assert(sample >= 0);
assert(close(sample) == 0);
char command[BUFSIZE];
spine_snprintf(command, sizeof(command), "/bin/cat %s", sample_path);
static const struct { const char *sample; int commands; const char *stored; } cycles[] = {
{"500", 0, "500"}, {"U", 0, "500"}, {"100", 1, "100"}
};
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex WHERE host_id=46"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command WHERE command='46:7'"));
spine_snprintf(query, sizeof(query), "INSERT INTO poller_reindex(host_id,data_query_id,action,op,assert_value,arg1) VALUES (46,7,%d,'<','0','%s')", POLLER_ACTION_SCRIPT, command);
assert(db_insert(mysql, LOCAL, query));
for (size_t cycle = 0; cycle < sizeof(cycles) / sizeof(cycles[0]); cycle++) {
FILE *file = fopen(sample_path, "w");
assert(file != NULL && fputs(cycles[cycle].sample, file) >= 0 && fclose(file) == 0);
reindex_test_work_t *work = calloc(1, sizeof(*work));
assert(work != NULL);
work->thread.host_id = 46;
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);
details = prior_details;
free(owned_details);
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_reindex WHERE host_id=46 AND assert_value='%s'", cycles[cycle].stored);
if (reindex_count(mysql, "SELECT COUNT(*) FROM poller_command WHERE command='46:7'") != (unsigned long)cycles[cycle].commands ||
reindex_count(mysql, query) != 1) {
fprintf(stderr, "reindex baseline: cycle=%zu sample=%s expected commands=%d stored=%s\n", cycle, cycles[cycle].sample, cycles[cycle].commands, cycles[cycle].stored);
assert(0);
}
free(work);
}
assert(unlink(sample_path) == 0);
assert(spine_permits_destroy(&available_scripts) == 0);
db_close_connection_pool(LOCAL);
db_pool_local = previous_pool;
set = previous_config;
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_reindex WHERE host_id=46"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_command WHERE command='46:7'"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output WHERE local_data_id=811"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_output_boost WHERE local_data_id=811"));
assert(db_insert(mysql, LOCAL, "DELETE FROM poller_item WHERE local_data_id=811"));
assert(db_insert(mysql, LOCAL, "DELETE FROM host_errors WHERE host_id=46"));
assert(db_insert(mysql, LOCAL, "DELETE FROM host WHERE id=46"));
puts("production reindex result contracts passed");
}
+188
View File
@@ -0,0 +1,188 @@
/* Table tests for classify_result(), linked against the shipped util.c.
*
* Each row is a raw script or SNMP output, what Spine must decide it is, and
* the exact text that reaches poller_output. An output that cannot be read
* as a whole value must come back as U, never as a guess.
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdlib.h>
#include "common.h"
#include "spine.h"
#include "util.h"
typedef struct {
const char *raw;
result_kind_t kind;
const char *text;
} classify_case_t;
static const classify_case_t cases[] = {
/* integers, including the padding scripts and BSD wc -l emit */
{"42", RESULT_COUNTER, "42"},
{" 42", RESULT_COUNTER, "42"},
{"42\n", RESULT_COUNTER, "42"},
{" 10\n", RESULT_COUNTER, "10"},
{"42\r\n", RESULT_COUNTER, "42"},
{"\"42\"", RESULT_COUNTER, "42"},
{"'42'", RESULT_COUNTER, "42"},
{"+7", RESULT_COUNTER, "+7"},
{"-5", RESULT_SIGNED, "-5"},
{"18446744073709551615", RESULT_COUNTER, "18446744073709551615"},
{"20000000000001", RESULT_COUNTER, "20000000000001"},
{"-9223372036854775808", RESULT_SIGNED, "-9223372036854775808"},
/* beyond 64 bits it is still a number, so the text is kept whole */
{"18446744073709551616", RESULT_FLOAT, "18446744073709551616"},
{"-9223372036854775809", RESULT_FLOAT, "-9223372036854775809"},
/* floats */
{"3.14", RESULT_FLOAT, "3.14"},
{"1.5e3", RESULT_FLOAT, "1.5e3"},
{".5", RESULT_FLOAT, ".5"},
{"-12.5", RESULT_FLOAT, "-12.5"},
{"1e999", RESULT_UNKNOWN, "U"},
/* strtod() accepts these; Cacti does not, and neither may we */
{"0x1A", RESULT_UNKNOWN, "U"},
{"nan", RESULT_UNKNOWN, "U"},
{"NaN", RESULT_UNKNOWN, "U"},
{"inf", RESULT_UNKNOWN, "U"},
{"U", RESULT_UNKNOWN, "U"},
/* hex octet strings, converted without a double in between */
{"1F FF FF FF FF FF FF F1", RESULT_HEX_COUNTER, "2305843009213693937"},
{"20 00 00 00 00 00 01", RESULT_HEX_COUNTER, "9007199254740993"},
{"FF FF FF FF FF FF FF FF", RESULT_HEX_COUNTER, "18446744073709551615"},
{"00 00 00 00 00 00 00 00 01", RESULT_HEX_COUNTER, "1"},
{"AA-BB-CC", RESULT_HEX_COUNTER, "11189196"},
{"\"FF ff\t\"", RESULT_HEX_COUNTER, "65535"},
{"01 FF FF FF FF FF FF FF FF", RESULT_UNKNOWN, "U"},
{"FFFFFFFFFFFFFFFF", RESULT_UNKNOWN, "U"},
{"AA BB CC", RESULT_HEX_COUNTER, "11189196"},
/* Cacti splits octets on space, '-' and ':' only */
{"AA\tBB", RESULT_UNKNOWN, "U"},
{"zz", RESULT_UNKNOWN, "U"},
/* a lone octet is hex once it is not decimal, as in Cacti */
{"FF", RESULT_HEX_COUNTER, "255"},
{"ff", RESULT_HEX_COUNTER, "255"},
{" FF", RESULT_HEX_COUNTER, "255"},
{"0A", RESULT_HEX_COUNTER, "10"},
{"1F", RESULT_HEX_COUNTER, "31"},
{"9E", RESULT_HEX_COUNTER, "158"},
{"00", RESULT_COUNTER, "00"},
/* nothing that could be a value */
{"", RESULT_UNKNOWN, "U"},
{" ", RESULT_UNKNOWN, "U"},
{"---", RESULT_UNKNOWN, "U"},
{"no sample", RESULT_UNKNOWN, "U"},
{"1.2.3", RESULT_UNKNOWN, "U"},
/* Cacti multipart output; it wins over hex, as it always has */
{"a:1 b:2", RESULT_MULTIPART, "a:1 b:2"},
{"a:1 b!2", RESULT_MULTIPART, "a:1 b!2"},
{" a:1 b:2\n", RESULT_MULTIPART, "a:1 b:2"},
{"00:1b:44:11:3a:b7", RESULT_MULTIPART, "00:1b:44:11:3a:b7"},
{"ff:ff:ff:ff:ff:ff:ff:ff", RESULT_MULTIPART, "ff:ff:ff:ff:ff:ff:ff:ff"},
{"AA\tBB:CC", RESULT_MULTIPART, "AA\tBB:CC"},
{"a:1 b:2", RESULT_UNKNOWN, "U"},
/* a number wrapped in text, which Cacti's strip_alpha() also accepts */
{"4096 Bytes", RESULT_COUNTER, "4096"},
{"text -12.5 Bytes", RESULT_FLOAT, "-12.5"},
/* once read as the octets 0x042C */
{"-42C", RESULT_SIGNED, "-42"},
};
static void test_classify_table(void **state) {
(void) state;
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
classified_result_t out;
char raw[64];
/* classify a copy, then check the copy is untouched */
strncopy(raw, cases[i].raw, sizeof(raw));
result_kind_t kind = classify_result(raw, &out);
print_message("case %zu: '%s'\n", i, cases[i].raw);
assert_int_equal(kind, cases[i].kind);
assert_int_equal(out.kind, cases[i].kind);
assert_string_equal(out.text, cases[i].text);
assert_string_equal(raw, cases[i].raw);
}
}
static void test_classify_values(void **state) {
classified_result_t out;
(void) state;
assert_int_equal(classify_result("18446744073709551615", &out), RESULT_COUNTER);
assert_true(out.value.counter == UINT64_MAX);
assert_int_equal(classify_result("-9223372036854775808", &out), RESULT_SIGNED);
assert_true(out.value.integer == INT64_MIN);
assert_int_equal(classify_result("1.5e3", &out), RESULT_FLOAT);
assert_true(out.value.real == 1500.0);
/* 2^53 + 1 has no exact double; the octet path must not use one */
assert_int_equal(classify_result("20 00 00 00 00 00 01", &out), RESULT_HEX_COUNTER);
assert_true(out.value.counter == 9007199254740993ULL);
assert_int_equal(classify_result(" 10\n", &out), RESULT_COUNTER);
assert_true(out.value.counter == 10);
assert_int_equal(classify_result(NULL, &out), RESULT_UNKNOWN);
assert_string_equal(out.text, "U");
}
static void test_classify_longer_than_results_buffer(void **state) {
char *raw = malloc(RESULTS_BUFFER * 2);
classified_result_t out;
(void) state;
assert_non_null(raw);
/* Truncating a long value would store a different one. */
memset(raw, '7', RESULTS_BUFFER + 10);
raw[RESULTS_BUFFER + 10] = '\0';
assert_int_equal(classify_result(raw, &out), RESULT_UNKNOWN);
assert_string_equal(out.text, "U");
memset(raw, 'a', RESULTS_BUFFER + 10);
raw[1] = ':';
assert_int_equal(classify_result(raw, &out), RESULT_UNKNOWN);
/* Padding does not count against the buffer. */
memset(raw, ' ', RESULTS_BUFFER + 10);
raw[RESULTS_BUFFER] = '4';
raw[RESULTS_BUFFER + 1] = '2';
assert_int_equal(classify_result(raw, &out), RESULT_COUNTER);
assert_string_equal(out.text, "42");
/* A short value behind a long run of padding is still that value. */
memset(raw, ' ', RESULTS_BUFFER + 10);
raw[RESULTS_BUFFER + 8] = 'f';
raw[RESULTS_BUFFER + 9] = 'f';
assert_int_equal(classify_result(raw, &out), RESULT_HEX_COUNTER);
assert_string_equal(out.text, "255");
/* The longest storable value still fits. */
memset(raw, 'a', RESULTS_BUFFER - 1);
raw[1] = ':';
raw[RESULTS_BUFFER - 1] = '\0';
assert_int_equal(classify_result(raw, &out), RESULT_MULTIPART);
assert_int_equal(strlen(out.text), RESULTS_BUFFER - 1);
free(raw);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_classify_table),
cmocka_unit_test(test_classify_values),
cmocka_unit_test(test_classify_longer_than_results_buffer),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+17 -92
View File
@@ -211,28 +211,6 @@ static void test_is_ipaddress(void **state) {
assert_int_equal(is_ipaddress(""), TRUE); /* vacuously true */ assert_int_equal(is_ipaddress(""), TRUE); /* vacuously true */
} }
static void test_is_numeric(void **state) {
char i[16], d[16], neg[16], txt[16], mixed[16], empty[16];
(void) state;
strcpy(i, "42"); assert_int_equal(is_numeric(i), TRUE);
strcpy(d, "3.14"); assert_int_equal(is_numeric(d), TRUE);
strcpy(neg, "-7"); assert_int_equal(is_numeric(neg), TRUE);
strcpy(txt, "abc"); assert_int_equal(is_numeric(txt), FALSE);
strcpy(mixed, "12abc"); assert_int_equal(is_numeric(mixed), FALSE);
strcpy(empty, ""); assert_int_equal(is_numeric(empty), FALSE);
}
static void test_is_hexadecimal(void **state) {
(void) state;
assert_int_equal(is_hexadecimal("AA BB CC", 0), TRUE);
assert_int_equal(is_hexadecimal("AA\tBB", 0), FALSE);
assert_int_equal(is_hexadecimal("AA\tBB", 1), FALSE);
assert_int_equal(is_hexadecimal("AA\tBB:CC", 1), TRUE);
assert_int_equal(is_hexadecimal("zz", 0), FALSE);
assert_int_equal(is_hexadecimal("", 0), FALSE);
}
/* --- string surgery ------------------------------------------------------- */ /* --- string surgery ------------------------------------------------------- */
static void test_trim_family(void **state) { static void test_trim_family(void **state) {
@@ -303,71 +281,6 @@ static void test_add_slashes_passes_plain_text_through(void **state) {
free(out); free(out);
} }
static void test_hex2dec(void **state) {
char a[32], b[16], overflow[160];
unsigned long long value;
(void) state;
strcpy(a, "FF"); assert_true(hex2dec(a, &value)); assert_int_equal(value, 255);
strcpy(b, "00"); assert_true(hex2dec(b, &value)); assert_int_equal(value, 0);
strcpy(a, "00:1b:44:11:3a:b7");
assert_true(hex2dec(a, &value));
assert_int_equal(value, 0x001b44113ab7ULL);
strcpy(a, "- 0a:1B- 2c :3D ");
assert_true(hex2dec(a, &value));
assert_int_equal(value, 0x0a1b2c3dULL);
strcpy(a, "ff:ff:ff:ff:ff:ff:ff:ff");
assert_true(hex2dec(a, &value));
assert_int_equal(value, ULLONG_MAX);
strcpy(overflow, "10000000000000000");
assert_false(hex2dec(overflow, &value));
strcpy(overflow, "80:00:1f:88:80:00:1f:88:80:00:1f:88:80:00:1f:88:80:00:1f:88:80:00:1f:88:80:00:1f:88:80:00:1f:88");
assert_false(hex2dec(overflow, &value));
strcpy(overflow, "ffff ffff ffff ffff ffff ffff ffff ffff");
assert_false(hex2dec(overflow, &value));
assert_false(hex2dec(NULL, &value));
assert_false(hex2dec("ff", NULL));
assert_false(hex2dec("", &value));
assert_false(hex2dec(":::", &value));
}
static void test_poller_hex_overflow_is_undefined(void **state) {
char result[RESULTS_BUFFER];
char exact[4] = "ff";
char tiny[2] = "f";
char too_small[3] = "ff";
char empty[1] = "";
char long_hex[RESULTS_BUFFER + 16];
int errors = 0;
(void) state;
assert_true(poller_store_hex_result(exact, sizeof(exact), exact, &errors));
assert_string_equal(exact, "255");
strcpy(result, "ff:ff:ff:ff:ff:ff:ff:ff");
assert_true(poller_store_hex_result(result, sizeof(result), result, &errors));
assert_string_equal(result, "18446744073709551615");
assert_int_equal(errors, 0);
strcpy(result, "1:00:00:00:00:00:00:00:00");
assert_false(poller_store_hex_result(result, sizeof(result), result, &errors));
assert_true(IS_UNDEFINED(result));
assert_int_equal(errors, 1);
assert_false(poller_store_hex_result(NULL, 0, "ff", &errors));
assert_false(poller_store_hex_result(empty, 0, "ff", &errors));
assert_false(poller_store_hex_result(tiny, 1, "ff", &errors));
assert_false(poller_store_hex_result(too_small, sizeof(too_small), too_small, &errors));
assert_true(IS_UNDEFINED(too_small));
assert_int_equal(errors, 5);
memset(long_hex, ' ', sizeof(long_hex));
long_hex[sizeof(long_hex) - 3] = 'f';
long_hex[sizeof(long_hex) - 2] = 'f';
long_hex[sizeof(long_hex) - 1] = '\0';
assert_true(poller_store_hex_result(result, sizeof(result), long_hex, &errors));
assert_string_equal(result, "255");
assert_int_equal(errors, 5);
}
static void test_row_alias_upsert_version_gate(void **state) { static void test_row_alias_upsert_version_gate(void **state) {
(void) state; (void) state;
assert_false(db_row_alias_upsert_supported(NULL, 80020)); assert_false(db_row_alias_upsert_supported(NULL, 80020));
@@ -897,6 +810,9 @@ static void test_existing_pipe_on_stdout_does_not_close_a_new_child_redirect(voi
int writer; int writer;
int reader; int reader;
int writer_status; int writer_status;
int wait_status = -1;
pid_t writer_pid;
siginfo_t writer_info = {0};
int reader_status; int reader_status;
ssize_t bytes; ssize_t bytes;
char output[32] = {0}; char output[32] = {0};
@@ -912,10 +828,19 @@ static void test_existing_pipe_on_stdout_does_not_close_a_new_child_redirect(voi
/* The write-mode parent retains fd 1 in PidList. The following read-mode /* The write-mode parent retains fd 1 in PidList. The following read-mode
* child also redirects its new pipe onto fd 1. Its later PidList close walk * child also redirects its new pipe onto fd 1. Its later PidList close walk
* must not close that newly installed stdout. */ * must not close that newly installed stdout. */
writer = nft_popen("cat >/dev/null", "w"); writer = nft_popen("exit 0", "w");
writer_pid = writer >= 0 ? nft_pchild(writer) : -1;
reader = nft_popen("printf second-child-visible", "r"); reader = nft_popen("printf second-child-visible", "r");
bytes = reader >= 0 ? read(reader, output, sizeof(output) - 1) : -1; bytes = reader >= 0 ? read(reader, output, sizeof(output) - 1) : -1;
reader_status = reader >= 0 ? nft_pclose(reader) : -1; reader_status = reader >= 0 ? nft_pclose(reader) : -1;
/* Keep the writer entry and fd 1 registered until after the reader has
* spawned. Observe its exit without reaping, so close tests ownership
* rather than the runner scheduling the child within the reap budget. */
if (writer_pid > 0) {
do {
wait_status = waitid(P_PID, (id_t)writer_pid, &writer_info, WEXITED | WNOWAIT);
} while (wait_status < 0 && errno == EINTR);
}
writer_status = writer >= 0 ? nft_pclose(writer) : -1; writer_status = writer >= 0 ? nft_pclose(writer) : -1;
dup2(saved_stdin, STDIN_FILENO); dup2(saved_stdin, STDIN_FILENO);
@@ -929,6 +854,10 @@ static void test_existing_pipe_on_stdout_does_not_close_a_new_child_redirect(voi
assert_string_equal(output, "second-child-visible"); assert_string_equal(output, "second-child-visible");
assert_true(WIFEXITED(reader_status)); assert_true(WIFEXITED(reader_status));
assert_int_equal(WEXITSTATUS(reader_status), 0); assert_int_equal(WEXITSTATUS(reader_status), 0);
assert_int_equal(wait_status, 0);
assert_int_equal(writer_info.si_pid, writer_pid);
assert_int_equal(writer_info.si_code, CLD_EXITED);
assert_int_equal(writer_info.si_status, 0);
assert_true(WIFEXITED(writer_status)); assert_true(WIFEXITED(writer_status));
assert_int_equal(WEXITSTATUS(writer_status), 0); assert_int_equal(WEXITSTATUS(writer_status), 0);
} }
@@ -1153,8 +1082,6 @@ int main(void) {
cmocka_unit_test(test_spine_appendf_reports_truncation_and_guards), cmocka_unit_test(test_spine_appendf_reports_truncation_and_guards),
cmocka_unit_test(test_all_digits), cmocka_unit_test(test_all_digits),
cmocka_unit_test(test_is_ipaddress), cmocka_unit_test(test_is_ipaddress),
cmocka_unit_test(test_is_numeric),
cmocka_unit_test(test_is_hexadecimal),
cmocka_unit_test(test_trim_family), cmocka_unit_test(test_trim_family),
cmocka_unit_test(test_reverse), cmocka_unit_test(test_reverse),
cmocka_unit_test(test_strpos), cmocka_unit_test(test_strpos),
@@ -1162,8 +1089,6 @@ int main(void) {
cmocka_unit_test(test_strip_alpha), cmocka_unit_test(test_strip_alpha),
cmocka_unit_test(test_add_slashes_doubles_a_backslash), cmocka_unit_test(test_add_slashes_doubles_a_backslash),
cmocka_unit_test(test_add_slashes_passes_plain_text_through), cmocka_unit_test(test_add_slashes_passes_plain_text_through),
cmocka_unit_test(test_hex2dec),
cmocka_unit_test(test_poller_hex_overflow_is_undefined),
cmocka_unit_test(test_row_alias_upsert_version_gate), cmocka_unit_test(test_row_alias_upsert_version_gate),
cmocka_unit_test(test_file_exists), cmocka_unit_test(test_file_exists),
cmocka_unit_test(test_get_time_as_double_advances), cmocka_unit_test(test_get_time_as_double_advances),
+515
View File
@@ -0,0 +1,515 @@
/* Drives the shipped poll_host() without a database or an SNMP agent.
*
* GNU ld --wrap replaces the database and SNMP calls that poller.o makes into
* sql.o, snmp.o and the client library with an in-memory fake, so the flush,
* reindex and completion paths run in an unprivileged "make check". The live
* suites in tests/test_spine_*.c cover the same paths against real servers.
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <stdlib.h>
#include <string.h>
#include "common.h"
#include "spine.h"
#define HOST_COLUMNS 37
#define REINDEX_COLUMNS 5
#define ITEM_COLUMNS 21
#define MAX_FAKE_ROWS 4
#define MAX_QUERIES 64
extern poller_thread_t **details;
typedef struct {
char **rows[MAX_FAKE_ROWS];
int count;
int columns;
int next;
} fake_result_t;
static struct {
const char *host[HOST_COLUMNS];
char reindex[MAX_FAKE_ROWS][REINDEX_COLUMNS][BUFSIZE];
int reindex_count;
const char *items[MAX_FAKE_ROWS][ITEM_COLUMNS];
int item_count;
const char *multi_value;
const char *engine_uptime;
const char *legacy_uptime;
int session_opens;
int session_timeouts[MAX_QUERIES];
char session_hostnames[MAX_QUERIES][BUFSIZE];
char *queries[MAX_QUERIES];
int query_count;
int completion_queries;
int completion_queries_locked;
} fake;
static pool_t fake_pool;
static int fake_session;
static void reset_fake(void) {
for (int i = 0; i < fake.query_count; i++) free(fake.queries[i]);
memset(&fake, 0, sizeof(fake));
}
static void capture(const char *query) {
assert_true(fake.query_count < MAX_QUERIES);
fake.queries[fake.query_count] = strdup(query);
assert_non_null(fake.queries[fake.query_count]);
fake.query_count++;
}
static int captured(const char *needle) {
int found = 0;
for (int i = 0; i < fake.query_count; i++) {
if (strstr(fake.queries[i], needle) != NULL) found++;
}
return found;
}
static fake_result_t *make_result(int count, int columns, const char *(*cell)(int, int)) {
fake_result_t *result = calloc(1, sizeof(*result));
assert_non_null(result);
result->count = count;
result->columns = columns;
for (int row = 0; row < count; row++) {
result->rows[row] = calloc((size_t)columns, sizeof(char *));
assert_non_null(result->rows[row]);
for (int column = 0; column < columns; column++) {
const char *value = cell(row, column);
result->rows[row][column] = value == NULL ? NULL : strdup(value);
}
}
return result;
}
static const char *host_cell(int row, int column) {
(void) row;
return fake.host[column];
}
static const char *reindex_cell(int row, int column) {
return fake.reindex[row][column];
}
static const char *item_cell(int row, int column) {
return fake.items[row][column];
}
/* Persist the one write the next cycle reads back: the reindex baseline. */
static void apply_reindex_update(const char *query) {
const char *prefix = "UPDATE poller_reindex SET assert_value='";
if (strncmp(query, prefix, strlen(prefix)) != 0) return;
const char *value = query + strlen(prefix);
const char *end = strchr(value, '\'');
assert_non_null(end);
snprintf(fake.reindex[0][3], sizeof(fake.reindex[0][3]), "%.*s", (int)(end - value), value);
}
/* Device completion writes must not hold LOCK_THDET while they wait on the
* server, whichever db_* call carries them. */
static void observe_completion(const char *query) {
if (strncmp(query, "UPDATE poller_item SET rrd_next_step", 36) == 0 ||
strncmp(query, "UPDATE host SET polling_time", 28) == 0 ||
strncmp(query, "INSERT INTO host_errors", 23) == 0) {
fake.completion_queries++;
if (thread_mutex_trylock(LOCK_THDET) == 0) thread_mutex_unlock(LOCK_THDET);
else fake.completion_queries_locked++;
}
}
MYSQL_RES *__wrap_db_query(MYSQL *mysql, int type, const char *query) {
(void) mysql;
(void) type;
if (strstr(query, "FROM host WHERE id") != NULL) {
return (MYSQL_RES *) make_result(1, HOST_COLUMNS, host_cell);
}
if (strstr(query, "FROM poller_reindex") != NULL) {
return (MYSQL_RES *) make_result(fake.reindex_count, REINDEX_COLUMNS, reindex_cell);
}
if (strstr(query, "SELECT SQL_NO_CACHE action") != NULL) {
return (MYSQL_RES *) make_result(fake.item_count, ITEM_COLUMNS, item_cell);
}
capture(query);
observe_completion(query);
return NULL;
}
int __wrap_db_insert(MYSQL *mysql, int type, const char *query) {
(void) mysql;
(void) type;
capture(query);
observe_completion(query);
apply_reindex_update(query);
return TRUE;
}
void __wrap_db_escape(MYSQL *mysql, char *output, int max_size, const char *input) {
(void) mysql;
if (input == NULL || output == NULL || max_size <= 0) return;
snprintf(output, (size_t) max_size, "%s", input);
}
pool_t *__wrap_db_get_connection(int type) {
(void) type;
return &fake_pool;
}
void __wrap_db_release_connection(int type, int id) {
(void) type;
(void) id;
}
void __wrap_db_free_result(MYSQL_RES *opaque) {
fake_result_t *result = (fake_result_t *) opaque;
if (result == NULL) return;
for (int row = 0; row < result->count; row++) {
for (int column = 0; column < result->columns; column++) free(result->rows[row][column]);
free(result->rows[row]);
}
free(result);
}
uint64_t __wrap_mysql_num_rows(MYSQL_RES *opaque) {
return (uint64_t) ((fake_result_t *) opaque)->count;
}
MYSQL_ROW __wrap_mysql_fetch_row(MYSQL_RES *opaque) {
fake_result_t *result = (fake_result_t *) opaque;
if (result->next >= result->count) return NULL;
return result->rows[result->next++];
}
void *__wrap_snmp_host_init(const snmp_connection_t *options) {
assert_true(fake.session_opens < MAX_QUERIES);
fake.session_timeouts[fake.session_opens] = options->snmp_timeout;
snprintf(fake.session_hostnames[fake.session_opens], BUFSIZE, "%s", options->hostname);
fake.session_opens++;
return &fake_session;
}
void __wrap_snmp_host_cleanup(void *session) {
assert_ptr_equal(session, &fake_session);
}
/* Mirrors snmp_get_base(): a failure is "" when allowed and U otherwise. */
static char *fake_get(host_t *host, const char *oid, bool should_fail) {
const char *value = NULL;
if (strcmp(oid, ".1.3.6.1.6.3.10.2.1.3.0") == 0) value = fake.engine_uptime;
else if (strcmp(oid, ".1.3.6.1.2.1.1.3.0") == 0) value = fake.legacy_uptime;
else value = "fake-system-field";
if (value == NULL) {
if (should_fail) host->ignore_host = TRUE;
value = should_fail ? "U" : "";
}
char *output = calloc(RESULTS_BUFFER, 1);
assert_non_null(output);
snprintf(output, RESULTS_BUFFER, "%s", value);
return output;
}
char *__wrap_snmp_get_base(host_t *host, char *oid, bool should_fail) {
return fake_get(host, oid, should_fail);
}
char *__wrap_snmp_get(host_t *host, char *oid) {
return fake_get(host, oid, TRUE);
}
char *__wrap_snmp_get_allow_fail(host_t *host, char *oid) {
return fake_get(host, oid, FALSE);
}
void __wrap_snmp_get_multi(host_t *host, const target_t *items, snmp_oids_t *oids, int count) {
(void) host;
(void) items;
for (int i = 0; i < count; i++) {
snprintf(oids[i].result, sizeof(oids[i].result), "%s", fake.multi_value);
}
}
int __wrap_ping_host(host_t *host, ping_t *ping) {
(void) host;
(void) ping;
return HOST_UP;
}
static void set_host(const char *community, const char *max_oids, int availability) {
static char method[16];
snprintf(method, sizeof(method), "%d", availability);
fake.host[0] = "47";
fake.host[1] = "127.0.0.1";
fake.host[2] = community;
fake.host[3] = "2";
fake.host[11] = "161";
fake.host[12] = "500";
fake.host[13] = max_oids;
fake.host[14] = method;
fake.host[21] = "0";
fake.host[22] = "0";
}
static void add_reindex(int action, const char *op, const char *assert_value, const char *arg1) {
int row = fake.reindex_count++;
snprintf(fake.reindex[row][0], BUFSIZE, "7");
snprintf(fake.reindex[row][1], BUFSIZE, "%d", action);
snprintf(fake.reindex[row][2], BUFSIZE, "%s", op);
snprintf(fake.reindex[row][3], BUFSIZE, "%s", assert_value);
snprintf(fake.reindex[row][4], BUFSIZE, "%s", arg1);
}
static void add_snmp_item(const char *local_data_id, const char *rrd_name, const char *community,
const char *version, const char *regex) {
const char **row = fake.items[fake.item_count++];
row[0] = "0";
row[1] = "127.0.0.1";
row[2] = community;
row[3] = version;
row[6] = rrd_name;
row[8] = ".1.3.6.1.2.1.1.3.0";
row[11] = local_data_id;
row[13] = "161";
row[14] = "500";
row[20] = regex;
}
static void add_script_item(const char *local_data_id, const char *rrd_name, const char *command) {
const char **row = fake.items[fake.item_count++];
row[0] = "1";
row[6] = rrd_name;
row[8] = command;
row[11] = local_data_id;
}
static int run_poll(void) {
poller_thread_t work = {0};
poller_thread_t *device = &work;
int errors = 0;
work.host_id = 47;
work.host_thread = 1;
work.host_threads = 1;
work.host_data_ids = 0;
work.host_time_double = get_time_as_double();
STRNCOPY(work.host_time, "1791158400");
details = &device;
poll_host(&work, &errors);
details = NULL;
assert_true(work.complete);
return errors;
}
static int setup(void **state) {
(void) state;
reset_fake();
config_defaults();
set.poller.poller_id = 1;
set.poller.threads = 1;
set.poller.active_profiles = 1;
set.poller.mode = REMOTE_OFFLINE;
set.php.script_timeout = 2;
set.hosts.has_output_regex = TRUE;
set.snmp.total_snmp_ports = 1;
fake_pool.free = TRUE;
return 0;
}
/* Each item leaves through a different flush. 601 goes out when the batch is
* full (max_oids=1) or when 602's credentials differ; 602 is the final flush.
* All three apply the same regex, to the normalized value, and spike kill. */
static void test_flush_paths_agree(void **state) {
(void) state;
for (int scenario = 0; scenario < 4; scenario++) {
bool full_batch = (scenario & 1) != 0;
bool spike = (scenario & 2) != 0;
setup(NULL);
set_host("regression", full_batch ? "1" : "5", AVAIL_NONE);
add_snmp_item("601", "a", "regression", "2", "^[0-9][0-9]*");
add_snmp_item("602", "b", "regression", full_batch ? "2" : "1", "^[0-9][0-9]*");
if (spike) add_reindex(POLLER_ACTION_SCRIPT, "<", "124", "/usr/bin/printf 123");
fake.multi_value = "00 00 01 0A";
run_poll();
const char *value = spike ? "'U')" : "'266')";
char row[128];
snprintf(row, sizeof(row), "(601, 'a', FROM_UNIXTIME(1791158400), %s", value);
print_message("scenario %d full_batch=%d spike=%d\n", scenario, full_batch, spike);
assert_int_equal(captured(row), 1);
snprintf(row, sizeof(row), "(602, 'b', FROM_UNIXTIME(1791158400), %s", value);
assert_int_equal(captured(row), 1);
}
}
/* A community longer than the old 49-byte key must still batch. */
static void test_long_community_batches(void **state) {
static const char community[] = "regression-community-wider-than-the-old-fifty-byte-batch-key";
(void) state;
set_host(community, "10", AVAIL_NONE);
add_snmp_item("611", "a", community, "2", NULL);
add_snmp_item("612", "b", community, "2", NULL);
add_snmp_item("613", "c", community, "2", NULL);
fake.multi_value = "42";
assert_int_equal(run_poll(), 0);
/* one session for the device checks, one for the single batch */
assert_int_equal(fake.session_opens, 2);
assert_int_equal(captured("'42')"), 1);
}
static void test_snmp_batches_on_hostname(void **state) {
(void) state;
set_host("regression", "10", AVAIL_NONE);
add_snmp_item("621", "a", "regression", "2", NULL);
add_snmp_item("622", "b", "regression", "2", NULL);
fake.items[1][1] = "127.0.0.2";
fake.multi_value = "42";
assert_int_equal(run_poll(), 0);
assert_int_equal(fake.session_opens, 3);
assert_string_equal(fake.session_hostnames[1], "127.0.0.1");
assert_string_equal(fake.session_hostnames[2], "127.0.0.2");
}
/* SNMPv3 items batch on every credential field; a different context
* starts a new session. */
static void test_snmpv3_batches_on_profile(void **state) {
(void) state;
set_host("regression", "10", AVAIL_NONE);
add_snmp_item("631", "a", "", "3", NULL);
add_snmp_item("632", "b", "", "3", NULL);
add_snmp_item("633", "c", "", "3", NULL);
for (int i = 0; i < 3; i++) {
fake.items[i][4] = "regression-user";
fake.items[i][5] = "regression-pass";
fake.items[i][15] = "SHA";
fake.items[i][16] = "regression-priv";
fake.items[i][17] = "AES";
fake.items[i][19] = "80001f8880";
}
fake.items[0][18] = "one";
fake.items[1][18] = "one";
fake.items[2][18] = "two";
fake.multi_value = "7";
assert_int_equal(run_poll(), 0);
/* device session, then one per distinct profile */
assert_int_equal(fake.session_opens, 3);
assert_int_equal(captured("(633, 'c', FROM_UNIXTIME(1791158400), '7')"), 1);
}
/* Otherwise identical items share a session only while their timeouts match. */
static void test_snmp_batches_on_timeout(void **state) {
(void) state;
set_host("regression", "10", AVAIL_NONE);
add_snmp_item("641", "a", "regression", "2", NULL);
add_snmp_item("642", "b", "regression", "2", NULL);
add_snmp_item("643", "c", "regression", "2", NULL);
fake.items[2][14] = "1500";
fake.multi_value = "7";
assert_int_equal(run_poll(), 0);
assert_int_equal(fake.session_opens, 3);
assert_int_equal(fake.session_timeouts[1], 500);
assert_int_equal(fake.session_timeouts[2], 1500);
assert_int_equal(captured("(643, 'c', FROM_UNIXTIME(1791158400), '7')"), 1);
}
/* Good baseline, unreadable cycle, reboot: the reboot must still fire. */
static void test_uptime_baseline_survives_unknown(void **state) {
static const struct {
const char *engine;
const char *legacy;
int commands;
const char *stored;
} cycles[] = {
{"5", NULL, 0, "500"},
{NULL, NULL, 0, "500"},
/* a negative engine time is not an uptime; the legacy OID answers */
{"-5", "600", 0, "600"},
{"4096 Bytes", "600", 0, "600"},
{"3.5", "600", 0, "600"},
{"3e2", "600", 0, "600"},
{"+3", "600", 0, "600"},
{"", "4096 Bytes", 0, "600"},
{"18446744073709551616", "600", 0, "600"},
{"18446744073709551615", "600", 0, "600"},
{" \t6\r\n", NULL, 0, "600"},
{"1", NULL, 1, "100"},
};
(void) state;
set_host("regression", "5", AVAIL_NONE);
add_reindex(POLLER_ACTION_SNMP, "<", "0", ".1.3.6.1.2.1.1.3.0");
for (size_t i = 0; i < sizeof(cycles) / sizeof(cycles[0]); i++) {
for (int q = 0; q < fake.query_count; q++) free(fake.queries[q]);
fake.query_count = 0;
fake.engine_uptime = cycles[i].engine;
fake.legacy_uptime = cycles[i].legacy;
run_poll();
print_message("cycle %zu stored=%s\n", i, fake.reindex[0][3]);
assert_int_equal(captured("REPLACE INTO poller_command"), cycles[i].commands);
assert_string_equal(fake.reindex[0][3], cycles[i].stored);
}
}
/* Padded script output is trimmed, then freed through its own pointer; the
* device's error list starts empty, and completion writes run unlocked. */
static void test_script_reindex_and_completion(void **state) {
(void) state;
set_host("", "5", AVAIL_NONE);
set.logging.spine_log_level = 1;
set.poller.active_profiles = 2;
set.poller.poller_interval = 300;
add_reindex(POLLER_ACTION_SCRIPT, "=", "122", "/usr/bin/printf ' 123'");
add_script_item("621", "a", "/usr/bin/printf 123");
assert_int_equal(run_poll(), 1);
assert_int_equal(captured("REPLACE INTO poller_command"), 1);
assert_string_equal(fake.reindex[0][3], "123");
assert_int_equal(captured("VALUES(47, 1, 1, '')"), 1);
assert_int_equal(fake.completion_queries, 3);
assert_int_equal(fake.completion_queries_locked, 0);
}
/* sysUpTimeInstance takes the engine time in ticks when both OIDs read, and
* ignores an uptime that is not a whole non-negative number. */
static void test_system_uptime(void **state) {
static const struct { const char *legacy; const char *engine; const char *stored; } cases[] = {
{"4200", "42", "snmp_sysUpTimeInstance='4200'"},
{"4200", "-1", "snmp_sysUpTimeInstance='4200'"},
{"-5", "42", "snmp_sysUpTimeInstance='0'"},
{"4096 Bytes", "42", "snmp_sysUpTimeInstance='0'"},
{"4200", "4096 Bytes", "snmp_sysUpTimeInstance='4200'"},
{"4200", "18446744073709551615", "snmp_sysUpTimeInstance='4200'"},
{"18446744073709551616", "42", "snmp_sysUpTimeInstance='0'"},
{" \t4200\r\n", " \t42\r\n", "snmp_sysUpTimeInstance='4200'"},
};
(void) state;
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
setup(NULL);
set.snmp.mibs = TRUE;
set_host("regression", "5", AVAIL_SNMP);
fake.legacy_uptime = cases[i].legacy;
fake.engine_uptime = cases[i].engine;
run_poll();
print_message("case %zu\n", i);
assert_int_equal(captured(cases[i].stored), 1);
}
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test_setup(test_flush_paths_agree, setup),
cmocka_unit_test_setup(test_long_community_batches, setup),
cmocka_unit_test_setup(test_snmp_batches_on_hostname, setup),
cmocka_unit_test_setup(test_snmpv3_batches_on_profile, setup),
cmocka_unit_test_setup(test_snmp_batches_on_timeout, setup),
cmocka_unit_test_setup(test_uptime_baseline_survives_unknown, setup),
cmocka_unit_test_setup(test_script_reindex_and_completion, setup),
cmocka_unit_test_setup(test_system_uptime, setup),
};
int failures;
init_mutexes();
assert(spine_permits_init(&available_scripts, 2) == 0);
failures = cmocka_run_group_tests(tests, NULL, NULL);
reset_fake();
return failures;
}
+179 -129
View File
@@ -1640,104 +1640,203 @@ int is_ipaddress(const char *string) {
return TRUE; return TRUE;
} }
/*! \fn int is_numeric(const char *string) /* Cacti's prepare_validate_result() trims quotes and line ends; Spine has
* \brief check to see if a string is long or double * always trimmed blanks, tabs and backslashes as well. */
* \param string the string to check static const char result_padding[] = " \"'\\\t\n\r";
*
* \return TRUE if long or double, FALSE if not
*
*/
int is_numeric(char *string) {
char *end_ptr_long;
char *end_ptr_double;
int conv_base=10;
size_t length;
length = strlen(trim(string)); /* PHP is_numeric() grammar: no hex, no "0x", no inf or nan, which strtod()
* would all accept. */
static bool decimal_syntax(const char *text, bool *integral) {
const char *cursor = text;
size_t digits = 0;
if (!length) { *integral = TRUE;
return FALSE; if (*cursor == '+' || *cursor == '-') cursor++;
while (isdigit((unsigned char)*cursor)) {
cursor++;
digits++;
} }
if (*cursor == '.') {
/* check for an integer */ *integral = FALSE;
errno = 0; cursor++;
strtol(string, &end_ptr_long, conv_base); while (isdigit((unsigned char)*cursor)) {
cursor++;
if (errno != ERANGE) { digits++;
if (end_ptr_long == string + length) { /* integer string */
return TRUE;
} else if ((end_ptr_long == string) && (*end_ptr_long != '\0' &&
*end_ptr_long != '.' &&
*end_ptr_long != '-' &&
*end_ptr_long != '+')) { /* ignore partial string matches but doubles can begin with '+', '-', '.' */
return FALSE;
} }
} else {
end_ptr_long = NULL;
} }
if (digits == 0) return FALSE;
/* check for a float */ if (*cursor == 'e' || *cursor == 'E') {
errno = 0; *integral = FALSE;
strtod(string, &end_ptr_double); cursor++;
if (errno != ERANGE) { if (*cursor == '+' || *cursor == '-') cursor++;
if (end_ptr_double == string + length) { /* floating point string */ if (!isdigit((unsigned char)*cursor)) return FALSE;
return TRUE; while (isdigit((unsigned char)*cursor)) cursor++;
}
} else {
end_ptr_double = NULL;
} }
return *cursor == '\0';
return FALSE;
} }
/*! \fn int is_hexadecimal(const char *str, const short ignore_space) static bool parse_decimal(const char *text, classified_result_t *out) {
* \brief test whether a string represents a hex number. bool integral;
* \param str string to test char *end;
* \param ignore_space nonzero to skip tabs and spaces double real;
*
* \return TRUE if the string is valid hex, FALSE otherwise
*
* The function is modified where the string needs to include
* at least one of the following string ' ', '-', or ':'
*
*/
int is_hexadecimal(const char * str, const short ignore_special) {
int i = 0;
int delim_found = FALSE;
if (!str) return FALSE; if (!decimal_syntax(text, &integral)) return FALSE;
while (*str) { if (integral) {
switch (*str) { errno = 0;
case '0': case '1': case '2': case '3': if (text[0] == '-') {
case '4': case '5': case '6': case '7': long long value = strtoll(text, &end, 10);
case '8': case '9': if (errno == 0 && *end == '\0') {
case 'a': case 'A': case 'b': case 'B': out->kind = RESULT_SIGNED;
case 'c': case 'C': case 'd': case 'D': out->value.integer = (int64_t)value;
case 'e': case 'E': case 'f': case 'F': return TRUE;
case '"': }
break; } else {
case '-': case ':': case ' ': unsigned long long value = strtoull(text, &end, 10);
delim_found = TRUE; if (errno == 0 && *end == '\0') {
break; out->kind = RESULT_COUNTER;
case '\t': out->value.counter = (uint64_t)value;
if (!ignore_special) return FALSE; return TRUE;
break; }
default:
return FALSE;
} }
/* Wider than 64 bits is still a number; the text is stored as is. */
str++;
i++;
} }
if ((i < 3) || delim_found == FALSE) { errno = 0;
real = strtod(text, &end);
if (*end != '\0' || !isfinite(real) || (errno == ERANGE && fabs(real) == HUGE_VAL)) {
return FALSE; return FALSE;
} }
out->kind = RESULT_FLOAT;
out->value.real = real;
return TRUE; return TRUE;
} }
/* Cacti's "name:value name:value" form: one ':' or '!' per field, fields
* separated by single spaces. */
static bool multipart_syntax(const char *text) {
size_t spaces = 0;
size_t delimiters = 0;
if (strchr(text, ':') == NULL && strchr(text, '!') == NULL) return FALSE;
for (const char *cursor = text; *cursor != '\0'; cursor++) {
if (*cursor == ':' || *cursor == '!') delimiters++;
else if (*cursor == ' ') spaces++;
}
return spaces == 0 || spaces + 1 == delimiters;
}
static int hex_digit(int c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
typedef enum {
OCTETS_NONE,
OCTETS_VALUE,
OCTETS_OVERFLOW
} octets_t;
/* An OctetString such as a Hex-STRING Counter64: two-digit octets separated
* by one space, '-' or ':', as Cacti's is_hexadecimal() requires. Like Cacti,
* a lone octet such as "FF" counts; "42" never gets here as it is decimal. */
static octets_t parse_octets(const char *text, uint64_t *value) {
const char *cursor = text;
uint64_t number = 0;
bool overflow = FALSE;
for (;;) {
int high = hex_digit((unsigned char)cursor[0]);
int low = high < 0 ? -1 : hex_digit((unsigned char)cursor[1]);
if (low < 0) return OCTETS_NONE;
if (number > (UINT64_MAX >> 8)) overflow = TRUE;
number = (number << 8) | (uint64_t)(high * 16 + low);
cursor += 2;
if (*cursor == '\0') break;
if (*cursor != ' ' && *cursor != ':' && *cursor != '-') {
return OCTETS_NONE;
}
cursor++;
}
if (overflow) return OCTETS_OVERFLOW;
*value = number;
return OCTETS_VALUE;
}
/*! \fn result_kind_t classify_result(const char *raw, classified_result_t *out)
* \brief decide what a script or SNMP agent returned and what to store
* \param raw the output as received; it is not modified
* \param out receives the kind, the parsed value and the text to store
*
* The order is the one Spine has always used: a decimal number, Cacti's
* multipart output, a hex octet string, then a decimal number wrapped in
* text such as "4096 Bytes". Anything else, including output too long to
* store whole, is unknown and stored as U rather than as a guess.
*
* \return the kind, also stored in out->kind
*/
result_kind_t classify_result(const char *raw, classified_result_t *out) {
const char *start;
const char *end;
const char *core;
size_t length;
uint64_t octets;
assert(out != NULL);
out->kind = RESULT_UNKNOWN;
out->value.counter = 0;
SET_UNDEFINED(out->text);
if (raw == NULL) return RESULT_UNKNOWN;
start = raw + strspn(raw, result_padding);
if (*start == '\0') return RESULT_UNKNOWN;
/* start[0] is not padding, so this stops with at least one byte left */
end = start + strlen(start);
while (strchr(result_padding, end[-1]) != NULL) end--;
length = (size_t)(end - start);
if (length >= sizeof(out->text)) return RESULT_UNKNOWN;
memcpy(out->text, start, length);
out->text[length] = '\0';
if (parse_decimal(out->text, out)) return out->kind;
if (multipart_syntax(out->text)) {
out->kind = RESULT_MULTIPART;
return out->kind;
}
switch (parse_octets(out->text, &octets)) {
case OCTETS_VALUE:
out->kind = RESULT_HEX_COUNTER;
out->value.counter = octets;
snprintf(out->text, sizeof(out->text), "%llu", (unsigned long long)octets);
return out->kind;
case OCTETS_OVERFLOW:
/* Stripping letters from a counter wider than 64 bits would leave
* its leading octet looking like a small decimal sample. */
break;
case OCTETS_NONE:
/* strip_alpha() points into out->text, so move the number to the front. */
core = strip_alpha(out->text);
if (parse_decimal(core, out)) {
memmove(out->text, core, strlen(core) + 1);
return out->kind;
}
break;
}
out->kind = RESULT_UNKNOWN;
out->value.counter = 0;
SET_UNDEFINED(out->text);
return RESULT_UNKNOWN;
}
/*! \fn char *strip_alpha(char *string) /*! \fn char *strip_alpha(char *string)
* \brief remove trailing alpha characters from a string. * \brief remove trailing alpha characters from a string.
* \param string the string to strip characters from * \param string the string to strip characters from
@@ -1930,55 +2029,6 @@ int char_count(const char *str, int chr) {
return count; return count;
} }
int hex2dec(const char *str, unsigned long long *result) {
unsigned long long number = 0;
unsigned int digit;
int saw_digit = FALSE;
if (str == NULL || result == NULL) return FALSE;
while (*str) {
switch (*str) {
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
digit = (unsigned int) (*str - '0');
break;
case 'a': case 'A':
case 'b': case 'B':
case 'c': case 'C':
case 'd': case 'D':
case 'e': case 'E':
case 'f': case 'F':
digit = (unsigned int) (tolower((unsigned char) *str) - 'a' + 10);
break;
/* separators. is_hexadecimal() accepts '-' and ':' as well as space,
* so anything it lets through has to be convertible here; skipping
* only space meant a dash-separated octet string validated and then
* converted to zero. */
case '"': case ' ': case '\t': case '-': case ':':
str++;
continue;
default:
return FALSE;
}
/* A device can return an arbitrarily long string. Refuse overflow
* before multiplying rather than converting an out-of-range double. */
if (number > (ULLONG_MAX - digit) / 16) {
return FALSE;
}
number = (number * 16) + digit;
saw_digit = TRUE;
str++;
}
if (!saw_digit) return FALSE;
*result = number;
return TRUE;
}
#ifdef HAVE_LCAP #ifdef HAVE_LCAP
/* This patch is adapted (copied) patch for ntpd from Jarno Huuskonen and /* This patch is adapted (copied) patch for ntpd from Jarno Huuskonen and
* Pekka Savola that was adapted (copied) from a patch by Chris Wings to drop * Pekka Savola that was adapted (copied) from a patch by Chris Wings to drop
+22 -5
View File
@@ -52,10 +52,30 @@ extern void die(const char *format, ...)
extern void set_option(const char *setting, const char *value); extern void set_option(const char *setting, const char *value);
/* number validation functions */ /* number validation functions */
extern int is_numeric(char *string);
extern int is_ipaddress(const char *string); extern int is_ipaddress(const char *string);
extern int all_digits(const char *str); extern int all_digits(const char *str);
extern int is_hexadecimal(const char * str, const short ignore_special);
/* what a poll returned; text is what poller_output stores, U if unknown */
typedef enum {
RESULT_UNKNOWN = 0,
RESULT_COUNTER,
RESULT_SIGNED,
RESULT_FLOAT,
RESULT_HEX_COUNTER,
RESULT_MULTIPART
} result_kind_t;
typedef struct {
result_kind_t kind;
union {
uint64_t counter;
int64_t integer;
double real;
} value;
char text[RESULTS_BUFFER];
} classified_result_t;
extern result_kind_t classify_result(const char *raw, classified_result_t *out);
/* determine if a device is a debug device */ /* determine if a device is a debug device */
extern int is_debug_device(int device_id); extern int is_debug_device(int device_id);
@@ -78,9 +98,6 @@ 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); extern int char_count(const char *str, int chr);
/* convert a delimited hexadecimal value without conflating overflow with 0 */
int hex2dec(const char *str, unsigned long long *result);
/* custom regex replace to return a value if matches */ /* custom regex replace to return a value if matches */
#define MAX_MATCHES 5 #define MAX_MATCHES 5
#define REGEX_NUMBER "([-+]*)([0-9]*)([.][0-9]+)" #define REGEX_NUMBER "([-+]*)([0-9]*)([.][0-9]+)"