Files
cacti/tests/Unit/DataCollection/Snmp/SnmpUptimeSelectionTest.php
TheWitness af8449e487 Fix RECACHE ASSERT loop caused by SNMP uptime selection diverging from spine (develop) (#8058)
* fix: match spine's unconditional engine-time preference for SNMP uptime recache (develop)

PR #7982 changed cacti_snmp_select_uptime() to prefer whichever of sysUpTime
and snmpEngineTime is numerically larger. Spine's own reindex-assert re-check
(poller.c) has no such magnitude comparison - it always prefers a numeric
engine time, only falling back to sysUpTime when the engine OID isn't present.

For any device where snmpEngineTime is legitimately smaller than sysUpTime
(e.g. the SNMP agent/engine restarted more recently than the OS, a common,
unremarkable scenario), lib/poller.php's update_reindex_cache() now picks
sysUpTime as the recache baseline while spine always re-derives its live
comparison value from the smaller engine time. Every poll then reports
"RECACHE ASSERT FAILED" and queues a reindex; Cacti recomputes the baseline
the same way, so it never converges - an infinite RECACHE ASSERT loop that
looks like a spine bug even though spine is unchanged.

Fix: add an opt-in $prefer_engine_time parameter to cacti_snmp_select_uptime()
that keeps the existing wall-clock rejection (issue#7342) but skips the
"prefer whichever is larger" comparison, matching spine's rule exactly. Use it
for the two call sites that must stay consistent with spine's own live
re-check: lib/poller.php's recache baseline, and both uptime reads in
cmd.php (update_system_mibs()'s host.snmp_sysUpTimeInstance update, and
ping_and_reindex_check()'s own PHP-poller equivalent of spine's reindex
re-check). The display-only call sites (lib/api_device.php,
lib/api_automation.php) keep the existing wrap-aware "prefer larger" logic,
since they aren't compared against a persisted, spine-shared baseline.

Ported from the 1.2.x fix (#8057). Added tests covering the new parameter and
asserting both call sites opt into it.

* test(snmp): cover prefer_engine_time in the coverage-gated suite

phpunit-snmp.xml's "SNMP coverage" testsuite only runs
SnmpCoverageTest.php, not SnmpUptimeSelectionTest.php where the new
$prefer_engine_time assertions were added. The engine-time-preference
branch in cacti_snmp_select_uptime() was never executed under the
100% line coverage gate, so the "lib/snmp.php 100% coverage" check
failed at 99.8%.

Mirror the same prefer_engine_time assertions into SnmpCoverageTest.php
so the coverage-gated suite exercises the new branch.

* fix: match spine's lack of wall-clock awareness for prefer_engine_time paths

The wall-clock rejection guard ran before the prefer_engine_time branch,
so an OpenBSD-style agent (engine time == Unix clock) still fell back to
sysUpTime on the spine-facing paths even with prefer_engine_time=true.
Spine's own re-check has no wall-clock awareness at all - it unconditionally
prefers any numeric engine time - so that class of device kept the RECACHE
ASSERT loop. Move the prefer_engine_time short-circuit before the wall-clock
check so those paths replicate spine exactly; the wall-clock rejection still
protects the display-only call sites.

* test(snmp): fix stale wall-clock assumption in prefer_engine_time coverage test
2026-09-23 11:59:35 -04:00

77 lines
3.4 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
declare(strict_types = 1);
$root = dirname(__DIR__, 4);
require_once $root . '/lib/snmp.php';
test('issue 7342 rejects an snmpEngineTime value that is the Unix clock', function (): void {
$now = 1784363931;
expect(cacti_snmp_select_uptime(3015, $now, $now))->toBe(3015);
});
test('normal engine time still covers a wrapped sysUpTime value', function (): void {
$now = 1784363931;
expect(cacti_snmp_select_uptime(250000, 50000000, $now))->toBe(5000000000)
->and(cacti_snmp_select_uptime(4000000, 600, $now))->toBe(4000000)
->and(cacti_snmp_select_uptime(false, 600, $now))->toBe(60000)
->and(cacti_snmp_select_uptime('U', 'U', $now))->toBeFalse();
});
test('prefer_engine_time matches spine\'s own unconditional engine-OID preference', function (): void {
$now = 1784363931;
// spine (poller.c) always prefers a numeric engine time over sysUpTime with no
// magnitude comparison and no wall-clock awareness of its own; a smaller-but-
// legitimate engine time (e.g. the SNMP agent restarted more recently than the OS)
// must not fall back to sysUpTime here, or the recache baseline permanently
// disagrees with spine's live re-check
expect(cacti_snmp_select_uptime(999999999, 600, $now, true))->toBe(60000)
->and(cacti_snmp_select_uptime(4000000, 600, $now, true))->toBe(60000)
->and(cacti_snmp_select_uptime(false, 600, $now, true))->toBe(60000)
// spine has no wall-clock rejection either, so an OpenBSD-style engine time
// that looks like the Unix clock must still be used here, not rejected -
// otherwise this exact class of device keeps the RECACHE ASSERT loop
->and(cacti_snmp_select_uptime(3015, $now, $now, true))->toBe($now * 100)
->and(cacti_snmp_select_uptime('U', 'U', $now, true))->toBeFalse();
});
test('every system uptime consumer delegates to the shared selector', function () use ($root): void {
$callCounts = [
'cmd.php' => 2,
'lib/poller.php' => 1,
'lib/api_device.php' => 1,
'lib/api_automation.php' => 1,
];
foreach ($callCounts as $path => $count) {
$source = file_get_contents($root . '/' . $path);
expect($source)->not->toBeFalse("$path must be readable")
->and(substr_count($source, 'cacti_snmp_select_uptime('))->toBeGreaterThanOrEqual($count);
}
});
test('the recache baseline and cmd.php reindex re-check opt into spine-compatible engine preference', function () use ($root): void {
$pollerSource = file_get_contents($root . '/lib/poller.php');
$cmdSource = file_get_contents($root . '/cmd.php');
expect($pollerSource)->toContain('cacti_snmp_select_uptime($system_uptime, $engine_time, null, true)')
->and(substr_count($cmdSource, 'cacti_snmp_select_uptime($system_uptime, $engine_time, null, true)'))->toBe(2);
});