mirror of
https://github.com/Cacti/spine.git
synced 2026-10-23 03:27:10 +09:00
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:
1 parent
898d8c692c
commit
9be5f8ad2e
16 files changed
+1508
-422
No files matched your search
@@ -132,7 +132,7 @@ jobs:
|
||||
./bootstrap
|
||||
./configure --enable-warnings --with-snmp="$net_snmp_prefix"
|
||||
make -j"$(sysctl -n hw.ncpu)"
|
||||
make check
|
||||
make check || { cat test-suite.log; exit 1; }
|
||||
|
||||
cppcheck:
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
+21
-1
@@ -122,6 +122,7 @@ check_PROGRAMS += \
|
||||
tests/unit/test_build_fixes \
|
||||
tests/unit/test_safety_fixes \
|
||||
tests/unit/test_linked \
|
||||
tests/unit/test_classify_result \
|
||||
tests/unit/test_util_paths \
|
||||
tests/unit/test_snmpv3_session \
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -171,6 +172,11 @@ tests_unit_test_linked_LDFLAGS = -Wl,--wrap=waitpid
|
||||
tests_unit_test_linked_CPPFLAGS += -DSPINE_TEST_WRAP_WAITPID
|
||||
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
|
||||
tests_unit_test_util_paths_SOURCES = tests/unit/test_util_paths.c
|
||||
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
|
||||
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 \
|
||||
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
|
||||
|
||||
+92
-6
@@ -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_safety_fixes \
|
||||
@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_snmpv3_session \
|
||||
@HAVE_CMOCKA_TRUE@ tests/unit/test_ping_icmp
|
||||
|
||||
@HAVE_CMOCKA_TRUE@noinst_PROGRAMS = \
|
||||
@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
|
||||
subdir = .
|
||||
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_safety_fixes$(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_snmpv3_session$(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)
|
||||
am__dirstamp = $(am__leading_dot)dirstamp
|
||||
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__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 = \
|
||||
tests/unit/test_linked-test_linked.$(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)
|
||||
tests_unit_test_ping_icmp_DEPENDENCIES = $(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 = \
|
||||
tests/unit/test_safety_fixes.$(OBJEXT)
|
||||
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/unit/$(DEPDIR)/php_test_server.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_php_runtime-test_php_runtime.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_snmpv3_session.Po \
|
||||
tests/unit/$(DEPDIR)/test_util_paths.Po \
|
||||
@@ -411,9 +440,11 @@ SOURCES = $(spine_SOURCES) $(test_spine_faults_SOURCES) \
|
||||
$(test_spine_regressions_SOURCES) \
|
||||
$(tests_unit_php_test_server_SOURCES) \
|
||||
$(tests_unit_test_build_fixes_SOURCES) \
|
||||
$(tests_unit_test_classify_result_SOURCES) \
|
||||
$(tests_unit_test_linked_SOURCES) \
|
||||
$(tests_unit_test_php_runtime_SOURCES) \
|
||||
$(tests_unit_test_ping_icmp_SOURCES) \
|
||||
$(tests_unit_test_poll_host_SOURCES) \
|
||||
$(tests_unit_test_safety_fixes_SOURCES) \
|
||||
$(tests_unit_test_snmpv3_session_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) \
|
||||
$(am__tests_unit_php_test_server_SOURCES_DIST) \
|
||||
$(tests_unit_test_build_fixes_SOURCES) \
|
||||
$(tests_unit_test_classify_result_SOURCES) \
|
||||
$(tests_unit_test_linked_SOURCES) \
|
||||
$(tests_unit_test_php_runtime_SOURCES) \
|
||||
$(tests_unit_test_ping_icmp_SOURCES) \
|
||||
$(tests_unit_test_poll_host_SOURCES) \
|
||||
$(tests_unit_test_safety_fixes_SOURCES) \
|
||||
$(tests_unit_test_snmpv3_session_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.
|
||||
tests_unit_test_linked_CPPFLAGS = $(AM_CPPFLAGS) $(am__append_4)
|
||||
@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
|
||||
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
|
||||
|
||||
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 \
|
||||
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
|
||||
@@ -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)
|
||||
@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)
|
||||
tests/unit/test_linked-test_linked.$(OBJEXT): \
|
||||
tests/unit/$(am__dirstamp) \
|
||||
tests/unit/test_classify_result.$(OBJEXT): tests/unit/$(am__dirstamp) \
|
||||
tests/unit/$(DEPDIR)/$(am__dirstamp)
|
||||
tests/fuzz/$(am__dirstamp):
|
||||
@$(MKDIR_P) tests/fuzz
|
||||
@@ -1129,6 +1182,15 @@ tests/fuzz/$(am__dirstamp):
|
||||
tests/fuzz/$(DEPDIR)/$(am__dirstamp):
|
||||
@$(MKDIR_P) tests/fuzz/$(DEPDIR)
|
||||
@: > 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/$(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)
|
||||
tests/unit/test_ping_icmp.$(OBJEXT): tests/unit/$(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)
|
||||
@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)
|
||||
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/$(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/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_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_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_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_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
|
||||
@@ -2388,6 +2456,13 @@ tests/unit/test_linked.log: tests/unit/test_linked$(EXEEXT)
|
||||
--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_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)
|
||||
@p='tests/unit/test_util_paths$(EXEEXT)'; \
|
||||
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 \
|
||||
$(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \
|
||||
"$$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:
|
||||
@p='$<'; \
|
||||
$(am__set_b); \
|
||||
@@ -2727,9 +2809,11 @@ distclean: distclean-am
|
||||
-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)/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_php_runtime-test_php_runtime.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_snmpv3_session.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/unit/$(DEPDIR)/php_test_server.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_php_runtime-test_php_runtime.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_snmpv3_session.Po
|
||||
-rm -f tests/unit/$(DEPDIR)/test_util_paths.Po
|
||||
|
||||
@@ -135,62 +135,19 @@ void *child(void *arg) {
|
||||
* This function is core to Spine. It takes a host_id and polls it, first
|
||||
* 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) {
|
||||
classified_result_t classified;
|
||||
bool unknown_marker;
|
||||
|
||||
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;
|
||||
}
|
||||
if (is_hexadecimal(result, TRUE)) {
|
||||
/* Values wider than 64 bits become U and count as collection errors. */
|
||||
return poller_store_hex_result(result, RESULTS_BUFFER, result, NULL) ? POLL_RESULT_VALID : 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;
|
||||
/* An agent's "u" or "NaN" is still unknown; store the U RRDtool expects. */
|
||||
SET_UNDEFINED(result);
|
||||
return unknown_marker ? POLL_RESULT_UNDEFINED : POLL_RESULT_INVALID;
|
||||
}
|
||||
|
||||
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
|
||||
* reindex for good: the next poll compares against the new value. */
|
||||
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. */
|
||||
if (failed && (STRMATCH(reindex->op, "<") ||
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
if (host->snmp.session == NULL) {
|
||||
*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);
|
||||
} 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")) {
|
||||
unsigned long long ticks;
|
||||
bool uptime_use_engine_oid;
|
||||
|
||||
// Ensure uptime is empty to start with
|
||||
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. */
|
||||
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) {
|
||||
snprintf(sysUptime, BUFSIZE, "%lld", atoll(poll_result) * 100);
|
||||
snprintf(sysUptime, BUFSIZE, "%llu", ticks * 100);
|
||||
}
|
||||
|
||||
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
|
||||
poll_result = snmp_get(host, ".1.3.6.1.2.1.1.3.0");
|
||||
|
||||
if (poll_result && is_numeric(poll_result)) {
|
||||
snprintf(sysUptime, BUFSIZE, "%s", poll_result);
|
||||
if (parse_uptime(poll_result, &ticks)) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Use the primed uptime to repopulate the poll_result
|
||||
// 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) {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
char *poll_result = NULL;
|
||||
int php_process;
|
||||
@@ -704,7 +700,7 @@ static char *poll_reindex_action(host_t *host, reindex_t *reindex, int host_thre
|
||||
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));
|
||||
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) */
|
||||
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));
|
||||
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};
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
int initialized;
|
||||
int version;
|
||||
int port;
|
||||
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];
|
||||
char hostname[sizeof(((target_t *)0)->hostname)];
|
||||
snmp_profile_t profile;
|
||||
} snmp_item_key_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -1076,17 +1066,8 @@ typedef struct {
|
||||
} snmp_poll_batch_t;
|
||||
|
||||
static void remember_snmp_item(snmp_item_key_t *key, const target_t *item) {
|
||||
key->port = item->snmp.port;
|
||||
key->version = item->snmp.version;
|
||||
|
||||
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);
|
||||
key->profile = item->snmp;
|
||||
STRNCOPY(key->hostname, item->hostname);
|
||||
}
|
||||
|
||||
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) {
|
||||
return (key->port != item->snmp.port) ||
|
||||
(key->version != item->snmp.version) ||
|
||||
return !STRMATCH(key->hostname, item->hostname) ||
|
||||
(key->profile.port != item->snmp.port) ||
|
||||
(key->profile.timeout != item->snmp.timeout) ||
|
||||
(key->profile.version != item->snmp.version) ||
|
||||
(item->snmp.version < 3 &&
|
||||
(!STRMATCH(key->community, item->snmp.community))) ||
|
||||
(!STRMATCH(key->profile.community, item->snmp.community))) ||
|
||||
(item->snmp.version > 2 &&
|
||||
((!STRMATCH(key->username, item->snmp.username)) ||
|
||||
(!STRMATCH(key->password, item->snmp.password)) ||
|
||||
(!STRMATCH(key->auth_protocol, item->snmp.auth_protocol)) ||
|
||||
(!STRMATCH(key->priv_passphrase, item->snmp.priv_passphrase)) ||
|
||||
(!STRMATCH(key->priv_protocol, item->snmp.priv_protocol)) ||
|
||||
(!STRMATCH(key->context, item->snmp.context)) ||
|
||||
(!STRMATCH(key->engine_id, item->snmp.engine_id))));
|
||||
((!STRMATCH(key->profile.username, item->snmp.username)) ||
|
||||
(!STRMATCH(key->profile.password, item->snmp.password)) ||
|
||||
(!STRMATCH(key->profile.auth_protocol, item->snmp.auth_protocol)) ||
|
||||
(!STRMATCH(key->profile.priv_passphrase, item->snmp.priv_passphrase)) ||
|
||||
(!STRMATCH(key->profile.priv_protocol, item->snmp.priv_protocol)) ||
|
||||
(!STRMATCH(key->profile.context, item->snmp.context)) ||
|
||||
(!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,
|
||||
double thread_start, bool spike_kill) {
|
||||
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;
|
||||
}
|
||||
if (snmp_item_changed(&batch->key, item)) {
|
||||
/* Credential-switch flushes preserve the legacy no-discard policy. */
|
||||
flush_snmp_batch(host, batch, thread_start, FALSE);
|
||||
flush_snmp_batch(host, batch, thread_start, spike_kill);
|
||||
if (host->snmp.session != NULL) {
|
||||
snmp_host_cleanup(host->snmp.session);
|
||||
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 */
|
||||
if (batch->count > 0) {
|
||||
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);
|
||||
}
|
||||
flush_snmp_batch(host, batch, thread_start, spike_kill);
|
||||
|
||||
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) {
|
||||
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_errors = 0;
|
||||
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) {
|
||||
char *poll_result;
|
||||
unsigned long long ticks;
|
||||
// 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));
|
||||
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));
|
||||
|
||||
if (poll_result && is_numeric(poll_result)) {
|
||||
host->system.snmp_sysUpTimeInstance = atoll(poll_result);
|
||||
if (parse_uptime(poll_result, &ticks)) {
|
||||
host->system.snmp_sysUpTimeInstance = ticks;
|
||||
SPINE_FREE(poll_result);
|
||||
|
||||
// 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");
|
||||
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)) {
|
||||
host->system.snmp_sysUpTimeInstance = atoll(poll_result) * 100;
|
||||
if (parse_uptime(poll_result, &ticks) && ticks <= ULLONG_MAX / 100) {
|
||||
host->system.snmp_sysUpTimeInstance = ticks * 100;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
if (set.php.script_timeout <= 0) return EINVAL;
|
||||
/* 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);
|
||||
return FALSE;
|
||||
}
|
||||
/* Preserve the single-read response contract; later output is ignored. */
|
||||
ssize_t bytes = read(fd, result, RESULTS_BUFFER - 1);
|
||||
if (bytes > 0 && bytes < RESULTS_BUFFER) {
|
||||
/* Use the full capacity to detect truncation without waiting for EOF. */
|
||||
ssize_t bytes = read(fd, result, RESULTS_BUFFER);
|
||||
if (bytes > 0 && bytes < RESULTS_BUFFER && memchr(result, '\0', (size_t)bytes) == NULL) {
|
||||
result[bytes] = '\0';
|
||||
} else {
|
||||
char script[SMALL_BUFSIZE];
|
||||
|
||||
@@ -52,12 +52,9 @@ extern void *child(void *arg);
|
||||
extern void child_cleanup(void *arg);
|
||||
extern void child_cleanup_thread(void *arg);
|
||||
extern void child_cleanup_script(void *arg);
|
||||
extern int poller_store_hex_result(char *result, size_t result_size, const char *hex, int *errors);
|
||||
extern void poll_host(const poller_thread_t *work, int *host_errors);
|
||||
extern char *exec_poll(host_t *current_host, char *command, int id, const char *type);
|
||||
extern void get_system_information(host_t *host, MYSQL *mysql, int system);
|
||||
extern int is_multipart_output(const char *result);
|
||||
extern int validate_result(char *result);
|
||||
enum poll_result_status {
|
||||
POLL_RESULT_VALID,
|
||||
POLL_RESULT_UNDEFINED,
|
||||
|
||||
Vendored
+3
@@ -3,5 +3,8 @@
|
||||
# Isolated regression agent; never use this configuration in production.
|
||||
agentaddress udp:127.0.0.1:1161
|
||||
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
|
||||
sysContact regression
|
||||
# Octets that normalize to 266, for output_regex ordering.
|
||||
sysName 00 00 01 0A
|
||||
@@ -47,8 +47,11 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
|
||||
(void)format_poller_output_row(row, sizeof(row), 1,
|
||||
"fuzz", "1700000000", result);
|
||||
(void)is_multipart_output(value);
|
||||
(void)validate_result(value);
|
||||
classified_result_t classified;
|
||||
if (classify_result(value, &classified) == RESULT_UNKNOWN &&
|
||||
!IS_UNDEFINED(classified.text)) {
|
||||
abort();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -174,14 +174,17 @@ static void test_multipart_boundaries(void) {
|
||||
{NULL, FALSE}, {"", FALSE}, {"123", FALSE}, {"hello", FALSE},
|
||||
{"a:1", TRUE}, {"a!1", TRUE}, {":", TRUE}, {"!", TRUE},
|
||||
{"a:1 b:2", TRUE}, {"a!1 b!2", TRUE}, {"a:1 b!2", TRUE},
|
||||
{"a:1 b:2", FALSE}, {" a:1", FALSE}, {"a:1 ", FALSE},
|
||||
{" a:1 b:2 ", FALSE}, {"a:1 b:2 c:3", TRUE},
|
||||
{"a:1 b:2", FALSE}, {" a:1", TRUE}, {"a:1 ", TRUE},
|
||||
{" a:1 b:2 ", TRUE}, {"a:1 b:2 c:3", TRUE},
|
||||
{"a::1 b:2", FALSE}, {"a::1", TRUE},
|
||||
{"a:1\tb:2", TRUE}, {"a:1\nb!2", TRUE},
|
||||
{"a:1\t b!2", TRUE}, {"a:1 b:2\n c!3", TRUE},
|
||||
{"a:1 \t b:2", FALSE}, {"a b", FALSE}
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) assert(is_multipart_output(cases[i].input) == cases[i].expected);
|
||||
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) {
|
||||
|
||||
@@ -1298,6 +1298,103 @@ static void test_ping_only_session_lifetime(void) {
|
||||
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) {
|
||||
assert(strcmp(scenario, "admitted") == 0 || strcmp(scenario, "rejected") == 0 || strcmp(scenario, "retry") == 0);
|
||||
launch_case_active = TRUE;
|
||||
@@ -1424,6 +1521,7 @@ int main(int argc, char **argv) {
|
||||
if (argc == 2 && strcmp(argv[1], "--database") == 0) {
|
||||
test_real_database_retry();
|
||||
test_ping_only_session_lifetime();
|
||||
test_snmp_batch_key_width();
|
||||
test_worker_launch_failures();
|
||||
}
|
||||
else assert(argc == 1);
|
||||
|
||||
+112
-22
@@ -29,6 +29,7 @@ extern void test_script_stream_contracts(void);
|
||||
extern void test_additional_database_contracts(MYSQL *mysql);
|
||||
extern void test_settings_write_contracts(MYSQL *mysql);
|
||||
extern void test_additional_reindex_contracts(MYSQL *mysql);
|
||||
extern void test_reindex_result_contracts(MYSQL *mysql);
|
||||
extern void test_additional_snmp_session_boundaries(void);
|
||||
extern void test_snmpv3_agent_contracts(void);
|
||||
|
||||
@@ -56,15 +57,11 @@ static void test_string_conversions(void) {
|
||||
assert(strcmp(reverse(empty), "") == 0);
|
||||
assert(strcmp(reverse(one), "x") == 0);
|
||||
assert(strcmp(reverse(word), "dcba") == 0);
|
||||
char hex[] = "\"FF ff\t\"";
|
||||
char maximum[] = "FFFFFFFFFFFFFFFF";
|
||||
char overflow[] = "10000000000000000";
|
||||
char invalid[] = "not hex";
|
||||
unsigned long long value = 0;
|
||||
assert(hex2dec(hex, &value) && value == 65535);
|
||||
assert(hex2dec(maximum, &value) && value == ULLONG_MAX);
|
||||
assert(!hex2dec(overflow, &value));
|
||||
assert(!hex2dec(invalid, &value) && !hex2dec(NULL, &value));
|
||||
classified_result_t hex;
|
||||
assert(classify_result("\"FF ff\t\"", &hex) == RESULT_HEX_COUNTER && hex.value.counter == 65535);
|
||||
assert(classify_result("FF:FF:FF:FF FF-FF-FF-FF", &hex) == RESULT_HEX_COUNTER && hex.value.counter == ULLONG_MAX);
|
||||
assert(classify_result("01 00 00 00 00 00 00 00 00", &hex) == RESULT_UNKNOWN && IS_UNDEFINED(hex.text));
|
||||
assert(classify_result("not hex", &hex) == RESULT_UNKNOWN);
|
||||
char input[BUFSIZE * 2];
|
||||
memset(input, '\\', sizeof(input) - 1);
|
||||
input[sizeof(input) - 1] = '\0';
|
||||
@@ -668,7 +665,16 @@ static void test_poll_result_formats(void) {
|
||||
{"a:1 b!2", "a:1 b!2", POLL_RESULT_VALID},
|
||||
{"a!1", "a!1", POLL_RESULT_VALID},
|
||||
{"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 (int snmp = 0; snmp <= 1; snmp++) {
|
||||
@@ -678,10 +684,17 @@ static void test_poll_result_formats(void) {
|
||||
assert(strcmp(result, cases[i].output) == 0);
|
||||
}
|
||||
}
|
||||
char undefined[RESULTS_BUFFER] = "u";
|
||||
assert(normalize_poll_result(undefined, true) == POLL_RESULT_UNDEFINED);
|
||||
assert(strcmp(undefined, "u") == 0);
|
||||
assert(normalize_poll_result(undefined, false) == POLL_RESULT_INVALID);
|
||||
/* An agent's "u" or "NaN" is unknown, and poller_output stores it as U. */
|
||||
static const char *const unknown[] = {"u", "NaN", "nan"};
|
||||
for (size_t i = 0; i < sizeof(unknown) / sizeof(unknown[0]); i++) {
|
||||
char undefined[RESULTS_BUFFER];
|
||||
strncopy(undefined, unknown[i], sizeof(undefined));
|
||||
assert(normalize_poll_result(undefined, true) == POLL_RESULT_UNDEFINED);
|
||||
assert(strcmp(undefined, "U") == 0);
|
||||
strncopy(undefined, unknown[i], sizeof(undefined));
|
||||
assert(normalize_poll_result(undefined, false) == POLL_RESULT_INVALID);
|
||||
assert(strcmp(undefined, "U") == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_hostnames(void) {
|
||||
@@ -1105,11 +1118,18 @@ static void test_script_execution(void) {
|
||||
for (int excess = 0; excess <= 1; excess++) {
|
||||
spine_snprintf(output_command, sizeof(output_command), "/usr/bin/printf '%%%ds' x", RESULTS_BUFFER - 1 + excess);
|
||||
result = exec_poll(&host, output_command, 1, "DS");
|
||||
assert(strlen(result) == RESULTS_BUFFER - 1);
|
||||
assert(result[RESULTS_BUFFER - 1] == '\0');
|
||||
if (excess) assert(strcmp(result, "U") == 0);
|
||||
else {
|
||||
assert(strlen(result) == RESULTS_BUFFER - 1);
|
||||
assert(result[RESULTS_BUFFER - 1] == '\0');
|
||||
}
|
||||
free(result);
|
||||
assert(spine_permits_available(&available_scripts) == 1);
|
||||
}
|
||||
char embedded_nul[] = "/usr/bin/printf '42\\000junk'";
|
||||
result = exec_poll(&host, embedded_nul, 1, "DS");
|
||||
assert(strcmp(result, "U") == 0);
|
||||
free(result);
|
||||
char empty[] = "/usr/bin/printf ''";
|
||||
result = exec_poll(&host, empty, 1, "DQ");
|
||||
assert(strcmp(result, "U") == 0);
|
||||
@@ -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"},
|
||||
{"=", "123", "/usr/bin/printf 'a\\nb\\n'", POLLER_ACTION_SCRIPT_COUNT, 1, "2", "123"},
|
||||
{"=", "123", "/usr/bin/printf U", POLLER_ACTION_SCRIPT, 0, "123", "123"},
|
||||
{"<", "124", "/usr/bin/printf U", POLLER_ACTION_SCRIPT, 0, "U", "123"},
|
||||
{"<", "124", "/usr/bin/printf U", POLLER_ACTION_SCRIPT, 0, "124", "123"},
|
||||
{"<", "124", "unknown", 99, 0, "124", "123"}
|
||||
};
|
||||
for (int level = 0; level <= 2; level++) {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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];
|
||||
bool first_discarded = spike && !change_version;
|
||||
const char *first_check = first_discarded ? "output='U'" : "output REGEXP '^[0-9]+$' AND CAST(output AS UNSIGNED)>0";
|
||||
/* A restarted agent invalidates every sample of the cycle, including
|
||||
* those flushed early because the next item's credentials differ. */
|
||||
const char *first_check = spike ? "output='U'" : "output REGEXP '^[0-9]+$' AND CAST(output AS UNSIGNED)>0";
|
||||
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output WHERE local_data_id=601 AND %s", first_check);
|
||||
assert(database_count(mysql, query) == 1);
|
||||
spine_snprintf(query, sizeof(query), "SELECT COUNT(*) FROM poller_output_boost WHERE local_data_id=601 AND %s", first_check);
|
||||
@@ -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->errors == (spike ? 0 : 1));
|
||||
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");
|
||||
}
|
||||
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"));
|
||||
}
|
||||
|
||||
/* 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(®ex_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) {
|
||||
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);
|
||||
@@ -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);
|
||||
assert(database_count(mysql, query) == 1);
|
||||
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) {
|
||||
test_reindex_pipeline(mysql, work);
|
||||
test_reindex_query_shortcut(mysql, work);
|
||||
@@ -2315,6 +2404,7 @@ static void test_database_configuration(void) {
|
||||
test_collector_transfer(&mysql);
|
||||
test_poll_pipeline(&mysql);
|
||||
test_output_write_contracts(&mysql);
|
||||
test_reindex_result_contracts(&mysql);
|
||||
const char *reindex_agent = getenv("SPINE_TEST_SNMP_HOST");
|
||||
if (reindex_agent != NULL && reindex_agent[0] != '\0') test_additional_reindex_contracts(&mysql);
|
||||
test_cli_transfer_exit(&mysql);
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
*/
|
||||
#include "common.h"
|
||||
#include "spine.h"
|
||||
#if defined(__GLIBC__)
|
||||
#include <malloc.h>
|
||||
#endif
|
||||
|
||||
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"));
|
||||
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");
|
||||
}
|
||||
@@ -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
@@ -211,28 +211,6 @@ static void test_is_ipaddress(void **state) {
|
||||
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 ------------------------------------------------------- */
|
||||
|
||||
static void test_trim_family(void **state) {
|
||||
@@ -303,71 +281,6 @@ static void test_add_slashes_passes_plain_text_through(void **state) {
|
||||
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) {
|
||||
(void) state;
|
||||
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 reader;
|
||||
int writer_status;
|
||||
int wait_status = -1;
|
||||
pid_t writer_pid;
|
||||
siginfo_t writer_info = {0};
|
||||
int reader_status;
|
||||
ssize_t bytes;
|
||||
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
|
||||
* child also redirects its new pipe onto fd 1. Its later PidList close walk
|
||||
* 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");
|
||||
bytes = reader >= 0 ? read(reader, output, sizeof(output) - 1) : -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;
|
||||
|
||||
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_true(WIFEXITED(reader_status));
|
||||
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_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_all_digits),
|
||||
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_reverse),
|
||||
cmocka_unit_test(test_strpos),
|
||||
@@ -1162,8 +1089,6 @@ int main(void) {
|
||||
cmocka_unit_test(test_strip_alpha),
|
||||
cmocka_unit_test(test_add_slashes_doubles_a_backslash),
|
||||
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_file_exists),
|
||||
cmocka_unit_test(test_get_time_as_double_advances),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1640,104 +1640,203 @@ int is_ipaddress(const char *string) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/*! \fn int is_numeric(const char *string)
|
||||
* \brief check to see if a string is long or double
|
||||
* \param string the string to check
|
||||
*
|
||||
* \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;
|
||||
/* Cacti's prepare_validate_result() trims quotes and line ends; Spine has
|
||||
* always trimmed blanks, tabs and backslashes as well. */
|
||||
static const char result_padding[] = " \"'\\\t\n\r";
|
||||
|
||||
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) {
|
||||
return FALSE;
|
||||
*integral = TRUE;
|
||||
if (*cursor == '+' || *cursor == '-') cursor++;
|
||||
while (isdigit((unsigned char)*cursor)) {
|
||||
cursor++;
|
||||
digits++;
|
||||
}
|
||||
|
||||
/* check for an integer */
|
||||
errno = 0;
|
||||
strtol(string, &end_ptr_long, conv_base);
|
||||
|
||||
if (errno != ERANGE) {
|
||||
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;
|
||||
if (*cursor == '.') {
|
||||
*integral = FALSE;
|
||||
cursor++;
|
||||
while (isdigit((unsigned char)*cursor)) {
|
||||
cursor++;
|
||||
digits++;
|
||||
}
|
||||
} else {
|
||||
end_ptr_long = NULL;
|
||||
}
|
||||
|
||||
/* check for a float */
|
||||
errno = 0;
|
||||
strtod(string, &end_ptr_double);
|
||||
if (errno != ERANGE) {
|
||||
if (end_ptr_double == string + length) { /* floating point string */
|
||||
return TRUE;
|
||||
}
|
||||
} else {
|
||||
end_ptr_double = NULL;
|
||||
if (digits == 0) return FALSE;
|
||||
if (*cursor == 'e' || *cursor == 'E') {
|
||||
*integral = FALSE;
|
||||
cursor++;
|
||||
if (*cursor == '+' || *cursor == '-') cursor++;
|
||||
if (!isdigit((unsigned char)*cursor)) return FALSE;
|
||||
while (isdigit((unsigned char)*cursor)) cursor++;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
return *cursor == '\0';
|
||||
}
|
||||
|
||||
/*! \fn int is_hexadecimal(const char *str, const short ignore_space)
|
||||
* \brief test whether a string represents a hex number.
|
||||
* \param str string to test
|
||||
* \param ignore_space nonzero to skip tabs and spaces
|
||||
*
|
||||
* \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;
|
||||
static bool parse_decimal(const char *text, classified_result_t *out) {
|
||||
bool integral;
|
||||
char *end;
|
||||
double real;
|
||||
|
||||
if (!str) return FALSE;
|
||||
if (!decimal_syntax(text, &integral)) 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':
|
||||
case 'a': case 'A': case 'b': case 'B':
|
||||
case 'c': case 'C': case 'd': case 'D':
|
||||
case 'e': case 'E': case 'f': case 'F':
|
||||
case '"':
|
||||
break;
|
||||
case '-': case ':': case ' ':
|
||||
delim_found = TRUE;
|
||||
break;
|
||||
case '\t':
|
||||
if (!ignore_special) return FALSE;
|
||||
break;
|
||||
default:
|
||||
return FALSE;
|
||||
if (integral) {
|
||||
errno = 0;
|
||||
if (text[0] == '-') {
|
||||
long long value = strtoll(text, &end, 10);
|
||||
if (errno == 0 && *end == '\0') {
|
||||
out->kind = RESULT_SIGNED;
|
||||
out->value.integer = (int64_t)value;
|
||||
return TRUE;
|
||||
}
|
||||
} else {
|
||||
unsigned long long value = strtoull(text, &end, 10);
|
||||
if (errno == 0 && *end == '\0') {
|
||||
out->kind = RESULT_COUNTER;
|
||||
out->value.counter = (uint64_t)value;
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
str++;
|
||||
i++;
|
||||
/* Wider than 64 bits is still a number; the text is stored as is. */
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
out->kind = RESULT_FLOAT;
|
||||
out->value.real = real;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Cacti's "name:value name:value" form: one ':' or '!' per field, fields
|
||||
* separated by single spaces. */
|
||||
static bool multipart_syntax(const char *text) {
|
||||
size_t spaces = 0;
|
||||
size_t delimiters = 0;
|
||||
|
||||
if (strchr(text, ':') == NULL && strchr(text, '!') == NULL) return FALSE;
|
||||
for (const char *cursor = text; *cursor != '\0'; cursor++) {
|
||||
if (*cursor == ':' || *cursor == '!') delimiters++;
|
||||
else if (*cursor == ' ') spaces++;
|
||||
}
|
||||
return spaces == 0 || spaces + 1 == delimiters;
|
||||
}
|
||||
|
||||
static int hex_digit(int c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
OCTETS_NONE,
|
||||
OCTETS_VALUE,
|
||||
OCTETS_OVERFLOW
|
||||
} octets_t;
|
||||
|
||||
/* An OctetString such as a Hex-STRING Counter64: two-digit octets separated
|
||||
* by one space, '-' or ':', as Cacti's is_hexadecimal() requires. Like Cacti,
|
||||
* a lone octet such as "FF" counts; "42" never gets here as it is decimal. */
|
||||
static octets_t parse_octets(const char *text, uint64_t *value) {
|
||||
const char *cursor = text;
|
||||
uint64_t number = 0;
|
||||
bool overflow = FALSE;
|
||||
|
||||
for (;;) {
|
||||
int high = hex_digit((unsigned char)cursor[0]);
|
||||
int low = high < 0 ? -1 : hex_digit((unsigned char)cursor[1]);
|
||||
|
||||
if (low < 0) return OCTETS_NONE;
|
||||
if (number > (UINT64_MAX >> 8)) overflow = TRUE;
|
||||
number = (number << 8) | (uint64_t)(high * 16 + low);
|
||||
cursor += 2;
|
||||
if (*cursor == '\0') break;
|
||||
if (*cursor != ' ' && *cursor != ':' && *cursor != '-') {
|
||||
return OCTETS_NONE;
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
|
||||
if (overflow) return OCTETS_OVERFLOW;
|
||||
*value = number;
|
||||
return OCTETS_VALUE;
|
||||
}
|
||||
|
||||
/*! \fn result_kind_t classify_result(const char *raw, classified_result_t *out)
|
||||
* \brief decide what a script or SNMP agent returned and what to store
|
||||
* \param raw the output as received; it is not modified
|
||||
* \param out receives the kind, the parsed value and the text to store
|
||||
*
|
||||
* The order is the one Spine has always used: a decimal number, Cacti's
|
||||
* multipart output, a hex octet string, then a decimal number wrapped in
|
||||
* text such as "4096 Bytes". Anything else, including output too long to
|
||||
* store whole, is unknown and stored as U rather than as a guess.
|
||||
*
|
||||
* \return the kind, also stored in out->kind
|
||||
*/
|
||||
result_kind_t classify_result(const char *raw, classified_result_t *out) {
|
||||
const char *start;
|
||||
const char *end;
|
||||
const char *core;
|
||||
size_t length;
|
||||
uint64_t octets;
|
||||
|
||||
assert(out != NULL);
|
||||
out->kind = RESULT_UNKNOWN;
|
||||
out->value.counter = 0;
|
||||
SET_UNDEFINED(out->text);
|
||||
|
||||
if (raw == NULL) return RESULT_UNKNOWN;
|
||||
|
||||
start = raw + strspn(raw, result_padding);
|
||||
if (*start == '\0') return RESULT_UNKNOWN;
|
||||
/* start[0] is not padding, so this stops with at least one byte left */
|
||||
end = start + strlen(start);
|
||||
while (strchr(result_padding, end[-1]) != NULL) end--;
|
||||
|
||||
length = (size_t)(end - start);
|
||||
if (length >= sizeof(out->text)) return RESULT_UNKNOWN;
|
||||
memcpy(out->text, start, length);
|
||||
out->text[length] = '\0';
|
||||
|
||||
if (parse_decimal(out->text, out)) return out->kind;
|
||||
|
||||
if (multipart_syntax(out->text)) {
|
||||
out->kind = RESULT_MULTIPART;
|
||||
return out->kind;
|
||||
}
|
||||
|
||||
switch (parse_octets(out->text, &octets)) {
|
||||
case OCTETS_VALUE:
|
||||
out->kind = RESULT_HEX_COUNTER;
|
||||
out->value.counter = octets;
|
||||
snprintf(out->text, sizeof(out->text), "%llu", (unsigned long long)octets);
|
||||
return out->kind;
|
||||
case OCTETS_OVERFLOW:
|
||||
/* Stripping letters from a counter wider than 64 bits would leave
|
||||
* its leading octet looking like a small decimal sample. */
|
||||
break;
|
||||
case OCTETS_NONE:
|
||||
/* strip_alpha() points into out->text, so move the number to the front. */
|
||||
core = strip_alpha(out->text);
|
||||
if (parse_decimal(core, out)) {
|
||||
memmove(out->text, core, strlen(core) + 1);
|
||||
return out->kind;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
out->kind = RESULT_UNKNOWN;
|
||||
out->value.counter = 0;
|
||||
SET_UNDEFINED(out->text);
|
||||
return RESULT_UNKNOWN;
|
||||
}
|
||||
|
||||
/*! \fn char *strip_alpha(char *string)
|
||||
* \brief remove trailing alpha characters from a string.
|
||||
* \param string the string to strip characters from
|
||||
@@ -1930,55 +2029,6 @@ int char_count(const char *str, int chr) {
|
||||
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
|
||||
/* 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
|
||||
|
||||
@@ -52,10 +52,30 @@ extern void die(const char *format, ...)
|
||||
extern void set_option(const char *setting, const char *value);
|
||||
|
||||
/* number validation functions */
|
||||
extern int is_numeric(char *string);
|
||||
extern int is_ipaddress(const char *string);
|
||||
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 */
|
||||
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 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 */
|
||||
#define MAX_MATCHES 5
|
||||
#define REGEX_NUMBER "([-+]*)([0-9]*)([.][0-9]+)"
|
||||
|
||||
Reference in new issue
Block a user