issue#7355: add ENTITY-SENSOR, JUNIPER and F5-BIGIP script data queries (#7839)

* issue#7355: add a vendor-neutral ENTITY-SENSOR-MIB script data query

ENTITY-SENSOR-MIB (RFC 3433) exposes physical sensors (temperature, fan, PSU,
voltage, current) on most modern gear, so one data query covers F5, Juniper,
Arista, Cisco NX-OS and others. ss_entity_sensor collects the sensor table and
scales each reading through entPhySensorScale and entPhySensorPrecision; the
SNMP I/O sits behind an injectable walker so every logic branch is unit tested.

A dedicated coverage job enforces 100% line coverage on the collector.

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

* issue#7355: add a JUNIPER-MIB jnxOperatingTable script data query

Junos routers and switches expose per-subject health (Routing Engine, FPC, PSU,
fan) through JUNIPER-MIB jnxOperatingTable, which is the signature Juniper data
beyond the IF-MIB and HOST-RESOURCES standards. ss_juniper_operating collects
CPU, temperature, buffer and state; the multi-index table is keyed on the full
OID suffix. SNMP I/O sits behind an injectable walker so every branch is unit
tested, and a dedicated job enforces 100% line coverage.

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

* issue#7355: add an F5-BIGIP-SYSTEM-MIB per-CPU utilization script data query

BIG-IP reports per-CPU utilization as sliding 5s/1m/5m averages in
sysMultiHostCpuTable, the signature F5 metric beyond the IF-MIB and
HOST-RESOURCES standards. ss_f5_cpu collects that multi-indexed table, keyed on
the full OID suffix. Every OID was resolved from the published
F5-BIGIP-SYSTEM-MIB with snmptranslate. SNMP I/O sits behind an injectable
walker so every branch is unit tested, and a dedicated job enforces 100% line
coverage.

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

* style: apply php-cs-fixer to ss_entity_sensor.php

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

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
Thomas Vincent authored and GitHub committed 2026-09-01 21:00:18 -07:00
1 parent 648ae108af
commit 22d19cfcc6
16 files changed
+1363

No files matched your search

@@ -0,0 +1,66 @@
name: Entity Sensor Coverage
on:
push:
branches: [develop]
paths:
- '.github/workflows/entity-sensor-coverage.yml'
- '.github/actions/setup-php-composer/**'
- 'composer.json'
- 'composer.lock'
- 'scripts/ss_entity_sensor.php'
- 'phpunit-entity-sensor.xml'
- 'tests/Unit/DataCollection/Scripts/EntitySensorTest.php'
pull_request:
branches: [develop]
paths:
- '.github/workflows/entity-sensor-coverage.yml'
- '.github/actions/setup-php-composer/**'
- 'composer.json'
- 'composer.lock'
- 'scripts/ss_entity_sensor.php'
- 'phpunit-entity-sensor.xml'
- 'tests/Unit/DataCollection/Scripts/EntitySensorTest.php'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: entity-sensor-coverage-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
COMPOSER_ALLOW_SUPERUSER: 1
jobs:
coverage:
name: scripts/ss_entity_sensor.php 100% coverage
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- name: Setup locked PHP 8.2 dependencies
uses: ./.github/actions/setup-php-composer
with:
php-version: '8.2'
coverage: xdebug
extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring, snmp
dependency-mode: locked
install-args: --prefer-dist --no-progress --no-interaction
- name: Enforce line coverage
env:
XDEBUG_MODE: coverage
run: |
set -euo pipefail
include/vendor/bin/pest --colors=never \
--configuration=phpunit-entity-sensor.xml \
--coverage \
--coverage-text \
--min=100
+66
View File
@@ -0,0 +1,66 @@
name: F5 Cpu Coverage
on:
push:
branches: [develop]
paths:
- '.github/workflows/f5-cpu-coverage.yml'
- '.github/actions/setup-php-composer/**'
- 'composer.json'
- 'composer.lock'
- 'scripts/ss_f5_cpu.php'
- 'phpunit-f5-cpu.xml'
- 'tests/Unit/DataCollection/Scripts/F5CpuTest.php'
pull_request:
branches: [develop]
paths:
- '.github/workflows/f5-cpu-coverage.yml'
- '.github/actions/setup-php-composer/**'
- 'composer.json'
- 'composer.lock'
- 'scripts/ss_f5_cpu.php'
- 'phpunit-f5-cpu.xml'
- 'tests/Unit/DataCollection/Scripts/F5CpuTest.php'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: f5-cpu-coverage-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
COMPOSER_ALLOW_SUPERUSER: 1
jobs:
coverage:
name: scripts/ss_f5_cpu.php 100% coverage
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- name: Setup locked PHP 8.2 dependencies
uses: ./.github/actions/setup-php-composer
with:
php-version: '8.2'
coverage: xdebug
extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring, snmp
dependency-mode: locked
install-args: --prefer-dist --no-progress --no-interaction
- name: Enforce line coverage
env:
XDEBUG_MODE: coverage
run: |
set -euo pipefail
include/vendor/bin/pest --colors=never \
--configuration=phpunit-f5-cpu.xml \
--coverage \
--coverage-text \
--min=100
@@ -0,0 +1,66 @@
name: Juniper Operating Coverage
on:
push:
branches: [develop]
paths:
- '.github/workflows/juniper-operating-coverage.yml'
- '.github/actions/setup-php-composer/**'
- 'composer.json'
- 'composer.lock'
- 'scripts/ss_juniper_operating.php'
- 'phpunit-juniper-operating.xml'
- 'tests/Unit/DataCollection/Scripts/JuniperOperatingTest.php'
pull_request:
branches: [develop]
paths:
- '.github/workflows/juniper-operating-coverage.yml'
- '.github/actions/setup-php-composer/**'
- 'composer.json'
- 'composer.lock'
- 'scripts/ss_juniper_operating.php'
- 'phpunit-juniper-operating.xml'
- 'tests/Unit/DataCollection/Scripts/JuniperOperatingTest.php'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: juniper-operating-coverage-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
COMPOSER_ALLOW_SUPERUSER: 1
jobs:
coverage:
name: scripts/ss_juniper_operating.php 100% coverage
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- name: Setup locked PHP 8.2 dependencies
uses: ./.github/actions/setup-php-composer
with:
php-version: '8.2'
coverage: xdebug
extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring, snmp
dependency-mode: locked
install-args: --prefer-dist --no-progress --no-interaction
- name: Enforce line coverage
env:
XDEBUG_MODE: coverage
run: |
set -euo pipefail
include/vendor/bin/pest --colors=never \
--configuration=phpunit-juniper-operating.xml \
--coverage \
--coverage-text \
--min=100
+3
View File
@@ -167,6 +167,9 @@ Cacti CHANGELOG
-issue#7466: Escape path_stderrlog before it reaches the poller stderr shell redirect
-issue#7658: Resolve client addresses only through explicitly trusted proxies
-issue#7720: Data Source item fields imported from a Template XML are not held to the data_sources.php field rules
-issue#7355: Add a vendor-neutral ENTITY-SENSOR-MIB script data query for physical sensors
-issue#7355: Add a JUNIPER-MIB jnxOperatingTable script data query for RE and FPC health
-issue#7355: Add an F5-BIGIP-SYSTEM-MIB script data query for per-CPU utilization
-feature#7835: Add cacti_snmp_session_from_host() to build SNMP sessions from a device row
-feature#412: Import Export for Graph and Tree rules
-feature#1214: Move Tree Create/Remove/Modify Functions to lib/api_tree.php
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap-unit.php" colors="false" beStrictAboutOutputDuringTests="false">
<testsuites>
<testsuite name="EntitySensor">
<file>tests/Unit/DataCollection/Scripts/EntitySensorTest.php</file>
</testsuite>
</testsuites>
<source>
<include>
<file>scripts/ss_entity_sensor.php</file>
</include>
</source>
</phpunit>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap-unit.php" colors="false" beStrictAboutOutputDuringTests="false">
<testsuites>
<testsuite name="F5Cpu">
<file>tests/Unit/DataCollection/Scripts/F5CpuTest.php</file>
</testsuite>
</testsuites>
<source>
<include>
<file>scripts/ss_f5_cpu.php</file>
</include>
</source>
</phpunit>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap-unit.php" colors="false" beStrictAboutOutputDuringTests="false">
<testsuites>
<testsuite name="JuniperOperating">
<file>tests/Unit/DataCollection/Scripts/JuniperOperatingTest.php</file>
</testsuite>
</testsuites>
<source>
<include>
<file>scripts/ss_juniper_operating.php</file>
</include>
</source>
</phpunit>
+54
View File
@@ -0,0 +1,54 @@
<interface>
<name>Get ENTITY-SENSOR Physical Sensors</name>
<script_path>|path_cacti|/scripts/ss_entity_sensor.php</script_path>
<script_function>ss_entity_sensor</script_function>
<script_server>php</script_server>
<arg_prepend>|host_hostname| |host_id| |host_snmp_version|:|host_snmp_port|:|host_snmp_timeout|:|host_ping_retries|:|host_max_oids|:|host_snmp_community|:|host_snmp_username|:|host_snmp_password|:|host_snmp_auth_protocol|:|host_snmp_priv_passphrase|:|host_snmp_priv_protocol|:|host_snmp_context|</arg_prepend>
<arg_index>index</arg_index>
<arg_query>query</arg_query>
<arg_get>get</arg_get>
<arg_num_indexes>num_indexes</arg_num_indexes>
<output_delimiter>!</output_delimiter>
<index_order>entPhysicalName:entPhysicalIndex</index_order>
<index_order_type>numeric</index_order_type>
<index_title_format>|chosen_order_field|</index_title_format>
<fields>
<entPhysicalIndex>
<name>Sensor Index</name>
<direction>input</direction>
<query_name>index</query_name>
</entPhysicalIndex>
<entPhysicalName>
<name>Sensor Name</name>
<direction>input</direction>
<query_name>name</query_name>
</entPhysicalName>
<entPhysicalDescr>
<name>Sensor Description</name>
<direction>input</direction>
<query_name>descr</query_name>
</entPhysicalDescr>
<entPhySensorType>
<name>Sensor Type</name>
<direction>input</direction>
<query_name>type</query_name>
</entPhySensorType>
<entPhySensorUnitsDisplay>
<name>Units</name>
<direction>input</direction>
<query_name>units</query_name>
</entPhySensorUnitsDisplay>
<entPhySensorValue>
<name>Sensor Value</name>
<direction>output</direction>
<query_name>value</query_name>
</entPhySensorValue>
<entPhySensorOperStatus>
<name>Operational Status</name>
<direction>output</direction>
<query_name>status</query_name>
</entPhySensorOperStatus>
</fields>
</interface>
+44
View File
@@ -0,0 +1,44 @@
<interface>
<name>Get F5 BIG-IP Per-CPU Utilization</name>
<script_path>|path_cacti|/scripts/ss_f5_cpu.php</script_path>
<script_function>ss_f5_cpu</script_function>
<script_server>php</script_server>
<arg_prepend>|host_hostname| |host_id| |host_snmp_version|:|host_snmp_port|:|host_snmp_timeout|:|host_ping_retries|:|host_max_oids|:|host_snmp_community|:|host_snmp_username|:|host_snmp_password|:|host_snmp_auth_protocol|:|host_snmp_priv_passphrase|:|host_snmp_priv_protocol|:|host_snmp_context|</arg_prepend>
<arg_index>index</arg_index>
<arg_query>query</arg_query>
<arg_get>get</arg_get>
<arg_num_indexes>num_indexes</arg_num_indexes>
<output_delimiter>!</output_delimiter>
<index_order>sysMultiHostCpuId:sysMultiHostCpuIndex</index_order>
<index_order_type>alphabetic</index_order_type>
<index_title_format>|chosen_order_field|</index_title_format>
<fields>
<sysMultiHostCpuIndex>
<name>CPU Index</name>
<direction>input</direction>
<query_name>index</query_name>
</sysMultiHostCpuIndex>
<sysMultiHostCpuId>
<name>CPU Id</name>
<direction>input</direction>
<query_name>name</query_name>
</sysMultiHostCpuId>
<sysMultiHostCpuUsageRatio5s>
<name>Utilization (5s)</name>
<direction>output</direction>
<query_name>avg5s</query_name>
</sysMultiHostCpuUsageRatio5s>
<sysMultiHostCpuUsageRatio1m>
<name>Utilization (1m)</name>
<direction>output</direction>
<query_name>avg1m</query_name>
</sysMultiHostCpuUsageRatio1m>
<sysMultiHostCpuUsageRatio5m>
<name>Utilization (5m)</name>
<direction>output</direction>
<query_name>avg5m</query_name>
</sysMultiHostCpuUsageRatio5m>
</fields>
</interface>
@@ -0,0 +1,49 @@
<interface>
<name>Get Juniper Operating Health</name>
<script_path>|path_cacti|/scripts/ss_juniper_operating.php</script_path>
<script_function>ss_juniper_operating</script_function>
<script_server>php</script_server>
<arg_prepend>|host_hostname| |host_id| |host_snmp_version|:|host_snmp_port|:|host_snmp_timeout|:|host_ping_retries|:|host_max_oids|:|host_snmp_community|:|host_snmp_username|:|host_snmp_password|:|host_snmp_auth_protocol|:|host_snmp_priv_passphrase|:|host_snmp_priv_protocol|:|host_snmp_context|</arg_prepend>
<arg_index>index</arg_index>
<arg_query>query</arg_query>
<arg_get>get</arg_get>
<arg_num_indexes>num_indexes</arg_num_indexes>
<output_delimiter>!</output_delimiter>
<index_order>jnxOperatingDescr:jnxOperatingIndex</index_order>
<index_order_type>alphabetic</index_order_type>
<index_title_format>|chosen_order_field|</index_title_format>
<fields>
<jnxOperatingIndex>
<name>Operating Index</name>
<direction>input</direction>
<query_name>index</query_name>
</jnxOperatingIndex>
<jnxOperatingDescr>
<name>Description</name>
<direction>input</direction>
<query_name>descr</query_name>
</jnxOperatingDescr>
<jnxOperatingState>
<name>State</name>
<direction>input</direction>
<query_name>state</query_name>
</jnxOperatingState>
<jnxOperatingCPU>
<name>CPU Utilization</name>
<direction>output</direction>
<query_name>cpu</query_name>
</jnxOperatingCPU>
<jnxOperatingTemp>
<name>Temperature</name>
<direction>output</direction>
<query_name>temp</query_name>
</jnxOperatingTemp>
<jnxOperatingBuffer>
<name>Buffer Utilization</name>
<direction>output</direction>
<query_name>buffer</query_name>
</jnxOperatingBuffer>
</fields>
</interface>
+252
View File
@@ -0,0 +1,252 @@
#!/usr/bin/env 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 |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
error_reporting(0);
// @codeCoverageIgnoreStart
// Entry-point glue: CLI invocation versus in-process script-server dispatch.
if (!isset($called_by_script_server)) {
include_once(__DIR__ . '/../include/cli_check.php');
include_once(__DIR__ . '/../lib/snmp.php');
array_shift($_SERVER['argv']);
print call_user_func_array('ss_entity_sensor', $_SERVER['argv']);
} else {
include_once(__DIR__ . '/../lib/snmp.php');
}
/** @codeCoverageIgnoreEnd */
/**
* Base OIDs for the vendor-neutral ENTITY-SENSOR / ENTITY MIB collector.
*
* ENTITY-SENSOR-MIB (RFC 3433) exposes physical sensors (temperature, fan,
* PSU, voltage, current) on most modern network gear. The sensor table is
* indexed by ENTITY-MIB entPhysicalIndex, so the labels come from the same
* index in the ENTITY-MIB physical table.
*
* @return array<string,string> Symbolic name to numeric base OID.
*/
function ss_entity_sensor_oids() : array {
return [
'type' => '.1.3.6.1.2.1.99.1.1.1.1', // entPhySensorType
'scale' => '.1.3.6.1.2.1.99.1.1.1.2', // entPhySensorScale
'precision' => '.1.3.6.1.2.1.99.1.1.1.3', // entPhySensorPrecision
'value' => '.1.3.6.1.2.1.99.1.1.1.4', // entPhySensorValue
'status' => '.1.3.6.1.2.1.99.1.1.1.5', // entPhySensorOperStatus
'units' => '.1.3.6.1.2.1.99.1.1.1.6', // entPhySensorUnitsDisplay
'descr' => '.1.3.6.1.2.1.47.1.1.1.1.2', // entPhysicalDescr
'name' => '.1.3.6.1.2.1.47.1.1.1.1.7', // entPhysicalName
];
}
/**
* Resolve a raw entPhySensorValue into its real-world magnitude.
*
* Per RFC 3433 the displayed value is entPhySensorValue divided by ten to the
* entPhySensorPrecision, expressed in the SI magnitude named by
* entPhySensorScale. entPhySensorScale is an IANA enumeration where units(9)
* is the base and each neighbouring step is a factor of one thousand, so the
* scale multiplier is 1000 raised to (scale - 9). Out-of-range enumerations
* are treated as units(9) and non-positive precision leaves the value intact,
* matching how LibreNMS and Observium normalise the same table.
*
* @param int|float $value The raw entPhySensorValue.
* @param int $scale The entPhySensorScale enumeration (1..17).
* @param int $precision The entPhySensorPrecision (decimal places).
*
* @return int|float The scaled value.
*/
function ss_entity_sensor_scale(int|float $value, int $scale, int $precision) : int|float {
$result = $value;
if ($scale >= 1 && $scale <= 17 && $scale != 9) {
$result *= 1000 ** ($scale - 9);
}
if ($precision > 0) {
$result /= 10 ** $precision;
}
return $result;
}
/**
* ss_entity_sensor - collect ENTITY-SENSOR-MIB physical sensor readings.
*
* Implements Cacti's script-server data-query contract: 'index' lists the
* sensor indexes, 'num_indexes' counts them, 'query' returns index!value pairs
* for a named field, and 'get' returns one field for one index. Output values
* are scaled through ss_entity_sensor_scale().
*
* The final $walker parameter is a seam for testing; the script server never
* passes it, so production runs fall back to cacti_snmp_walk().
*
* @param string $hostname Device hostname.
* @param int $host_id Cacti host id.
* @param mixed $snmp_auth Colon-joined SNMP auth string from the query.
* @param string $cmd One of index, num_indexes, query, get.
* @param string $arg1 Field name (query) or field name (get).
* @param string $arg2 Sensor index (get only).
* @param callable|null $walker Optional walk override: fn(string $oid): array.
*
* @return mixed Printed lines for index/query, a count for num_indexes, or a
* scalar for get.
*/
function ss_entity_sensor(string $hostname = '', int $host_id = 0, mixed $snmp_auth = '', string $cmd = 'index', string $arg1 = '', string $arg2 = '', ?callable $walker = null) : mixed {
$oids = ss_entity_sensor_oids();
if ($walker === null) {
$walker = ss_entity_sensor_snmp_walker($hostname, explode(':', (string) $snmp_auth));
}
if ($cmd == 'index') {
$rows = $walker($oids['type']);
foreach (array_keys($rows) as $index) {
print $index . "\n";
}
return '';
}
if ($cmd == 'num_indexes') {
return cacti_count($walker($oids['type']));
}
if ($cmd == 'query') {
$rows = ss_entity_sensor_field($walker, $oids, $arg1);
foreach ($rows as $index => $value) {
print $index . '!' . $value . "\n";
}
return '';
}
if ($cmd == 'get') {
$rows = ss_entity_sensor_field($walker, $oids, $arg1);
return $rows[$arg2] ?? 'U';
}
return '';
}
/**
* Build the production SNMP walk callable for ss_entity_sensor().
*
* The returned closure is the boundary to lib/snmp.php; its body performs live
* network I/O and is exercised by integration runs, not unit tests.
*
* @param string $hostname Device hostname.
* @param array<int,string> $snmp The colon-split SNMP auth fields.
*
* @return callable fn(string $oid): array<string,string>
*/
function ss_entity_sensor_snmp_walker(string $hostname, array $snmp) : callable {
return static function (string $oid) use ($hostname, $snmp) : array {
/** @codeCoverageIgnoreStart */
$version = $snmp[0] ?? '1';
$results = cacti_snmp_walk(
$hostname,
$version == 3 ? '' : ($snmp[5] ?? ''),
$oid,
$version,
$snmp[6] ?? '', $snmp[7] ?? '', $snmp[8] ?? '', $snmp[9] ?? '', $snmp[10] ?? '', $snmp[11] ?? '',
$snmp[1] ?? 161, $snmp[2] ?? 500, $snmp[3] ?? 0, $snmp[4] ?? 10, SNMP_POLLER
);
return ss_entity_sensor_flatten($oid, $results);
// @codeCoverageIgnoreEnd
};
}
/**
* Normalise a walk result into an entPhysicalIndex-keyed map.
*
* cacti_snmp_walk() returns rows carrying the fully qualified OID; the sensor
* table is single-indexed by entPhysicalIndex, so the map key is the final OID
* component.
*
* @param string $base The walked base OID.
* @param array<int,array{oid?:string,value?:string}> $results Rows from cacti_snmp_walk().
*
* @return array<string,string> entPhysicalIndex to value.
*/
function ss_entity_sensor_flatten(string $base, array $results) : array {
$map = [];
foreach ($results as $row) {
$oid = $row['oid'] ?? '';
$parts = explode('.', trim($oid, '.'));
$index = end($parts);
if ($index !== '') {
$map[$index] = trim((string) ($row['value'] ?? ''));
}
}
return $map;
}
/**
* Resolve one query field into an index-keyed map, scaling sensor values.
*
* The 'value' field is the only computed one: it multiplies the raw reading
* through ss_entity_sensor_scale() using the per-sensor scale and precision.
* Every other field is a direct walk of the ENTITY or ENTITY-SENSOR column.
*
* @param callable $walker Walk callable: fn(string $oid): array.
* @param array<string,string> $oids The base OID map from ss_entity_sensor_oids().
* @param string $field Requested field name.
*
* @return array<string,string> entPhysicalIndex to field value.
*/
function ss_entity_sensor_field(callable $walker, array $oids, string $field) : array {
if ($field == 'value') {
$values = $walker($oids['value']);
$scales = $walker($oids['scale']);
$precisions = $walker($oids['precision']);
$out = [];
foreach ($values as $index => $raw) {
$out[$index] = (string) ss_entity_sensor_scale(
is_numeric($raw) ? $raw + 0 : 0,
(int) ($scales[$index] ?? 9),
(int) ($precisions[$index] ?? 0)
);
}
return $out;
}
if ($field == 'index') {
$rows = $walker($oids['type']);
return array_combine(array_keys($rows), array_keys($rows));
}
if (isset($oids[$field])) {
return $walker($oids[$field]);
}
return [];
}
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env 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 |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
error_reporting(0);
// @codeCoverageIgnoreStart
// Entry-point glue: CLI invocation versus in-process script-server dispatch.
if (!isset($called_by_script_server)) {
include_once(__DIR__ . '/../include/cli_check.php');
include_once(__DIR__ . '/../lib/snmp.php');
array_shift($_SERVER['argv']);
print call_user_func_array('ss_f5_cpu', $_SERVER['argv']);
} else {
include_once(__DIR__ . '/../lib/snmp.php');
}
/** @codeCoverageIgnoreEnd */
/**
* Base OIDs for the F5-BIGIP-SYSTEM-MIB sysMultiHostCpuTable (enterprise .3375).
*
* BIG-IP reports per-CPU utilisation as sliding averages in this table, which
* is the signature F5 metric beyond the IF-MIB and HOST-RESOURCES standards.
* The table is multi-indexed by sysMultiHostCpuHostId and sysMultiHostCpuIndex,
* so the collector keys rows on the full OID suffix. Every OID here was
* resolved from the published F5-BIGIP-SYSTEM-MIB, not inferred.
*
* @return array<string,string> Symbolic name to numeric base OID.
*/
function ss_f5_cpu_oids() : array {
return [
'name' => '.1.3.6.1.4.1.3375.2.1.7.5.2.1.3', // sysMultiHostCpuId
'avg5s' => '.1.3.6.1.4.1.3375.2.1.7.5.2.1.19', // sysMultiHostCpuUsageRatio5s
'avg1m' => '.1.3.6.1.4.1.3375.2.1.7.5.2.1.27', // sysMultiHostCpuUsageRatio1m
'avg5m' => '.1.3.6.1.4.1.3375.2.1.7.5.2.1.35', // sysMultiHostCpuUsageRatio5m
];
}
/**
* ss_f5_cpu - collect F5 BIG-IP per-CPU utilisation from sysMultiHostCpuTable.
*
* Implements Cacti's script-server data-query contract: 'index' lists the CPU
* indexes, 'num_indexes' counts them, 'query' returns index!value pairs for a
* named field, and 'get' returns one field for one index. The final $walker
* parameter is a seam for testing; the script server never passes it, so
* production runs fall back to cacti_snmp_walk().
*
* @param string $hostname Device hostname.
* @param int $host_id Cacti host id.
* @param mixed $snmp_auth Colon-joined SNMP auth string from the query.
* @param string $cmd One of index, num_indexes, query, get.
* @param string $arg1 Field name (query and get).
* @param string $arg2 CPU index (get only).
* @param callable|null $walker Optional walk override: fn(string $oid): array.
*
* @return mixed Printed lines for index/query, a count for num_indexes, or a
* scalar for get.
*/
function ss_f5_cpu(string $hostname = '', int $host_id = 0, mixed $snmp_auth = '', string $cmd = 'index', string $arg1 = '', string $arg2 = '', ?callable $walker = null) : mixed {
$oids = ss_f5_cpu_oids();
if ($walker === null) {
$walker = ss_f5_cpu_snmp_walker($hostname, explode(':', (string) $snmp_auth));
}
if ($cmd == 'index') {
foreach (array_keys($walker($oids['name'])) as $index) {
print $index . "\n";
}
return '';
}
if ($cmd == 'num_indexes') {
return cacti_count($walker($oids['name']));
}
if ($cmd == 'query') {
foreach (ss_f5_cpu_field($walker, $oids, $arg1) as $index => $value) {
print $index . '!' . $value . "\n";
}
return '';
}
if ($cmd == 'get') {
$rows = ss_f5_cpu_field($walker, $oids, $arg1);
return $rows[$arg2] ?? 'U';
}
return '';
}
/**
* Build the production SNMP walk callable for ss_f5_cpu().
*
* The returned closure is the boundary to lib/snmp.php; its body performs live
* network I/O and is exercised by integration runs, not unit tests.
*
* @param string $hostname Device hostname.
* @param array<int,string> $snmp The colon-split SNMP auth fields.
*
* @return callable fn(string $oid): array<string,string>
*/
function ss_f5_cpu_snmp_walker(string $hostname, array $snmp) : callable {
return static function (string $oid) use ($hostname, $snmp) : array {
/** @codeCoverageIgnoreStart */
$version = $snmp[0] ?? '1';
$results = cacti_snmp_walk(
$hostname,
$version == 3 ? '' : ($snmp[5] ?? ''),
$oid,
$version,
$snmp[6] ?? '', $snmp[7] ?? '', $snmp[8] ?? '', $snmp[9] ?? '', $snmp[10] ?? '', $snmp[11] ?? '',
$snmp[1] ?? 161, $snmp[2] ?? 500, $snmp[3] ?? 0, $snmp[4] ?? 10, SNMP_POLLER
);
return ss_f5_cpu_flatten($oid, $results);
// @codeCoverageIgnoreEnd
};
}
/**
* Normalise a walk result into a sysMultiHostCpu-index-keyed map.
*
* sysMultiHostCpuTable is multi-indexed, so the key is the full OID suffix that
* follows the walked base OID, not just its final component.
*
* @param string $base The walked base OID.
* @param array<int,array{oid?:string,value?:string}> $results Rows from cacti_snmp_walk().
*
* @return array<string,string> Composite index suffix to value.
*/
function ss_f5_cpu_flatten(string $base, array $results) : array {
$prefix = rtrim($base, '.') . '.';
$map = [];
foreach ($results as $row) {
$oid = '.' . ltrim(trim($row['oid'] ?? ''), '.');
if (str_starts_with($oid, $prefix)) {
$index = substr($oid, strlen($prefix));
if ($index !== '') {
$map[$index] = trim((string) ($row['value'] ?? ''));
}
}
}
return $map;
}
/**
* Resolve one query field into an index-keyed map.
*
* Every field is a direct walk of a sysMultiHostCpuTable column; an unknown
* field yields an empty map so the data query degrades cleanly.
*
* @param callable $walker Walk callable: fn(string $oid): array.
* @param array<string,string> $oids The base OID map from ss_f5_cpu_oids().
* @param string $field Requested field name.
*
* @return array<string,string> Composite index to field value.
*/
function ss_f5_cpu_field(callable $walker, array $oids, string $field) : array {
if ($field == 'index') {
$rows = $walker($oids['name']);
return array_combine(array_keys($rows), array_keys($rows));
}
if (isset($oids[$field])) {
return $walker($oids[$field]);
}
return [];
}
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env 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 |
+-------------------------------------------------------------------------+
| This code is designed, written, and maintained by the Cacti Group. See |
| about.php and/or the AUTHORS file for specific developer information. |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+
*/
error_reporting(0);
// @codeCoverageIgnoreStart
// Entry-point glue: CLI invocation versus in-process script-server dispatch.
if (!isset($called_by_script_server)) {
include_once(__DIR__ . '/../include/cli_check.php');
include_once(__DIR__ . '/../lib/snmp.php');
array_shift($_SERVER['argv']);
print call_user_func_array('ss_juniper_operating', $_SERVER['argv']);
} else {
include_once(__DIR__ . '/../lib/snmp.php');
}
/** @codeCoverageIgnoreEnd */
/**
* Base OIDs for the JUNIPER-MIB jnxOperatingTable (enterprise .2636).
*
* The table reports the health of each operating subject (Routing Engine, FPC,
* PSU, fan). It is multi-indexed by jnxContentsContainerIndex.L1Index.L2Index.
* L3Index, so the collector keys rows on the full OID suffix rather than a
* single component. The signature Juniper metrics beyond the IF-MIB and
* HOST-RESOURCES standards are the per-engine CPU, temperature and buffer use.
*
* @return array<string,string> Symbolic name to numeric base OID.
*/
function ss_juniper_operating_oids() : array {
return [
'descr' => '.1.3.6.1.4.1.2636.3.1.13.1.5', // jnxOperatingDescr
'state' => '.1.3.6.1.4.1.2636.3.1.13.1.6', // jnxOperatingState
'temp' => '.1.3.6.1.4.1.2636.3.1.13.1.7', // jnxOperatingTemp (celsius)
'cpu' => '.1.3.6.1.4.1.2636.3.1.13.1.8', // jnxOperatingCPU (percent)
'buffer' => '.1.3.6.1.4.1.2636.3.1.13.1.11', // jnxOperatingBuffer (percent)
];
}
/**
* ss_juniper_operating - collect JUNIPER-MIB jnxOperatingTable health metrics.
*
* Implements Cacti's script-server data-query contract: 'index' lists the
* operating-subject indexes, 'num_indexes' counts them, 'query' returns
* index!value pairs for a named field, and 'get' returns one field for one
* index. The final $walker parameter is a seam for testing; the script server
* never passes it, so production runs fall back to cacti_snmp_walk().
*
* @param string $hostname Device hostname.
* @param int $host_id Cacti host id.
* @param mixed $snmp_auth Colon-joined SNMP auth string from the query.
* @param string $cmd One of index, num_indexes, query, get.
* @param string $arg1 Field name (query and get).
* @param string $arg2 Operating index (get only).
* @param callable|null $walker Optional walk override: fn(string $oid): array.
*
* @return mixed Printed lines for index/query, a count for num_indexes, or a
* scalar for get.
*/
function ss_juniper_operating(string $hostname = '', int $host_id = 0, mixed $snmp_auth = '', string $cmd = 'index', string $arg1 = '', string $arg2 = '', ?callable $walker = null) : mixed {
$oids = ss_juniper_operating_oids();
if ($walker === null) {
$walker = ss_juniper_operating_snmp_walker($hostname, explode(':', (string) $snmp_auth));
}
if ($cmd == 'index') {
foreach (array_keys($walker($oids['descr'])) as $index) {
print $index . "\n";
}
return '';
}
if ($cmd == 'num_indexes') {
return cacti_count($walker($oids['descr']));
}
if ($cmd == 'query') {
foreach (ss_juniper_operating_field($walker, $oids, $arg1) as $index => $value) {
print $index . '!' . $value . "\n";
}
return '';
}
if ($cmd == 'get') {
$rows = ss_juniper_operating_field($walker, $oids, $arg1);
return $rows[$arg2] ?? 'U';
}
return '';
}
/**
* Build the production SNMP walk callable for ss_juniper_operating().
*
* The returned closure is the boundary to lib/snmp.php; its body performs live
* network I/O and is exercised by integration runs, not unit tests.
*
* @param string $hostname Device hostname.
* @param array<int,string> $snmp The colon-split SNMP auth fields.
*
* @return callable fn(string $oid): array<string,string>
*/
function ss_juniper_operating_snmp_walker(string $hostname, array $snmp) : callable {
return static function (string $oid) use ($hostname, $snmp) : array {
/** @codeCoverageIgnoreStart */
$version = $snmp[0] ?? '1';
$results = cacti_snmp_walk(
$hostname,
$version == 3 ? '' : ($snmp[5] ?? ''),
$oid,
$version,
$snmp[6] ?? '', $snmp[7] ?? '', $snmp[8] ?? '', $snmp[9] ?? '', $snmp[10] ?? '', $snmp[11] ?? '',
$snmp[1] ?? 161, $snmp[2] ?? 500, $snmp[3] ?? 0, $snmp[4] ?? 10, SNMP_POLLER
);
return ss_juniper_operating_flatten($oid, $results);
// @codeCoverageIgnoreEnd
};
}
/**
* Normalise a walk result into a jnxOperating-index-keyed map.
*
* jnxOperatingTable is multi-indexed, so the key is the full OID suffix that
* follows the walked base OID, not just its final component.
*
* @param string $base The walked base OID.
* @param array<int,array{oid?:string,value?:string}> $results Rows from cacti_snmp_walk().
*
* @return array<string,string> Composite index suffix to value.
*/
function ss_juniper_operating_flatten(string $base, array $results) : array {
$prefix = rtrim($base, '.') . '.';
$map = [];
foreach ($results as $row) {
$oid = '.' . ltrim(trim($row['oid'] ?? ''), '.');
if (str_starts_with($oid, $prefix)) {
$index = substr($oid, strlen($prefix));
if ($index !== '') {
$map[$index] = trim((string) ($row['value'] ?? ''));
}
}
}
return $map;
}
/**
* Resolve one query field into an index-keyed map.
*
* Every field is a direct walk of a jnxOperatingTable column; an unknown field
* yields an empty map so the data query degrades cleanly.
*
* @param callable $walker Walk callable: fn(string $oid): array.
* @param array<string,string> $oids The base OID map from ss_juniper_operating_oids().
* @param string $field Requested field name.
*
* @return array<string,string> Composite index to field value.
*/
function ss_juniper_operating_field(callable $walker, array $oids, string $field) : array {
if ($field == 'index') {
$rows = $walker($oids['descr']);
return array_combine(array_keys($rows), array_keys($rows));
}
if (isset($oids[$field])) {
return $walker($oids[$field]);
}
return [];
}
@@ -0,0 +1,126 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* Unit tests for the ENTITY-SENSOR-MIB collector (issue #7355).
* Runs under the no-DB bootstrap. The collector's SNMP I/O is injected through
* its $walker seam, so every branch is exercised without a live device.
*/
// The script guards its CLI/script-server dispatch behind $called_by_script_server;
// set it so requiring the file only defines the functions.
$called_by_script_server = true;
if (!defined('SNMP_POLLER')) {
define('SNMP_POLLER', 'SNMP');
}
if (!function_exists('cacti_count')) {
function cacti_count(mixed $a): int { return is_array($a) ? count($a) : 0; }
}
require_once dirname(__DIR__, 4) . '/scripts/ss_entity_sensor.php';
/** A fake walker backed by an OID-keyed fixture. */
function _es_walker(array $fixture): callable {
return static function (string $oid) use ($fixture): array {
return $fixture[$oid] ?? [];
};
}
$OIDS = ss_entity_sensor_oids();
$FIXTURE = [
$OIDS['type'] => ['1' => '8', '2' => '10'], // celsius, rpm
$OIDS['value'] => ['1' => '235', '2' => '4200'],
$OIDS['scale'] => ['1' => '9', '2' => '9'], // units
$OIDS['precision'] => ['1' => '1', '2' => '0'],
$OIDS['status'] => ['1' => '1', '2' => '1'],
$OIDS['descr'] => ['1' => 'Inlet Temp', '2' => 'Fan 1'],
$OIDS['name'] => ['1' => 'Temp/1', '2' => 'Fan/1'],
];
test('scale: units precision one divides by ten', function () {
expect(ss_entity_sensor_scale(235, 9, 1))->toBe(23.5);
});
test('scale: milli magnitude applies a thousandths multiplier', function () {
expect(ss_entity_sensor_scale(5000, 8, 0))->toBe(5.0); // 5000 * 1000^-1 = milli
});
test('scale: out-of-range scale is treated as units and precision zero is a no-op', function () {
expect(ss_entity_sensor_scale(42, 99, 0))->toBe(42);
expect(ss_entity_sensor_scale(42, 9, 0))->toBe(42);
});
test('index prints one line per sensor', function () use ($FIXTURE) {
ob_start();
ss_entity_sensor('h', 1, '', 'index', '', '', _es_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("1\n2");
});
test('num_indexes counts the sensors', function () use ($FIXTURE) {
expect(ss_entity_sensor('h', 1, '', 'num_indexes', '', '', _es_walker($FIXTURE)))->toBe(2);
});
test('query value returns scaled index!value pairs', function () use ($FIXTURE) {
ob_start();
ss_entity_sensor('h', 1, '', 'query', 'value', '', _es_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("1!23.5\n2!4200");
});
test('query description returns the ENTITY-MIB label', function () use ($FIXTURE) {
ob_start();
ss_entity_sensor('h', 1, '', 'query', 'descr', '', _es_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("1!Inlet Temp\n2!Fan 1");
});
test('query index maps each sensor index to itself', function () use ($FIXTURE) {
ob_start();
ss_entity_sensor('h', 1, '', 'query', 'index', '', _es_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("1!1\n2!2");
});
test('query of an unknown field yields nothing', function () use ($FIXTURE) {
ob_start();
ss_entity_sensor('h', 1, '', 'query', 'bogus', '', _es_walker($FIXTURE));
expect(ob_get_clean())->toBe('');
});
test('get returns one scaled value for one index', function () use ($FIXTURE) {
expect(ss_entity_sensor('h', 1, '', 'get', 'value', '1', _es_walker($FIXTURE)))->toBe('23.5');
});
test('get for a missing index returns U', function () use ($FIXTURE) {
expect(ss_entity_sensor('h', 1, '', 'get', 'value', '9', _es_walker($FIXTURE)))->toBe('U');
});
test('get with a non-numeric raw value scales from zero', function () use ($OIDS) {
$fx = [$OIDS['value'] => ['1' => 'n/a'], $OIDS['scale'] => ['1' => '9'], $OIDS['precision'] => ['1' => '0']];
expect(ss_entity_sensor('h', 1, '', 'get', 'value', '1', _es_walker($fx)))->toBe('0');
});
test('an unknown command returns empty', function () use ($FIXTURE) {
expect(ss_entity_sensor('h', 1, '', 'reboot', '', '', _es_walker($FIXTURE)))->toBe('');
});
test('flatten keys rows by the trailing OID component and drops empties', function () {
$rows = [
['oid' => '.1.3.6.1.2.1.99.1.1.1.4.7', 'value' => ' 100 '],
['oid' => '', 'value' => 'x'],
];
expect(ss_entity_sensor_flatten('.1.3.6.1.2.1.99.1.1.1.4', $rows))->toBe(['7' => '100']);
});
test('with no injected walker the default SNMP walker is constructed', function () {
// An unknown command builds the default walker but never calls it, so this
// exercises the walker === null branch without live network I/O.
expect(ss_entity_sensor('127.0.0.1', 1, '2:161:500:0:10:public', 'noop'))->toBe('');
});
test('the SNMP walker factory returns a callable', function () {
expect(ss_entity_sensor_snmp_walker('127.0.0.1', ['2', '161']))->toBeCallable();
});
@@ -0,0 +1,102 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* Unit tests for the F5 BIG-IP sysMultiHostCpuTable collector (issue #7355).
* The SNMP walk is injected through the $walker seam so every branch runs
* without a device. The table is multi-indexed, so the fixture uses composite
* index suffixes (host id . cpu index).
*/
$called_by_script_server = true;
if (!defined('SNMP_POLLER')) {
define('SNMP_POLLER', 'SNMP');
}
if (!function_exists('cacti_count')) {
function cacti_count(mixed $a): int { return is_array($a) ? count($a) : 0; }
}
require_once dirname(__DIR__, 4) . '/scripts/ss_f5_cpu.php';
function _f5_walker(array $fixture): callable {
return static function (string $oid) use ($fixture): array {
return $fixture[$oid] ?? [];
};
}
$OIDS = ss_f5_cpu_oids();
// Two CPUs on host 0: index 0 and 1.
$FIXTURE = [
$OIDS['name'] => ['0.0' => 'cpu0', '0.1' => 'cpu1'],
$OIDS['avg1m'] => ['0.0' => '18', '0.1' => '22'],
$OIDS['avg5s'] => ['0.0' => '25', '0.1' => '30'],
];
test('index prints one line per cpu', function () use ($FIXTURE) {
ob_start();
ss_f5_cpu('h', 1, '', 'index', '', '', _f5_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("0.0\n0.1");
});
test('num_indexes counts the cpus', function () use ($FIXTURE) {
expect(ss_f5_cpu('h', 1, '', 'num_indexes', '', '', _f5_walker($FIXTURE)))->toBe(2);
});
test('query avg1m returns composite-index!value pairs', function () use ($FIXTURE) {
ob_start();
ss_f5_cpu('h', 1, '', 'query', 'avg1m', '', _f5_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("0.0!18\n0.1!22");
});
test('query name returns the cpu label', function () use ($FIXTURE) {
ob_start();
ss_f5_cpu('h', 1, '', 'query', 'name', '', _f5_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("0.0!cpu0\n0.1!cpu1");
});
test('query index maps each cpu index to itself', function () use ($FIXTURE) {
ob_start();
ss_f5_cpu('h', 1, '', 'query', 'index', '', _f5_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("0.0!0.0\n0.1!0.1");
});
test('query of an unknown field yields nothing', function () use ($FIXTURE) {
ob_start();
ss_f5_cpu('h', 1, '', 'query', 'bogus', '', _f5_walker($FIXTURE));
expect(ob_get_clean())->toBe('');
});
test('get returns one field for one index', function () use ($FIXTURE) {
expect(ss_f5_cpu('h', 1, '', 'get', 'avg1m', '0.1', _f5_walker($FIXTURE)))->toBe('22');
});
test('get for a missing index returns U', function () use ($FIXTURE) {
expect(ss_f5_cpu('h', 1, '', 'get', 'avg1m', '9.9', _f5_walker($FIXTURE)))->toBe('U');
});
test('an unknown command returns empty', function () use ($FIXTURE) {
expect(ss_f5_cpu('h', 1, '', 'reload', '', '', _f5_walker($FIXTURE)))->toBe('');
});
test('with no injected walker the default SNMP walker is constructed', function () {
expect(ss_f5_cpu('127.0.0.1', 1, '2:161:500:0:10:public', 'noop'))->toBe('');
});
test('the SNMP walker factory returns a callable', function () {
expect(ss_f5_cpu_snmp_walker('127.0.0.1', ['2', '161']))->toBeCallable();
});
test('flatten keys rows by the full suffix and skips foreign rows', function () use ($OIDS) {
$rows = [
['oid' => $OIDS['avg1m'] . '.0.0', 'value' => ' 18 '],
['oid' => '.1.3.6.1.2.1.1.3.0', 'value' => 'x'],
['oid' => $OIDS['avg1m'], 'value' => 'y'],
];
expect(ss_f5_cpu_flatten($OIDS['avg1m'], $rows))->toBe(['0.0' => '18']);
});
@@ -0,0 +1,103 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* Unit tests for the JUNIPER-MIB jnxOperatingTable collector (issue #7355).
* The SNMP walk is injected through the $walker seam so every branch runs
* without a device. jnxOperatingTable is multi-indexed, so the fixture uses
* composite index suffixes.
*/
$called_by_script_server = true;
if (!defined('SNMP_POLLER')) {
define('SNMP_POLLER', 'SNMP');
}
if (!function_exists('cacti_count')) {
function cacti_count(mixed $a): int { return is_array($a) ? count($a) : 0; }
}
require_once dirname(__DIR__, 4) . '/scripts/ss_juniper_operating.php';
function _jnx_walker(array $fixture): callable {
return static function (string $oid) use ($fixture): array {
return $fixture[$oid] ?? [];
};
}
$OIDS = ss_juniper_operating_oids();
// Two operating subjects: Routing Engine 0 (9.1.0.0) and FPC 0 (7.1.0.0).
$FIXTURE = [
$OIDS['descr'] => ['9.1.0.0' => 'Routing Engine 0', '7.1.0.0' => 'FPC: 0'],
$OIDS['cpu'] => ['9.1.0.0' => '12', '7.1.0.0' => '5'],
$OIDS['temp'] => ['9.1.0.0' => '41', '7.1.0.0' => '38'],
$OIDS['state'] => ['9.1.0.0' => '2', '7.1.0.0' => '2'],
];
test('index prints one line per operating subject', function () use ($FIXTURE) {
ob_start();
ss_juniper_operating('h', 1, '', 'index', '', '', _jnx_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("9.1.0.0\n7.1.0.0");
});
test('num_indexes counts the subjects', function () use ($FIXTURE) {
expect(ss_juniper_operating('h', 1, '', 'num_indexes', '', '', _jnx_walker($FIXTURE)))->toBe(2);
});
test('query cpu returns composite-index!value pairs', function () use ($FIXTURE) {
ob_start();
ss_juniper_operating('h', 1, '', 'query', 'cpu', '', _jnx_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("9.1.0.0!12\n7.1.0.0!5");
});
test('query descr returns the subject label', function () use ($FIXTURE) {
ob_start();
ss_juniper_operating('h', 1, '', 'query', 'descr', '', _jnx_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("9.1.0.0!Routing Engine 0\n7.1.0.0!FPC: 0");
});
test('query index maps each subject index to itself', function () use ($FIXTURE) {
ob_start();
ss_juniper_operating('h', 1, '', 'query', 'index', '', _jnx_walker($FIXTURE));
expect(trim(ob_get_clean()))->toBe("9.1.0.0!9.1.0.0\n7.1.0.0!7.1.0.0");
});
test('query of an unknown field yields nothing', function () use ($FIXTURE) {
ob_start();
ss_juniper_operating('h', 1, '', 'query', 'bogus', '', _jnx_walker($FIXTURE));
expect(ob_get_clean())->toBe('');
});
test('get returns one field for one index', function () use ($FIXTURE) {
expect(ss_juniper_operating('h', 1, '', 'get', 'temp', '9.1.0.0', _jnx_walker($FIXTURE)))->toBe('41');
});
test('get for a missing index returns U', function () use ($FIXTURE) {
expect(ss_juniper_operating('h', 1, '', 'get', 'cpu', '3.1.0.0', _jnx_walker($FIXTURE)))->toBe('U');
});
test('an unknown command returns empty', function () use ($FIXTURE) {
expect(ss_juniper_operating('h', 1, '', 'restart', '', '', _jnx_walker($FIXTURE)))->toBe('');
});
test('with no injected walker the default SNMP walker is constructed', function () {
expect(ss_juniper_operating('127.0.0.1', 1, '2:161:500:0:10:public', 'noop'))->toBe('');
});
test('the SNMP walker factory returns a callable', function () {
expect(ss_juniper_operating_snmp_walker('127.0.0.1', ['2', '161']))->toBeCallable();
});
test('flatten keys rows by the full suffix after the base and skips foreign rows', function () use ($OIDS) {
$rows = [
['oid' => $OIDS['cpu'] . '.9.1.0.0', 'value' => ' 12 '],
['oid' => '.1.3.6.1.2.1.1.3.0', 'value' => 'x'], // outside the base, dropped
['oid' => $OIDS['cpu'], 'value' => 'y'], // exactly the base, no suffix, dropped
];
expect(ss_juniper_operating_flatten($OIDS['cpu'], $rows))->toBe(['9.1.0.0' => '12']);
});