From f39437189be46734c34fe388127aba826e89fae5 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Thu, 22 Oct 2026 20:57:40 -0600 Subject: [PATCH 1/3] fix: store interface counters in the right columns The multicast and broadcast deltas were concatenated ahead of the unicast, discard and error deltas, so every value from int_ifInUcastPkts onwards was written into the wrong column of mac_track_interfaces. Signed-off-by: Thomas Vincent --- CHANGELOG.md | 1 + lib/mactrack_functions.php | 6 +- tests/Unit/InterfaceColumnMappingTest.php | 218 ++++++++++++++++++++++ 3 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/InterfaceColumnMappingTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad5743..3ebc660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ * issue: Mactrack does not shutdown properly due to incorrect use of unregister_process * issue: Fixing some internationalization (i18n) issues * issue: Mac Addresses are not displaying correctly +* issue: Store interface packet, error and discard rates in their own columns instead of shifted ones * feature#68: Proxy Option for downloading MAC/OUI Database * feature#228: Add more MAC address formats * feature: Add a root manifest.json and upgrade-time file pruning (mactrack_prune_files()) that removes tombstoned and dev-only paths diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index 5381fce..7d0501e 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -1089,12 +1089,12 @@ function build_InterfacesTable(&$device, &$ifIndexes, $getLinkPorts = false, $ge (isset($ifOutBroadcastPkts[$ifIndex]) ? $ifOutBroadcastPkts[$ifIndex] : '') . "', '" . @$int_ifInOctets . "', '" . @$int_ifOutOctets . "', '" . @$int_ifHCInOctets . "', '" . @$int_ifHCOutOctets . "', '" . - @$int_ifInMulticastPkts . "', '" . @$int_ifOutMulticastPkts . "', '" . - @$int_ifInBroadcastPkts . "', '" . @$int_ifOutBroadcastPkts . "', '" . @$int_ifInUcastPkts . "', '" . @$int_ifOutUcastPkts . "', '" . @$int_ifInDiscards . "', '" . @$int_ifInErrors . "', '" . @$int_ifInUnknownProtos . "', '" . @$int_ifOutDiscards . "', '" . - @$int_ifOutErrors . "', '" . @$int_discards_present . "', '" . + @$int_ifOutErrors . "', '" . @$int_ifInMulticastPkts . "', '" . + @$int_ifOutMulticastPkts . "', '" . @$int_ifInBroadcastPkts . "', '" . + @$int_ifOutBroadcastPkts . "', '" . @$int_discards_present . "', '" . $int_errors_present . "', '" . $last_down_time . "', '" . $last_up_time . "', '" . $stateChanges . "', '" . "1')"; diff --git a/tests/Unit/InterfaceColumnMappingTest.php b/tests/Unit/InterfaceColumnMappingTest.php new file mode 100644 index 0000000..fa11c28 --- /dev/null +++ b/tests/Unit/InterfaceColumnMappingTest.php @@ -0,0 +1,218 @@ + $value) { + $walk[] = array('oid' => $oid . '.' . $index, 'value' => $value); + } + + return $walk; + } +} + +beforeAll(function () { + require_once dirname(__DIR__, 2) . '/lib/mactrack_functions.php'; +}); + +beforeEach(function () { + $GLOBALS['__test_db_calls'] = array(); + $GLOBALS['__test_next_returns'] = array(); + $GLOBALS['__test_matched_returns'] = array(); + $GLOBALS['__test_config_options'] = array('mt_collection_timing' => 'disabled'); + $GLOBALS['__test_snmp_walks'] = array(); +}); + +/** + * Return the first db_execute() statement that starts with $prefix. + * + * @param string $prefix + * + * @return string + */ +function mactrack_test_find_insert($prefix) { + foreach ($GLOBALS['__test_db_calls'] as $call) { + if ($call['fn'] === 'db_execute' && strpos(ltrim($call['sql']), $prefix) === 0) { + return $call['sql']; + } + } + + throw new RuntimeException("No db_execute() call starting with $prefix"); +} + +/** + * Split a single-row INSERT into column => value. + * + * @param string $sql + * + * @return array + */ +function mactrack_test_insert_row($sql) { + preg_match('/^\s*INSERT INTO \w+\s*\(([^)]*)\)\s*VALUES\s*\((.*)\)\s*ON DUPLICATE KEY UPDATE/s', $sql, $parts); + + $columns = array_map('trim', explode(',', $parts[1])); + $values = array_map('trim', str_getcsv($parts[2], ',', "'", '\\')); + + expect(count($values))->toBe(count($columns)); + + return array_combine($columns, $values); +} + +test('interface counter deltas are stored in the columns named for them', function () { + $counters = array( + 'ifInOctets' => '.1.3.6.1.2.1.2.2.1.10', + 'ifOutOctets' => '.1.3.6.1.2.1.2.2.1.16', + 'ifHCInOctets' => '.1.3.6.1.2.1.31.1.1.1.6', + 'ifHCOutOctets' => '.1.3.6.1.2.1.31.1.1.1.10', + 'ifInUcastPkts' => '.1.3.6.1.2.1.2.2.1.11', + 'ifOutUcastPkts' => '.1.3.6.1.2.1.2.2.1.17', + 'ifInDiscards' => '.1.3.6.1.2.1.2.2.1.13', + 'ifInErrors' => '.1.3.6.1.2.1.2.2.1.14', + 'ifInUnknownProtos' => '.1.3.6.1.2.1.2.2.1.15', + 'ifOutDiscards' => '.1.3.6.1.2.1.2.2.1.19', + 'ifOutErrors' => '.1.3.6.1.2.1.2.2.1.20', + 'ifInMulticastPkts' => '.1.3.6.1.2.1.31.1.1.1.2', + 'ifOutMulticastPkts' => '.1.3.6.1.2.1.31.1.1.1.4', + 'ifInBroadcastPkts' => '.1.3.6.1.2.1.31.1.1.1.3', + 'ifOutBroadcastPkts' => '.1.3.6.1.2.1.31.1.1.1.5', + ); + + // Each counter grows by a different multiple of 1000 since the last scan. + $previous = array( + 'ifIndex' => 1, + 'ifOperStatus' => 1, + 'ifSpeed' => 1000000000, + 'ifHighSpeed' => 1000, + 'last_up_time' => '2026-01-01 00:00:00', + 'last_down_time' => '2026-01-01 00:00:00', + 'stateChanges' => 0, + ); + $step = array(); + $k = 1; + + foreach ($counters as $name => $oid) { + $step[$name] = $k; + $previous[$name] = 50000; + $GLOBALS['__test_snmp_walks'][$oid][1] = 50000 + $k * 1000; + $k++; + } + + $GLOBALS['__test_snmp_walks']['.1.3.6.1.2.1.2.2.1.1'][1] = 1; + $GLOBALS['__test_snmp_walks']['.1.3.6.1.2.1.31.1.1.1.1'][1] = 'Gi1/0/1'; + $GLOBALS['__test_snmp_walks']['.1.3.6.1.2.1.2.2.1.8'][1] = 1; + + // Only db_fetch_cell_prepared() matches on SQL, so the remaining stubs answer in call order. + mactrack_test_queue_return('db_fetch_assoc_prepared', array($previous)); + + $device = array( + 'site_id' => 1, + 'device_id' => 7, + 'host_id' => 0, + 'hostname' => '192.0.2.1', + 'snmp_sysUptime' => 100, + 'snmp_readstring' => 'public', + 'snmp_version' => 2, + 'snmp_context' => '', + 'snmp_username' => '', + 'snmp_password' => '', + 'snmp_auth_protocol' => '', + 'snmp_priv_passphrase' => '', + 'snmp_priv_protocol' => '', + 'snmp_engine_id' => '', + 'snmp_port' => 161, + 'snmp_timeout' => 500, + 'snmp_retries' => 0, + 'max_oids' => 10, + 'last_rundate' => date('Y-m-d H:i:s', time() - 300), + ); + $ifIndexes = array(); + + build_InterfacesTable($device, $ifIndexes, false, false); + + $row = mactrack_test_insert_row(mactrack_test_find_insert('INSERT INTO mac_track_interfaces')); + + // The scan interval divides every delta alike, so compare against ifInOctets (step 1). + $base = (float) $row['int_ifInOctets']; + + expect($base)->toBeGreaterThan(0.0); + + $stored = array(); + + foreach ($step as $name => $expected) { + $stored[$name] = (int) round((float) $row['int_' . $name] / $base); + } + + expect($stored)->toBe($step); +}); From 0ad0e544185ebf59633908fdbf2ee4f04c3bc56c Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Thu, 22 Oct 2026 20:58:46 -0600 Subject: [PATCH 2/3] fix: store interface graph template and data query ids in the right columns mactrack_find_host_graphs() built each row as graph template then data query, but the column list names snmp_query_id first, so the two ids were stored in each other's columns. Signed-off-by: Thomas Vincent --- CHANGELOG.md | 1 + lib/mactrack_functions.php | 4 ++-- tests/Unit/InterfaceColumnMappingTest.php | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ebc660..f609bff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ * issue: Fixing some internationalization (i18n) issues * issue: Mac Addresses are not displaying correctly * issue: Store interface packet, error and discard rates in their own columns instead of shifted ones +* issue: Store the graph template and data query of each interface graph in their own columns instead of swapped * feature#68: Proxy Option for downloading MAC/OUI Database * feature#228: Add more MAC address formats * feature: Add a root manifest.json and upgrade-time file pruning (mactrack_prune_files()) that removes tombstoned and dev-only paths diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index 7d0501e..2927af4 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -1238,8 +1238,8 @@ function mactrack_find_host_graphs($device_id, $host_id): void { $local_graph_id . ',' . $device_id . ',' . $host_id . ',' . - $graph_details[0] . ',' . - $graph_details[1] . ",'" . + $graph_details[1] . ',' . + $graph_details[0] . ",'" . $key . "','" . $field_name . "', 1)"; $found++; diff --git a/tests/Unit/InterfaceColumnMappingTest.php b/tests/Unit/InterfaceColumnMappingTest.php index fa11c28..29746bc 100644 --- a/tests/Unit/InterfaceColumnMappingTest.php +++ b/tests/Unit/InterfaceColumnMappingTest.php @@ -216,3 +216,25 @@ function mactrack_test_insert_row($sql) { expect($stored)->toBe($step); }); + +test('interface graph rows keep the graph template and data query ids in their own columns', function () { + mactrack_test_queue_return('db_fetch_assoc_prepared', array( + array('id' => 11, 'snmp_query_id' => 5, 'field_name' => 'ifName', 'field_value' => 'Gi1/0/1'), + )); + mactrack_test_queue_return('db_fetch_assoc_prepared', array( + array('id' => 21, 'graph_template_id' => 9), + )); + mactrack_test_queue_return('db_fetch_assoc', array( + array('device_id' => 7, 'ifIndex' => 1, 'ifName' => 'Gi1/0/1'), + )); + + mactrack_find_host_graphs(7, 3); + + $row = mactrack_test_insert_row(mactrack_test_find_insert('INSERT INTO mac_track_interface_graphs')); + + expect($row['local_graph_id'])->toBe('21'); + expect($row['device_id'])->toBe('7'); + expect($row['host_id'])->toBe('3'); + expect($row['graph_template_id'])->toBe('9'); + expect($row['snmp_query_id'])->toBe('5'); +}); From 8afae8531f30070a082bc62ab3a0ec7c39f62694 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sat, 24 Oct 2026 00:33:06 -0600 Subject: [PATCH 3/3] test(mactrack): keep graph IDs aligned in prepared query assertions Signed-off-by: Thomas Vincent --- tests/Unit/DeviceReportedValuesBindingTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/DeviceReportedValuesBindingTest.php b/tests/Unit/DeviceReportedValuesBindingTest.php index 73fc6ae..5f76aef 100644 --- a/tests/Unit/DeviceReportedValuesBindingTest.php +++ b/tests/Unit/DeviceReportedValuesBindingTest.php @@ -171,7 +171,7 @@ function mactrack_test_expect_bound($call, $values) { mactrack_test_expect_bound($call, ['Gi1/0/5-lab']); - expect($call['params'])->toBe([5, 'Gi1/0/5-lab', 41, 9, 12, 7, 2, 'Gi1/0/5-lab', 'ifName']); + expect($call['params'])->toBe([5, 'Gi1/0/5-lab', 41, 9, 12, 2, 7, 'Gi1/0/5-lab', 'ifName']); }); test('Cisco DHCP snooping rows bind the port, MAC and IP the device reports', function () {