Skip to content

Poll apcupsd UPSes with a native NIS client instead of apcaccess - #61

Merged
TheWitness merged 3 commits into
developfrom
feature/native-apcupsd-nis
Oct 19, 2026
Merged

TheWitness merged 3 commits into
developfrom
feature/native-apcupsd-nis

Conversation

@TheWitness

@TheWitness TheWitness commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Description

Polls apcupsd UPS' with a native PHP client for the apcupsd network protocol instead of running apcaccess, the same approach the NUT collector uses. apcupsd and apcaccess no longer have to be installed on the Cacti poller, and apcupsd UPS' can be polled from Windows pollers (the old code only looked for apcaccess in /usr/sbin, /usr/bin and /usr/local/bin).

This branch was stacked on #60, which has since merged; it is rebased onto develop and now contains only this change.

How it works

  • New includes/nis.php speaks the apcupsd Network Information Server (NIS) protocol: it sends the text status with a 2-byte big-endian length, then reads length-prefixed KEY : value records until the empty record that ends the reply. This is what apcaccess -u -h host:port does.
  • Units are removed from numeric values as apcaccess -u did (121.0 Volts becomes 121.0). Only a number followed by a known unit word is changed, so text and dates pass through untouched.
  • collect_ups_data() in poller_apcupsd.php now calls apcupsd_nis_query() and keeps its existing field mapping and host up/down handling, using the existing set_ups_host_state().
  • The same per-UPS timeout applies exactly as it does for NUT: normalised to 1-120 seconds (5 by default), the connect is bounded by it, and the whole exchange by twice it. Reads are non-blocking and polled against the deadline, so a server that trickles bytes cannot hold the poller open.
  • Hostnames are validated before any socket is opened, and the reply is capped (1000 records, 4 KB each).
  • The shell-command helpers (apcupsd_build_apcaccess_command(), apcupsd_escape_shellcmd(), apcupsd_escape_shellarg()) and the exec_with_timeout() call are gone, so the apcupsd collector no longer executes anything.

Behaviour changes to be aware of

  • A failed apcupsd query is now written to the Cacti log (WARNING: apcupsd query for UPS ... failed: ...), matching what Log NUT query failures and save the UPS Poller ID #58 did for NUT.
  • A successful poll now clears the UPS's stored error_message; before, a recovered apcupsd UPS kept showing its old error.
  • Values that contain : are no longer cut short (the old parser split on every : ).
  • Not changed: a failed apcupsd query still does not mark the linked Cacti host down (NUT does); I left that out of scope.

Related Issue

Follow-up to the discussion about removing the apcaccess requirement; no issue filed.

Motivation and Context

apcaccess was the only thing the apcupsd collector needed on the poller, it needed a fixed binary path and a shell, and it made the collector Linux-only. The protocol is small, read-only and unauthenticated, so a native client is simpler and safer than shelling out.

The apcupsd daemon on the UPS host is still required, with its network information server enabled (NETSERVER on, port 3551 by default) and the poller able to reach it: open the NIS port to the poller in the UPS host's firewall (or use TCP Wrappers). NISIP is the interface the daemon listens on, not an allow-list, so it must not be set to the poller's address. The README now says so.

How Has This Been Tested?

  • php -l is clean on every changed PHP file.
  • Pest (Windows, PHP 8.4, Pest 5): 240 passed. The 6 warnings are socket-timeout warnings from connection tests, the same kind NutClientTest already produces.
  • New tests/Unit/NisClientTest.php covers unit stripping, record parsing, the session (framing, empty reply, early close, oversized record, runaway reply), the socket layer (a reply split across reads, hang-up, the overall deadline, a stall mid-record, TCP and refused connections, IPv6 bracketing) and the timeout bounds.
  • Existing tests that referenced the removed command builder were rewritten to cover the native path: no shell calls in the poller, host/port validation before connecting, and the timeout wiring.
  • locales/po/cacti.pot has the six new messages added and the removed one deleted.

Not verified against a real apcupsd. The tests use scripted servers, so they prove the code matches the protocol as I understand it (length-prefixed records, an empty record ends the reply), not that a real daemon agrees. Two details come from my reading of the protocol, not from apcupsd's source, which I couldn't reach: the empty-record terminator, and the exact list of unit words apcaccess -u strips. Please check this draft against a real apcupsd host (compare apcaccess -u output with a poll) before it is marked ready.

Types of changes

  • New feature (non-breaking change which adds functionality)

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.

@TheWitness
TheWitness marked this pull request as ready for review October 17, 2026 23:25
Copilot AI balanced review requested due to automatic review settings October 17, 2026 23:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The collector replacement still needs validation against a real apcupsd daemon before its protocol compatibility can be confirmed.

2 open issues
What changed in this PR

Replaces the Cacti plugin’s apcaccess subprocess with native PHP network polling, removing poller-side binary requirements and enabling Windows support.

Changes:

  • Adds a bounded NIS client and integrates polling, failure logging, and recovery handling.
  • Includes #60’s translation-formatting fixes and regression tests.
  • Updates documentation, translation messages, and coverage configuration.
File Description
upses.php Fixes Graph Automation message formatting.
tests/​Unit/​SecurityHelpersTest.php Removes obsolete shell-helper tests.
tests/​Unit/​NutWiringTest.php Updates timeout and logging assertions.
tests/​Unit/​NisClientTest.php Adds protocol and socket tests.
tests/​Unit/​I18nPlaceholderTest.php Adds translation-formatting regression checks.
tests/​Integration/​SecurityWiringTest.php Checks native polling integration.
tests/​Integration/​PollerCommandSecurityTest.php Tests network-input validation.
tests/​bootstrap-unit.php Models Cacti translation formatting.
README.md Documents native polling requirements.
poller_apcupsd.php Uses NIS polling and clears recovered errors.
phpunit.xml Includes NIS client in coverage.
locales/​po/​cacti.pot Updates collector messages.
includes/​nut.php Fixes translated error formatting.
includes/​nis.php Implements the native NIS client.
includes/​functions.php Removes shell-command helpers.
includes/​dashboard.php Fixes translated metric captions.
CHANGELOG.md Records features and fixes.

Review effort: Balanced

Comment thread README.md Outdated
Comment thread tests/Unit/NisClientTest.php Outdated
TheWitness and others added 3 commits October 19, 2026 09:10
Add includes/nis.php, a client for the apcupsd network information server (TCP 3551: a length-prefixed 'status' request, then length-prefixed KEY : value records ended by an empty one), and use it from collect_ups_data() in place of running apcaccess. Units are stripped from numeric values as apcaccess -u did. The per-UPS timeout is applied as it is for NUT: normalised to 1-120 seconds, bounding the connect and, at twice its length, the whole exchange.

Remove the shell command helpers and the exec_with_timeout() call, log failed apcupsd queries to the Cacti log, clear a recovered UPS's stored error, and update the README, CHANGELOG, translation template and tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The changed-line coverage check failed because the new client was neither in the Clover report nor allowlisted; list it in phpunit.xml's coverage sources, as nut.php is.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…parate reads

- README: NISIP is the interface apcupsd listens on (default 0.0.0.0, all
  interfaces; 127.0.0.1 means local only), not a list of allowed clients.
  Setting it to the poller's address would stop the daemon binding. Say so,
  and describe letting the poller in separately: a firewall rule for the NIS
  port, or TCP Wrappers (/etc/hosts.allow) when apcupsd was built with it.
- NisClientTest: the pieces test wrote the whole reply to a socket pair
  before reading, and stream sockets do not keep write boundaries, so the
  client could read it in one fread() and the test passed without
  accumulating short reads. Feed the client from a stream that returns
  exactly the pieces it is given (one byte at a time; split inside a length
  and a record; with empty reads between). Checked against a client that
  discards earlier partial reads: the old test still passed, the new ones fail.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TheWitness
TheWitness force-pushed the feature/native-apcupsd-nis branch from 6cf15a6 to df842c9 Compare October 19, 2026 13:16
@sonarqubecloud

Copy link
Copy Markdown

@TheWitness
TheWitness merged commit 2c75033 into develop Oct 19, 2026
5 checks passed
@TheWitness
TheWitness deleted the feature/native-apcupsd-nis branch October 19, 2026 13:56
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.

3 participants