mirror of
https://github.com/Cacti/spine.git
synced 2026-10-23 03:27:10 +09:00
ci: enforce cross-platform quality gates and connector coverage (#618)
## Summary Consolidates the connector/build work from #613 and the broader test-infrastructure work extracted from #597 into one review: - discover MariaDB Connector/C first while retaining MySQL and Percona-compatible support - test MariaDB and MySQL Connector/C with strict GCC and Clang builds on Ubuntu 24.04 - add a real macOS 15 build and unit-test lane with portable Autotools platform detection - run unit tests under ASan/UBSan and ThreadSanitizer - enforce the measured 7% global coverage baseline and 70% changed-line coverage - add libFuzzer coverage for output parsing alongside the existing hostname parser - exercise the SNMPv3 integration suite against MariaDB and MySQL 8.4 - add cppcheck, shellcheck, DCO verification, CODEOWNERS, distcheck, and Linux-capability checks while retaining the repository-managed default CodeQL setup - expose a stable `CI / required` aggregate check for branch protection - pin runners and third-party actions and keep workflow permissions read-only by default ## Correctness fixes found by the new gates - reject truncated capability strings and poller-output SQL tuples instead of publishing partial data - make `bootstrap` fail closed and select `libtoolize`/`glibtoolize` portably - use Autoconf's canonical host for Darwin detection instead of an empty native-build alias - remove stale initialization/name-shadow findings and make read-only interfaces const-correct - make the ICMP parser test portable across Linux and BSD header names ## Verification - strict GCC and Clang builds: 4/4 unit targets passed - ASan/UBSan: 4/4 passed - ThreadSanitizer: 4/4 passed - macOS Clang/Homebrew build: 4/4 passed - MariaDB integration: passed - MySQL 8.4 integration: passed - both libFuzzer targets: passed sustained local runs - coverage: 7.2% global and 84% on changed lines - cppcheck, shellcheck, actionlint, DCO verification, and `git diff --check`: passed locally ## Post-merge repository settings After these workflows exist on `develop`, an administrator should require `CI / required`, Fuzz, Integration, Rocky Linux, and the repository-managed CodeQL/code-scanning result in the active ruleset; enable code-owner approval; and require the branch to be current (or use a merge queue). Enabling those settings before this PR merges would create required checks that the base branch cannot yet emit. Consolidates and supersedes #613. Closes #612. --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Signed-off-by: TheWitness <thewitness@cacti.net> Signed-off-by: TheWitness <copilot@users.noreply.github.com> Co-authored-by: TheWitness <thewitness@cacti.net> Co-authored-by: TheWitness <copilot@users.noreply.github.com>
This commit is contained in:
42 files changed
+1423
-268
No files matched your search
@@ -0,0 +1,18 @@
|
|||||||
|
# Default review routing.
|
||||||
|
* @Cacti/cacti-developers
|
||||||
|
|
||||||
|
# CI, release and dependency policy changes need both implementation and
|
||||||
|
# security review. These requirements take effect when the repository ruleset
|
||||||
|
# enables code-owner approval.
|
||||||
|
/.github/ @Cacti/cacti-developers @Cacti/cacti-security
|
||||||
|
/configure.ac @Cacti/cacti-developers @Cacti/cacti-security
|
||||||
|
/Makefile.am @Cacti/cacti-developers @Cacti/cacti-security
|
||||||
|
/Dockerfile @Cacti/cacti-developers @Cacti/cacti-security
|
||||||
|
|
||||||
|
# High-risk runtime boundaries.
|
||||||
|
/sql.c @Cacti/cacti-developers @Cacti/cacti-security
|
||||||
|
/php.c @Cacti/cacti-developers @Cacti/cacti-security
|
||||||
|
/nft_popen.c @Cacti/cacti-developers @Cacti/cacti-security
|
||||||
|
/snmp.c @Cacti/cacti-developers @Cacti/cacti-security
|
||||||
|
/ping.c @Cacti/cacti-developers @Cacti/cacti-security
|
||||||
|
/poller.c @Cacti/cacti-developers @Cacti/cacti-security
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
name: Build Spine
|
||||||
|
description: Install the build dependencies, then bootstrap and configure Spine.
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
mysql-client-package:
|
||||||
|
description: MySQL-compatible client development package to install.
|
||||||
|
required: false
|
||||||
|
default: libmysqlclient-dev
|
||||||
|
extra-packages:
|
||||||
|
description: Additional apt packages, beyond the set every Spine build needs.
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
configure-args:
|
||||||
|
description: Arguments passed through to ./configure.
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
cflags:
|
||||||
|
description: CFLAGS for ./configure.
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
ldflags:
|
||||||
|
description: LDFLAGS for ./configure.
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
# mysql-server is deliberately absent: configure only needs the selected
|
||||||
|
# client headers and library, and no job starts or connects to a server.
|
||||||
|
- name: Install build dependencies
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
MYSQL_CLIENT_PACKAGE: ${{ inputs.mysql-client-package }}
|
||||||
|
EXTRA_PACKAGES: ${{ inputs.extra-packages }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sudo apt-get update
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
sudo apt-get install -y \
|
||||||
|
autoconf automake libtool build-essential help2man dos2unix \
|
||||||
|
"$MYSQL_CLIENT_PACKAGE" libsnmp-dev libssl-dev libcmocka-dev \
|
||||||
|
$EXTRA_PACKAGES
|
||||||
|
|
||||||
|
- name: Bootstrap and configure
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
CONFIGURE_ARGS: ${{ inputs.configure-args }}
|
||||||
|
IN_CFLAGS: ${{ inputs.cflags }}
|
||||||
|
IN_LDFLAGS: ${{ inputs.ldflags }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
./bootstrap
|
||||||
|
args=()
|
||||||
|
if [ -n "$CONFIGURE_ARGS" ]; then
|
||||||
|
read -ra args <<< "$CONFIGURE_ARGS"
|
||||||
|
fi
|
||||||
|
if [ -n "$IN_CFLAGS" ]; then
|
||||||
|
args+=("CFLAGS=$IN_CFLAGS")
|
||||||
|
fi
|
||||||
|
if [ -n "$IN_LDFLAGS" ]; then
|
||||||
|
args+=("LDFLAGS=$IN_LDFLAGS")
|
||||||
|
fi
|
||||||
|
./configure ${args[@]+"${args[@]}"}
|
||||||
+275
-87
@@ -14,33 +14,90 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
dco:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
# pull_request normally checks out GitHub's synthetic merge commit,
|
||||||
|
# which cannot carry the contributor's DCO trailer.
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
|
- name: Verify Developer Certificate of Origin trailers
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
BASE_REF: ${{ github.base_ref }}
|
||||||
|
run: scripts/check-dco.sh "origin/${BASE_REF}..HEAD"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
name: Build (${{ matrix.compiler }}, ${{ matrix.client.name }})
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
compiler: [gcc, clang]
|
compiler: [gcc, clang]
|
||||||
|
client:
|
||||||
|
- name: MariaDB Connector/C
|
||||||
|
package: libmariadb-dev
|
||||||
|
config: mariadb_config
|
||||||
|
- name: MySQL Connector/C
|
||||||
|
package: libmysqlclient-dev
|
||||||
|
config: mysql_config
|
||||||
env:
|
env:
|
||||||
CC: ${{ matrix.compiler }}
|
CC: ${{ matrix.compiler }}
|
||||||
|
MYSQL_CONFIG: ${{ matrix.client.config }}
|
||||||
|
# get_date_format() deliberately constructs the bounded strftime
|
||||||
|
# format; keep every other warning fatal.
|
||||||
|
STRICT_CFLAGS: -Wall -Wextra -Wformat=2 -Wno-format-nonliteral
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install build dependencies
|
- uses: ./.github/actions/build-spine
|
||||||
run: |
|
with:
|
||||||
sudo apt-get update
|
mysql-client-package: ${{ matrix.client.package }}
|
||||||
sudo apt-get install -y \
|
cflags: ${{ env.STRICT_CFLAGS }}
|
||||||
mysql-server libmysqlclient-dev \
|
|
||||||
libsnmp-dev libssl-dev build-essential \
|
|
||||||
help2man autoconf automake libtool dos2unix libcmocka-dev
|
|
||||||
|
|
||||||
- name: Prepare for Spine Build
|
|
||||||
run: |
|
|
||||||
./bootstrap
|
|
||||||
./configure --enable-warnings
|
|
||||||
|
|
||||||
- name: Build Spine
|
- name: Build Spine
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
configured_cflags=$(sed -n 's/^CFLAGS = //p' Makefile)
|
||||||
|
make -j"$(nproc)" CFLAGS="$configured_cflags -Werror"
|
||||||
|
|
||||||
|
- name: Run the unit tests
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
configured_cflags=$(sed -n 's/^CFLAGS = //p' Makefile)
|
||||||
|
# Test doubles and cmocka callbacks intentionally leave parameters
|
||||||
|
# unused; tests/unit/test_build_fixes.c also inlines strncpy-based
|
||||||
|
# helpers specifically to exercise their truncation behavior, which
|
||||||
|
# -Wstringop-truncation flags on principle even though it's the
|
||||||
|
# scenario under test. Every other test-source warning stays fatal.
|
||||||
|
# -Wstringop-truncation is GCC-only; -Wno-unknown-warning-option
|
||||||
|
# keeps clang from erroring on it under -Werror.
|
||||||
|
make check CFLAGS="$configured_cflags -Werror -Wno-unused-parameter -Wno-unknown-warning-option -Wno-stringop-truncation"
|
||||||
|
|
||||||
|
# Linux capabilities are opt-in at configure time. Keep one build on that
|
||||||
|
# path so hasCaps() and the CAP_NET_RAW handling cannot silently rot behind
|
||||||
|
# an otherwise-green default build.
|
||||||
|
linux-capabilities:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- uses: ./.github/actions/build-spine
|
||||||
|
with:
|
||||||
|
extra-packages: libcap-dev
|
||||||
|
# Exercise the supported low-buffer configuration as well as the
|
||||||
|
# Linux-capability path; formatter tests must not assume defaults.
|
||||||
|
configure-args: --enable-lcap --enable-warnings --with-results-buffer=512
|
||||||
|
|
||||||
|
- name: Build Spine with Linux capabilities
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
make -j"$(nproc)"
|
make -j"$(nproc)"
|
||||||
|
|
||||||
- name: Run the unit tests
|
- name: Run the unit tests
|
||||||
@@ -48,58 +105,108 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
make check
|
make check
|
||||||
|
|
||||||
# cppcheck:
|
macos:
|
||||||
# runs-on: ubuntu-latest
|
runs-on: macos-15
|
||||||
# steps:
|
timeout-minutes: 25
|
||||||
# - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
||||||
#
|
|
||||||
# - name: Install cppcheck
|
|
||||||
# run: |
|
|
||||||
# sudo apt-get update
|
|
||||||
# sudo apt-get install -y cppcheck build-essential
|
|
||||||
#
|
|
||||||
# - name: Run cppcheck
|
|
||||||
# run: |
|
|
||||||
# cppcheck \
|
|
||||||
# --enable=all \
|
|
||||||
# --std=c11 \
|
|
||||||
# --error-exitcode=1 \
|
|
||||||
# --suppress=missingIncludeSystem \
|
|
||||||
# --suppress=unusedFunction \
|
|
||||||
# --suppress=checkersReport \
|
|
||||||
# --suppress=variableScope \
|
|
||||||
# --suppress=unreadVariable \
|
|
||||||
# --suppress=shadowVariable \
|
|
||||||
# --suppress=constVariablePointer \
|
|
||||||
# --suppress=redundantAssignment \
|
|
||||||
# --suppress=toomanyconfigs \
|
|
||||||
# *.c *.h
|
|
||||||
|
|
||||||
# Coverage is reported, not gated. The number is only meaningful once
|
|
||||||
# something measures it; the threshold conversation comes after that.
|
|
||||||
coverage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 20
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
brew install autoconf automake libtool help2man \
|
||||||
|
mariadb-connector-c net-snmp openssl@3 cmocka
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
env:
|
||||||
|
CC: clang
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mariadb_prefix=$(brew --prefix mariadb-connector-c)
|
||||||
|
openssl_prefix=$(brew --prefix openssl@3)
|
||||||
|
net_snmp_prefix=$(brew --prefix net-snmp)
|
||||||
|
cmocka_prefix=$(brew --prefix cmocka)
|
||||||
|
export MYSQL_CONFIG="$mariadb_prefix/bin/mariadb_config"
|
||||||
|
export CPPFLAGS="-I$openssl_prefix/include -I$net_snmp_prefix/include -I$cmocka_prefix/include"
|
||||||
|
export LDFLAGS="-L$mariadb_prefix/lib -L$openssl_prefix/lib -L$net_snmp_prefix/lib -L$cmocka_prefix/lib"
|
||||||
|
./bootstrap
|
||||||
|
./configure --enable-warnings --with-snmp="$net_snmp_prefix"
|
||||||
|
make -j"$(sysctl -n hw.ncpu)"
|
||||||
|
make check
|
||||||
|
|
||||||
|
cppcheck:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- name: Run cppcheck
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y mysql-server libmysqlclient-dev libsnmp-dev \
|
sudo apt-get install -y cppcheck
|
||||||
libssl-dev build-essential help2man autoconf automake libtool \
|
# Project data structures are consumed across translation units;
|
||||||
dos2unix libcmocka-dev gcovr
|
# checking headers in isolation otherwise reports every member. A
|
||||||
|
# few externally visible helpers are deliberate unit-test seams.
|
||||||
|
cppcheck \
|
||||||
|
--enable=all \
|
||||||
|
--std=c11 \
|
||||||
|
--error-exitcode=1 \
|
||||||
|
--suppress=missingInclude \
|
||||||
|
--suppress=missingIncludeSystem \
|
||||||
|
--suppress=normalCheckLevelMaxBranches \
|
||||||
|
--suppress=checkLevelNormal \
|
||||||
|
--suppress=unmatchedSuppression \
|
||||||
|
--suppress=unusedFunction \
|
||||||
|
--suppress=checkersReport \
|
||||||
|
--suppress=variableScope \
|
||||||
|
--suppress=unreadVariable \
|
||||||
|
--suppress=unusedStructMember \
|
||||||
|
--suppress=staticFunction \
|
||||||
|
--suppress=constVariablePointer \
|
||||||
|
--suppress=redundantAssignment \
|
||||||
|
--suppress=toomanyconfigs \
|
||||||
|
./*.c ./*.h
|
||||||
|
|
||||||
|
shellcheck:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- name: Run shellcheck
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y shellcheck
|
||||||
|
find scripts tests -type f -name '*.sh' -print0 \
|
||||||
|
| xargs -0 shellcheck
|
||||||
|
|
||||||
|
# Preserve the measured baseline and require stronger coverage on changed
|
||||||
|
# lines. Raise both thresholds as integration coverage grows.
|
||||||
|
coverage:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: ./.github/actions/build-spine
|
||||||
|
with:
|
||||||
|
extra-packages: gcovr python3-pip
|
||||||
|
cflags: -g -O0 --coverage
|
||||||
|
ldflags: --coverage
|
||||||
|
|
||||||
- name: Build instrumented
|
- name: Build instrumented
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
./bootstrap
|
|
||||||
./configure CFLAGS="-g -O0 --coverage" LDFLAGS="--coverage"
|
|
||||||
make -j"$(nproc)"
|
make -j"$(nproc)"
|
||||||
|
|
||||||
- name: Run the unit tests
|
- name: Run the unit tests
|
||||||
run: make check
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
make check
|
||||||
|
|
||||||
- name: Report coverage
|
- name: Report coverage
|
||||||
run: |
|
run: |
|
||||||
@@ -107,15 +214,42 @@ jobs:
|
|||||||
# configure leaves instrumented conftest objects behind and gcovr
|
# configure leaves instrumented conftest objects behind and gcovr
|
||||||
# cannot resolve their sources
|
# cannot resolve their sources
|
||||||
rm -f ./*conftest*.gcno ./*conftest*.gcda
|
rm -f ./*conftest*.gcno ./*conftest*.gcda
|
||||||
gcovr -r . --exclude 'tests/.*' --txt --print-summary
|
gcovr -r . \
|
||||||
|
--exclude 'tests/.*' \
|
||||||
|
--txt --print-summary \
|
||||||
|
--xml coverage.xml \
|
||||||
|
--html-details coverage.html \
|
||||||
|
--fail-under-line 7
|
||||||
|
|
||||||
|
- name: Enforce changed-line coverage
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
BASE_REF: ${{ github.base_ref }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 -m pip install --break-system-packages diff-cover==10.5.1
|
||||||
|
diff-cover coverage.xml \
|
||||||
|
--compare-branch="origin/${BASE_REF}" \
|
||||||
|
--fail-under=70
|
||||||
|
|
||||||
|
- name: Upload coverage reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4.4.3
|
||||||
|
with:
|
||||||
|
name: coverage-report
|
||||||
|
path: |
|
||||||
|
coverage.xml
|
||||||
|
coverage.html
|
||||||
|
coverage.*.html
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
flawfinder:
|
flawfinder:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
|
|
||||||
@@ -127,6 +261,7 @@ jobs:
|
|||||||
# informational so we have a baseline to chip away at.
|
# informational so we have a baseline to chip away at.
|
||||||
- name: Run flawfinder
|
- name: Run flawfinder
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
flawfinder \
|
flawfinder \
|
||||||
--minlevel=3 \
|
--minlevel=3 \
|
||||||
--error-level=5 \
|
--error-level=5 \
|
||||||
@@ -134,64 +269,117 @@ jobs:
|
|||||||
--context \
|
--context \
|
||||||
. | tee flawfinder-report.txt
|
. | tee flawfinder-report.txt
|
||||||
|
|
||||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: flawfinder-report
|
name: flawfinder-report
|
||||||
path: flawfinder-report.txt
|
path: flawfinder-report.txt
|
||||||
|
|
||||||
# Spine is a threaded network daemon; ASan and UBSan are the checks most
|
# Spine is a threaded network daemon; ASan and UBSan are the checks most
|
||||||
# likely to catch a real defect here.
|
# likely to catch a real defect here. The unit tests are what actually
|
||||||
|
# exercises the code, so they are what runs instrumented.
|
||||||
sanitizers:
|
sanitizers:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
env:
|
||||||
|
CC: clang
|
||||||
|
ASAN_OPTIONS: detect_leaks=1:exitcode=1
|
||||||
|
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install build dependencies
|
- uses: ./.github/actions/build-spine
|
||||||
run: |
|
with:
|
||||||
set -euo pipefail
|
extra-packages: clang
|
||||||
sudo apt-get update
|
cflags: -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1
|
||||||
sudo apt-get install -y clang mysql-server libmysqlclient-dev libsnmp-dev \
|
ldflags: -fsanitize=address,undefined
|
||||||
libssl-dev build-essential help2man autoconf automake libtool dos2unix libcmocka-dev
|
|
||||||
|
|
||||||
- name: Build with ASan and UBSan
|
- name: Build with ASan and UBSan
|
||||||
env:
|
|
||||||
CC: clang
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
./bootstrap
|
|
||||||
./configure \
|
|
||||||
CFLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1" \
|
|
||||||
LDFLAGS="-fsanitize=address,undefined"
|
|
||||||
make -j"$(nproc)"
|
make -j"$(nproc)"
|
||||||
|
|
||||||
- name: Run the binary under the sanitizers
|
- name: Run the unit tests under the sanitizers
|
||||||
env:
|
run: |
|
||||||
ASAN_OPTIONS: detect_leaks=1:exitcode=1
|
set -euo pipefail
|
||||||
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
|
make check
|
||||||
|
|
||||||
|
- name: Smoke-test the binary under the sanitizers
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
./spine --version
|
./spine --version
|
||||||
./spine --help > /dev/null
|
./spine --help > /dev/null
|
||||||
|
|
||||||
|
# Spine is highly concurrent. Keep race detection separate from ASan/UBSan
|
||||||
|
# because the sanitizer runtimes cannot be combined.
|
||||||
|
thread-sanitizer:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 20
|
||||||
|
env:
|
||||||
|
CC: clang
|
||||||
|
TSAN_OPTIONS: halt_on_error=1:second_deadlock_stack=1
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- uses: ./.github/actions/build-spine
|
||||||
|
with:
|
||||||
|
extra-packages: clang
|
||||||
|
cflags: -fsanitize=thread -fno-omit-frame-pointer -g -O1
|
||||||
|
ldflags: -fsanitize=thread
|
||||||
|
|
||||||
|
- name: Build with ThreadSanitizer
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
make -j"$(nproc)"
|
||||||
|
|
||||||
|
- name: Run the unit tests under ThreadSanitizer
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
make check
|
||||||
|
|
||||||
# A release tarball that cannot be compiled is a release blocker, and the
|
# A release tarball that cannot be compiled is a release blocker, and the
|
||||||
# only way to catch it is to build from the tarball the way a packager does.
|
# only way to catch it is to build from the tarball the way a packager does.
|
||||||
distcheck:
|
distcheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install build dependencies
|
- uses: ./.github/actions/build-spine
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y mysql-server libmysqlclient-dev libsnmp-dev \
|
|
||||||
libssl-dev build-essential help2man autoconf automake libtool dos2unix libcmocka-dev
|
|
||||||
|
|
||||||
- name: make distcheck
|
- name: make distcheck
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
./bootstrap
|
|
||||||
./configure
|
|
||||||
make distcheck
|
make distcheck
|
||||||
|
|
||||||
|
# Branch protection should require this stable name rather than every matrix
|
||||||
|
# child. Any failed, cancelled or unexpectedly skipped dependency fails it.
|
||||||
|
required:
|
||||||
|
name: CI / required
|
||||||
|
if: always()
|
||||||
|
needs:
|
||||||
|
- dco
|
||||||
|
- build
|
||||||
|
- linux-capabilities
|
||||||
|
- macos
|
||||||
|
- cppcheck
|
||||||
|
- shellcheck
|
||||||
|
- coverage
|
||||||
|
- flawfinder
|
||||||
|
- sanitizers
|
||||||
|
- thread-sanitizer
|
||||||
|
- distcheck
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- name: Verify every required job passed
|
||||||
|
env:
|
||||||
|
RESULTS: ${{ toJSON(needs) }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
failures=$(jq -r 'to_entries[] | select(.value.result != "success") | "\(.key)=\(.value.result)"' <<<"$RESULTS")
|
||||||
|
if [ -n "$failures" ]; then
|
||||||
|
echo "Required CI jobs did not pass:"
|
||||||
|
echo "$failures"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
+12
-16
@@ -3,7 +3,12 @@ name: Fuzz
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [develop]
|
branches: [develop]
|
||||||
paths: ['**.c', '**.h', 'tests/fuzz/**', '.github/workflows/fuzz.yml']
|
paths:
|
||||||
|
- '**.c'
|
||||||
|
- '**.h'
|
||||||
|
- 'tests/fuzz/**'
|
||||||
|
- '.github/actions/build-spine/**'
|
||||||
|
- '.github/workflows/fuzz.yml'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 3 * * 0'
|
- cron: '0 3 * * 0'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -17,23 +22,14 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
fuzz:
|
fuzz:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Install build dependencies
|
- uses: ./.github/actions/build-spine
|
||||||
run: |
|
with:
|
||||||
set -euo pipefail
|
extra-packages: clang pkg-config
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y clang libmysqlclient-dev libsnmp-dev libssl-dev \
|
|
||||||
autoconf automake libtool help2man pkg-config
|
|
||||||
|
|
||||||
- name: Generate config.h
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
./bootstrap
|
|
||||||
./configure
|
|
||||||
|
|
||||||
# A pull request gets a short regression run against the committed corpus.
|
# A pull request gets a short regression run against the committed corpus.
|
||||||
# The weekly schedule runs long enough to explore.
|
# The weekly schedule runs long enough to explore.
|
||||||
@@ -47,7 +43,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
make -C tests/fuzz FUZZ_SECONDS="$seconds" run
|
make -C tests/fuzz FUZZ_SECONDS="$seconds" run
|
||||||
|
|
||||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
|
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: fuzz-crashes
|
name: fuzz-crashes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: Integration
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [develop]
|
branches: [develop]
|
||||||
paths: ['**.c', '**.h', 'Dockerfile', '.dockerignore', 'tests/snmpv3/**', 'tests/integration/test_script_server_reap.sh', '.github/workflows/integration.yml']
|
paths: ['**.c', '**.h', 'Dockerfile', '.dockerignore', 'tests/integration/**', 'tests/test-harness.sh', 'tests/snmpv3/**', '.github/workflows/integration.yml']
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -15,21 +15,68 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
snmpv3:
|
snmpv3:
|
||||||
runs-on: ubuntu-latest
|
name: SNMPv3 integration (${{ matrix.database }})
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
database: [mariadb, mysql]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
# Brings up MariaDB and an snmpd, then polls a real device through
|
# Brings up MariaDB and an snmpd, then polls a real device through
|
||||||
# poll_host() and checks the value lands in poller_output.
|
# poll_host() and checks the value lands in poller_output.
|
||||||
- name: Poll an SNMPv3 device end to end
|
- name: Poll an SNMPv3 device end to end
|
||||||
run: tests/snmpv3/scripts/run-integration.sh
|
env:
|
||||||
|
SPINE_TEST_DATABASE: ${{ matrix.database }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
tests/snmpv3/scripts/run-integration.sh
|
||||||
|
|
||||||
|
- name: Exercise concurrent host polling
|
||||||
|
if: matrix.database == 'mariadb'
|
||||||
|
env:
|
||||||
|
SPINE_TEST_DATABASE: mariadb
|
||||||
|
SPINE_TEST_HOSTS: 24
|
||||||
|
SPINE_TEST_THREAD_COUNTS: "1 4 12"
|
||||||
|
SPINE_TEST_REPEATS: 1
|
||||||
|
SPINE_TEST_TIMEOUT_SECONDS: 120
|
||||||
|
SPINE_TEST_ARTIFACT_DIR: test-artifacts/concurrency
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
tests/integration/test_concurrency.sh
|
||||||
|
|
||||||
|
- name: Exercise SNMP packet loss and recovery
|
||||||
|
if: matrix.database == 'mariadb'
|
||||||
|
env:
|
||||||
|
SPINE_TEST_TIMEOUT_SECONDS: 120
|
||||||
|
SPINE_TEST_ARTIFACT_DIR: test-artifacts/network-faults
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
tests/integration/test_network_faults.sh
|
||||||
|
|
||||||
- name: Reap a real PHP script-server process
|
- name: Reap a real PHP script-server process
|
||||||
run: tests/integration/test_script_server_reap.sh
|
run: tests/integration/test_script_server_reap.sh
|
||||||
|
|
||||||
- name: Show container logs on failure
|
- name: Show container logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
|
env:
|
||||||
|
SPINE_TEST_DATABASE: ${{ matrix.database }}
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
cd tests/snmpv3
|
cd tests/snmpv3
|
||||||
docker compose logs --no-color || true
|
compose=(docker compose)
|
||||||
|
if [[ "$SPINE_TEST_DATABASE" == mysql ]]; then
|
||||||
|
compose+=(-f docker-compose.yml -f docker-compose.mysql.yml)
|
||||||
|
fi
|
||||||
|
"${compose[@]}" logs --no-color || true
|
||||||
|
|
||||||
|
- name: Upload integration diagnostics
|
||||||
|
if: always() && matrix.database == 'mariadb'
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4.4.3
|
||||||
|
with:
|
||||||
|
name: integration-diagnostics-${{ matrix.database }}
|
||||||
|
path: test-artifacts
|
||||||
|
if-no-files-found: warn
|
||||||
|
retention-days: 14
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
name: Concurrency soak
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '23 5 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: spine-concurrency-soak
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
concurrency-soak:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 45
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
|
- name: Run repeated many-host polling
|
||||||
|
env:
|
||||||
|
SPINE_TEST_DATABASE: mariadb
|
||||||
|
SPINE_TEST_HOSTS: 100
|
||||||
|
SPINE_TEST_THREAD_COUNTS: "1 8 32"
|
||||||
|
SPINE_TEST_REPEATS: 5
|
||||||
|
SPINE_TEST_TIMEOUT_SECONDS: 180
|
||||||
|
SPINE_TEST_ARTIFACT_DIR: test-artifacts/concurrency-soak
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
tests/integration/test_concurrency.sh
|
||||||
|
|
||||||
|
- name: Upload soak diagnostics
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v4.4.3
|
||||||
|
with:
|
||||||
|
name: concurrency-soak
|
||||||
|
path: test-artifacts/concurrency-soak
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 30
|
||||||
@@ -32,3 +32,4 @@ libtool
|
|||||||
*.o
|
*.o
|
||||||
*.m4
|
*.m4
|
||||||
tests/unit/bin/
|
tests/unit/bin/
|
||||||
|
test-artifacts/
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
The Cacti Group | spine
|
The Cacti Group | spine
|
||||||
|
|
||||||
1.3.0
|
1.3.0
|
||||||
|
-feature#612: Prefer MariaDB Connector/C discovery while retaining MySQL Connector/C compatibility
|
||||||
-issue#60: Warn at startup when the spine and Cacti versions come from different release lines
|
-issue#60: Warn at startup when the spine and Cacti versions come from different release lines
|
||||||
-issue#263: Unify the debug message prefix so a single poll no longer logs two orders
|
-issue#263: Unify the debug message prefix so a single poll no longer logs two orders
|
||||||
-issue#360: If a Remote Data Collector has not devices, spine does not properly register itself
|
-issue#360: If a Remote Data Collector has not devices, spine does not properly register itself
|
||||||
|
|||||||
@@ -72,8 +72,7 @@ CYGWIN Prerequisite
|
|||||||
* gcc-debuginfo
|
* gcc-debuginfo
|
||||||
* gzip
|
* gzip
|
||||||
* help2man
|
* help2man
|
||||||
* libmysqlclient
|
* libmariadb-devel (preferred) or libmysqlclient
|
||||||
* libmariadb-devel
|
|
||||||
* libtool
|
* libtool
|
||||||
* m4
|
* m4
|
||||||
* make
|
* make
|
||||||
|
|||||||
+5
-1
@@ -53,7 +53,11 @@ noinst_HEADERS = common.h error.h src/core/keywords.h src/core/locks.h \
|
|||||||
nft_popen.h php.h ping.h poller.h snmp.h spine.h spine_sem.h sql.h \
|
nft_popen.h php.h ping.h poller.h snmp.h spine.h spine_sem.h sql.h \
|
||||||
uthash.h util.h
|
uthash.h util.h
|
||||||
|
|
||||||
EXTRA_DIST = spine.1 spine.conf.dist
|
EXTRA_DIST = spine.1 spine.conf.dist \
|
||||||
|
tests/README.md \
|
||||||
|
tests/test-harness.sh \
|
||||||
|
tests/integration/test_concurrency.sh \
|
||||||
|
tests/integration/test_network_faults.sh
|
||||||
|
|
||||||
# Docker targets — require Dockerfile and Dockerfile.dev (from PR #401)
|
# Docker targets — require Dockerfile and Dockerfile.dev (from PR #401)
|
||||||
.PHONY: docker docker-dev verify cppcheck
|
.PHONY: docker docker-dev verify cppcheck
|
||||||
|
|||||||
+4
-1
@@ -622,6 +622,7 @@ LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
|
|||||||
MAKEINFO = @MAKEINFO@
|
MAKEINFO = @MAKEINFO@
|
||||||
MANIFEST_TOOL = @MANIFEST_TOOL@
|
MANIFEST_TOOL = @MANIFEST_TOOL@
|
||||||
MKDIR_P = @MKDIR_P@
|
MKDIR_P = @MKDIR_P@
|
||||||
|
MYSQL_CONFIG = @MYSQL_CONFIG@
|
||||||
NM = @NM@
|
NM = @NM@
|
||||||
NMEDIT = @NMEDIT@
|
NMEDIT = @NMEDIT@
|
||||||
OBJDUMP = @OBJDUMP@
|
OBJDUMP = @OBJDUMP@
|
||||||
@@ -711,7 +712,9 @@ noinst_HEADERS = common.h error.h src/core/keywords.h src/core/locks.h \
|
|||||||
nft_popen.h php.h ping.h poller.h snmp.h spine.h spine_sem.h sql.h \
|
nft_popen.h php.h ping.h poller.h snmp.h spine.h spine_sem.h sql.h \
|
||||||
uthash.h util.h
|
uthash.h util.h
|
||||||
|
|
||||||
EXTRA_DIST = spine.1 spine.conf.dist \
|
EXTRA_DIST = spine.1 spine.conf.dist tests/README.md \
|
||||||
|
tests/test-harness.sh tests/integration/test_concurrency.sh \
|
||||||
|
tests/integration/test_network_faults.sh \
|
||||||
tests/regression/test_remote_push.sh \
|
tests/regression/test_remote_push.sh \
|
||||||
tests/integration/test_alloc_failure.sh \
|
tests/integration/test_alloc_failure.sh \
|
||||||
tests/regression/test_child_process_safety.sh
|
tests/regression/test_child_process_safety.sh
|
||||||
|
|||||||
@@ -64,8 +64,7 @@ chmod +s /usr/local/spine/bin/spine
|
|||||||
* gzip
|
* gzip
|
||||||
* help2man
|
* help2man
|
||||||
* inetutils-src
|
* inetutils-src
|
||||||
* libmysqlclient
|
* libmariadb-devel (preferred) or libmysqlclient
|
||||||
* libmariadb-devel
|
|
||||||
* libssl-devel
|
* libssl-devel
|
||||||
* libtool
|
* libtool
|
||||||
* m4
|
* m4
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
#
|
#
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
|
|
||||||
|
set -eu
|
||||||
# Name: bootstrap
|
# Name: bootstrap
|
||||||
#
|
#
|
||||||
# Function: build spine from scratch
|
# Function: build spine from scratch
|
||||||
@@ -49,7 +51,7 @@ display_help () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Check for parameters
|
# Check for parameters
|
||||||
if [ "${1}" = "--help" -o "${1}" = "-h" ]; then
|
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
|
||||||
display_help
|
display_help
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -61,22 +63,28 @@ echo "INFO: Removing cache directories"
|
|||||||
rm -rf autom4te.cache .deps
|
rm -rf autom4te.cache .deps
|
||||||
|
|
||||||
# Make sure all files are unix formatted files
|
# Make sure all files are unix formatted files
|
||||||
which dos2unix > /dev/null 2>&1
|
if command -v dos2unix > /dev/null 2>&1; then
|
||||||
if [ $? -eq 0 ]; then
|
for e in ac am c h in md mdlrc rb sh yml; do
|
||||||
for e in $(echo "ac am c h in md mdlrc rb sh yml"); do
|
|
||||||
echo "INFO: Ensuring UNIX format for *.$e"
|
echo "INFO: Ensuring UNIX format for *.$e"
|
||||||
find . -type f -name \*.$e -exec dos2unix --d2u \{\} \; > /dev/null 2>&1
|
find . -type f -name "*.${e}" -exec dos2unix --d2u {} \; > /dev/null 2>&1
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Prepare a build state
|
# Prepare a build state
|
||||||
echo "INFO: Running auto-tools to verify buildability"
|
echo "INFO: Running auto-tools to verify buildability"
|
||||||
aclocal --install
|
mkdir -p config m4
|
||||||
libtoolize
|
|
||||||
autoheader
|
if command -v libtoolize > /dev/null 2>&1; then
|
||||||
automake --add-missing
|
LIBTOOLIZE=libtoolize
|
||||||
|
elif command -v glibtoolize > /dev/null 2>&1; then
|
||||||
|
LIBTOOLIZE=glibtoolize
|
||||||
|
else
|
||||||
|
echo "ERROR: GNU libtoolize (or glibtoolize on macOS) is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
export LIBTOOLIZE
|
||||||
|
|
||||||
autoreconf --force --install
|
autoreconf --force --install
|
||||||
[ $? -ne 0 ] && echo "ERROR: 'autoreconf' exited with errors" && exit -1
|
|
||||||
|
|
||||||
|
|
||||||
# Provide some meaningful notes
|
# Provide some meaningful notes
|
||||||
|
|||||||
+61
-91
@@ -57,15 +57,10 @@ AC_ARG_WITH(snmp,
|
|||||||
[SNMP_DIR=$withval]
|
[SNMP_DIR=$withval]
|
||||||
)
|
)
|
||||||
|
|
||||||
# if host_alias is empty, ac_cv_host_alias may still have the info
|
|
||||||
if test -z "$host_alias"; then
|
|
||||||
host_alias=$ac_cv_host_alias
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Platform-specific tweaks
|
# Platform-specific tweaks
|
||||||
ShLib="so"
|
ShLib="so"
|
||||||
|
|
||||||
case $host_alias in
|
case $host in
|
||||||
*sparc-sun-solaris2.8)
|
*sparc-sun-solaris2.8)
|
||||||
CPPFLAGS="$CPPFLAGS -D_POSIX_PTHREAD_SEMANTICS"
|
CPPFLAGS="$CPPFLAGS -D_POSIX_PTHREAD_SEMANTICS"
|
||||||
AC_DEFINE(SOLAR_THREAD, 1, [Correct issue around Solaris threading model]);;
|
AC_DEFINE(SOLAR_THREAD, 1, [Correct issue around Solaris threading model]);;
|
||||||
@@ -217,98 +212,73 @@ else
|
|||||||
AC_MSG_RESULT([no])
|
AC_MSG_RESULT([no])
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ****************** MySQL Checks ***********************
|
# ****************** MySQL-compatible client checks ***********************
|
||||||
AC_DEFUN([MYSQL_LIB_CHK],
|
AC_ARG_VAR([MYSQL_CONFIG], [path to mariadb_config or mysql_config])
|
||||||
[ str="$1/libmysqlclient.*"
|
|
||||||
for j in `echo $str`; do
|
if test -n "$MYSQL_DIR" && test -z "$MYSQL_CONFIG"; then
|
||||||
if test -r $j; then
|
for mysql_config_candidate in mariadb_config mysql_config; do
|
||||||
MYSQL_LIB_DIR=$1
|
if test -x "$MYSQL_DIR/bin/$mysql_config_candidate"; then
|
||||||
|
MYSQL_CONFIG="$MYSQL_DIR/bin/$mysql_config_candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -z "$MYSQL_CONFIG"; then
|
||||||
|
AC_PATH_PROGS([MYSQL_CONFIG], [mariadb_config mysql_config])
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -n "$MYSQL_CONFIG"; then
|
||||||
|
AC_MSG_NOTICE([using $MYSQL_CONFIG for database client flags])
|
||||||
|
if ! MYSQL_CFLAGS=`"$MYSQL_CONFIG" --cflags`; then
|
||||||
|
AC_MSG_ERROR([$MYSQL_CONFIG --cflags failed])
|
||||||
|
fi
|
||||||
|
if ! MYSQL_LIBS=`"$MYSQL_CONFIG" --libs`; then
|
||||||
|
AC_MSG_ERROR([$MYSQL_CONFIG --libs failed])
|
||||||
|
fi
|
||||||
|
CFLAGS="$MYSQL_CFLAGS $CFLAGS"
|
||||||
|
LIBS="$MYSQL_LIBS $LIBS"
|
||||||
|
else
|
||||||
|
AC_MSG_NOTICE([no client config tool found; using header and library discovery])
|
||||||
|
|
||||||
|
mysql_header_prefix=
|
||||||
|
|
||||||
|
for mysql_prefix in $MYSQL_DIR /usr /usr/local /opt /opt/mysql /usr/pkg /usr/local/mysql; do
|
||||||
|
for mysql_include_dir in include include/mysql include/mariadb mysql; do
|
||||||
|
if test -f "$mysql_prefix/$mysql_include_dir/mysql.h"; then
|
||||||
|
CFLAGS="-I$mysql_prefix/$mysql_include_dir $CFLAGS"
|
||||||
|
mysql_header_prefix=$mysql_prefix
|
||||||
break 2
|
break 2
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Determine MySQL installation paths
|
|
||||||
MYSQL_SUB_DIR="include include/mysql include/mariadb mysql";
|
|
||||||
for i in $MYSQL_DIR /usr /usr/local /opt /opt/mysql /usr/pkg /usr/local/mysql; do
|
|
||||||
for d in $MYSQL_SUB_DIR; do
|
|
||||||
if [[ -f $i/$d/mysql.h ]]; then
|
|
||||||
MYSQL_INC_DIR=$i/$d
|
|
||||||
break;
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ ! -z $MYSQL_INC_DIR ]]; then
|
dnl The header can turn up under a prefix the caller never named, so search
|
||||||
break;
|
dnl for the library under that same prefix. Adding only the MYSQL_DIR paths
|
||||||
|
dnl leaves mysql_init unlinkable when the client lives anywhere else.
|
||||||
|
mysql_library_prefixes="$MYSQL_DIR"
|
||||||
|
|
||||||
|
if test -n "$mysql_header_prefix" && test "x$mysql_header_prefix" != "x$MYSQL_DIR"; then
|
||||||
|
mysql_library_prefixes="$mysql_library_prefixes $mysql_header_prefix"
|
||||||
fi
|
fi
|
||||||
# test -f $i/include/mysql.h && MYSQL_INC_DIR=$i/include && break
|
|
||||||
# test -f $i/include/mysql/mysql.h && MYSQL_INC_DIR=$i/include/mysql && break
|
|
||||||
# test -f $i/include/mariadb/mysql.h && MYSQL_INC_DIR=$i/include/mariadb && break
|
|
||||||
# test -f $i/mysql/include/mysql.h && MYSQL_INC_DIR=$i/mysql/include && break
|
|
||||||
done
|
|
||||||
|
|
||||||
if test -z "$MYSQL_INC_DIR"; then
|
for mysql_prefix in $mysql_library_prefixes; do
|
||||||
if test "x$MYSQL_DIR" != "x"; then
|
for mysql_library_dir in lib64 lib64/mysql lib lib/mysql; do
|
||||||
AC_MSG_ERROR(Cannot find MySQL header files under $MYSQL_DIR)
|
if test -d "$mysql_prefix/$mysql_library_dir"; then
|
||||||
else
|
LDFLAGS="-L$mysql_prefix/$mysql_library_dir $LDFLAGS"
|
||||||
AC_MSG_ERROR(Cannot find MySQL headers. Use --with-mysql= to specify non-default path.)
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
for i in $MYSQL_DIR /usr /usr/local /opt /opt/mysql /usr/pkg /usr/local/mysql; do
|
|
||||||
MYSQL_LIB_CHK($i/lib64)
|
|
||||||
MYSQL_LIB_CHK($i/lib64/mysql)
|
|
||||||
MYSQL_LIB_CHK($i/lib/x86_64-linux-gnu)
|
|
||||||
MYSQL_LIB_CHK($i/lib/x86_64-linux-gnu/mysql)
|
|
||||||
MYSQL_LIB_CHK($i/lib)
|
|
||||||
MYSQL_LIB_CHK($i/lib/mysql)
|
|
||||||
done
|
|
||||||
|
|
||||||
if test -n "$MYSQL_LIB_DIR" ; then
|
|
||||||
LDFLAGS="-L$MYSQL_LIB_DIR $LDFLAGS"
|
|
||||||
fi
|
|
||||||
CFLAGS="-I$MYSQL_INC_DIR $CFLAGS"
|
|
||||||
|
|
||||||
unamestr=$(uname)
|
|
||||||
if test $unamestr = 'OpenBSD'; then
|
|
||||||
AC_CHECK_LIB(mysqlclient, mysql_init,
|
|
||||||
[ LIBS="-lmysqlclient -lexecinfo -lm $LIBS"
|
|
||||||
AC_DEFINE(HAVE_MYSQL, 1, MySQL Client API)
|
|
||||||
HAVE_MYSQL=yes ],
|
|
||||||
[ HAVE_MYSQL=no ]
|
|
||||||
)
|
|
||||||
else
|
|
||||||
AC_CHECK_LIB(mysqlclient, mysql_init,
|
|
||||||
[ LIBS="-lmysqlclient -lm -ldl $LIBS"
|
|
||||||
AC_DEFINE(HAVE_MYSQL, 1, MySQL Client API)
|
|
||||||
HAVE_MYSQL=yes ],
|
|
||||||
[ HAVE_MYSQL=no ]
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if test -f $MYSQL_LIB_DIR/libmysqlclient_r.a -o -f $MYSQL_LIB_DIR/libmysqlclient_r.$ShLib; then
|
|
||||||
LIBS="-lmysqlclient_r -lm -ldl $LIBS"
|
|
||||||
else
|
|
||||||
if test -f $MYSQL_LIB_DIR/libmysqlclient_r.a -o -f $MYSQL_LIB_DIR/libmysqlclient_r.$ShLib ; then
|
|
||||||
LIBS="-lmysqlclient_r -lm -ldl $LIBS"
|
|
||||||
else
|
|
||||||
if test "$HAVE_MYSQL" = "yes"; then
|
|
||||||
if test $unamestr = 'OpenBSD'; then
|
|
||||||
LIBS="-lmysqlclient -lm $LIBS"
|
|
||||||
else
|
|
||||||
LIBS="-lmysqlclient -lm -ldl $LIBS"
|
|
||||||
fi
|
fi
|
||||||
else
|
done
|
||||||
if test -f $MYSQL_LIB_DIR/libperconaserverclient.a -o -f $MYSQL_LIB_DIR/libperconaserverclient.$ShLib; then
|
done
|
||||||
LIBS="-lperconaserverclient -lm -ldl $LIBS"
|
|
||||||
else
|
|
||||||
LIBS="-lmariadbclient -lm -ldl $LIBS"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
AC_CHECK_HEADER([mysql.h], [],
|
||||||
|
[AC_MSG_ERROR([Cannot find mysql.h. Install MariaDB Connector/C or MySQL Connector/C development files.])])
|
||||||
|
|
||||||
|
AC_SEARCH_LIBS([mysql_init], [mariadb mysqlclient perconaserverclient],
|
||||||
|
[AC_DEFINE([HAVE_MYSQL], [1], [MySQL-compatible Client API])
|
||||||
|
HAVE_MYSQL=yes],
|
||||||
|
[AC_MSG_ERROR([Cannot link a MySQL-compatible client library.])])
|
||||||
|
|
||||||
# ****************** Net-SNMP Checks ***********************
|
# ****************** Net-SNMP Checks ***********************
|
||||||
if test "x$SNMP_DIR" != "x"; then
|
if test "x$SNMP_DIR" != "x"; then
|
||||||
for i in / /net-snmp /include/net-snmp; do
|
for i in / /net-snmp /include/net-snmp; do
|
||||||
@@ -501,7 +471,7 @@ fi
|
|||||||
AC_MSG_CHECKING([if we can support mysql/mariadb retry count])
|
AC_MSG_CHECKING([if we can support mysql/mariadb retry count])
|
||||||
AC_LINK_IFELSE([AC_LANG_PROGRAM([[
|
AC_LINK_IFELSE([AC_LANG_PROGRAM([[
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include "$MYSQL_INC_DIR/mysql.h"
|
#include <mysql.h>
|
||||||
]], [[
|
]], [[
|
||||||
if (MYSQL_OPT_RETRY_COUNT) {
|
if (MYSQL_OPT_RETRY_COUNT) {
|
||||||
exit(0);
|
exit(0);
|
||||||
@@ -516,7 +486,7 @@ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
|
|||||||
AC_MSG_CHECKING([if we mysql/mariadb supports verify certificates])
|
AC_MSG_CHECKING([if we mysql/mariadb supports verify certificates])
|
||||||
AC_LINK_IFELSE([AC_LANG_PROGRAM([[
|
AC_LINK_IFELSE([AC_LANG_PROGRAM([[
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include "$MYSQL_INC_DIR/mysql.h"
|
#include <mysql.h>
|
||||||
]], [[
|
]], [[
|
||||||
if (MYSQL_OPT_SSL_VERIFY_SERVER_CERT) {
|
if (MYSQL_OPT_SSL_VERIFY_SERVER_CERT) {
|
||||||
exit(0);
|
exit(0);
|
||||||
@@ -532,7 +502,7 @@ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
|
|||||||
AC_MSG_CHECKING([if we can support mysql/mariadb ssl keys])
|
AC_MSG_CHECKING([if we can support mysql/mariadb ssl keys])
|
||||||
AC_LINK_IFELSE([AC_LANG_PROGRAM([[
|
AC_LINK_IFELSE([AC_LANG_PROGRAM([[
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include "$MYSQL_INC_DIR/mysql.h"
|
#include <mysql.h>
|
||||||
]], [[
|
]], [[
|
||||||
if (MYSQL_OPT_SSL_KEY) {
|
if (MYSQL_OPT_SSL_KEY) {
|
||||||
exit(0);
|
exit(0);
|
||||||
|
|||||||
@@ -533,7 +533,7 @@ int php_init(int php_process) {
|
|||||||
int cacti2php_pdes[2] = { -1, -1 };
|
int cacti2php_pdes[2] = { -1, -1 };
|
||||||
int php2cacti_pdes[2] = { -1, -1 };
|
int php2cacti_pdes[2] = { -1, -1 };
|
||||||
pid_t pid;
|
pid_t pid;
|
||||||
char poller_id[TINY_BUFSIZE];
|
char poller_id[SMALL_BUFSIZE];
|
||||||
char *argv[7];
|
char *argv[7];
|
||||||
char arg_q[] = "-q";
|
char arg_q[] = "-q";
|
||||||
char arg_spine[] = "spine";
|
char arg_spine[] = "spine";
|
||||||
@@ -602,7 +602,7 @@ int php_init(int php_process) {
|
|||||||
argv[1] = arg_q;
|
argv[1] = arg_q;
|
||||||
argv[2] = set.path_php_server;
|
argv[2] = set.path_php_server;
|
||||||
argv[3] = arg_spine;
|
argv[3] = arg_spine;
|
||||||
snprintf(poller_id, TINY_BUFSIZE, "%d", set.poller_id);
|
snprintf(poller_id, SMALL_BUFSIZE, "%d", set.poller_id);
|
||||||
argv[4] = poller_id;
|
argv[4] = poller_id;
|
||||||
argv[5] = NULL;
|
argv[5] = NULL;
|
||||||
} else if (set.poller_id > 1) {
|
} else if (set.poller_id > 1) {
|
||||||
@@ -611,7 +611,7 @@ int php_init(int php_process) {
|
|||||||
argv[2] = set.path_php_server;
|
argv[2] = set.path_php_server;
|
||||||
argv[3] = arg_environ_spine;
|
argv[3] = arg_environ_spine;
|
||||||
|
|
||||||
snprintf(poller_id, TINY_BUFSIZE, "--poller=%d", set.poller_id);
|
snprintf(poller_id, SMALL_BUFSIZE, "--poller=%d", set.poller_id);
|
||||||
argv[4] = poller_id;
|
argv[4] = poller_id;
|
||||||
|
|
||||||
if (set.mode == REMOTE_ONLINE) {
|
if (set.mode == REMOTE_ONLINE) {
|
||||||
@@ -626,7 +626,7 @@ int php_init(int php_process) {
|
|||||||
argv[1] = arg_q;
|
argv[1] = arg_q;
|
||||||
argv[2] = set.path_php_server;
|
argv[2] = set.path_php_server;
|
||||||
argv[3] = arg_environ_spine;
|
argv[3] = arg_environ_spine;
|
||||||
snprintf(poller_id, TINY_BUFSIZE, "--poller=%d", set.poller_id);
|
snprintf(poller_id, SMALL_BUFSIZE, "--poller=%d", set.poller_id);
|
||||||
argv[4] = poller_id;
|
argv[4] = poller_id;
|
||||||
|
|
||||||
argv[5] = NULL;
|
argv[5] = NULL;
|
||||||
|
|||||||
@@ -399,7 +399,6 @@ int ping_icmp(host_t *host, ping_t *ping) {
|
|||||||
|
|
||||||
static unsigned int seq = 0;
|
static unsigned int seq = 0;
|
||||||
struct icmp *icmp;
|
struct icmp *icmp;
|
||||||
struct icmp *pkt;
|
|
||||||
unsigned char *packet = NULL;
|
unsigned char *packet = NULL;
|
||||||
|
|
||||||
if (is_debug_device(host->id)) {
|
if (is_debug_device(host->id)) {
|
||||||
@@ -611,8 +610,6 @@ int ping_icmp(host_t *host, ping_t *ping) {
|
|||||||
goto keep_listening;
|
goto keep_listening;
|
||||||
}
|
}
|
||||||
|
|
||||||
pkt = (struct icmp *) reply_pkt;
|
|
||||||
|
|
||||||
if (fromname.sin_addr.s_addr == recvname.sin_addr.s_addr) {
|
if (fromname.sin_addr.s_addr == recvname.sin_addr.s_addr) {
|
||||||
if (verdict == SPINE_ICMP_REPLY_OK) {
|
if (verdict == SPINE_ICMP_REPLY_OK) {
|
||||||
|
|
||||||
@@ -1564,7 +1561,7 @@ name_t *get_namebyhost(char *hostname, name_t *name) {
|
|||||||
if (tokens == 1) {
|
if (tokens == 1) {
|
||||||
if (strlen(token) && token[0] == '[') {
|
if (strlen(token) && token[0] == '[') {
|
||||||
SPINE_LOG_DEBUG(("DEBUG: get_namebyhost(%s) - Have TCPv6 method", hostname));
|
SPINE_LOG_DEBUG(("DEBUG: get_namebyhost(%s) - Have TCPv6 method", hostname));
|
||||||
strncpy(name->hostname, hostname, sizeof(name->hostname));
|
strncopy(name->hostname, hostname, sizeof(name->hostname));
|
||||||
break;
|
break;
|
||||||
} else if (strlen(token) == 3) {
|
} else if (strlen(token) == 3) {
|
||||||
if (strncasecmp(token, "TCP", 3) == 0) {
|
if (strncasecmp(token, "TCP", 3) == 0) {
|
||||||
|
|||||||
@@ -34,6 +34,44 @@
|
|||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "spine.h"
|
#include "spine.h"
|
||||||
|
|
||||||
|
int format_poller_output_row(char *output, size_t output_size,
|
||||||
|
int local_data_id, const char *escaped_rrd_name,
|
||||||
|
const char *host_time, const char *escaped_result) {
|
||||||
|
const char *timep;
|
||||||
|
int decimal_points = 0;
|
||||||
|
int digits = 0;
|
||||||
|
int written;
|
||||||
|
|
||||||
|
if (output == NULL || output_size == 0 || escaped_rrd_name == NULL ||
|
||||||
|
host_time == NULL || host_time[0] == '\0' || escaped_result == NULL) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* host_time is emitted outside SQL quotes. Accept the integer timestamps
|
||||||
|
* Spine generates and a single fractional part, but no SQL syntax. */
|
||||||
|
for (timep = host_time; *timep != '\0'; timep++) {
|
||||||
|
if (*timep == '.') {
|
||||||
|
decimal_points++;
|
||||||
|
if (decimal_points > 1) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
} else if (!isdigit((unsigned char)*timep)) {
|
||||||
|
return FALSE;
|
||||||
|
} else {
|
||||||
|
digits++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (digits == 0) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
written = snprintf(output, output_size,
|
||||||
|
" (%i, '%s', FROM_UNIXTIME(%s), '%s')",
|
||||||
|
local_data_id, escaped_rrd_name, host_time, escaped_result);
|
||||||
|
|
||||||
|
return written >= 0 && (size_t)written < output_size;
|
||||||
|
}
|
||||||
|
|
||||||
void child_cleanup(void *arg) {
|
void child_cleanup(void *arg) {
|
||||||
poller_thread_t poller_details = *(poller_thread_t*) arg;
|
poller_thread_t poller_details = *(poller_thread_t*) arg;
|
||||||
|
|
||||||
@@ -158,14 +196,14 @@ int poller_store_hex_result(char *result, size_t result_size, const char *hex, i
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*! \fn void poll_host(int device_counter, int host_id, int host_thread, int host_threads, int host_data_ids, char *host_time, int *host_errors, double host_time_double)
|
/*! \fn void poll_host(int device_counter, int host_id, int host_thread, int host_threads, int host_data_ids, const char *host_time, int *host_errors, double host_time_double)
|
||||||
* \brief core Spine function that polls a host
|
* \brief core Spine function that polls a host
|
||||||
* \param host_id integer value for the host_id from the hosts table in Cacti
|
* \param host_id integer value for the host_id from the hosts table in Cacti
|
||||||
*
|
*
|
||||||
* This function is core to Spine. It takes a host_id and polls it, first
|
* This function is core to Spine. It takes a host_id and polls it, first
|
||||||
* checking reachability and any required data-query reindexing.
|
* checking reachability and any required data-query reindexing.
|
||||||
*/
|
*/
|
||||||
void poll_host(int device_counter, int host_id, int host_thread, int host_threads, int host_data_ids, char *host_time, int *host_errors, double host_time_double) {
|
void poll_host(int device_counter, int host_id, int host_thread, int host_threads, int host_data_ids, const char *host_time, int *host_errors, double host_time_double) {
|
||||||
char query1[BUFSIZE];
|
char query1[BUFSIZE];
|
||||||
char query2[BIG_BUFSIZE];
|
char query2[BIG_BUFSIZE];
|
||||||
char *query3 = NULL;
|
char *query3 = NULL;
|
||||||
@@ -266,10 +304,16 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
|||||||
|
|
||||||
//db_connect(LOCAL, &mysql);
|
//db_connect(LOCAL, &mysql);
|
||||||
local_cnn = db_get_connection(LOCAL);
|
local_cnn = db_get_connection(LOCAL);
|
||||||
|
if (local_cnn == NULL) { // GCOVR_EXCL_START: pool exhaustion is not reachable in tests
|
||||||
|
die("ERROR: Fatal error: poller.c could not obtain a local database connection!");
|
||||||
|
} // GCOVR_EXCL_STOP
|
||||||
mysql = local_cnn->mysql;
|
mysql = local_cnn->mysql;
|
||||||
|
|
||||||
if (set.poller_id > 1 && set.mode == REMOTE_ONLINE) {
|
if (set.poller_id > 1 && set.mode == REMOTE_ONLINE) {
|
||||||
remote_cnn = db_get_connection(REMOTE);
|
remote_cnn = db_get_connection(REMOTE);
|
||||||
|
if (remote_cnn == NULL) { // GCOVR_EXCL_START: pool exhaustion is not reachable in tests
|
||||||
|
die("ERROR: Fatal error: poller.c could not obtain a remote database connection!");
|
||||||
|
} // GCOVR_EXCL_STOP
|
||||||
mysqlr = remote_cnn->mysql;
|
mysqlr = remote_cnn->mysql;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1964,17 +2008,17 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
|||||||
db_escape(&mysqlt, escaped_result, sizeof(escaped_result), poller_items[i].result);
|
db_escape(&mysqlt, escaped_result, sizeof(escaped_result), poller_items[i].result);
|
||||||
db_escape(&mysqlt, escaped_rrd_name, sizeof(escaped_rrd_name), poller_items[i].rrd_name);
|
db_escape(&mysqlt, escaped_rrd_name, sizeof(escaped_rrd_name), poller_items[i].rrd_name);
|
||||||
|
|
||||||
/* escaped_result can now legitimately exceed result_string's capacity for a
|
if (!format_poller_output_row(result_string, sizeof(result_string),
|
||||||
* very large result; snprintf's own bound below still truncates it safely.
|
|
||||||
* Silently-truncated large rows are the separate, already-tracked gap in #598. */
|
|
||||||
#pragma GCC diagnostic push
|
|
||||||
#pragma GCC diagnostic ignored "-Wformat-truncation"
|
|
||||||
snprintf(result_string, RESULTS_BUFFER+SMALL_BUFSIZE, " (%i, '%s', FROM_UNIXTIME(%s), '%s')",
|
|
||||||
poller_items[i].local_data_id,
|
poller_items[i].local_data_id,
|
||||||
escaped_rrd_name,
|
escaped_rrd_name,
|
||||||
host_time,
|
host_time,
|
||||||
escaped_result);
|
escaped_result)) {
|
||||||
#pragma GCC diagnostic pop
|
SPINE_LOG(("Device[%i] HT[%i] ERROR: Poller output for DS[%i] "
|
||||||
|
"exceeds the configured result buffer and was skipped",
|
||||||
|
host_id, host_thread, poller_items[i].local_data_id));
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
result_length = strlen(result_string);
|
result_length = strlen(result_string);
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,15 @@ extern void *child(void *arg);
|
|||||||
extern void child_cleanup(void *arg);
|
extern void child_cleanup(void *arg);
|
||||||
extern void child_cleanup_thread(void *arg);
|
extern void child_cleanup_thread(void *arg);
|
||||||
extern void child_cleanup_script(void *arg);
|
extern void child_cleanup_script(void *arg);
|
||||||
extern void poll_host(int device_counter, int host_id, int host_thread, int host_threads, int host_data_ids, char *host_time, int *host_errors, double host_time_double);
|
extern void poll_host(int device_counter, int host_id, int host_thread, int host_threads, int host_data_ids, const char *host_time, int *host_errors, double host_time_double);
|
||||||
extern int poller_store_hex_result(char *result, size_t result_size, const char *hex, int *errors);
|
extern int poller_store_hex_result(char *result, size_t result_size, const char *hex, int *errors);
|
||||||
extern char *exec_poll(host_t *current_host, char *command, int id, const char *type);
|
extern char *exec_poll(host_t *current_host, char *command, int id, const char *type);
|
||||||
extern void get_system_information(host_t *host, MYSQL *mysql, int system);
|
extern void get_system_information(host_t *host, MYSQL *mysql, int system);
|
||||||
extern int is_multipart_output(char *result);
|
extern int is_multipart_output(char *result);
|
||||||
extern int validate_result(char *result);
|
extern int validate_result(char *result);
|
||||||
|
extern int format_poller_output_row(char *output, size_t output_size,
|
||||||
|
int local_data_id, const char *escaped_rrd_name,
|
||||||
|
const char *host_time, const char *escaped_result);
|
||||||
extern void buffer_output_errors(char * error_string, int * buf_size, int * buf_errors, int device_id, int thread_id, int local_data_id, bool flush);
|
extern void buffer_output_errors(char * error_string, int * buf_size, int * buf_errors, int device_id, int thread_id, int local_data_id, bool flush);
|
||||||
|
|
||||||
#endif /* SPINE_POLLER_H */
|
#endif /* SPINE_POLLER_H */
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Verify that every commit in a revision range has a Signed-off-by trailer
|
||||||
|
# matching that commit's author. This enforces the Developer Certificate of
|
||||||
|
# Origin without granting a third-party action access to the repository.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -ne 1 || -z $1 ]]; then
|
||||||
|
echo "usage: $0 <git-revision-range>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
range=$1
|
||||||
|
failures=0
|
||||||
|
|
||||||
|
while IFS= read -r commit; do
|
||||||
|
author=$(git show -s --format='%an <%ae>' "$commit")
|
||||||
|
if ! git show -s --format='%B' "$commit" \
|
||||||
|
| git interpret-trailers --parse \
|
||||||
|
| grep -Fqi "Signed-off-by: $author"; then
|
||||||
|
echo "${commit}: missing Signed-off-by: ${author}" >&2
|
||||||
|
failures=$((failures + 1))
|
||||||
|
fi
|
||||||
|
done < <(git rev-list --reverse "$range")
|
||||||
|
|
||||||
|
if ((failures > 0)); then
|
||||||
|
echo "$failures commit(s) failed the DCO check." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "All commits in $range have matching Signed-off-by trailers."
|
||||||
@@ -214,8 +214,8 @@ void snmp_spine_close(void) {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_community,
|
void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_community,
|
||||||
char *snmp_username, char *snmp_password, char *snmp_auth_protocol,
|
char *snmp_username, const char *snmp_password, char *snmp_auth_protocol,
|
||||||
char *snmp_priv_passphrase, char *snmp_priv_protocol,
|
const char *snmp_priv_passphrase, char *snmp_priv_protocol,
|
||||||
char *snmp_context, char *snmp_engine_id, int snmp_port, int snmp_timeout) {
|
char *snmp_context, char *snmp_engine_id, int snmp_port, int snmp_timeout) {
|
||||||
|
|
||||||
void *sessp = NULL;
|
void *sessp = NULL;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
extern void snmp_spine_init(void);
|
extern void snmp_spine_init(void);
|
||||||
extern void snmp_spine_close(void);
|
extern void snmp_spine_close(void);
|
||||||
extern void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_community, char *snmp_username, char *snmp_password, char *snmp_auth_protocol, char *snmp_priv_passphrase, char *snmp_priv_protocol, char *snmp_context, char *snmp_engine_id, int snmp_port, int snmp_timeout);
|
extern void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_community, char *snmp_username, const char *snmp_password, char *snmp_auth_protocol, const char *snmp_priv_passphrase, char *snmp_priv_protocol, char *snmp_context, char *snmp_engine_id, int snmp_port, int snmp_timeout);
|
||||||
extern void snmp_host_cleanup(void *snmp_session);
|
extern void snmp_host_cleanup(void *snmp_session);
|
||||||
extern char *snmp_get_base(host_t *current_host, const char *snmp_oid, bool should_fail);
|
extern char *snmp_get_base(host_t *current_host, const char *snmp_oid, bool should_fail);
|
||||||
extern int snmp_varbind_is_exception(const struct variable_list *vars);
|
extern int snmp_varbind_is_exception(const struct variable_list *vars);
|
||||||
|
|||||||
@@ -509,7 +509,7 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (i=0; i<CONFIG_PATHS; i++) {
|
for (i=0; i<CONFIG_PATHS; i++) {
|
||||||
snprintf(conf_file, DBL_BUFSIZE, "%s%s", config_paths[i], DEFAULT_CONF_FILE);
|
snprintf(conf_file, DBL_BUFSIZE, "%.*s%s", (int) sizeof(config_paths[i]) - 1, config_paths[i], DEFAULT_CONF_FILE);
|
||||||
|
|
||||||
if (read_spine_config(conf_file) >= 0) {
|
if (read_spine_config(conf_file) >= 0) {
|
||||||
valid_conf_file = TRUE;
|
valid_conf_file = TRUE;
|
||||||
@@ -517,7 +517,7 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (i == CONFIG_PATHS-1) {
|
if (i == CONFIG_PATHS-1) {
|
||||||
snprintf(conf_file, DBL_BUFSIZE, "%s%s", config_paths[0], DEFAULT_CONF_FILE);
|
snprintf(conf_file, DBL_BUFSIZE, "%.*s%s", (int) sizeof(config_paths[0]) - 1, config_paths[0], DEFAULT_CONF_FILE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -851,7 +851,7 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
snprintf(host_time, SMALL_BUFSIZE, "%lu", (unsigned long) time(NULL));
|
snprintf(host_time, SMALL_BUFSIZE, "%lu", (unsigned long) time(NULL));
|
||||||
host_time_double = get_time_as_double();
|
host_time_double = get_time_as_double();
|
||||||
} else if (host_time_double == 0 || host_time == 0 || host_time == NULL) {
|
} else if (host_time_double == 0) {
|
||||||
snprintf(host_time, SMALL_BUFSIZE, "%lu", (unsigned long) time(NULL));
|
snprintf(host_time, SMALL_BUFSIZE, "%lu", (unsigned long) time(NULL));
|
||||||
host_time_double = get_time_as_double();
|
host_time_double = get_time_as_double();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Spine test harness
|
||||||
|
|
||||||
|
`make check` runs the cmocka unit tests. The Docker-backed integration fixture
|
||||||
|
uses a real Net-SNMP agent and MariaDB or MySQL to exercise the complete poll
|
||||||
|
and persistence path.
|
||||||
|
|
||||||
|
Run the standard end-to-end test with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
SPINE_TEST_DATABASE=mariadb tests/snmpv3/scripts/run-integration.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the concurrency contract with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
SPINE_TEST_HOSTS=24 \
|
||||||
|
SPINE_TEST_THREAD_COUNTS="1 4 12" \
|
||||||
|
SPINE_TEST_REPEATS=1 \
|
||||||
|
tests/integration/test_concurrency.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Exercise timeout and recovery behavior with real UDP packet loss:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tests/integration/test_network_faults.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The concurrency test creates an isolated Compose project, clones independent
|
||||||
|
SNMPv3 host records, sweeps worker counts, and verifies exact output and host
|
||||||
|
accounting after every run. It is intentionally transport-neutral so it can
|
||||||
|
also test a future libuv engine and reactor-count matrix without changing its
|
||||||
|
assertions. Fixture services communicate only on their project-local Compose
|
||||||
|
network, so independent test projects can run without competing for host ports.
|
||||||
|
The fault test uses `tc netem` inside the test-only SNMP agent container to
|
||||||
|
drop every response, verifies bounded completion without stale output, removes
|
||||||
|
the fault, and verifies the next poll recovers.
|
||||||
|
|
||||||
|
Useful controls:
|
||||||
|
|
||||||
|
- `SPINE_TEST_TIMEOUT_SECONDS`: per-Spine-run timeout (default `120`).
|
||||||
|
- `SPINE_TEST_ARTIFACT_DIR`: destination for logs and TAP results.
|
||||||
|
- `SPINE_TEST_KEEP_ENV_ON_FAILURE=1`: preserve containers after a failure.
|
||||||
|
- `SPINE_TEST_PROJECT`: explicit Compose project name for reproducibility.
|
||||||
|
- `SPINE_TEST_EXTRA_ARGS`: additional whitespace-separated Spine arguments.
|
||||||
|
This provides the hook for future `--snmp-engine` and reactor-count sweeps;
|
||||||
|
arguments are split but never evaluated as shell code.
|
||||||
|
|
||||||
|
Keep CI workloads bounded. Larger host counts and repeat counts belong in a
|
||||||
|
scheduled soak workflow rather than the pull-request critical path.
|
||||||
+9
-4
@@ -13,6 +13,8 @@
|
|||||||
# fuzzer is always built with a clang that supports -fsanitize=fuzzer.
|
# fuzzer is always built with a clang that supports -fsanitize=fuzzer.
|
||||||
FUZZ_CC ?= clang
|
FUZZ_CC ?= clang
|
||||||
FUZZ_SECONDS ?= 60
|
FUZZ_SECONDS ?= 60
|
||||||
|
FUZZ_TIMEOUT ?= 5
|
||||||
|
FUZZ_RSS_MB ?= 2048
|
||||||
SPINE_ROOT := ../..
|
SPINE_ROOT := ../..
|
||||||
BINDIR := bin
|
BINDIR := bin
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ SANITIZERS := -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -fno-l
|
|||||||
# hardening options that clash with the sanitizer build. Keep only the includes.
|
# hardening options that clash with the sanitizer build. Keep only the includes.
|
||||||
NETSNMP_CFLAGS := $(filter -I% -D%, $(shell net-snmp-config --base-cflags 2>/dev/null))
|
NETSNMP_CFLAGS := $(filter -I% -D%, $(shell net-snmp-config --base-cflags 2>/dev/null))
|
||||||
NETSNMP_LIBS := $(filter -l% -L%, $(shell net-snmp-config --libs 2>/dev/null))
|
NETSNMP_LIBS := $(filter -l% -L%, $(shell net-snmp-config --libs 2>/dev/null))
|
||||||
|
DB_CONFIG ?= $(shell command -v mariadb_config 2>/dev/null || command -v mysql_config 2>/dev/null)
|
||||||
|
|
||||||
SPINE_SRC := $(addprefix $(SPINE_ROOT)/, \
|
SPINE_SRC := $(addprefix $(SPINE_ROOT)/, \
|
||||||
ping.c util.c sql.c snmp.c src/core/locks.c poller.c nft_popen.c php.c \
|
ping.c util.c sql.c snmp.c src/core/locks.c poller.c nft_popen.c php.c \
|
||||||
@@ -30,11 +33,11 @@ SPINE_SRC := $(addprefix $(SPINE_ROOT)/, \
|
|||||||
FUZZ_CFLAGS := $(SANITIZERS) -g -O1 -DHAVE_CONFIG_H \
|
FUZZ_CFLAGS := $(SANITIZERS) -g -O1 -DHAVE_CONFIG_H \
|
||||||
-I$(SPINE_ROOT) -I$(SPINE_ROOT)/src/core -I$(SPINE_ROOT)/config \
|
-I$(SPINE_ROOT) -I$(SPINE_ROOT)/src/core -I$(SPINE_ROOT)/config \
|
||||||
$(NETSNMP_CFLAGS) \
|
$(NETSNMP_CFLAGS) \
|
||||||
$(shell mysql_config --include 2>/dev/null)
|
$(shell $(DB_CONFIG) --include 2>/dev/null)
|
||||||
|
|
||||||
FUZZ_LDLIBS := $(NETSNMP_LIBS) $(filter -l% -L%, $(shell mysql_config --libs 2>/dev/null))
|
FUZZ_LDLIBS := $(NETSNMP_LIBS) $(filter -l% -L%, $(shell $(DB_CONFIG) --libs 2>/dev/null))
|
||||||
|
|
||||||
TARGETS := $(BINDIR)/fuzz_namebyhost $(BINDIR)/fuzz_icmp_reply
|
TARGETS := $(BINDIR)/fuzz_namebyhost $(BINDIR)/fuzz_output_parser $(BINDIR)/fuzz_icmp_reply
|
||||||
|
|
||||||
.PHONY: all run clean
|
.PHONY: all run clean
|
||||||
|
|
||||||
@@ -51,7 +54,9 @@ run: $(TARGETS)
|
|||||||
name=$$(basename $$t); \
|
name=$$(basename $$t); \
|
||||||
echo "== $$name ($(FUZZ_SECONDS)s)"; \
|
echo "== $$name ($(FUZZ_SECONDS)s)"; \
|
||||||
mkdir -p corpus/$$name; \
|
mkdir -p corpus/$$name; \
|
||||||
$$t corpus/$$name -max_total_time=$(FUZZ_SECONDS) -print_final_stats=1 || rc=1; \
|
$$t corpus/$$name -max_total_time=$(FUZZ_SECONDS) \
|
||||||
|
-timeout=$(FUZZ_TIMEOUT) -rss_limit_mb=$(FUZZ_RSS_MB) \
|
||||||
|
-print_final_stats=1 || rc=1; \
|
||||||
done; exit $$rc
|
done; exit $$rc
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
([unterminated
|
||||||
|
value
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
^[^ ]+$
|
||||||
|
in:1 out:2
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[0-9]+
|
||||||
|
42
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
| Copyright (C) 2004-2026 The Cacti Group |
|
||||||
|
| |
|
||||||
|
| This program is free software; you can redistribute it and/or |
|
||||||
|
| modify it under the terms of the GNU Lesser General Public License |
|
||||||
|
| as published by the Free Software Foundation; either version 2.1 |
|
||||||
|
| of the License, or (at your option) any later version. |
|
||||||
|
+-------------------------------------------------------------------------+
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include "spine.h"
|
||||||
|
#include "poller.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||||
|
char value[1025];
|
||||||
|
char row[RESULTS_BUFFER + SMALL_BUFSIZE];
|
||||||
|
const char *result;
|
||||||
|
static const char *expressions[] = {
|
||||||
|
REGEX_NUMBER,
|
||||||
|
"[0-9][0-9]*",
|
||||||
|
"[a-zA-Z][a-zA-Z0-9_.:-]*",
|
||||||
|
".*"
|
||||||
|
};
|
||||||
|
size_t value_len;
|
||||||
|
|
||||||
|
if (data == NULL || size == 0 || size > sizeof(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
value_len = size - 1;
|
||||||
|
memcpy(value, data + 1, value_len);
|
||||||
|
value[value_len] = '\0';
|
||||||
|
|
||||||
|
result = regex_replace(expressions[data[0] %
|
||||||
|
(sizeof(expressions) / sizeof(expressions[0]))], value);
|
||||||
|
if (result == NULL) {
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)format_poller_output_row(row, sizeof(row), 1,
|
||||||
|
"fuzz", "1700000000", result);
|
||||||
|
(void)is_multipart_output(value);
|
||||||
|
(void)validate_result(value);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -50,6 +50,31 @@ if [[ -u "$SPINE" ]]; then
|
|||||||
exit 77
|
exit 77
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ASan/UBSan/TSan install their own allocator interceptors ahead of anything
|
||||||
|
# LD_PRELOAD can add, so a preloaded calloc() never sees (and cannot fail)
|
||||||
|
# the process's real allocations under those runtimes. Detect a sanitized
|
||||||
|
# binary by its exported runtime-init symbols and skip rather than silently
|
||||||
|
# testing nothing (or, worse, forcing the sanitizer runtime's own bootstrap
|
||||||
|
# allocation to fail and hang instead of exercising the guard at all).
|
||||||
|
#
|
||||||
|
# The symbol table is captured into a variable before grep runs against it
|
||||||
|
# (rather than piping straight into `grep -q`) because grep -q exits as soon
|
||||||
|
# as it finds a match, which under `set -o pipefail` can SIGPIPE the still-
|
||||||
|
# writing nm/strings process and make the pipeline look like it failed even
|
||||||
|
# though the match was found.
|
||||||
|
if command -v nm >/dev/null 2>&1; then
|
||||||
|
spine_symbols="$(nm -D "$SPINE" 2>/dev/null || true)"
|
||||||
|
elif command -v strings >/dev/null 2>&1; then
|
||||||
|
spine_symbols="$(strings "$SPINE" 2>/dev/null || true)"
|
||||||
|
else
|
||||||
|
spine_symbols=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -qE '__asan_init|__tsan_init|__msan_init|__ubsan_(handle|default_options)' <<<"$spine_symbols"; then
|
||||||
|
echo "the spine binary is built with a sanitizer runtime, which takes over calloc() ahead of LD_PRELOAD; skipping"
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
|
||||||
if ! command -v cc >/dev/null 2>&1; then
|
if ! command -v cc >/dev/null 2>&1; then
|
||||||
echo "no compiler available for the interposer; skipping"
|
echo "no compiler available for the interposer; skipping"
|
||||||
exit 77
|
exit 77
|
||||||
|
|||||||
Executable
+191
@@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Exercise Spine against many independent hosts while sweeping worker counts.
|
||||||
|
# This is deliberately backend-neutral so the same contract can exercise the
|
||||||
|
# legacy pthread poller and the future sharded libuv reactor.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
HOSTS=${SPINE_TEST_HOSTS:-24}
|
||||||
|
THREADS=${SPINE_TEST_THREAD_COUNTS:-"1 4 12"}
|
||||||
|
REPEATS=${SPINE_TEST_REPEATS:-1}
|
||||||
|
TIMEOUT_SECONDS=${SPINE_TEST_TIMEOUT_SECONDS:-120}
|
||||||
|
DATABASE=${SPINE_TEST_DATABASE:-mariadb}
|
||||||
|
PROJECT=${SPINE_TEST_PROJECT:-spine-concurrency-${DATABASE}-$$}
|
||||||
|
ARTIFACT_ROOT=${SPINE_TEST_ARTIFACT_DIR:-"$REPO_ROOT/test-artifacts/concurrency"}
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
if [[ -n "${SPINE_TEST_EXTRA_ARGS:-}" ]]; then
|
||||||
|
# This is argument splitting, not evaluation: shell metacharacters remain data.
|
||||||
|
read -r -a EXTRA_ARGS <<< "$SPINE_TEST_EXTRA_ARGS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for numeric_value in "$HOSTS" "$REPEATS" "$TIMEOUT_SECONDS"; do
|
||||||
|
[[ "$numeric_value" =~ ^[0-9]+$ ]] || {
|
||||||
|
echo "host, repeat, and timeout values must be positive integers" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
done
|
||||||
|
(( HOSTS >= 2 && HOSTS <= 500 )) || { echo "SPINE_TEST_HOSTS must be between 2 and 500" >&2; exit 2; }
|
||||||
|
(( REPEATS >= 1 && REPEATS <= 100 )) || { echo "SPINE_TEST_REPEATS must be between 1 and 100" >&2; exit 2; }
|
||||||
|
(( TIMEOUT_SECONDS >= 10 )) || { echo "SPINE_TEST_TIMEOUT_SECONDS must be at least 10" >&2; exit 2; }
|
||||||
|
|
||||||
|
export COMPOSE_PROJECT_NAME=$PROJECT
|
||||||
|
COMPOSE=(docker compose -f "$REPO_ROOT/tests/snmpv3/docker-compose.yml")
|
||||||
|
case "$DATABASE" in
|
||||||
|
mariadb) ;;
|
||||||
|
mysql) COMPOSE+=(-f "$REPO_ROOT/tests/snmpv3/docker-compose.mysql.yml") ;;
|
||||||
|
*) echo "unsupported SPINE_TEST_DATABASE: $DATABASE" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mkdir -p "$ARTIFACT_ROOT"
|
||||||
|
export SPINE_TEST_ARTIFACT_DIR=$ARTIFACT_ROOT
|
||||||
|
# shellcheck source=tests/test-harness.sh
|
||||||
|
source "$REPO_ROOT/tests/test-harness.sh"
|
||||||
|
harness_init "Spine concurrency integration"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
local rc=$?
|
||||||
|
"${COMPOSE[@]}" ps --all > "$ARTIFACT_ROOT/compose-ps.txt" 2>&1 || true
|
||||||
|
"${COMPOSE[@]}" logs --no-color > "$ARTIFACT_ROOT/compose.log" 2>&1 || true
|
||||||
|
if [[ $rc -eq 0 || "${SPINE_TEST_KEEP_ENV_ON_FAILURE:-0}" != 1 ]]; then
|
||||||
|
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
printf 'Preserving failed Compose project %s\n' "$COMPOSE_PROJECT_NAME" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
db_query() {
|
||||||
|
"${COMPOSE[@]}" exec -T -e MYSQL_PWD=spine db \
|
||||||
|
"$DATABASE" -uspine cacti -N -B -e "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_fixture() {
|
||||||
|
local elapsed=0
|
||||||
|
while (( elapsed < 120 )); do
|
||||||
|
if [[ "$(db_query 'SELECT COUNT(*) FROM host;' 2>/dev/null || true)" == 1 ]] && \
|
||||||
|
"${COMPOSE[@]}" exec -T snmpd snmpget -v3 -u testuser -l authPriv \
|
||||||
|
-a SHA-256 -A authpass1234 -x AES -X privpass1234 \
|
||||||
|
localhost:1161 .1.3.6.1.2.1.1.3.0 >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
elapsed=$((elapsed + 2))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_with_timeout() {
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout --signal=TERM --kill-after=10 "$TIMEOUT_SECONDS" "$@"
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Build and start the isolated fixture ==="
|
||||||
|
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true
|
||||||
|
"${COMPOSE[@]}" build spine
|
||||||
|
"${COMPOSE[@]}" up -d db snmpd
|
||||||
|
if wait_for_fixture; then
|
||||||
|
harness_pass "database and SNMP agent became ready"
|
||||||
|
else
|
||||||
|
harness_fail "fixture did not become ready within 120 seconds"
|
||||||
|
harness_finish || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clone the real v3 host and its SNMP item. All rows address the same agent but
|
||||||
|
# maintain independent host/session state, which exposes lost completions and
|
||||||
|
# host ownership errors without requiring hundreds of containers.
|
||||||
|
db_query "
|
||||||
|
DELIMITER //
|
||||||
|
CREATE PROCEDURE seed_stress_hosts(IN wanted INT)
|
||||||
|
BEGIN
|
||||||
|
DECLARE current_id INT DEFAULT 2;
|
||||||
|
WHILE current_id <= wanted DO
|
||||||
|
INSERT INTO host (
|
||||||
|
id, hostname, snmp_community, snmp_version, snmp_username, snmp_password,
|
||||||
|
snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol,
|
||||||
|
snmp_context, snmp_engine_id, snmp_port, snmp_timeout, max_oids,
|
||||||
|
availability_method, ping_method, ping_port, ping_timeout, ping_retries,
|
||||||
|
status, poller_id, device_threads, deleted
|
||||||
|
) SELECT
|
||||||
|
current_id, hostname, snmp_community, snmp_version, snmp_username, snmp_password,
|
||||||
|
snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol,
|
||||||
|
snmp_context, '', snmp_port, snmp_timeout, max_oids,
|
||||||
|
availability_method, ping_method, ping_port, ping_timeout, ping_retries,
|
||||||
|
status, poller_id, device_threads, deleted
|
||||||
|
FROM host WHERE id = 1;
|
||||||
|
|
||||||
|
INSERT INTO poller_item (
|
||||||
|
local_data_id, host_id, action, hostname, snmp_community,
|
||||||
|
snmp_version, snmp_username, snmp_password, snmp_auth_protocol,
|
||||||
|
snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id,
|
||||||
|
snmp_port, snmp_timeout, rrd_name, rrd_path, rrd_num, rrd_step,
|
||||||
|
arg1, deleted, poller_id
|
||||||
|
) SELECT
|
||||||
|
1000 + current_id, current_id, action, hostname, snmp_community,
|
||||||
|
snmp_version, snmp_username, snmp_password, snmp_auth_protocol,
|
||||||
|
snmp_priv_passphrase, snmp_priv_protocol, snmp_context, '',
|
||||||
|
snmp_port, snmp_timeout, rrd_name, rrd_path, rrd_num, rrd_step,
|
||||||
|
arg1, deleted, poller_id
|
||||||
|
FROM poller_item WHERE local_data_id = 1;
|
||||||
|
|
||||||
|
SET current_id = current_id + 1;
|
||||||
|
END WHILE;
|
||||||
|
END//
|
||||||
|
DELIMITER ;
|
||||||
|
CALL seed_stress_hosts($HOSTS);
|
||||||
|
DROP PROCEDURE seed_stress_hosts;"
|
||||||
|
|
||||||
|
harness_assert_eq "$HOSTS" "$(db_query 'SELECT COUNT(*) FROM host;')" \
|
||||||
|
"stress fixture has the requested host count"
|
||||||
|
|
||||||
|
for thread_count in $THREADS; do
|
||||||
|
if [[ ! "$thread_count" =~ ^[0-9]+$ ]] || \
|
||||||
|
(( thread_count < 1 || thread_count > 256 )); then
|
||||||
|
echo "invalid thread count: $thread_count" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
for (( iteration=1; iteration<=REPEATS; iteration++ )); do
|
||||||
|
case_id="threads-${thread_count}-iteration-${iteration}"
|
||||||
|
log_file="$ARTIFACT_ROOT/$case_id.log"
|
||||||
|
db_query "TRUNCATE poller_output; TRUNCATE host_errors;
|
||||||
|
UPDATE host SET total_polls=0, failed_polls=0, status=3;"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
run_with_timeout "${COMPOSE[@]}" run --rm --no-deps --entrypoint spine spine \
|
||||||
|
--conf=/etc/spine/spine.conf -f 1 -l "$HOSTS" -S -t "$thread_count" \
|
||||||
|
"${EXTRA_ARGS[@]}" \
|
||||||
|
> "$log_file" 2>&1
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
output=$(<"$log_file")
|
||||||
|
harness_assert_eq 0 "$rc" "$case_id exits successfully"
|
||||||
|
harness_assert_not_contains "$output" \
|
||||||
|
'segmentation fault|SIGSEGV|Aborted|core dump|FATAL|Polling timed out while waiting' \
|
||||||
|
"$case_id has no fatal, crash, or shutdown-timeout diagnostics"
|
||||||
|
harness_assert_contains "$output" "Threads: ${thread_count}, Devices: ${HOSTS}" \
|
||||||
|
"$case_id reports every device completed"
|
||||||
|
harness_assert_eq "$HOSTS" \
|
||||||
|
"$(db_query "SELECT COUNT(*) FROM poller_output WHERE rrd_name = 'uptime';")" \
|
||||||
|
"$case_id writes exactly one SNMP result per host"
|
||||||
|
harness_assert_eq "$((HOSTS + 1))" \
|
||||||
|
"$(db_query 'SELECT COUNT(*) FROM poller_output;')" \
|
||||||
|
"$case_id neither loses nor duplicates SNMP and script results"
|
||||||
|
harness_assert_eq "$HOSTS" \
|
||||||
|
"$(db_query 'SELECT COUNT(*) FROM host WHERE total_polls > 0;')" \
|
||||||
|
"$case_id accounts for every host poll"
|
||||||
|
harness_assert_eq 0 \
|
||||||
|
"$(db_query 'SELECT COUNT(*) FROM host WHERE failed_polls > 0 OR status <> 3;')" \
|
||||||
|
"$case_id leaves every host healthy"
|
||||||
|
harness_assert_eq 0 "$(db_query 'SELECT COUNT(*) FROM host_errors;')" \
|
||||||
|
"$case_id records no host-level errors"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
harness_finish
|
||||||
Executable
+146
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Verify that complete SNMP packet loss is bounded and polling recovers after
|
||||||
|
# connectivity returns. This is a lifecycle contract for both the current
|
||||||
|
# synchronous implementation and the future libuv reactor.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
TIMEOUT_SECONDS=${SPINE_TEST_TIMEOUT_SECONDS:-120}
|
||||||
|
PROJECT=${SPINE_TEST_PROJECT:-spine-network-faults-$$}
|
||||||
|
ARTIFACT_ROOT=${SPINE_TEST_ARTIFACT_DIR:-"$REPO_ROOT/test-artifacts/network-faults"}
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
if [[ -n "${SPINE_TEST_EXTRA_ARGS:-}" ]]; then
|
||||||
|
read -r -a EXTRA_ARGS <<< "$SPINE_TEST_EXTRA_ARGS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "$TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || (( TIMEOUT_SECONDS < 10 )); then
|
||||||
|
echo "SPINE_TEST_TIMEOUT_SECONDS must be an integer of at least 10" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
export COMPOSE_PROJECT_NAME=$PROJECT
|
||||||
|
COMPOSE=(docker compose -f "$REPO_ROOT/tests/snmpv3/docker-compose.yml")
|
||||||
|
mkdir -p "$ARTIFACT_ROOT"
|
||||||
|
export SPINE_TEST_ARTIFACT_DIR=$ARTIFACT_ROOT
|
||||||
|
# shellcheck source=tests/test-harness.sh
|
||||||
|
source "$REPO_ROOT/tests/test-harness.sh"
|
||||||
|
harness_init "Spine SNMP network-fault integration"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
local rc=$?
|
||||||
|
"${COMPOSE[@]}" exec -T snmpd tc qdisc del dev eth0 root >/dev/null 2>&1 || true
|
||||||
|
"${COMPOSE[@]}" ps --all > "$ARTIFACT_ROOT/compose-ps.txt" 2>&1 || true
|
||||||
|
"${COMPOSE[@]}" logs --no-color > "$ARTIFACT_ROOT/compose.log" 2>&1 || true
|
||||||
|
if [[ $rc -eq 0 || "${SPINE_TEST_KEEP_ENV_ON_FAILURE:-0}" != 1 ]]; then
|
||||||
|
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
printf 'Preserving failed Compose project %s\n' "$COMPOSE_PROJECT_NAME" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
db_query() {
|
||||||
|
"${COMPOSE[@]}" exec -T -e MYSQL_PWD=spine db \
|
||||||
|
mariadb -uspine cacti -N -B -e "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_fixture() {
|
||||||
|
local elapsed=0
|
||||||
|
while (( elapsed < 120 )); do
|
||||||
|
if [[ "$(db_query 'SELECT COUNT(*) FROM host;' 2>/dev/null || true)" == 1 ]] && \
|
||||||
|
"${COMPOSE[@]}" exec -T snmpd snmpget -v3 -u testuser -l authPriv \
|
||||||
|
-a SHA-256 -A authpass1234 -x AES -X privpass1234 \
|
||||||
|
localhost:1161 .1.3.6.1.2.1.1.3.0 >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
elapsed=$((elapsed + 2))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_spine() {
|
||||||
|
local log_file=$1
|
||||||
|
local rc
|
||||||
|
set +e
|
||||||
|
if command -v timeout >/dev/null 2>&1; then
|
||||||
|
timeout --signal=TERM --kill-after=10 "$TIMEOUT_SECONDS" \
|
||||||
|
"${COMPOSE[@]}" run --rm --no-deps --entrypoint spine spine \
|
||||||
|
--conf=/etc/spine/spine.conf -f 1 -l 1 -S "${EXTRA_ARGS[@]}" \
|
||||||
|
> "$log_file" 2>&1
|
||||||
|
else
|
||||||
|
"${COMPOSE[@]}" run --rm --no-deps --entrypoint spine spine \
|
||||||
|
--conf=/etc/spine/spine.conf -f 1 -l 1 -S "${EXTRA_ARGS[@]}" \
|
||||||
|
> "$log_file" 2>&1
|
||||||
|
fi
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
return "$rc"
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_poll_state() {
|
||||||
|
db_query "TRUNCATE poller_output; TRUNCATE host_errors;
|
||||||
|
UPDATE host SET total_polls=0, failed_polls=0, status=3;"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Build and start the isolated fixture ==="
|
||||||
|
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true
|
||||||
|
"${COMPOSE[@]}" build spine snmpd
|
||||||
|
"${COMPOSE[@]}" up -d db snmpd
|
||||||
|
if wait_for_fixture; then
|
||||||
|
harness_pass "database and SNMP agent became ready"
|
||||||
|
else
|
||||||
|
harness_fail "fixture did not become ready within 120 seconds"
|
||||||
|
harness_finish || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
baseline_log="$ARTIFACT_ROOT/baseline.log"
|
||||||
|
reset_poll_state
|
||||||
|
if run_spine "$baseline_log"; then
|
||||||
|
harness_pass "baseline poll exits successfully"
|
||||||
|
else
|
||||||
|
harness_fail "baseline poll exits successfully"
|
||||||
|
fi
|
||||||
|
harness_assert_eq 1 \
|
||||||
|
"$(db_query "SELECT COUNT(*) FROM poller_output WHERE rrd_name = 'uptime';")" \
|
||||||
|
"baseline poll writes its SNMP result"
|
||||||
|
|
||||||
|
echo "=== Drop every response from the SNMP agent ==="
|
||||||
|
"${COMPOSE[@]}" exec -T snmpd tc qdisc replace dev eth0 root netem loss 100%
|
||||||
|
loss_log="$ARTIFACT_ROOT/packet-loss.log"
|
||||||
|
reset_poll_state
|
||||||
|
if run_spine "$loss_log"; then
|
||||||
|
harness_pass "poll under complete packet loss exits successfully"
|
||||||
|
else
|
||||||
|
harness_fail "poll under complete packet loss exits successfully"
|
||||||
|
fi
|
||||||
|
loss_output=$(<"$loss_log")
|
||||||
|
harness_assert_not_contains "$loss_output" \
|
||||||
|
'segmentation fault|SIGSEGV|Aborted|core dump|Polling timed out while waiting' \
|
||||||
|
"packet loss does not crash or deadlock the poller"
|
||||||
|
harness_assert_eq 0 \
|
||||||
|
"$(db_query "SELECT COUNT(*) FROM poller_output WHERE rrd_name = 'uptime';")" \
|
||||||
|
"complete packet loss cannot produce a stale SNMP result"
|
||||||
|
|
||||||
|
echo "=== Restore connectivity and verify recovery ==="
|
||||||
|
"${COMPOSE[@]}" exec -T snmpd tc qdisc del dev eth0 root
|
||||||
|
recovery_log="$ARTIFACT_ROOT/recovery.log"
|
||||||
|
reset_poll_state
|
||||||
|
if run_spine "$recovery_log"; then
|
||||||
|
harness_pass "recovery poll exits successfully"
|
||||||
|
else
|
||||||
|
harness_fail "recovery poll exits successfully"
|
||||||
|
fi
|
||||||
|
recovery_output=$(<"$recovery_log")
|
||||||
|
harness_assert_not_contains "$recovery_output" \
|
||||||
|
'segmentation fault|SIGSEGV|Aborted|core dump|FATAL' \
|
||||||
|
"recovery poll has no fatal diagnostics"
|
||||||
|
harness_assert_eq 1 \
|
||||||
|
"$(db_query "SELECT COUNT(*) FROM poller_output WHERE rrd_name = 'uptime';")" \
|
||||||
|
"SNMP output resumes after packet loss"
|
||||||
|
harness_assert_eq 3 "$(db_query 'SELECT status FROM host WHERE id = 1;')" \
|
||||||
|
"host is healthy after connectivity recovers"
|
||||||
|
|
||||||
|
harness_finish
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mysql:8.4
|
||||||
|
# The Cacti 1.x schema intentionally uses zero-date sentinel values.
|
||||||
|
command: ["--sql-mode=NO_ENGINE_SUBSTITUTION"]
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: cacti
|
||||||
|
MYSQL_USER: spine
|
||||||
|
MYSQL_PASSWORD: spine
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -proot --silent"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
verify:
|
||||||
|
image: mysql:8.4
|
||||||
|
environment:
|
||||||
|
DB_CLIENT: mysql
|
||||||
@@ -19,8 +19,7 @@ services:
|
|||||||
build: ./snmpd
|
build: ./snmpd
|
||||||
cap_add:
|
cap_add:
|
||||||
- SYS_TIME # allows date -s inside container for clock-skew test
|
- SYS_TIME # allows date -s inside container for clock-skew test
|
||||||
ports:
|
- NET_ADMIN # allows tc netem in the network-fault integration test
|
||||||
- "127.0.0.1:10161:1161/udp"
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "snmpget", "-v3", "-u", "testuser", "-l", "authPriv",
|
test: ["CMD", "snmpget", "-v3", "-u", "testuser", "-l", "authPriv",
|
||||||
"-a", "SHA-256", "-A", "authpass1234",
|
"-a", "SHA-256", "-A", "authpass1234",
|
||||||
|
|||||||
@@ -7,20 +7,27 @@ set -euo pipefail
|
|||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
cleanup() { docker compose down -v --remove-orphans >/dev/null 2>&1 || true; }
|
compose=(docker compose)
|
||||||
|
case "${SPINE_TEST_DATABASE:-mariadb}" in
|
||||||
|
mariadb) ;;
|
||||||
|
mysql) compose+=(-f docker-compose.yml -f docker-compose.mysql.yml) ;;
|
||||||
|
*) echo "unsupported SPINE_TEST_DATABASE: ${SPINE_TEST_DATABASE}" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cleanup() { "${compose[@]}" down -v --remove-orphans >/dev/null 2>&1 || true; }
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
# Start from an empty database. Without this the verifier can pass on rows the
|
# Start from an empty database. Without this the verifier can pass on rows the
|
||||||
# previous run wrote, so a poll that produced nothing still looks green.
|
# previous run wrote, so a poll that produced nothing still looks green.
|
||||||
cleanup
|
cleanup
|
||||||
|
|
||||||
docker compose up --build -d
|
"${compose[@]}" up --build -d
|
||||||
|
|
||||||
verify_id=$(docker compose ps -qa verify)
|
verify_id=$("${compose[@]}" ps -qa verify)
|
||||||
[ -n "$verify_id" ] || { echo "verify container was never created"; docker compose logs --no-color; exit 1; }
|
[ -n "$verify_id" ] || { echo "verify container was never created"; "${compose[@]}" logs --no-color; exit 1; }
|
||||||
|
|
||||||
code=$(docker wait "$verify_id")
|
code=$(docker wait "$verify_id")
|
||||||
|
|
||||||
docker compose logs --no-color spine verify
|
"${compose[@]}" logs --no-color spine verify
|
||||||
|
|
||||||
exit "$code"
|
exit "$code"
|
||||||
@@ -42,6 +42,8 @@ echo "=== Phase 2: skew snmpd clock +200s to trigger notInTimeWindow ==="
|
|||||||
"${COMPOSE[@]}" ps snmpd 2>/dev/null | grep -qw "healthy" \
|
"${COMPOSE[@]}" ps snmpd 2>/dev/null | grep -qw "healthy" \
|
||||||
|| { echo " SKIP: snmpd not healthy, skipping clock-skew phase"; exit 0; }
|
|| { echo " SKIP: snmpd not healthy, skipping clock-skew phase"; exit 0; }
|
||||||
|
|
||||||
|
# Expansion must happen inside the container.
|
||||||
|
# shellcheck disable=SC2016
|
||||||
"${COMPOSE[@]}" exec -T snmpd /bin/sh -c \
|
"${COMPOSE[@]}" exec -T snmpd /bin/sh -c \
|
||||||
'date -s "$(date -d "+200 seconds" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -v+200S +%Y-%m-%dT%H:%M:%S)" 2>/dev/null' \
|
'date -s "$(date -d "+200 seconds" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -v+200S +%Y-%m-%dT%H:%M:%S)" 2>/dev/null' \
|
||||||
|| { echo " SKIP: SYS_TIME capability not available, skipping clock-skew phase"; exit 0; }
|
|| { echo " SKIP: SYS_TIME capability not available, skipping clock-skew phase"; exit 0; }
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
# to. Every check here is something a regression in poll_host() would break.
|
# to. Every check here is something a regression in poll_host() would break.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
Q="mariadb -h db -u spine -pspine cacti -N -B -e"
|
DB_CLIENT=${DB_CLIENT:-mariadb}
|
||||||
|
Q="$DB_CLIENT -h db -u spine -pspine cacti -N -B -e"
|
||||||
|
|
||||||
fail() { echo "FAIL: $1"; exit 1; }
|
fail() { echo "FAIL: $1"; exit 1; }
|
||||||
ok() { echo "ok: $1"; }
|
ok() { echo "ok: $1"; }
|
||||||
@@ -32,7 +33,6 @@ status=$($Q "SELECT status FROM host WHERE id = 1;")
|
|||||||
ok "host 1 recorded as up"
|
ok "host 1 recorded as up"
|
||||||
|
|
||||||
# 5. availability accounting ran
|
# 5. availability accounting ran
|
||||||
fails=$($Q "SELECT status_fail_date FROM host WHERE id = 1;")
|
|
||||||
errs=$($Q "SELECT COUNT(*) FROM host_errors WHERE host_id = 1;")
|
errs=$($Q "SELECT COUNT(*) FROM host_errors WHERE host_id = 1;")
|
||||||
[ "$errs" = "0" ] || fail "host_errors has $errs row(s) for host 1"
|
[ "$errs" = "0" ] || fail "host_errors has $errs row(s) for host 1"
|
||||||
ok "no host errors recorded"
|
ok "no host errors recorded"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
iproute2 \
|
||||||
snmpd \
|
snmpd \
|
||||||
snmp \
|
snmp \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
Executable
+113
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared assertions and TAP output for Spine's shell integration tests.
|
||||||
|
# shellcheck shell=bash
|
||||||
|
|
||||||
|
if [[ -n "${SPINE_TEST_HARNESS_LOADED:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
readonly SPINE_TEST_HARNESS_LOADED=1
|
||||||
|
|
||||||
|
HARNESS_PASS=0
|
||||||
|
HARNESS_FAIL=0
|
||||||
|
HARNESS_ASSERTIONS=0
|
||||||
|
HARNESS_SUITE=${HARNESS_SUITE:-spine}
|
||||||
|
HARNESS_ARTIFACT_DIR=${SPINE_TEST_ARTIFACT_DIR:-}
|
||||||
|
HARNESS_TAP_FILE=
|
||||||
|
|
||||||
|
harness_init() {
|
||||||
|
HARNESS_SUITE=$1
|
||||||
|
HARNESS_PASS=0
|
||||||
|
HARNESS_FAIL=0
|
||||||
|
HARNESS_ASSERTIONS=0
|
||||||
|
|
||||||
|
if [[ -n "$HARNESS_ARTIFACT_DIR" ]]; then
|
||||||
|
mkdir -p "$HARNESS_ARTIFACT_DIR"
|
||||||
|
HARNESS_TAP_FILE="$HARNESS_ARTIFACT_DIR/results.tap"
|
||||||
|
printf 'TAP version 13\n' > "$HARNESS_TAP_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
harness_tap_escape() {
|
||||||
|
local message=$1
|
||||||
|
message=${message//$'\n'/ }
|
||||||
|
message=${message//$'\r'/ }
|
||||||
|
printf '%s' "$message"
|
||||||
|
}
|
||||||
|
|
||||||
|
harness_record() {
|
||||||
|
local status=$1
|
||||||
|
shift
|
||||||
|
local message=$*
|
||||||
|
|
||||||
|
HARNESS_ASSERTIONS=$((HARNESS_ASSERTIONS + 1))
|
||||||
|
if [[ "$status" == pass ]]; then
|
||||||
|
HARNESS_PASS=$((HARNESS_PASS + 1))
|
||||||
|
printf ' PASS: %s\n' "$message"
|
||||||
|
if [[ -n "$HARNESS_TAP_FILE" ]]; then
|
||||||
|
printf 'ok %d - %s\n' "$HARNESS_ASSERTIONS" \
|
||||||
|
"$(harness_tap_escape "$message")" >> "$HARNESS_TAP_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
HARNESS_FAIL=$((HARNESS_FAIL + 1))
|
||||||
|
printf ' FAIL: %s\n' "$message" >&2
|
||||||
|
if [[ -n "$HARNESS_TAP_FILE" ]]; then
|
||||||
|
printf 'not ok %d - %s\n' "$HARNESS_ASSERTIONS" \
|
||||||
|
"$(harness_tap_escape "$message")" >> "$HARNESS_TAP_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
harness_pass() { harness_record pass "$@"; }
|
||||||
|
harness_fail() { harness_record fail "$@"; }
|
||||||
|
|
||||||
|
harness_assert_eq() {
|
||||||
|
local expected=$1
|
||||||
|
local actual=$2
|
||||||
|
shift 2
|
||||||
|
if [[ "$actual" == "$expected" ]]; then
|
||||||
|
harness_pass "$* (got $actual)"
|
||||||
|
else
|
||||||
|
harness_fail "$* (expected $expected, got $actual)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
harness_assert_contains() {
|
||||||
|
local value=$1
|
||||||
|
local pattern=$2
|
||||||
|
shift 2
|
||||||
|
if grep -Eq "$pattern" <<< "$value"; then
|
||||||
|
harness_pass "$*"
|
||||||
|
else
|
||||||
|
harness_fail "$* (pattern not found: $pattern)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
harness_assert_not_contains() {
|
||||||
|
local value=$1
|
||||||
|
local pattern=$2
|
||||||
|
shift 2
|
||||||
|
if grep -Eiq "$pattern" <<< "$value"; then
|
||||||
|
harness_fail "$* (unexpected pattern: $pattern)"
|
||||||
|
else
|
||||||
|
harness_pass "$*"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
harness_finish() {
|
||||||
|
printf '\n=== %s: %d passed, %d failed ===\n' \
|
||||||
|
"$HARNESS_SUITE" "$HARNESS_PASS" "$HARNESS_FAIL"
|
||||||
|
|
||||||
|
if [[ -n "$HARNESS_TAP_FILE" ]]; then
|
||||||
|
printf '1..%d\n' "$HARNESS_ASSERTIONS" >> "$HARNESS_TAP_FILE"
|
||||||
|
printf 'TAP results: %s\n' "$HARNESS_TAP_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
|
||||||
|
{
|
||||||
|
printf '### %s\n\n' "$HARNESS_SUITE"
|
||||||
|
printf -- '- Passed: %d\n- Failed: %d\n' "$HARNESS_PASS" "$HARNESS_FAIL"
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ $HARNESS_FAIL -eq 0 ]]
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
#include "spine.h"
|
#include "spine.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "ping.h"
|
#include "ping.h"
|
||||||
|
#include "poller.h"
|
||||||
|
|
||||||
#include "nft_popen.h"
|
#include "nft_popen.h"
|
||||||
|
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
@@ -124,6 +126,52 @@ static void test_spine_appendf_reports_truncation_and_guards(void **state) {
|
|||||||
assert_false(spine_appendf(&cursor, &zero, "%s", "x"));
|
assert_false(spine_appendf(&cursor, &zero, "%s", "x"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_bounded_formatters(void **state) {
|
||||||
|
char capabilities[BUFSIZE];
|
||||||
|
char row[DBL_BUFSIZE + SMALL_BUFSIZE];
|
||||||
|
char small[32];
|
||||||
|
char long_value[DBL_BUFSIZE];
|
||||||
|
const char *auth_start;
|
||||||
|
const char *auth_end;
|
||||||
|
const char *priv_start;
|
||||||
|
const char *priv_end;
|
||||||
|
(void) state;
|
||||||
|
|
||||||
|
memset(long_value, '7', sizeof(long_value) - 1);
|
||||||
|
long_value[sizeof(long_value) - 1] = '\0';
|
||||||
|
|
||||||
|
assert_true(format_spine_capabilities(capabilities,
|
||||||
|
sizeof(capabilities), long_value, long_value));
|
||||||
|
assert_true(strlen(capabilities) < sizeof(capabilities));
|
||||||
|
auth_start = strstr(capabilities, "authProtocols: \"");
|
||||||
|
assert_non_null(auth_start);
|
||||||
|
auth_start += strlen("authProtocols: \"");
|
||||||
|
auth_end = strchr(auth_start, '"');
|
||||||
|
assert_non_null(auth_end);
|
||||||
|
priv_start = strstr(capabilities, "privProtocols: \"");
|
||||||
|
assert_non_null(priv_start);
|
||||||
|
priv_start += strlen("privProtocols: \"");
|
||||||
|
priv_end = strchr(priv_start, '"');
|
||||||
|
assert_non_null(priv_end);
|
||||||
|
assert_int_equal(auth_end - auth_start, CAPABILITY_PROTOCOL_LIST_MAX);
|
||||||
|
assert_int_equal(priv_end - priv_start, CAPABILITY_PROTOCOL_LIST_MAX);
|
||||||
|
assert_string_equal(priv_end, "\" }");
|
||||||
|
assert_false(format_spine_capabilities(small,
|
||||||
|
sizeof(small), long_value, long_value));
|
||||||
|
assert_false(format_spine_capabilities(NULL, 0, long_value, long_value));
|
||||||
|
|
||||||
|
assert_true(format_poller_output_row(row, sizeof(row), 17,
|
||||||
|
"traffic_in", "1700000000.25", long_value));
|
||||||
|
assert_non_null(strstr(row,
|
||||||
|
"(17, 'traffic_in', FROM_UNIXTIME(1700000000.25)"));
|
||||||
|
assert_false(format_poller_output_row(small, sizeof(small), 17,
|
||||||
|
"traffic_in", "1700000000.25", long_value));
|
||||||
|
assert_false(format_poller_output_row(row, sizeof(row), 17,
|
||||||
|
"traffic_in", "1700000000); DROP TABLE host", long_value));
|
||||||
|
assert_false(format_poller_output_row(NULL, 0, 17,
|
||||||
|
"traffic_in", "1700000000.25", long_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* --- predicates ----------------------------------------------------------- */
|
/* --- predicates ----------------------------------------------------------- */
|
||||||
|
|
||||||
@@ -161,6 +209,9 @@ static void test_is_numeric(void **state) {
|
|||||||
static void test_is_hexadecimal(void **state) {
|
static void test_is_hexadecimal(void **state) {
|
||||||
(void) state;
|
(void) state;
|
||||||
assert_int_equal(is_hexadecimal("AA BB CC", 0), TRUE);
|
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("zz", 0), FALSE);
|
||||||
assert_int_equal(is_hexadecimal("", 0), FALSE);
|
assert_int_equal(is_hexadecimal("", 0), FALSE);
|
||||||
}
|
}
|
||||||
@@ -398,7 +449,10 @@ static void test_icmp_classify_rejects_a_non_echo(void **state) {
|
|||||||
const struct icmp *out = NULL;
|
const struct icmp *out = NULL;
|
||||||
(void) state;
|
(void) state;
|
||||||
|
|
||||||
build_ip_icmp(buf, sizeof buf, 1, 1, ICMP_DEST_UNREACH);
|
/* Type 3 is Destination Unreachable in RFC 792. BSD names the symbol
|
||||||
|
* ICMP_UNREACH while Linux names it ICMP_DEST_UNREACH, so keep the wire
|
||||||
|
* value explicit in this platform-independent parser test. */
|
||||||
|
build_ip_icmp(buf, sizeof buf, 1, 1, 3);
|
||||||
assert_int_equal(spine_icmp_classify_reply(buf, sizeof buf, 1, 1, &out), SPINE_ICMP_REPLY_NOT_ECHO);
|
assert_int_equal(spine_icmp_classify_reply(buf, sizeof buf, 1, 1, &out), SPINE_ICMP_REPLY_NOT_ECHO);
|
||||||
assert_null(out);
|
assert_null(out);
|
||||||
}
|
}
|
||||||
@@ -450,6 +504,18 @@ static void test_namebyhost_is_reentrant_across_calls(void **state) {
|
|||||||
free(nb);
|
free(nb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_namebyhost_tcpv6_method_keeps_the_full_hostname(void **state) {
|
||||||
|
char host[64];
|
||||||
|
name_t *n;
|
||||||
|
(void) state;
|
||||||
|
|
||||||
|
strcpy(host, "[fe80::1]:161");
|
||||||
|
n = get_namebyhost(host, NULL);
|
||||||
|
assert_non_null(n);
|
||||||
|
assert_string_equal(n->hostname, "[fe80::1]:161");
|
||||||
|
free(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* --- configuration: defaults, the file parser, and set_option() ----------- */
|
/* --- configuration: defaults, the file parser, and set_option() ----------- */
|
||||||
|
|
||||||
@@ -1001,6 +1067,7 @@ int main(void) {
|
|||||||
cmocka_unit_test(test_regex_replace_returns_the_match),
|
cmocka_unit_test(test_regex_replace_returns_the_match),
|
||||||
cmocka_unit_test(test_regex_replace_passes_through_on_no_match),
|
cmocka_unit_test(test_regex_replace_passes_through_on_no_match),
|
||||||
cmocka_unit_test(test_regex_replace_passes_through_on_bad_pattern),
|
cmocka_unit_test(test_regex_replace_passes_through_on_bad_pattern),
|
||||||
|
cmocka_unit_test(test_bounded_formatters),
|
||||||
cmocka_unit_test(test_spine_appendf_reports_truncation_and_guards),
|
cmocka_unit_test(test_spine_appendf_reports_truncation_and_guards),
|
||||||
cmocka_unit_test(test_all_digits),
|
cmocka_unit_test(test_all_digits),
|
||||||
cmocka_unit_test(test_is_ipaddress),
|
cmocka_unit_test(test_is_ipaddress),
|
||||||
@@ -1027,6 +1094,7 @@ int main(void) {
|
|||||||
cmocka_unit_test(test_icmp_classify_rejects_a_null_buffer),
|
cmocka_unit_test(test_icmp_classify_rejects_a_null_buffer),
|
||||||
cmocka_unit_test(test_namebyhost_plain_hostname),
|
cmocka_unit_test(test_namebyhost_plain_hostname),
|
||||||
cmocka_unit_test(test_namebyhost_is_reentrant_across_calls),
|
cmocka_unit_test(test_namebyhost_is_reentrant_across_calls),
|
||||||
|
cmocka_unit_test(test_namebyhost_tcpv6_method_keeps_the_full_hostname),
|
||||||
cmocka_unit_test(test_config_defaults_populates_the_set),
|
cmocka_unit_test(test_config_defaults_populates_the_set),
|
||||||
cmocka_unit_test(test_read_spine_config_rejects_a_missing_file),
|
cmocka_unit_test(test_read_spine_config_rejects_a_missing_file),
|
||||||
cmocka_unit_test(test_read_spine_config_reads_settings),
|
cmocka_unit_test(test_read_spine_config_reads_settings),
|
||||||
|
|||||||
@@ -495,6 +495,22 @@ static void test_full_script_server_lifecycle(void **state) {
|
|||||||
assert_int_equal(php_processes[0].php_write_fd, -1);
|
assert_int_equal(php_processes[0].php_write_fd, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_legacy_cacti_version_uses_positional_poller_id(void **state) {
|
||||||
|
(void) state;
|
||||||
|
set.cacti_version = 1222;
|
||||||
|
assert_int_equal(php_init(0), TRUE);
|
||||||
|
assert_int_equal(php_processes[0].php_state, PHP_READY);
|
||||||
|
php_close(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_remote_poller_id_uses_environ_argument(void **state) {
|
||||||
|
(void) state;
|
||||||
|
set.poller_id = 2;
|
||||||
|
assert_int_equal(php_init(0), TRUE);
|
||||||
|
assert_int_equal(php_processes[0].php_state, PHP_READY);
|
||||||
|
php_close(0);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_php_child_restores_sigpipe_default(void **state) {
|
static void test_php_child_restores_sigpipe_default(void **state) {
|
||||||
(void) state;
|
(void) state;
|
||||||
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "check-sigpipe");
|
snprintf(set.path_php_server, sizeof(set.path_php_server), "%s", "check-sigpipe");
|
||||||
@@ -1066,6 +1082,8 @@ int main(void) {
|
|||||||
cmocka_unit_test_setup_teardown(test_preexisting_pending_sigpipe_is_preserved, php_setup, php_teardown),
|
cmocka_unit_test_setup_teardown(test_preexisting_pending_sigpipe_is_preserved, php_setup, php_teardown),
|
||||||
cmocka_unit_test_setup_teardown(test_spawnattr_sigpipe_failure_destroys_initialized_attr, php_setup, php_teardown),
|
cmocka_unit_test_setup_teardown(test_spawnattr_sigpipe_failure_destroys_initialized_attr, php_setup, php_teardown),
|
||||||
cmocka_unit_test_setup_teardown(test_full_script_server_lifecycle, php_setup, php_teardown),
|
cmocka_unit_test_setup_teardown(test_full_script_server_lifecycle, php_setup, php_teardown),
|
||||||
|
cmocka_unit_test_setup_teardown(test_legacy_cacti_version_uses_positional_poller_id, php_setup, php_teardown),
|
||||||
|
cmocka_unit_test_setup_teardown(test_remote_poller_id_uses_environ_argument, php_setup, php_teardown),
|
||||||
cmocka_unit_test_setup_teardown(test_php_child_restores_sigpipe_default, php_setup, php_teardown),
|
cmocka_unit_test_setup_teardown(test_php_child_restores_sigpipe_default, php_setup, php_teardown),
|
||||||
cmocka_unit_test_setup_teardown(test_script_server_lifecycle_with_stdio_closed, php_setup, php_teardown),
|
cmocka_unit_test_setup_teardown(test_script_server_lifecycle_with_stdio_closed, php_setup, php_teardown),
|
||||||
cmocka_unit_test_setup_teardown(test_php_spawn_does_not_inherit_an_nft_collision_descriptor, php_setup, php_teardown),
|
cmocka_unit_test_setup_teardown(test_php_spawn_does_not_inherit_an_nft_collision_descriptor, php_setup, php_teardown),
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
|
||||||
|
#define SPINE_STRINGIFY_INNER(value) #value
|
||||||
|
#define SPINE_STRINGIFY(value) SPINE_STRINGIFY_INNER(value)
|
||||||
|
|
||||||
static int nopts = 0;
|
static int nopts = 0;
|
||||||
|
|
||||||
/*! Override Options Structure
|
/*! Override Options Structure
|
||||||
@@ -537,7 +540,7 @@ void read_config_options(void) {
|
|||||||
int mode;
|
int mode;
|
||||||
char web_root[BUFSIZE];
|
char web_root[BUFSIZE];
|
||||||
char sqlbuf[HUGE_BUFSIZE];
|
char sqlbuf[HUGE_BUFSIZE];
|
||||||
char *sqlp = sqlbuf;
|
char *sqlp;
|
||||||
size_t remaining;
|
size_t remaining;
|
||||||
char *res;
|
char *res;
|
||||||
char spine_priv[BUFSIZE];
|
char spine_priv[BUFSIZE];
|
||||||
@@ -939,9 +942,15 @@ void read_config_options(void) {
|
|||||||
strcat(spine_priv, (strlen(spine_priv) > 0 ? ",AES256":"AES256"));
|
strcat(spine_priv, (strlen(spine_priv) > 0 ? ",AES256":"AES256"));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
snprintf(spine_capabilities, BUFSIZE, "{ authProtocols: \"%s\", privProtocols: \"%s\" }", spine_auth, spine_priv);
|
/* Each source buffer can be BUFSIZE bytes. Bound both fields so the
|
||||||
|
* combined capability document always fits in its destination. */
|
||||||
|
if (!format_spine_capabilities(spine_capabilities,
|
||||||
|
sizeof(spine_capabilities), spine_auth, spine_priv)) {
|
||||||
|
SPINE_LOG(("ERROR: Unable to format Spine SNMP capabilities"));
|
||||||
|
spine_capabilities[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
if (set.poller_id == 1) {
|
if (set.poller_id == 1 && spine_capabilities[0] != '\0') {
|
||||||
putsetting(&mysql, LOCAL, "spine_capabilities", spine_capabilities);
|
putsetting(&mysql, LOCAL, "spine_capabilities", spine_capabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,13 +970,13 @@ void read_config_options(void) {
|
|||||||
checking, so a statement cut inside a quoted value reached the server. */
|
checking, so a statement cut inside a quoted value reached the server. */
|
||||||
#define PUSH_ROW_MAX (24 * DBL_BUFSIZE)
|
#define PUSH_ROW_MAX (24 * DBL_BUFSIZE)
|
||||||
|
|
||||||
/*! \fn static void push_flush_batch(MYSQL *mysqlr, char *sqlbuf, char **sqlp, const char *suffix)
|
/*! \fn static void push_flush_batch(MYSQL *mysqlr, const char *sqlbuf, char **sqlp, const char *suffix)
|
||||||
* \brief terminate the accumulated batch and send it to the main server
|
* \brief terminate the accumulated batch and send it to the main server
|
||||||
*
|
*
|
||||||
* Refuses to send a statement that did not fit rather than shipping a
|
* Refuses to send a statement that did not fit rather than shipping a
|
||||||
* truncated one.
|
* truncated one.
|
||||||
*/
|
*/
|
||||||
static void push_flush_batch(MYSQL *mysqlr, char *sqlbuf, char **sqlp, const char *suffix) {
|
static void push_flush_batch(MYSQL *mysqlr, const char *sqlbuf, char **sqlp, const char *suffix) {
|
||||||
size_t remaining = HUGE_BUFSIZE - (*sqlp - sqlbuf);
|
size_t remaining = HUGE_BUFSIZE - (*sqlp - sqlbuf);
|
||||||
|
|
||||||
if (!spine_appendf(sqlp, &remaining, "%s", suffix)) {
|
if (!spine_appendf(sqlp, &remaining, "%s", suffix)) {
|
||||||
@@ -1571,8 +1580,15 @@ int spine_log(const char *format, ...) {
|
|||||||
ulog_len = LOGSIZE - flog_len - prefix_len - 1;
|
ulog_len = LOGSIZE - flog_len - prefix_len - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
strncat(flogmessage, logprefix, prefix_len);
|
/* strncat() here trips -Wstringop-truncation: prefix_len/ulog_len are
|
||||||
strncat(flogmessage, ulogmessage, ulog_len);
|
* runtime-clamped just above to fit, but GCC can't prove that across the
|
||||||
|
* branches. memcpy() plus an explicit terminator is exactly as safe and
|
||||||
|
* doesn't trigger the false positive. */
|
||||||
|
memcpy(flogmessage + flog_len, logprefix, (size_t) prefix_len);
|
||||||
|
flog_len += prefix_len;
|
||||||
|
memcpy(flogmessage + flog_len, ulogmessage, (size_t) ulog_len);
|
||||||
|
flog_len += ulog_len;
|
||||||
|
flogmessage[flog_len] = '\0';
|
||||||
|
|
||||||
/* output to syslog/eventlog */
|
/* output to syslog/eventlog */
|
||||||
if (IS_LOGGING_TO_SYSLOG()) {
|
if (IS_LOGGING_TO_SYSLOG()) {
|
||||||
@@ -1815,6 +1831,7 @@ int is_hexadecimal(const char * str, const short ignore_special) {
|
|||||||
if (ignore_special) {
|
if (ignore_special) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
/* fall through */
|
||||||
default:
|
default:
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
@@ -1872,14 +1889,14 @@ char *strip_alpha(char *string) {
|
|||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*! \fn char *add_slashes(char *string)
|
/*! \fn char *add_slashes(const char *string)
|
||||||
* \brief add escaping to back slashes on for Windows type commands.
|
* \brief add escaping to back slashes on for Windows type commands.
|
||||||
* \param string the string to replace slashes
|
* \param string the string to replace slashes
|
||||||
*
|
*
|
||||||
* \return a pointer to the modified string. Variable must be freed by parent.
|
* \return a pointer to the modified string. Variable must be freed by parent.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
char *add_slashes(char *string) {
|
char *add_slashes(const char *string) {
|
||||||
int length;
|
int length;
|
||||||
int position;
|
int position;
|
||||||
int new_position;
|
int new_position;
|
||||||
@@ -1986,14 +2003,14 @@ char *trim(char *str) {
|
|||||||
*/
|
*/
|
||||||
char *rtrim(char *str) {
|
char *rtrim(char *str) {
|
||||||
char *end;
|
char *end;
|
||||||
const char *trim = " \"\'\\\t\n\r";
|
const char *trim_chars = " \"\'\\\t\n\r";
|
||||||
|
|
||||||
if (!str) return NULL;
|
if (!str) return NULL;
|
||||||
|
|
||||||
end = str + strlen(str);
|
end = str + strlen(str);
|
||||||
|
|
||||||
while (end-- > str) {
|
while (end-- > str) {
|
||||||
if (!strchr(trim, *end)) return str;
|
if (!strchr(trim_chars, *end)) return str;
|
||||||
|
|
||||||
*end = 0;
|
*end = 0;
|
||||||
}
|
}
|
||||||
@@ -2008,12 +2025,12 @@ char *rtrim(char *str) {
|
|||||||
* \return the trimmed string.
|
* \return the trimmed string.
|
||||||
*/
|
*/
|
||||||
char *ltrim(char *str) {
|
char *ltrim(char *str) {
|
||||||
const char *trim = " \"\'\\\t\n\r";
|
const char *trim_chars = " \"\'\\\t\n\r";
|
||||||
|
|
||||||
if (!str) return NULL;
|
if (!str) return NULL;
|
||||||
|
|
||||||
while (*str) {
|
while (*str) {
|
||||||
if (!strchr(trim, *str)) return str;
|
if (!strchr(trim_chars, *str)) return str;
|
||||||
|
|
||||||
++str;
|
++str;
|
||||||
}
|
}
|
||||||
@@ -2337,6 +2354,23 @@ const char *regex_replace(const char *exp, const char *value) {
|
|||||||
return (reti) ? value : msgbuf;
|
return (reti) ? value : msgbuf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int format_spine_capabilities(char *output, size_t output_size,
|
||||||
|
const char *auth_protocols, const char *priv_protocols) {
|
||||||
|
int written;
|
||||||
|
|
||||||
|
if (output == NULL || output_size == 0 ||
|
||||||
|
auth_protocols == NULL || priv_protocols == NULL) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
written = snprintf(output, output_size,
|
||||||
|
"{ authProtocols: \"%." SPINE_STRINGIFY(CAPABILITY_PROTOCOL_LIST_MAX)
|
||||||
|
"s\", privProtocols: \"%." SPINE_STRINGIFY(CAPABILITY_PROTOCOL_LIST_MAX) "s\" }",
|
||||||
|
auth_protocols, priv_protocols);
|
||||||
|
|
||||||
|
return written >= 0 && (size_t)written < output_size;
|
||||||
|
}
|
||||||
|
|
||||||
/*! \fn int spine_appendf(char **cursor, size_t *remaining, const char *fmt, ...)
|
/*! \fn int spine_appendf(char **cursor, size_t *remaining, const char *fmt, ...)
|
||||||
* \brief append to a bounded buffer without walking off the end
|
* \brief append to a bounded buffer without walking off the end
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ extern int is_hexadecimal(const char * str, const short ignore_special);
|
|||||||
extern int is_debug_device(int device_id);
|
extern int is_debug_device(int device_id);
|
||||||
|
|
||||||
/* string and file functions */
|
/* string and file functions */
|
||||||
extern char *add_slashes(char *string);
|
extern char *add_slashes(const char *string);
|
||||||
extern int file_exists(const char *filename);
|
extern int file_exists(const char *filename);
|
||||||
extern char *strip_alpha(char *string);
|
extern char *strip_alpha(char *string);
|
||||||
extern char *strncopy(char *dst, const char *src, size_t n);
|
extern char *strncopy(char *dst, const char *src, size_t n);
|
||||||
@@ -75,7 +75,11 @@ int hex2dec(const char *str, unsigned long long *result);
|
|||||||
|
|
||||||
/* custom regex replace to return a value if matches */
|
/* custom regex replace to return a value if matches */
|
||||||
#define MAX_MATCHES 5
|
#define MAX_MATCHES 5
|
||||||
|
#define REGEX_NUMBER "([-+]*)([0-9]*)([.][0-9]+)"
|
||||||
|
#define CAPABILITY_PROTOCOL_LIST_MAX 480
|
||||||
const char *regex_replace(const char *exp, const char *value);
|
const char *regex_replace(const char *exp, const char *value);
|
||||||
|
int format_spine_capabilities(char *output, size_t output_size,
|
||||||
|
const char *auth_protocols, const char *priv_protocols);
|
||||||
|
|
||||||
/* macro to copy string to string with an ending null */
|
/* macro to copy string to string with an ending null */
|
||||||
#define STRNCOPY(dst, src) strncopy((dst), (src), sizeof(dst))
|
#define STRNCOPY(dst, src) strncopy((dst), (src), sizeof(dst))
|
||||||
|
|||||||
Reference in new issue
Block a user