diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 4fbbeea..7fd4adf 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -194,15 +194,39 @@ jobs: - name: Set expected Cacti version for unit tests run: echo -n "${{ env.CACTI }}" | sudo tee ${{ github.workspace }}/cacti/plugins/hmib/tests/.cacti-version > /dev/null + # The installer ran as root and created the Cacti log, but the end-to-end + # tests start Cacti's web server and CLI scripts as the runner, and Cacti + # serves nothing while its log is not writable. + - name: Make the Cacti log writable for the test run + run: | + sudo touch ${{ github.workspace }}/cacti/log/cacti.log + sudo chown www-data:runner ${{ github.workspace }}/cacti/log/cacti.log + sudo chmod 664 ${{ github.workspace }}/cacti/log/cacti.log + - name: Run Pest Unit Tests run: | cd ${{ github.workspace }}/cacti "$CACTI_PEST_BIN" --configuration=plugins/hmib/phpunit.xml \ --coverage-clover=plugins/hmib/coverage/clover.xml + # The page scripts and the pollers cannot be included in the test process, + # so the end-to-end tests run them as real requests and subprocesses. This + # second run records, test by test, which of their lines ran (see + # tests/bin/coverage-prepend.php and tests/support/coverage.php). + - name: Run Pest with per-test coverage of the entry points + if: github.event_name == 'pull_request' + env: + HMIB_COVERAGE_DIR: ${{ github.workspace }}/cacti/plugins/hmib/coverage/e2e + HMIB_COVERAGE_PER_TEST: '1' + run: | + cd ${{ github.workspace }}/cacti + mkdir -p "$HMIB_COVERAGE_DIR" + php -d auto_prepend_file="$PWD/plugins/hmib/tests/bin/coverage-prepend.php" -d xdebug.mode=coverage \ + "$CACTI_PEST_BIN" --configuration=plugins/hmib/phpunit.xml + # Whole-file coverage is meaningless here: most of the plugin only runs # inside a live Cacti. What is enforceable is that a change covers the - # lines it adds. + # lines it adds, from the unit run and from the end-to-end runs together. - name: Enforce coverage of changed lines if: github.event_name == 'pull_request' env: @@ -210,7 +234,8 @@ jobs: run: | cd ${{ github.workspace }}/cacti/plugins/hmib git config --global --add safe.directory ${{ github.workspace }}/cacti/plugins/hmib - php tests/bin/patch-coverage.php coverage/clover.xml "$BASE_REF" 100 + php tests/bin/merge-coverage.php coverage/e2e "$BASE_REF" --merge-clover=coverage/clover.xml --clover=coverage/merged.xml + php tests/bin/patch-coverage.php coverage/merged.xml "$BASE_REF" 100 - name: Upload coverage report if: always() @@ -248,6 +273,32 @@ jobs: exit 1 fi + - name: Run the Host MIB poller against the installed tables + run: | + cd ${{ github.workspace }}/cacti + + # The Cacti poller starts its own main run in the background; this one + # would be refused while that still holds the run lock. + for attempt in $(seq 1 120); do + pgrep -f 'poller_hmib.php -M' > /dev/null || break + sleep 1 + done + + mysql --defaults-file="$HOME/.my.cnf" -e "REPLACE INTO settings (name, value) VALUES ('hmib_enabled', 'on')" cacti + sudo -u www-data timeout 900 php plugins/hmib/poller_hmib.php -M --force --debug | tee "$RUNNER_TEMP/hmib-poller.log" + + if ! grep -q "Host Mib Polling Completed" "$RUNNER_TEMP/hmib-poller.log"; then + echo "Host MIB poller did not finish" + exit 1 + fi + + for table in plugin_hmib_hrSystem plugin_hmib_hrSystemTypes plugin_hmib_hrSWRun plugin_hmib_hrSWRun_last_seen plugin_hmib_hrSWInstalled plugin_hmib_hrDevices plugin_hmib_hrStorage plugin_hmib_hrProcessor plugin_hmib_processes plugin_hmib_types; do + if ! mysql --defaults-file="$HOME/.my.cnf" -N -e "SHOW TABLES LIKE '$table'" cacti | grep -qx "$table"; then + echo "Missing table $table" + exit 1 + fi + done + - name: View Cacti Logs if: always() run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d7fa63..f53d1a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,32 @@ ChangeLog --- develop --- +* issue: Keep shift-click multi-column sorting on the Host MIB tabs now that their sort is checked against the sortable columns, checking each column and direction of the stored sort +* feature: Add a Process History Retention setting that drops Use History rows for processes not seen running for the chosen number of days; the default, Never Purge, keeps them +* dev: Run the Host MIB main poller in CI against the installed plugin tables after the Cacti poller +* dev: Print the OS Type, Device and Template filters of the Host MIB tabs from one function instead of six copies; its Device list binds the chosen OS type and the option values are cast to numbers +* security: Restrict the sort column on the Devices, Storage, Hardware, Processes, Use History and Inventory tabs and on the OS type list to the columns each table shows, bind the OS type id on the edit form and the values of the OS type CSV import, and cast the remaining numeric filter option values +* issue: Purge the Host MIB rows of devices removed from Cacti with one statement per table instead of seven per device, including rows left without a system entry +* issue: Stop repeating per-row lookups: the Summary tab looks up the summary device once per page instead of once per device type, the graph links find an OS type's devices with a join instead of a separate query, and the processor collector asks once per device whether it runs Linux; the device and type ids in those queries and the summary statistics insert are bound or cast to numbers +* issue: Remove the autodiscovery process lock however the poller run ends, including on SIGTERM or SIGINT and on a fatal error, so a scan that dies no longer blocks every later run, and stop autodiscovery once the run has used its 10 minutes +* issue: Stop a main poller run from starting while another is still running by holding a database lock for the whole run, instead of relying on collector rows that are purged after 10 minutes; the main run now stops launching and waiting on collectors 10 minutes after it began, and the launch placeholders use keys that cannot match a process id +* issue: Keep a device's stored process, storage, hardware, software and processor rows when the SNMP walk for that table fails or times out, instead of deleting them; core returns an empty walk for a timeout too, so an empty table clears the stored rows only when the device still answers a sysUpTime GET +* issue: Fix PHP warnings on the Summary tab when a request names only the device type table or only the process table and the other table has no stored sort +* issue: Fix the process Time Seen total growing on every poller cycle instead of once per process performance collection +* issue: Fix graph automation not creating the Host MIB summary device graphs until the next scheduled run after the device is first created +* issue: Fix graph automation checking the users graph template instead of the processes graph template before adding the processes graph +* issue: Fix the host type CSV export producing broken rows for values that contain a double quote, and open values that look like formulas as text +* security: Run the OS type rescan from a POST request with a CSRF token instead of a GET link +* issue: Fix the OS type rescan failing with a duplicate key error when several unclassified devices report the same sysDescr and sysObjectID, and stop it creating an empty match type +* security: Escape the OS type name, version, sysObjectID and sysDescr match when listing host types and the type version on the Summary tab +* security: Restrict the Summary tab sort column to the columns each table can sort on and limit the direction to ASC or DESC +* issue: Fix the Devices tab processes glyph opening an empty page because of a malformed action value in its link +* issue: Fix the Summary Processes filter failing with a SQL error because the page count query had no WHERE clause +* issue: Fix the Summary tab dropping the data-query graph links on PHP 8 because a device id of 0 was treated as a device filter +* security: Bind the stored sysObjectID and sysDescr patterns in the OS type rescan and limit the match to devices that are still unclassified +* security: Bind the SNMP index as a query parameter in the graph link builder used by the Summary Processes view +* issue: Fix the Graphs tab failing with a fatal error on Cacti 1.2.x, where the graph filter calls a function that core declares only in graph_view.php +* issue: Speed up the OS type rescan by trying each stored type once against all unclassified devices instead of once per stored type and per distinct reported sysObjectID and sysDescr pair * issue: Fix the Fleet Dashboard Uptime Distribution showing all zeros by falling back to SNMP sysUpTime when the Host Resources hrSystemUptime is not reported, and apply the same fallback to the per-device Dashboard * issue: Stop the Dashboard card header from changing height when hovering the drag handle or tool buttons by reserving space for the theme's button hover border * feature: Make the Fleet Dashboard the first tab and the default landing view on the Host MIB interface diff --git a/hmib.php b/hmib.php index 3aac0c8..63ab315 100644 --- a/hmib.php +++ b/hmib.php @@ -167,6 +167,104 @@ bottom_footer(); +/** + * Prints the OS Type, Device and Template filter cells the Host MIB tabs + * share. Each list offers only what has Host MIB data, and the Device list + * follows the chosen OS type. + * + * @param bool $device Whether to include the Device filter. + * + * @return void + */ +function hmib_filter_type_device_template(bool $device = true): void { + ?> + + + + + + + + + + + + + + + + + + + + + - - - - - - +
- - - - - - - - - - - - @@ -366,7 +398,7 @@ function hmib_history(): void { $name) { - print "'; + print "'; } } ?> @@ -427,7 +459,7 @@ function clearFilter() { $sql_where = "WHERE hrswls.name!='' AND hrswls.name!='System Idle Process'"; $sql_params = []; $sql_limit = ' LIMIT ' . ($num_rows * (get_request_var('page') - 1)) . ',' . $num_rows; - $sql_order = get_order_string(); + $sql_order = hmib_summary_order_by('hist', ['name', 'description', 'hrswls.name', 'last_seen', 'total_time'], 'name'); if (get_request_var('template') != '-1') { $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' host.host_template_id = ?'; @@ -665,73 +697,7 @@ function hmib_running(): void {
- - - - - - +
- - - - - - - - - - - - @@ -777,7 +743,7 @@ function hmib_running(): void { $name) { - print "'; + print "'; } } ?> @@ -838,7 +804,7 @@ function clearFilter() { $sql_limit = ' LIMIT ' . ($num_rows * (get_request_var('page') - 1)) . ',' . $num_rows; $sql_where = "WHERE hrswr.name != '' AND hrswr.name != 'System Idle Process'"; $sql_params = []; - $sql_order = get_order_string(); + $sql_order = hmib_summary_order_by('run', ['name', 'description', 'hrswr.name', 'path', 'parameters', 'perfCpu', 'perfMemory', 'type', 'status'], 'name'); if (get_request_var('template') != '-1') { $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' host.host_template_id = ?'; @@ -1125,73 +1091,7 @@ function hmib_hardware(): void { - - - - - - +
- - - - - - - - - - - - @@ -1222,7 +1122,7 @@ function hmib_hardware(): void { if (cacti_sizeof($types)) { foreach ($types as $t) { - print "'; + print "'; } } ?> @@ -1236,7 +1136,7 @@ function hmib_hardware(): void { $sval) { - print "'; + print "'; } ?> @@ -1250,7 +1150,7 @@ function hmib_hardware(): void { $name) { - print "'; + print "'; } } ?> @@ -1322,7 +1222,7 @@ function clearFilter() { $sql_where = "WHERE (hrd.description IS NOT NULL AND hrd.description!='')"; $sql_params = []; $sql_limit = ' LIMIT ' . ($num_rows * (get_request_var('page') - 1)) . ',' . $num_rows; - $sql_order = get_order_string(); + $sql_order = hmib_summary_order_by('hw', ['hrd.description', 'host.description', 'type', 'status', 'errors'], 'hrd.description'); if (get_request_var('template') != '-1') { $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' host.host_template_id = ?'; @@ -1634,73 +1534,7 @@ function hmib_storage(): void { - - - - - - +
- - - - - - - - - - - - @@ -1712,13 +1546,13 @@ function hmib_storage(): void {
- + '> - + + - - - -
- - - - - - - - - + - - - - - - +
- - - - - - - - - - - - @@ -2715,7 +2439,7 @@ function hmib_software(): void { if (cacti_sizeof($types)) { foreach ($types as $t) { - print "'; + print "'; } } ?> @@ -2730,7 +2454,7 @@ function hmib_software(): void { $name) { - print "'; + print "'; } } ?> @@ -2791,7 +2515,7 @@ function clearFilter() { $sql_limit = ' LIMIT ' . ($num_rows * (get_request_var('page') - 1)) . ',' . $num_rows; $sql_where = ''; $sql_params = []; - $sql_order = get_order_string(); + $sql_order = hmib_summary_order_by('sw', ['name', 'description', 'type', 'date'], 'name'); if (get_request_var('template') != '-1') { $sql_where .= ($sql_where != '' ? ' AND' : 'WHERE') . ' host.host_template_id = ?'; @@ -2970,6 +2694,42 @@ function hmib_tabs(): void { print ''; } +/** + * Builds the ORDER BY clause for a Host MIB table from the sort stored in the + * session. The column is accepted only when the table can sort on it, so a + * stored or posted value never reaches the SQL text unchecked. + * + * @param string $table The table's session prefix after sess_hmib_, such as host, proc or hist. + * @param array $allowed The columns the table can sort on. + * @param string $default The column used when the stored one is missing or not allowed. + * + * @return string The ORDER BY clause. + */ +function hmib_summary_order_by(string $table, array $allowed, string $default): string { + $column = (string) ($_SESSION['sess_hmib_' . $table . '_sort_column'] ?? $default); + $direction = (string) ($_SESSION['sess_hmib_' . $table . '_sort_direction'] ?? 'DESC'); + $parts = []; + + // a shift-click sort is kept by core per page as column => direction; each part is checked + if (function_exists('get_order_string_page') && is_array($_SESSION['sort_data'][get_order_string_page(false)] ?? null)) { + foreach ($_SESSION['sort_data'][get_order_string_page(false)] as $sort_column => $sort_direction) { + if (in_array($sort_column, $allowed, true)) { + $parts[] = $sort_column . ' ' . (strtoupper((string) $sort_direction) === 'ASC' ? 'ASC' : 'DESC'); + } + } + } + + if (cacti_sizeof($parts)) { + return 'ORDER BY ' . implode(', ', $parts); + } + + if (!in_array($column, $allowed, true)) { + $column = $default; + } + + return 'ORDER BY ' . $column . ' ' . (strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC'); +} + /** * Renders the Host MIB dashboard/summary view: an overview of monitored * device counts by status/OS type, top running processes, and other @@ -3203,7 +2963,7 @@ function clearHostFilter() { $sql_limit = 'LIMIT 20'; } - $sql_order = ' ORDER BY ' . $_SESSION['sess_hmib_host_sort_column'] . ' ' . $_SESSION['sess_hmib_host_sort_direction']; + $sql_order = hmib_summary_order_by('host', ['name', 'version/1', '(version/1)', 'upHosts', 'recHosts', 'downHosts', 'disabledHosts', 'users', 'cpus', 'avgCpuPercent', 'maxCpuPercent', 'avgMem', 'maxMem', 'avgSwap', 'maxSwap', 'avgProcesses', 'maxProcesses'], 'upHosts'); $sql = "SELECT hrst.id AS id, @@ -3321,7 +3081,13 @@ function clearHostFilter() { ] ]; - html_header_sort($display_text, $_SESSION['sess_hmib_host_sort_column'], $_SESSION['sess_hmib_host_sort_direction'], 1, 'hmib.php?action=summary&area=hosts'); + html_header_sort($display_text, $_SESSION['sess_hmib_host_sort_column'] ?? 'upHosts', $_SESSION['sess_hmib_host_sort_direction'] ?? 'DESC', 1, 'hmib.php?action=summary&area=hosts'); + + if (!$templates_missing) { + $host_id = db_fetch_cell_prepared('SELECT id FROM host WHERE host_template_id = ?', [$htsd]); + } else { + $host_id = '-1'; + } if (cacti_sizeof($rows)) { $id = 0; @@ -3329,12 +3095,6 @@ function clearHostFilter() { foreach ($rows as $row) { form_alternate_row(); - if (!$templates_missing) { - $host_id = db_fetch_cell("SELECT id FROM host WHERE host_template_id=$htsd"); - } else { - $host_id = '-1'; - } - $graph_url = hmib_get_graph_url($htdq, 0, $host_id, $row['id']); $graph_ncpu = hmib_get_graph_url($hcpudq, $row['id'], 0, '', $row['cpus'], false); @@ -3368,7 +3128,7 @@ function clearHostFilter() { form_selectable_cell(($row['name'] != '' ? html_escape($row['name']) : __('Unknown', 'hmib')), $id); - form_selectable_cell($row['version'], $id, '', 'right'); + form_selectable_cell(html_escape($row['version']), $id, '', 'right'); form_selectable_cell($upHosts, $id, '', 'right'); form_selectable_cell($recHosts, $id, '', 'right'); form_selectable_cell($downHosts, $id, '', 'right'); @@ -3482,7 +3242,7 @@ function clearProcFilter() { $sql_where = ''; $sql_params = []; $sql_limit = 'LIMIT ' . ($num_rows * (get_request_var('page') - 1)) . ',' . $num_rows; - $sql_order = 'ORDER BY ' . $_SESSION['sess_hmib_proc_sort_column'] . ' ' . $_SESSION['sess_hmib_proc_sort_direction']; + $sql_order = hmib_summary_order_by('proc', ['name', 'paths', 'numHosts', 'numProcesses', 'avgCpu', 'maxCpu', 'avgMemory', 'maxMemory'], 'maxCpu'); if (strlen(get_request_var('filter'))) { $sql_where = 'AND ( @@ -3511,8 +3271,9 @@ function clearProcFilter() { $rows = db_fetch_assoc_prepared($sql, $sql_params); - $total_rows = db_fetch_cell_prepared("SELECT COUNT(DISTINCT name) - FROM plugin_hmib_hrSWRun + $total_rows = db_fetch_cell_prepared("SELECT COUNT(DISTINCT hrswr.name) + FROM plugin_hmib_hrSWRun AS hrswr + WHERE hrswr.name!='System Idle Process' AND hrswr.name!='' $sql_where", $sql_params); $display_text = [ @@ -3569,7 +3330,7 @@ function clearProcFilter() { html_start_box('Process Summary Statistics', '100%', false, 3, 'center', ''); - html_header_sort($display_text, $_SESSION['sess_hmib_proc_sort_column'], $_SESSION['sess_hmib_proc_sort_direction'], 1, 'hmib.php?action=summary&area=processes'); + html_header_sort($display_text, $_SESSION['sess_hmib_proc_sort_column'] ?? 'maxCpu', $_SESSION['sess_hmib_proc_sort_direction'] ?? 'DESC', 1, 'hmib.php?action=summary&area=processes'); // set some defaults $url = $config['url_path'] . 'plugins/hmib/hmib.php'; @@ -3758,29 +3519,37 @@ function hmib_get_graph_url(int $data_query, int $host_type, int $host_id, strin $nograph = $config['url_path'] . 'plugins/hmib/images/view_graphs_disabled.gif'; $graph = $config['url_path'] . 'plugins/hmib/images/view_graphs.gif'; - $hsql = ''; - $hstr = ''; + if (!empty($data_query)) { + $sql_join = ''; + $sql_params = []; - if ($host_type > 0) { - $hosts = db_fetch_assoc("SELECT host_id FROM plugin_hmib_hrSystem WHERE host_type=$host_type"); + // joined rather than looked up first, so each link costs one query + if ($host_type > 0) { + $sql_join = 'INNER JOIN plugin_hmib_hrSystem AS hrs ON hrs.host_id = gl.host_id AND hrs.host_type = ?'; + $sql_params[] = $host_type; + } - if (cacti_sizeof($hosts)) { - foreach ($hosts as $host) { - $hstr .= (strlen($hstr) ? ',' : '(') . $host['host_id']; - } - $hstr .= ')'; + $sql_params[] = $data_query; + + if ($index != '') { + $sql_index = ' AND gl.snmp_index = ?'; + $sql_params[] = $index; + } else { + $sql_index = ''; + } + + if ($host_id != 0) { + $sql_index .= ' AND gl.host_id = ?'; + $sql_params[] = $host_id; } - } - if (!empty($data_query)) { $sql = "SELECT DISTINCT gl.id FROM graph_local AS gl - WHERE gl.snmp_query_id=$data_query " . - ($index!='' ? " AND gl.snmp_index IN ('$index')" : '') . - ($host_id!='' ? " AND gl.host_id=$host_id" : '') . - ($hstr!='' ? " AND gl.host_id IN $hstr" : ''); + $sql_join + WHERE gl.snmp_query_id = ?" . + $sql_index; - $graphs = db_fetch_assoc($sql); + $graphs = db_fetch_assoc_prepared($sql, $sql_params); $graph_add = ''; @@ -3836,6 +3605,7 @@ function hmib_view_graphs(): void { include('./lib/timespan_settings.php'); include('./lib/html_graph.php'); + require_once(__DIR__ . '/includes/compat.php'); html_graph_validate_preview_request_vars(); diff --git a/hmib_types.php b/hmib_types.php index 135ff35..7c8bcff 100644 --- a/hmib_types.php +++ b/hmib_types.php @@ -25,6 +25,7 @@ chdir('../../'); include('./include/auth.php'); include_once('./lib/snmp.php'); +require_once(__DIR__ . '/includes/types.php'); $host_types_actions = [ 1 => __('Delete', 'hmib'), @@ -56,7 +57,7 @@ break; default: - if (isset_request_var('scan')) { + if (isset_request_var('scan') && $_SERVER['REQUEST_METHOD'] === 'POST') { rescan_types(); header('Location: hmib_types.php?header=false'); exit; @@ -444,6 +445,25 @@ function hmib_validate_request_vars(): void { // ================= input validation ================= } +/** + * Quotes one value for the host type CSV export. Embedded quotes are doubled, + * and a value that a spreadsheet would read as a formula gets a leading quote + * so it opens as text. + * + * @param mixed $value The value to export. + * + * @return string The quoted CSV field. + */ +function hmib_csv_field(mixed $value): string { + $value = (string) $value; + + if ($value !== '' && strpos("=+-@\t\r", $value[0]) !== false) { + $value = "'" . $value; + } + + return '"' . str_replace('"', '""', $value) . '"'; +} + /** * Exports all host types matching the current filter as a downloadable * CSV file. Called from this script's main request-dispatch switch @@ -476,12 +496,12 @@ function hmib_host_type_export(): void { if (cacti_sizeof($host_types)) { foreach ($host_types as $host_type) { - array_push($xport_array,'"' . - $host_type['id'] . '","' . - $host_type['name'] . '","' . - $host_type['version'] . '","' . - $host_type['sysDescrMatch'] . '","' . - $host_type['sysObjectID'] . '"'); + array_push($xport_array, + hmib_csv_field($host_type['id']) . ',' . + hmib_csv_field($host_type['name']) . ',' . + hmib_csv_field($host_type['version']) . ',' . + hmib_csv_field($host_type['sysDescrMatch']) . ',' . + hmib_csv_field($host_type['sysObjectID'])); } } @@ -525,36 +545,20 @@ function rescan_types(): void { WHERE name='" . $new_name . "' AND version='" . $new_version . "'"); - // get all known devices types from the device type database - $known_types = db_fetch_assoc('SELECT id, sysDescrMatch, sysObjectID FROM plugin_hmib_hrSystemTypes'); - - // loop through all device rows and look for a matching type + // look for a matching type for the devices that have none if (cacti_sizeof($unknown_host_types)) { - foreach ($unknown_host_types as $type) { - $found = false; - - if (cacti_sizeof($known_types)) { - foreach ($known_types as $known) { - db_execute('UPDATE plugin_hmib_hrSystem SET host_type=' . (int) $known['id'] . " - WHERE host_type=0 AND (sysObjectID LIKE '%" . $known['sysObjectID'] . "%' AND - sysDescr LIKE '%" . $known['sysDescrMatch'] . "%') - OR (sysObjectID RLIKE '" . $known['sysObjectID'] . "' AND - sysDescr RLIKE '" . $known['sysDescrMatch'] . "')"); - - if (db_affected_rows() > 0) { - $found = true; - - break; - } - } - } - } + hmib_assign_host_types(); } // update the host types from unknown_host_types that have a value of 0 - db_execute("INSERT INTO plugin_hmib_hrSystemTypes + db_execute_prepared("INSERT IGNORE INTO plugin_hmib_hrSystemTypes (name, version, sysDescrMatch, sysObjectID) - SELECT '$new_name', '$new_version', sysDescr, sysObjectID FROM plugin_hmib_hrSystem WHERE host_type=0"); + SELECT DISTINCT ?, ?, LEFT(sysDescr, 100), LEFT(sysObjectID, 100) + FROM plugin_hmib_hrSystem + WHERE host_type = 0 + AND sysDescr != '' + AND sysObjectID != ''", + [$new_name, $new_version]); $new_types = db_affected_rows(); @@ -808,10 +812,11 @@ function hmib_host_type_import_processor(array &$host_types): array { for ($i = 1; $i < $line_count; $i++) { $line_array = str_getcsv((string) $lines[$i]); - $save_value = '('; + $save_value = []; + $save_params = []; $j = 0; - $first_column = true; - $sql_where = ''; + $sql_where = []; + $where_params = []; $name = ''; $version = ''; $sysDescrMatch = ''; @@ -819,48 +824,24 @@ function hmib_host_type_import_processor(array &$host_types): array { foreach ($line_array as $line_item) { if (in_array($j, $insert_columns, true)) { - if (!$first_column) { - $save_value .= ','; - } else { - $first_column = false; - } + switch ($j) { + case $host_type_id: + $sql_where[] = 'id = ?'; + $where_params[] = $line_item; + + break; + case $sysDescrMatch_id: + $sql_where[] = 'sysDescrMatch = ?'; + $where_params[] = $line_item; - if ($j == $host_type_id || $j == $sysDescrMatch_id || $j == $sysObjectID_id) { - if (strlen($sql_where)) { - switch ($j) { - case $host_type_id: - $sql_where .= ' AND id=' . db_qstr($line_item); - - break; - case $sysDescrMatch_id: - $sql_where .= ' AND sysDescrMatch=' . db_qstr($line_item); - - break; - case $sysObjectID_id: - $sql_where .= ' AND sysObjectID=' . db_qstr($line_item); - - break; - default: - // do nothing - } - } else { - switch ($j) { - case $host_type_id: - $sql_where .= 'WHERE id=' . db_qstr($line_item); - - break; - case $sysDescrMatch_id: - $sql_where .= 'WHERE sysDescrMatch=' . db_qstr($line_item); - - break; - case $sysObjectID_id: - $sql_where .= 'WHERE sysObjectID=' . db_qstr($line_item); - - break; - default: - // do nothing - } - } + break; + case $sysObjectID_id: + $sql_where[] = 'sysObjectID = ?'; + $where_params[] = $line_item; + + break; + default: + // not part of the existing row lookup } if ($j == $sysDescrMatch_id) { @@ -879,26 +860,28 @@ function hmib_host_type_import_processor(array &$host_types): array { $name = $line_item; } - $save_value .= db_qstr($line_item); + $save_value[] = '?'; + $save_params[] = $line_item; } $j++; } - $save_value .= ')'; + $save_value = '(' . implode(',', $save_value) . ')'; + $sql_where = (cacti_sizeof($sql_where) ? 'WHERE ' . implode(' AND ', $sql_where) : ''); if (isset_request_var('allow_update')) { $sql_execute = 'INSERT INTO plugin_hmib_hrSystemTypes ' . $save_order . ' VALUES' . $save_value . $update_suffix; - if (db_execute($sql_execute)) { + if (db_execute_prepared($sql_execute, $save_params)) { array_push($return_array,'INSERT SUCCEEDED: Name: ' . html_escape($name) . ', Version: ' . html_escape($version) . ', sysDescr: ' . html_escape($sysDescrMatch) . ', sysObjectID: ' . html_escape($sysObjectID)); } else { array_push($return_array,'INSERT FAILED: Name: ' . html_escape($name) . ', Version: ' . html_escape($version) . ', sysDescr: ' . html_escape($sysDescrMatch) . ', sysObjectID: ' . html_escape($sysObjectID)); } } else { // perform check to see if the row exists - $existing_row = db_fetch_row("SELECT * FROM plugin_hmib_hrSystemTypes $sql_where"); + $existing_row = db_fetch_row_prepared("SELECT * FROM plugin_hmib_hrSystemTypes $sql_where", $where_params); if (cacti_sizeof($existing_row)) { array_push($return_array,'INSERT SKIPPED, EXISTING: Name: ' . html_escape($name) . ', Version: ' . html_escape($version) . ', sysDescr: ' . html_escape($sysDescrMatch) . ', sysObjectID: ' . html_escape($sysObjectID)); @@ -906,7 +889,7 @@ function hmib_host_type_import_processor(array &$host_types): array { $sql_execute = 'INSERT INTO plugin_hmib_hrSystemTypes ' . $save_order . ' VALUES' . $save_value; - if (db_execute($sql_execute)) { + if (db_execute_prepared($sql_execute, $save_params)) { array_push($return_array,'INSERT SUCCEEDED: Name: ' . html_escape($name) . ', Version: ' . html_escape($version) . ', sysDescr: ' . html_escape($sysDescrMatch) . ', sysObjectID: ' . html_escape($sysObjectID)); } else { array_push($return_array,'INSERT FAILED: Name: ' . html_escape($name) . ', Version: ' . html_escape($version) . ', sysDescr: ' . html_escape($sysDescrMatch) . ', sysObjectID: ' . html_escape($sysObjectID)); @@ -987,7 +970,7 @@ function hmib_host_type_edit(): void { ]; if (!isempty_request_var('id')) { - $host_type = db_fetch_row('SELECT * FROM plugin_hmib_hrSystemTypes WHERE id=' . get_filter_request_var('id')); + $host_type = db_fetch_row_prepared('SELECT * FROM plugin_hmib_hrSystemTypes WHERE id = ?', [get_filter_request_var('id')]); $header_label = __esc('Host MIB OS Types [edit: %s]', is_array($host_type) ? $host_type['name'] : '', 'hmib'); } else { $header_label = __('Host MIB OS Types [new]', 'hmib'); @@ -1041,7 +1024,12 @@ function hmib_get_host_types(string &$sql_where, int $rows, bool $apply_limits = OR plugin_hmib_hrSystemTypes.sysDescrMatch LIKE ' . db_qstr('%' . get_request_var('filter') . '%') . ')'; } - $sql_order = get_order_string(); + // the column reaches the ORDER BY, so only a column this table shows is used + if (!in_array(get_request_var('sort_column'), ['name', 'version', 'totals', 'sysObjectID', 'sysDescrMatch'], true)) { + set_request_var('sort_column', 'name'); + } + + $sql_order = 'ORDER BY `' . get_request_var('sort_column') . '` ' . (get_request_var('sort_direction') == 'DESC' ? 'DESC' : 'ASC'); if ($apply_limits) { $sql_limit = ' LIMIT ' . ($rows * (get_request_var('page') - 1)) . ',' . $rows; @@ -1131,11 +1119,11 @@ function hmib_host_type(): void { if (cacti_sizeof($host_types)) { foreach ($host_types as $host_type) { form_alternate_row('line' . $host_type['id'], true); - form_selectable_cell('' . $host_type['name'] . '', $host_type['id']); - form_selectable_cell($host_type['version'], $host_type['id']); + form_selectable_cell('' . html_escape($host_type['name']) . '', $host_type['id']); + form_selectable_cell(html_escape($host_type['version']), $host_type['id']); form_selectable_cell($host_type['totals'], $host_type['id']); - form_selectable_cell($host_type['sysObjectID'], $host_type['id']); - form_selectable_cell($host_type['sysDescrMatch'], $host_type['id']); + form_selectable_cell(html_escape($host_type['sysObjectID']), $host_type['id']); + form_selectable_cell(html_escape($host_type['sysDescrMatch']), $host_type['id']); form_checkbox_cell($host_type['name'], $host_type['id']); form_end_row(); } @@ -1255,8 +1243,9 @@ function clearFilter() { } function rescanTypes() { - strURL = '?scan=true'; - loadPageNoHeader(strURL); + $.post('hmib_types.php', {scan: 'true', __csrf_magic: csrfMagicToken}).always(function() { + loadPageNoHeader('hmib_types.php?header=false'); + }); } function importTypes() { diff --git a/includes/compat.php b/includes/compat.php new file mode 100644 index 0000000..1e741f5 --- /dev/null +++ b/includes/compat.php @@ -0,0 +1,49 @@ + 0; + } +} diff --git a/includes/types.php b/includes/types.php new file mode 100644 index 0000000..e04dfc1 --- /dev/null +++ b/includes/types.php @@ -0,0 +1,59 @@ +\n" "Language-Team: Cacti Developers \n" @@ -69,10 +69,6 @@ msgstr "" msgid "Down" msgstr "" -#: hmib.php -msgid "Running Process History" -msgstr "" - #: hmib.php hmib_types.php msgid "OS Type" msgstr "" @@ -89,6 +85,10 @@ msgstr "" msgid "Template" msgstr "" +#: hmib.php +msgid "Running Process History" +msgstr "" + #: hmib.php hmib_types.php msgid "Go" msgstr "" @@ -361,6 +361,10 @@ msgstr "" msgid "No Software Packages Found" msgstr "" +#: hmib.php +msgid "Fleet Dashboard" +msgstr "" + #: hmib.php msgid "Summary" msgstr "" @@ -385,10 +389,6 @@ msgstr "" msgid "Graphs" msgstr "" -#: hmib.php -msgid "Fleet Dashboard" -msgstr "" - #: hmib.php msgid "Dashboard" msgstr "" @@ -1132,6 +1132,33 @@ msgstr "" msgid "Do you wish to automatically purge devices that are removed from the Cacti system?" msgstr "" +#: setup.php +msgid "Process History Retention" +msgstr "" + +#: setup.php +msgid "How long should the Use History tab keep a process after it was last seen running?" +msgstr "" + +#: setup.php +msgid "Never Purge" +msgstr "" + +#: setup.php +#, php-format +msgid "%d Days" +msgstr "" + +#: setup.php +#, php-format +msgid "%d Year" +msgstr "" + +#: setup.php +#, php-format +msgid "%d Years" +msgstr "" + #: setup.php msgid "Default Row Count" msgstr "" @@ -1218,11 +1245,6 @@ msgstr "" msgid "%d Day" msgstr "" -#: setup.php -#, php-format -msgid "%d Days" -msgstr "" - #: setup.php msgid "Host MIB Table Collection Frequencies" msgstr "" @@ -1298,19 +1320,19 @@ msgid "General Administration" msgstr "" #: setup.php -msgid "Host MIB Inventory Summary" +msgid "Host MIB Fleet Dashboard" msgstr "" #: setup.php -msgid "Host MIB Device Dashboard" +msgid "Host MIB Inventory Summary" msgstr "" #: setup.php -msgid "Host MIB Fleet Dashboard" +msgid "Host MIB Details" msgstr "" #: setup.php -msgid "Host MIB Details" +msgid "Host MIB Device Dashboard" msgstr "" #: setup.php diff --git a/phpunit.xml b/phpunit.xml index 340e81c..b032826 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -17,6 +17,7 @@ tests/Security tests/Unit tests/Integration + tests/E2E @@ -29,7 +30,8 @@ setup.php includes/database.php - poller_graphs.php + includes/compat.php + includes/types.php associate_os_type.php diff --git a/poller_graphs.php b/poller_graphs.php index 9838128..df10447 100644 --- a/poller_graphs.php +++ b/poller_graphs.php @@ -217,7 +217,7 @@ function add_host_based_graphs(): void { debug('Users Graph Template Not Set'); } - if ($host_users_gt) { + if ($host_procs_gt) { debug('Processing Processes'); hmib_gt_graph($h['host_id'], $host_procs_gt); } else { @@ -402,6 +402,11 @@ function add_summary_graphs(int $host_id, int $host_template): void { // add the host debug('Adding Host'); $result = exec("$php_bin -q $base/cli/add_device.php --description='Summary Device' --ip=summary --template=$host_template --version=0 --avail=none", $return_code); + + $host_id = (int) db_fetch_cell_prepared('SELECT id + FROM host + WHERE host_template_id = ?', + [$host_template]); } else { debug('Reindexing Host'); $result = exec("$php_bin -q $base/cli/poller_reindex_hosts.php -id=$host_id -qid=All", $return_code); diff --git a/poller_hmib.php b/poller_hmib.php index 371264b..628e0be 100644 --- a/poller_hmib.php +++ b/poller_hmib.php @@ -32,6 +32,10 @@ include_once('./lib/snmp.php'); include_once('./lib/ping.php'); +// Collector rows older than this are left by a process that died, and the main +// run stops waiting on its collectors once this long has passed since it began. +const HMIB_MAX_RUNTIME = 600; + // process calling arguments $parms = $_SERVER['argv']; array_shift($parms); @@ -123,6 +127,18 @@ exit(0); } +// Turn SIGTERM and SIGINT into a normal exit, so the shutdown functions still +// remove the locks this run holds. +if (function_exists('pcntl_signal')) { + $hmib_stop = function () { + exit(1); + }; + + pcntl_async_signals(true); + pcntl_signal(SIGTERM, $hmib_stop); + pcntl_signal(SIGINT, $hmib_stop); +} + if ($seed == '') { $seed = rand(); } @@ -251,7 +267,7 @@ function hmib_snmp_request_failed($result): bool { * the summary warning logged at the end. */ function autoDiscoverHosts(): bool { - global $debug, $snmp_errors; + global $debug, $snmp_errors, $start; $hosts = db_fetch_assoc("SELECT * FROM host @@ -262,12 +278,24 @@ function autoDiscoverHosts(): bool { debug("Starting AutoDiscovery for '" . sizeof($hosts) . "' Hosts"); // set a process lock - db_execute('REPLACE INTO plugin_hmib_processes (pid, taskid) VALUES (' . getmypid() . ', 0)'); + db_execute_prepared('REPLACE INTO plugin_hmib_processes (pid, taskid) VALUES (?, 0)', [getmypid()]); + + // a lock left behind by a scan that died would hold off every later run + register_shutdown_function(function () { + db_execute_prepared('DELETE FROM plugin_hmib_processes WHERE pid = ? AND taskid = 0', [getmypid()]); + }); $snmp_errors = 0; if (cacti_sizeof($hosts)) { foreach ($hosts as $host) { + if (microtime(true) > $start + HMIB_MAX_RUNTIME) { + print 'WARNING: AutoDiscovery stopped after ' . HMIB_MAX_RUNTIME . " seconds, the remaining hosts wait for its next run\n"; + cacti_log('WARNING: Host MIB AutoDiscovery stopped after ' . HMIB_MAX_RUNTIME . ' seconds', false, 'HMIB'); + + break; + } + debug("AutoDiscovery Check for Host '" . $host['description'] . '[' . $host['hostname'] . "]'"); $hostMib = cacti_snmp_walk($host['hostname'], $host['snmp_community'], '.1.3.6.1.2.1.25.1', $host['snmp_version'], $host['snmp_username'], $host['snmp_password'], @@ -290,14 +318,11 @@ function autoDiscoverHosts(): bool { } if (cacti_sizeof($hostMib)) { - $add = true; - - if ($add) { - debug("Host '" . $host['description'] . '[' . $host['hostname'] . "]' Supports Host MIB Resources"); - db_execute('INSERT INTO plugin_hmib_hrSystem - (host_id) VALUES (' . $host['id'] . ') - ON DUPLICATE KEY UPDATE host_id=VALUES(host_id)'); - } + debug("Host '" . $host['description'] . '[' . $host['hostname'] . "]' Supports Host MIB Resources"); + db_execute_prepared('INSERT INTO plugin_hmib_hrSystem + (host_id) VALUES (?) + ON DUPLICATE KEY UPDATE host_id=VALUES(host_id)', + [$host['id']]); } } } @@ -307,8 +332,8 @@ function autoDiscoverHosts(): bool { } // remove the process lock - db_execute('DELETE FROM plugin_hmib_processes WHERE pid=' . getmypid()); - db_execute("REPLACE INTO settings (name,value) VALUES ('hmib_autodiscovery_lastrun', '" . time() . "')"); + db_execute_prepared('DELETE FROM plugin_hmib_processes WHERE pid = ?', [getmypid()]); + db_execute_prepared("REPLACE INTO settings (name,value) VALUES ('hmib_autodiscovery_lastrun', ?)", [time()]); return true; } @@ -339,6 +364,17 @@ function process_hosts(): void { print "NOTE: Processing Hosts Begins\n"; + // The server frees a connection's lock when the connection ends, so a run + // that dies cannot leave it behind. NULL from an error counts as held. + if (db_fetch_cell("SELECT GET_LOCK(CONCAT('hmib_poller_', DATABASE()), 0)") != 1) { + print "WARNING: Another Host Mib Collector is still running! Exiting\n"; + exit(0); + } + + register_shutdown_function(function () { + db_fetch_cell("SELECT RELEASE_LOCK(CONCAT('hmib_poller_', DATABASE()))"); + }); + /* All time/dates will be stored in timestamps * Get Autodiscovery Lastrun Information */ @@ -359,7 +395,7 @@ function process_hosts(): void { } // Purge collectors that run longer than 10 minutes - db_execute('DELETE FROM plugin_hmib_processes WHERE (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(started)) > 600'); + db_execute_prepared('DELETE FROM plugin_hmib_processes WHERE (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(started)) > ?', [HMIB_MAX_RUNTIME]); // Do not process collectors are still running if (db_fetch_cell('SELECT count(*) FROM plugin_hmib_processes') > 0) { @@ -415,7 +451,8 @@ function process_hosts(): void { print "NOTE: Launching Collectors Starting\n"; - $i = 0; + $i = 0; + $deadline = $start + HMIB_MAX_RUNTIME; if (cacti_sizeof($hosts)) { foreach ($hosts as $host) { @@ -423,16 +460,21 @@ function process_hosts(): void { $processes = db_fetch_cell('SELECT COUNT(*) FROM plugin_hmib_processes'); if ($processes < $concurrent_processes) { - // put a placeholder in place to prevent overloads on slow systems - $key = rand(); + // put a placeholder in place to prevent overloads on slow systems; + // process ids stay below 2^31, so the key cannot match a collector's own row + $key = 2147483648 + $i++; - db_execute("INSERT INTO plugin_hmib_processes (pid, taskid, started) VALUES ($key, $seed, NOW())"); + db_execute_prepared('INSERT INTO plugin_hmib_processes (pid, taskid, started) VALUES (?, ?, NOW())', [$key, $seed]); print "NOTE: Launching Host Collector For: '" . $host['description'] . '[' . $host['hostname'] . "]'\n"; process_host($host['host_id'], $seed, $key); usleep(10000); break; + } elseif (microtime(true) > $deadline) { + print 'WARNING: No free collector after ' . HMIB_MAX_RUNTIME . " seconds, not launching the remaining hosts\n"; + + break 2; } else { sleep(1); } @@ -447,12 +489,16 @@ function process_hosts(): void { // wait for all processes to end or max run time while (true) { - $processes_left = db_fetch_cell("SELECT count(*) FROM plugin_hmib_processes WHERE taskid=$seed"); - $pl = db_fetch_cell('SELECT count(*) FROM plugin_hmib_processes'); + $processes_left = db_fetch_cell_prepared('SELECT COUNT(*) FROM plugin_hmib_processes WHERE taskid = ?', [$seed]); if ($processes_left == 0) { print "NOTE: All Processes Complete, Exiting\n"; + break; + } elseif (microtime(true) > $deadline) { + print "WARNING: Stopped waiting on '$processes_left' Processes after " . HMIB_MAX_RUNTIME . " seconds\n"; + cacti_log("WARNING: Stopped waiting on $processes_left Host MIB collectors after " . HMIB_MAX_RUNTIME . ' seconds', false, 'HMIB'); + break; } else { print "NOTE: Waiting on '$processes_left' Processes\n"; @@ -515,18 +561,26 @@ function process_hosts(): void { if (cacti_sizeof($dead_hosts)) { foreach ($dead_hosts as $host) { - db_execute('DELETE FROM plugin_hmib_hrSystem WHERE host_id=' . $host['host_id']); - db_execute('DELETE FROM plugin_hmib_hrSWRun WHERE host_id=' . $host['host_id']); - db_execute('DELETE FROM plugin_hmib_hrSWRun_last_seen WHERE host_id=' . $host['host_id']); - db_execute('DELETE FROM plugin_hmib_hrDevices WHERE host_id=' . $host['host_id']); - db_execute('DELETE FROM plugin_hmib_hrStorage WHERE host_id=' . $host['host_id']); - db_execute('DELETE FROM plugin_hmib_hrProcessor WHERE host_id=' . $host['host_id']); - db_execute('DELETE FROM plugin_hmib_hrSWInstalled WHERE host_id=' . $host['host_id']); print "Purging Host with ID '" . $host['host_id'] . "'\n"; } + + // one statement per table covers every removed device + foreach (['plugin_hmib_hrSWRun', 'plugin_hmib_hrSWRun_last_seen', 'plugin_hmib_hrDevices', 'plugin_hmib_hrStorage', 'plugin_hmib_hrProcessor', 'plugin_hmib_hrSWInstalled', 'plugin_hmib_hrSystem'] as $table) { + db_execute("DELETE hr FROM $table AS hr + LEFT JOIN host + ON host.id=hr.host_id + WHERE host.id IS NULL"); + } } } + // 0, the default, keeps the Use History rows for good + $history_days = (int) read_config_option('hmib_history_retention_days'); + + if ($history_days > 0) { + db_execute_prepared('DELETE FROM plugin_hmib_hrSWRun_last_seen WHERE last_seen < NOW() - INTERVAL ? DAY', [$history_days]); + } + print "NOTE: Updating Summary Statistics for Each Host\n"; // update some statistics in hrSystem @@ -557,10 +611,10 @@ function process_hosts(): void { foreach ($stats as $s) { $sql_insert .= (strlen($sql_insert) ? ', ' : '') . '(' . - $s['host_id'] . ', ' . - $s['host_status'] . ', ' . - (!empty($s['cpuPercent']) ? $s['cpuPercent'] : '0') . ', ' . - (!empty($s['numCpus']) ? $s['numCpus'] : '0') . ')'; + (int) $s['host_id'] . ', ' . + (int) $s['host_status'] . ', ' . + (float) $s['cpuPercent'] . ', ' . + (int) $s['numCpus'] . ')'; $j++; @@ -744,10 +798,10 @@ function checkHost(int $host_id): void { // remove the key process and insert the set a process lock if ($key != '') { - db_execute("DELETE FROM plugin_hmib_processes WHERE pid=$key"); + db_execute_prepared('DELETE FROM plugin_hmib_processes WHERE pid = ?', [$key]); } - db_execute('REPLACE INTO plugin_hmib_processes (pid, taskid) VALUES (' . getmypid() . ", $seed)"); + db_execute_prepared('REPLACE INTO plugin_hmib_processes (pid, taskid) VALUES (?, ?)', [getmypid(), $seed]); // obtain host information $host = db_fetch_row_prepared('SELECT * @@ -770,9 +824,13 @@ function checkHost(int $host_id): void { collect_hrSWRun($host); } + $perf_seconds = 0; + if (runCollector($start, $hrSWRunPerf_lastrun, $hrSWRunPerf_freq)) { cacti_log(sprintf('Running Running Process Performance Collection for Device[%s]', $host['id']), false, 'HMIB', POLLER_VERBOSITY_MEDIUM); collect_hrSWRunPerf($host); + + $perf_seconds = (int) $hrSWRunPerf_freq; } if (runCollector($start, $hrSWInstalled_lastrun, $hrSWInstalled_freq)) { @@ -794,15 +852,15 @@ function checkHost(int $host_id): void { $time = substr((string) time(), 0, 3); // update the most recent table - db_execute_prepared('INSERT INTO plugin_hmib_hrSWRun_last_seen (host_id, name, total_time) - SELECT DISTINCT host_id, name, ' . read_config_option('hmib_hrSWRunPerf_freq') . " AS `total_time` + db_execute_prepared("INSERT INTO plugin_hmib_hrSWRun_last_seen (host_id, name, total_time) + SELECT DISTINCT host_id, name, ? AS `total_time` FROM plugin_hmib_hrSWRun WHERE host_id = ? AND name NOT LIKE '$time%' ON DUPLICATE KEY UPDATE last_seen=NOW(), total_time=total_time+VALUES(total_time)", - [$host['id']]); + [$perf_seconds, $host['id']]); // remove the process lock db_execute_prepared('DELETE FROM plugin_hmib_processes @@ -1002,11 +1060,9 @@ function collectHostIndexedOid(array &$host, array $tree, string $table, string $cols = db_get_table_column_types($table); if (cacti_sizeof($host)) { - // mark for deletion - db_execute("UPDATE $table SET present=0 WHERE host_id=" . $host['id']); - debug("Polling $name from '" . $host['description'] . '[' . $host['hostname'] . "]'"); - $hostMib = []; + $hostMib = []; + $walk_failed = false; foreach ($tree as $mname => $oid) { // baseOID is a container node, not a pollable branch. @@ -1022,6 +1078,7 @@ function collectHostIndexedOid(array &$host, array $tree, string $table, string if (hmib_snmp_request_failed($walk)) { $snmp_errors++; + $walk_failed = true; } // A failed walk can return false/null; keep the merge operand an array. @@ -1032,6 +1089,31 @@ function collectHostIndexedOid(array &$host, array $tree, string $table, string $hostMib = array_merge($hostMib, $walk); } + // Core returns an empty array for a walk that timed out as well as for an + // empty table, so an empty answer only counts when the agent still + // answers a GET, which core does report as failed when it times out. + if (!$walk_failed && !cacti_sizeof($hostMib)) { + $uptime = cacti_snmp_get($host['hostname'], $host['snmp_community'], '.1.3.6.1.2.1.1.3.0', $host['snmp_version'], + $host['snmp_username'], $host['snmp_password'], + $host['snmp_auth_protocol'], $host['snmp_priv_passphrase'], $host['snmp_priv_protocol'], + $host['snmp_context'], $host['snmp_port'], $host['snmp_timeout'], + read_config_option('snmp_retries')); + + if (hmib_snmp_request_failed($uptime)) { + $snmp_errors++; + $walk_failed = true; + } + } + + if ($walk_failed) { + debug("No $name data from '" . $host['description'] . "', keeping the stored rows"); + + return; + } + + // mark for deletion + db_execute_prepared("UPDATE $table SET present = 0 WHERE host_id = ?", [$host['id']]); + $set_string = ''; $values = ''; $sql_suffix = ''; @@ -1055,6 +1137,7 @@ function collectHostIndexedOid(array &$host, array $tree, string $table, string $wonky = false; $hrProcValid = false; $effective = 0; + $is_linux = null; if (cacti_sizeof($hostMib)) { foreach ($hostMib as $mib) { @@ -1080,7 +1163,12 @@ function collectHostIndexedOid(array &$host, array $tree, string $table, string $key = 'load'; $mib['value'] = $effective; } elseif (!$hrProcValid) { - if (db_fetch_cell("SELECT count(*) FROM plugin_hmib_hrSystem WHERE sysDescr LIKE '%Linux%' AND host_id=" . $host['id'])) { + // asked once per device, not once per processor row + if ($is_linux === null) { + $is_linux = (bool) db_fetch_cell_prepared("SELECT COUNT(*) FROM plugin_hmib_hrSystem WHERE sysDescr LIKE '%Linux%' AND host_id = ?", [$host['id']]); + } + + if ($is_linux) { // look for the hrProcessorLoad value $temp_mib = $hostMib; @@ -1269,7 +1357,7 @@ function collectHostIndexedOid(array &$host, array $tree, string $table, string } // remove old records - db_execute("DELETE FROM $table WHERE present=0 AND host_id=" . $host['id']); + db_execute_prepared("DELETE FROM $table WHERE present = 0 AND host_id = ?", [$host['id']]); } } diff --git a/setup.php b/setup.php index f6cfcb4..0bbe69f 100644 --- a/setup.php +++ b/setup.php @@ -279,6 +279,19 @@ function hmib_config_settings(): void { 'method' => 'checkbox', 'default' => 'on' ], + 'hmib_history_retention_days' => [ + 'friendly_name' => __('Process History Retention', 'hmib'), + 'description' => __('How long should the Use History tab keep a process after it was last seen running?', 'hmib'), + 'method' => 'drop_array', + 'default' => '0', + 'array' => [ + 0 => __('Never Purge', 'hmib'), + 30 => __('%d Days', 30, 'hmib'), + 90 => __('%d Days', 90, 'hmib'), + 180 => __('%d Days', 180, 'hmib'), + 365 => __('%d Year', 1, 'hmib'), + 730 => __('%d Years', 2, 'hmib')] + ], 'hmib_os_type_rows' => [ 'friendly_name' => __('Default Row Count', 'hmib'), 'description' => __('How many rows do you wish to see on the HMIB OS Type by default?', 'hmib'), diff --git a/tests/E2E/DevicesPageTest.php b/tests/E2E/DevicesPageTest.php new file mode 100644 index 0000000..f4d2ef0 --- /dev/null +++ b/tests/E2E/DevicesPageTest.php @@ -0,0 +1,220 @@ + hmib_e2e_suite_open()); + +afterAll(fn () => hmib_e2e_suite_close()); + +beforeEach(function () { + hmib_e2e_test_start($this, HMIB_E2E_DEVICE_TABLES); + + hmib_fx_type(1, '.1.3.6', 'Linux', 'Linux', '5'); + hmib_fx_host(9001, 'alpha'); + hmib_fx_host(9002, 'beta'); + hmib_fx_system(9001, 1, 'Linux box', '.1.3.6', array('users' => 3, 'cpu' => 10, 'processes' => 50, 'mem' => 40)); + hmib_fx_system(9002, 1, 'Linux box', '.1.3.6', array('cpu' => 20)); + hmib_fx_process(9001, 1, 'alphad', array('cpu' => 100)); + hmib_fx_process(9002, 1, 'betad', array('cpu' => 50)); +}); + +afterEach(fn () => hmib_e2e_test_end(HMIB_E2E_DEVICE_TABLES)); + +function hmib_e2e_page($action, $query = '', $role = 'admin') { + return hmib_e2e_get($GLOBALS['__hmib_e2e'], 'hmib.php?action=' . $action . '&header=false&filter=' . $query, $role); +} + +/** The links to a page that name a device, as the row glyphs do. */ +function hmib_e2e_links($body, $action) { + preg_match_all("/href='([^']*action=" . $action . "[^']*device=[^']*)'/", html_entity_decode($body), $matches); + + return array_values(array_unique($matches[1])); +} + +it('lists every device with its row actions', function () { + $page = hmib_e2e_page('devices'); + + hmib_e2e_assert_clean($page); + expect($page['text'])->toContain('alpha'); + expect($page['text'])->toContain('beta'); + expect(hmib_e2e_links($page['body'], 'running'))->toHaveCount(2); +}); + +it('opens the processes of one device from the glyph on its row', function () { + $links = hmib_e2e_links(hmib_e2e_page('devices')['body'], 'running'); + + expect($links)->toContain('/cacti/plugins/hmib/hmib.php?action=running&reset=1&device=9001'); + + $opened = hmib_e2e_get($GLOBALS['__hmib_e2e'], substr($links[0], strlen('/cacti/plugins/hmib/')) . '&header=false'); + + hmib_e2e_assert_clean($opened); + expect($opened['text'])->toContain('alphad'); + expect($opened['text'])->not->toContain('betad'); +}); + +it('opens every other glyph target on the device row', function (string $action) { + $link = hmib_e2e_links(hmib_e2e_page('devices')['body'], $action)[0]; + + $opened = hmib_e2e_get($GLOBALS['__hmib_e2e'], substr($link, strlen('/cacti/plugins/hmib/')) . '&header=false'); + + hmib_e2e_assert_clean($opened); +})->with(array('storage', 'hardware', 'software', 'dashboard')); + +it('lists the running processes of all devices and of one device', function () { + $all = hmib_e2e_get($GLOBALS['__hmib_e2e'], 'hmib.php?action=running&header=false&device=-1&filter='); + $one = hmib_e2e_get($GLOBALS['__hmib_e2e'], 'hmib.php?action=running&header=false&device=9002&filter='); + + hmib_e2e_assert_clean($all); + expect($all['text'])->toContain('alphad'); + expect($all['text'])->toContain('betad'); + expect($one['text'])->toContain('betad'); + expect($one['text'])->not->toContain('alphad'); +}); + +it('links the device graph counts to that device only', function () { + $ids = hmib_fx_templates(); + + hmib_test_sql('INSERT INTO graph_local (id, host_id, snmp_query_id, snmp_index, graph_template_id) VALUES (601, 9001, ?, ?, 0), (602, 9002, ?, ?, 0)', array($ids['cpu_dq'], '1', $ids['cpu_dq'], '1')); + + $page = hmib_e2e_page('devices'); + + hmib_e2e_assert_clean($page); + expect($page['body'])->toContain('graph_add=601&'); + expect($page['body'])->toContain('graph_add=602&'); + expect($page['body'])->not->toContain('graph_add=601,602'); +}); + +it('shows no graph link for a device that has no graphs', function () { + $ids = hmib_fx_templates(); + + hmib_test_sql('INSERT INTO graph_local (id, host_id, snmp_query_id, snmp_index, graph_template_id) VALUES (603, 9001, ?, ?, 0)', array($ids['cpu_dq'], '1')); + + $page = hmib_e2e_page('devices'); + + expect($page['body'])->toContain('graph_add=603&'); + expect(substr_count($page['body'], 'graph_add=603'))->toBe(substr_count($page['body'], 'graph_add=603&')); +}); + +it('shows a user graphs of the selected devices on the Graphs tab', function () { + $ids = hmib_fx_templates(); + + hmib_test_sql('INSERT INTO graph_local (id, host_id, snmp_query_id, snmp_index, graph_template_id) VALUES (604, 9001, ?, ?, ?)', array($ids['cpu_dq'], '1', $ids['users_gt'])); + + $page = hmib_e2e_get($GLOBALS['__hmib_e2e'], 'hmib.php?action=graphs&header=false&reset=1&style=selective&graph_add=604&graph_list=&graph_template_id=0&filter='); + + hmib_e2e_assert_clean($page); +}); + +it('falls back to the default order for a sort column the tab does not show', function (string $action, string $row) { + hmib_test_sql('UPDATE plugin_hmib_hrSWRun SET path = ? WHERE host_id = 9001', array('/opt/runpath')); + hmib_test_sql('INSERT INTO plugin_hmib_hrSWRun_last_seen (host_id, name, last_seen) VALUES (9001, ?, ?)', array('alphad', '2001-02-03 04:05:06')); + hmib_test_sql('INSERT INTO plugin_hmib_hrSWInstalled (host_id, `index`, name) VALUES (9001, 1, ?)', array('alpha-tools')); + hmib_test_sql('INSERT INTO plugin_hmib_hrDevices (host_id, `index`, description) VALUES (9001, 1, ?)', array('alpha disk')); + hmib_test_sql('INSERT INTO plugin_hmib_hrStorage (host_id, `index`, description, size) VALUES (9001, 1, ?, 10)', array('/alpha')); + + // the tab keeps its filters in the session, so start from its defaults + hmib_e2e_get($GLOBALS['__hmib_e2e'], 'hmib.php?action=' . $action . '&header=false&clear=true'); + + $page = hmib_e2e_page($action, '&sort_column=nosuchcolumn&sort_direction=DESC'); + + hmib_e2e_get($GLOBALS['__hmib_e2e'], 'hmib.php?action=' . $action . '&header=false&clear=true'); + + hmib_e2e_assert_clean($page); + expect(html_entity_decode($page['body']))->toContain($row); +})->with(array( + 'devices' => array('devices', 'action=running&reset=1&device=9001'), + 'running' => array('running', '/opt/runpath'), + 'history' => array('history', '2001-02-03 04:05:06'), + 'hardware' => array('hardware', 'alpha disk'), + 'storage' => array('storage', '/alpha'), + 'software' => array('software', 'alpha-tools'), +)); + +it('offers in the Device filter only the devices of the chosen OS type', function (string $action) { + hmib_fx_type(2, '.1.3.7', 'Windows', 'Windows', '10'); + hmib_test_sql('UPDATE plugin_hmib_hrSystem SET host_type = 2 WHERE host_id = 9002'); + + $template = hmib_fx_template_row('host_template', str_repeat('e', 32), 'Server '); + + hmib_test_sql('UPDATE host SET host_template_id = ? WHERE id = 9001', array($template)); + + hmib_e2e_get($GLOBALS['__hmib_e2e'], 'hmib.php?action=' . $action . '&header=false&clear=true'); + + $all = hmib_e2e_page($action); + $page = hmib_e2e_page($action, '&ostype=1'); + + hmib_e2e_get($GLOBALS['__hmib_e2e'], 'hmib.php?action=' . $action . '&header=false&clear=true'); + + hmib_e2e_assert_clean($page); + + foreach (array('all' => $all, 'linux' => $page) as $name => $response) { + preg_match_all("/