Skip to content

Fix the Graphs tab on Cacti 1.2.x and classify device types with fewer queries (stacked on #77) - #79

Merged
TheWitness merged 2 commits into
hardening-review-fixesfrom
fix/graphs-tab-and-rescan
Oct 24, 2026
Merged

TheWitness merged 2 commits into
hardening-review-fixesfrom
fix/graphs-tab-and-rescan

Conversation

@somethingwithproof

Copy link
Copy Markdown
Member

Description

Stacked on #77 (base hardening-review-fixes). Merge #77 first; this branch is rebased onto its head fb5c39e and retargets to develop afterwards.

Two changes, each in its own commit:

  1. The Graphs tab no longer fails on Cacti 1.2.x. Core's lib/html_graph.php calls graph_view_has_untemplated_graphs(), which 1.2.x declares in graph_view.php only, so a plugin page that builds the graph filter dies with "Call to undefined function". includes/compat.php declares the same function when core has not. Cacti develop has no such function and does not call it, so nothing changes there.
  2. The OS type rescan no longer runs one UPDATE per stored type and per distinct sysObjectID/sysDescr pair. hmib_assign_host_types() in includes/types.php runs one UPDATE per stored type, in id order, against the devices that still have no type, with the bound LIKE match that Harden the Host MIB pages and poller, and fix review findings #77 introduced.

Related Issue

No open issue covers either change.

Motivation and Context

Graphs tab: on Cacti 1.2.32 the request hmib.php?action=graphs returned a page ending in Fatal error: Uncaught Error: Call to undefined function graph_view_has_untemplated_graphs() in lib/html_graph.php on line 217. The INFO file declares compatibility with 1.2.25 and later.

Rescan: rescan_types() (as of #77) loops over the distinct reported pairs of unclassified devices and, for each pair, over every stored type until one UPDATE changes a row. The UPDATE does not use the pair, so each pass of the outer loop repeats the same work. With 200 unclassified devices and 20 stored types that is about 4,000 statements; with 2,000 devices about 40,000 and 51 s.

What the new function does differently from the loop in #77, stated exactly:

  • Stored types are read ORDER BY id. The loop reads them in table order, which is not defined.
  • The loop stops after as many types as there are distinct pairs with both values reported. A device that reported only one of the two values is matched by the same statements but is not counted in that limit, so in a rare mix a matching type is never tried. Every stored type is now tried once.

Everything else is kept from #77: the bound LIKE comparison on sysObjectID and sysDescr (no regular expression branch, no OR), first matching type wins, only devices whose host_type is 0 change, and nothing runs when no device with both values reported is unclassified. All values stay strings, so IPv4 and IPv6 addresses, 0x prefixes and numeric-looking text are compared as text.

The two differences were accepted by the maintainers on #77's review thread and are kept documented here.

How Has This Been Tested?

Tests, in the repository's Pest layout:

  • tests/Unit/GraphViewCompatTest.php: the function is declared, returns the right answer for none, one and many untemplated graphs, asks core's get_allowed_graphs() for one row with the device restriction when there is one, leaves core's own declaration alone (a subprocess declares it first and requires the file), and is loaded by the Graphs tab right after core's graph library. tests/E2E/DevicesPageTest.php requests the Graphs tab; on 1.2.x that test was skipped on Harden the Host MIB pages and poller, and fix review findings #77 and now runs.
  • tests/Unit/AssignHostTypesTest.php: one UPDATE per stored type for 1, 20 and 200 types, in id order, with the id and the two patterns bound, the match limited to host_type = 0, no RLIKE, and no stored value in the SQL text for quotes, backslashes, IPv4, IPv6, 0x1A, 1e3, leading zeros, empty patterns and a statement fragment.
  • tests/Integration/AssignHostTypesDbTest.php, against MariaDB with the Cacti schema, in three SQL modes (strict, not strict, strict with ONLY_FULL_GROUP_BY): the new function and the per-pair loop of Harden the Host MIB pages and poller, and fix review findings #77 (kept in the test as the reference, with the stored types read in id order) classify 150 seeded fixtures identically, each with 3 to 12 stored types, 5 to 40 devices, some already typed, overlapping patterns, wildcards, IPv4, IPv6, 0x and 1e3 values; a check that the comparison is not vacuous; a device that has a type keeps it; IPv4/IPv6/numeric-looking values match as text; a stored pattern is a LIKE substring, not a regular expression; no stored types and no devices; the case where the loop stopped early; and the statement counts (5 types and 20 devices: 100 against 5; 20 types and 200 devices: 4,000 against 20). The fixtures come from a seeded xorshift generator in tests/support/prng.php, so a failing fixture is rebuilt from its seed number; the files added here use no rand, mt_rand, uniqid or lcg_value.

The test database tables are copied aside and restored. Tests skip when no Cacti schema is reachable.

Run in Docker (PHP 8.3 with Xdebug, MariaDB 11.8.9 loaded from each core's cacti.sql) in the CI order: volume owned by www-data:runner, root-owned log, php cli/plugin_manage.php --plugin=hmib --install --enable as root, the log step, Pest with Clover as a non-root user with date.timezone = America/New_York, the per-test coverage pass, then tests/bin/merge-coverage.php and tests/bin/patch-coverage.php <clover> fb5c39e 100:

Cacti core Pest (both passes) cacti.log lines matching FATAL/ERROR/WARNING Patch coverage
develop 268 tests, 267 passed, 1 skipped 0 100.00% (22/22 measured lines)
1.2.x (1.2.32) 268 tests, 267 passed, 1 skipped 0 100.00% (22/22 measured lines)

hmib.php and hmib_types.php are measured through the end-to-end tests; the new logic lives in includes/compat.php and includes/types.php, which are added to the coverage <source> list. The three lines changed in the two page scripts are a require_once in each and the call to the new function.

The Graphs tab was also checked on Cacti 1.2.25, 1.2.32 and develop with a throwaway request (200, no fatal error, no PHP warning).

Also run: php -l on every PHP file, tests/bin/validate-manifest.php, tests/bin/check-i18n-pot.php fb5c39e, all clean.

Not run: PHP 8.2 and 8.4 (the CI matrix) and the CI workflow itself.

Performance

Measured with a throwaway harness (not part of the pull request) that runs rescan_types() cut from #77 (fb5c39e) and from this branch against the same MariaDB 11.8.9 container, with PDO standing in for Cacti's database layer. Median of 3 runs (one run for the 2,000 device "before" rows); wall time from hrtime, CPU from getrusage, statements from SHOW SESSION STATUS LIKE 'Questions', which also counts the delete and insert of the placeholder type that both versions run. 20 stored types. "Unmatched": every device reports a pair no stored type matches (the worst case for the loop). "Matched": each device matches one stored type.

Devices Case Before (#77) After
200 unmatched 551 ms, 4,004 statements, 20.4 ms CPU 14.3 ms, 24 statements, 0.3 ms CPU
2,000 unmatched 50,638 ms, 40,004 statements, 293 ms CPU 49.3 ms, 24 statements, 0.4 ms CPU
200 matched 89.7 ms, 3,869 statements, 22.4 ms CPU 7.2 ms, 24 statements, 0.2 ms CPU
2,000 matched 812 ms, 39,869 statements, 218 ms CPU 27.3 ms, 24 statements, 0.4 ms CPU

Peak memory was the same in both versions (0.6 MB at 200 devices, 1.6 MB at 2,000). The statement count after the change grows with the number of stored types (one each), not with the number of devices. Each statement scans the unclassified rows of plugin_hmib_hrSystem once, with LIKE on both columns, so a very large table with very many stored types still pays for that scan per type; this was not measured beyond 2,000 devices and 20 types. The Graphs tab change adds no query beyond the one core's function makes when the filter is built on 1.2.x. Not measured: Cacti's own db_* wrappers and a browser.

Screenshots (if appropriate):

None.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

The rescan differences listed under Motivation concern undefined table order and a rare early stop of the previous loop, not the documented rules; they are the one place a result can differ.

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation repository accordingly.

… pages

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
@sonarqubecloud

Copy link
Copy Markdown

@TheWitness
TheWitness merged commit 47c5434 into hardening-review-fixes Oct 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants