session_id(), 'time' => time()])); } else { raise_message('lockout', __esc('Cacti maintenance lockout has been cleared by \'%s\'. Press the button again after Cacti maintenance is over.', get_username($admin)), MESSAGE_LEVEL_INFO); cacti_log('WARNING: Cacti maintenance lockout has been cleared by the primary administrator!'); set_config_option('cacti_lockout_status', ''); } } header('Location: support.php?tab=summary'); exit; } function support_view_tech() : void { global $database_hostname, $poller_options, $input_types, $local_db_cnn_id; // ================= input validation ================= gfrv('tab', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^([a-z_A-Z]+)$/']]); // redact is a display-only toggle; coerce to a strict 0/1 and never fail on bad input set_request_var('redact', gnrv('redact') === '1' ? 1 : 0); // ==================================================== // present a tabbed interface $tabs = [ 'summary' => [ 'display' => __('Summary'), 'header' => true ], 'environment' => [ 'display' => __('Environment'), 'header' => true ], 'database' => [ 'display' => __('Database Tables'), 'header' => true ], 'dbsettings' => [ 'display' => __('Database Settings'), 'header' => true ], 'dbstatus' => [ 'display' => __('Database Status'), 'header' => true ], 'dbperms' => [ 'display' => __('Database Permissions'), 'header' => true ], 'processes' => [ 'display' => __('Database Queries'), 'header' => false ], 'background' => [ 'display' => __('Background Processes'), 'header' => false ], 'poller' => [ 'display' => __('Poller Stats'), 'header' => true ], 'phpinfo' => [ 'display' => __('PHP Info'), 'header' => true ], 'log' => [ 'display' => __('Recent Log'), 'header' => true ], 'changelog' => [ 'display' => __('ChangeLog'), 'header' => true ], ]; // set the default tab load_current_session_value('tab', 'sess_ts_tabs', 'summary'); $current_tab = gnrv('tab'); // the regex above only guarantees the shape of the value; fall back to the // default when it is not one of the tabs this page actually renders if (!isset($tabs[$current_tab])) { $current_tab = 'summary'; set_request_var('tab', $current_tab); } // carried on the tab links so redaction stays in effect while navigating $redact = (grv('redact') == 1); // the processes and background will set their own timeouts $page = 'support.php?tab=' . $current_tab . ($redact ? '&redact=1' : ''); if ($current_tab != 'processes' && $current_tab != 'background') { $refresh = [ 'seconds' => 999999, 'page' => $page, 'logout' => 'false' ]; set_page_refresh($refresh); } $header_label = __esc('Technical Support [ %s ]', $tabs[$current_tab]['display']); top_header(); if (cacti_sizeof($tabs)) { // draw the tabs print "
'; } // Display tech information if ($tabs[$current_tab]['header'] === true) { html_start_box($header_label, '100%', false, 3, 'center', ''); } switch (grv('tab')) { case 'summary': show_tech_summary(); break; case 'environment': show_tech_environment(); break; case 'dbstatus': show_database_status(); break; case 'dbperms': show_database_permissions(); break; case 'dbsettings': show_database_settings(); break; case 'changelog': show_cacti_changelog(); break; case 'database': show_database_tables(); break; case 'poller': show_cacti_poller(); break; case 'phpinfo': show_php_modules(); break; case 'log': show_tech_log(); break; case 'processes': show_database_processes(); break; case 'background': show_cacti_processes(); break; } if ($tabs[$current_tab]['header'] === true) { html_end_box(); } ?> __('Any')]; $none = ['0' => __('None')]; $refresh = [ 1 => __esc('%d Seconds', 1), 3 => __esc('%d Seconds', 3), 5 => __esc('%d Seconds', 5), 10 => __esc('%d Seconds', 10), 15 => __esc('%d Seconds', 15), 20 => __esc('%d Seconds', 20) ]; $pollers = [ '0' => __('No'), '1' => __('Yes') ]; $chars = [ 150 => __esc('%d Chars', 150), 180 => __esc('%d Chars', 180), 300 => __esc('%d Chars', 300), 500 => __esc('%d Chars', 500), 1000 => __esc('%d Chars', 1000), ]; return [ 'rows' => [ [ 'filter' => [ 'method' => 'textbox', 'friendly_name' => __('Search'), 'filter' => FILTER_DEFAULT, 'placeholder' => __('Enter a search term'), 'size' => '30', 'default' => '', 'pageset' => true, 'max_length' => '120', 'value' => '' ], 'refresh' => [ 'method' => 'drop_array', 'friendly_name' => __('Refresh'), 'filter' => FILTER_VALIDATE_INT, 'default' => '5', 'pageset' => true, 'array' => $refresh, 'value' => '5' ], 'poller' => [ 'method' => 'drop_array', 'friendly_name' => __('Include Poller'), 'filter' => FILTER_VALIDATE_INT, 'default' => '0', 'pageset' => true, 'array' => $pollers, 'value' => '0' ], 'length' => [ 'method' => 'drop_array', 'friendly_name' => __('Template'), 'filter' => FILTER_VALIDATE_INT, 'default' => '180', 'pageset' => true, 'array' => $chars, 'value' => '180' ], 'rows' => [ 'method' => 'drop_array', 'friendly_name' => __('Queries'), 'filter' => FILTER_VALIDATE_INT, 'default' => '-1', 'pageset' => true, 'array' => $item_rows, 'value' => '-1' ] ] ], 'buttons' => [ 'go' => [ 'method' => 'submit', 'display' => __('Go'), 'title' => __('Apply Filter to Table'), ], 'clear' => [ 'method' => 'button', 'display' => __('Clear'), 'title' => __('Reset Filter to Default Values'), ] ], 'sort' => [ 'sort_column' => 'runtime', 'sort_direction' => 'DESC' ], 'javascript' => [ 'ready' => "$('#refresh').click(function() { clearTimeout(myRefresh); });" ] ]; } function draw_database_process_filter(bool $render = false) : void { $filters = create_database_process_filter(); $header = __('Technical Support [ Database Queries ]'); // create the page filter $pageFilter = new CactiTableFilter($header, 'support.php?tab=processes', 'form_db_stats', 'sess_ts_proc'); $pageFilter->set_filter_array($filters); if ($render) { $pageFilter->render(); } else { $pageFilter->sanitize(); } } function show_database_processes() : void { global $item_rows; draw_database_process_filter(true); if (grv('rows') == '-1') { $rows = read_config_option('num_rows_table'); } else { $rows = grv('rows'); } $sql_where = 'WHERE info NOT LIKE \'%FROM processlist%\' AND info IS NOT NULL'; $sql_params = []; // form the 'where' clause for our main sql query if (grv('filter') != '') { $sql_where .= ' AND ' . '(command LIKE ? OR info LIKE ?)'; $sql_params[] = '%' . grv('filter') . '%'; $sql_params[] = '%' . grv('filter') . '%'; } if (grv('poller') == '0') { $sql_where .= ' AND ' . 'info NOT LIKE \'%poller_output%\' AND ' . 'info NOT LIKE \'%poller_item%\' AND info NOT LIKE \'%SQL_NO_CACHE%\''; } $total_rows = db_fetch_cell_prepared("SELECT COUNT(*) FROM information_schema.processlist $sql_where", $sql_params); $sql_order = get_order_string(); $sql_limit = ' LIMIT ' . ((int) $rows * (max(1, (int) grv('page')) - 1)) . ',' . (int) $rows; $info_len = (int) grv('length'); $version = db_get_global_variable('innodb_version'); if (db_column_exists('information_schema`.`processlist', 'query_id')) { $query_id = 'query_id'; $time_ms = 'ROUND(time_ms/1000,2) AS runtime'; } else { $query_id = "'N/A' AS query_id"; $time_ms = '`time` AS runtime'; } $processes = db_fetch_assoc_prepared("SELECT id, $query_id, user, state, $time_ms, LENGTH(info) AS query_len, SUBSTRING(REPLACE(REPLACE(REPLACE(info, '\n', ' '), ',', ', '), '\t', ' '), 1, $info_len) AS info FROM information_schema.processlist $sql_where $sql_order $sql_limit", $sql_params); $display_text = [ 'id' => [ 'display' => __('Process ID'), 'align' => 'left', 'sort' => 'ASC', 'tip' => __('The Connection ID of the running process.') ], 'query_id' => [ 'display' => __('Query ID'), 'align' => 'left', 'sort' => 'ASC', 'tip' => __('The Query ID of the currently running Query.') ], 'user' => [ 'display' => __('User'), 'align' => 'left', 'sort' => 'ASC', 'tip' => __('The MariaDB/MySQL user currently running the Query.') ], 'runtime' => [ 'display' => __('Run Time'), 'align' => 'right', 'tip' => __('The Runtime of the current query in seconds.') ], 'query_len' => [ 'display' => __('Query Length'), 'align' => 'right', 'tip' => __('The total string length of the Query.') ], 'state' => [ 'display' => __('Query State'), 'align' => 'center', 'sort' => 'ASC', 'tip' => __('The MariaDB/MySQL process state.') ], 'info' => [ 'display' => __('Query Details'), 'align' => 'left', 'tip' => __('The Query Details for the current query upto a maximum string length.') ] ]; $nav = html_nav_bar('support.php?tab=processes', MAX_DISPLAY_PAGES, grv('page'), $rows, $total_rows, 7, __('Queries'), 'page', 'main'); print $nav; html_start_box('', '100%', false, 3, 'center', ''); html_header_sort($display_text, grv('sort_column'), grv('sort_direction'), 1, 'support.php?tab=processes', 'main'); if (cacti_sizeof($processes)) { foreach ($processes as $p) { form_alternate_row('line' . $p['id'], false); form_selectable_cell($p['id'], $p['id']); form_selectable_cell($p['query_id'], $p['id']); form_selectable_ecell($p['user'], $p['id']); form_selectable_cell(number_format_i18n($p['runtime'], 2), $p['id'], '', 'right'); form_selectable_cell(number_format_i18n($p['query_len']), $p['id'], '', 'right'); form_selectable_ecell($p['state'], $p['id'], '', 'center'); form_selectable_ecell($p['info'], $p['id'], '', 'white-space:pre-wrap'); form_end_row(); } } else { print "" . __('No Database Queries Found') . ''; } html_end_box(false); if (cacti_sizeof($processes)) { print $nav; } } function create_cacti_process_filter(array $tables) : array { global $item_rows; $all = ['all' => __('All')]; $refresh = [ 1 => __esc('%d Seconds', 1), 3 => __esc('%d Seconds', 3), 5 => __esc('%d Seconds', 5), 10 => __esc('%d Seconds', 10), 15 => __esc('%d Seconds', 15), 20 => __esc('%d Seconds', 20) ]; $tables = $all + $tables; return [ 'rows' => [ [ 'filter' => [ 'method' => 'textbox', 'friendly_name' => __('Search'), 'filter' => FILTER_DEFAULT, 'placeholder' => __('Enter a search term'), 'size' => '30', 'default' => '', 'pageset' => true, 'max_length' => '120', 'value' => '' ], 'tasks' => [ 'method' => 'drop_array', 'friendly_name' => __('Task Type'), 'filter' => FILTER_CALLBACK, 'filter_options' => ['options' => 'sanitize_search_string'], 'default' => 'all', 'pageset' => true, 'array' => $tables, 'value' => 'all' ], 'refresh' => [ 'method' => 'drop_array', 'friendly_name' => __('Refresh'), 'filter' => FILTER_VALIDATE_INT, 'default' => '5', 'pageset' => true, 'array' => $refresh, 'value' => '5' ], 'rows' => [ 'method' => 'drop_array', 'friendly_name' => __('Processes'), 'filter' => FILTER_VALIDATE_INT, 'default' => '-1', 'pageset' => true, 'array' => $item_rows, 'value' => '-1' ] ] ], 'buttons' => [ 'go' => [ 'method' => 'submit', 'display' => __('Go'), 'title' => __('Apply Filter to Table'), ], 'clear' => [ 'method' => 'button', 'display' => __('Clear'), 'title' => __('Reset Filter to Default Values'), ] ], 'sort' => [ 'sort_column' => 'runtime', 'sort_direction' => 'DESC' ], 'javascript' => [ 'ready' => "$('#refresh').click(function() { clearTimeout(myRefresh); });" ] ]; } function draw_cacti_process_filter(bool $render = false, array $tables = []) : void { $filters = create_cacti_process_filter($tables); $header = __('Technical Support [ Background Processes ]'); // create the page filter $pageFilter = new CactiTableFilter($header, 'support.php?tab=background', 'form_cacti_procs', 'sess_ts_bg'); $pageFilter->set_filter_array($filters); if ($render) { $pageFilter->render(); } else { $pageFilter->sanitize(); } } /** * support_process_tables - return the process-table definitions used to build * the background process view. Core registers the tables it ships with, then * exposes the set through the 'support_process_tables' plugin hook so a plugin * can add its own process table (or override a core one) instead of core having * to carry every plugin's schema. * * Definitions are keyed by a unique task key (also the value used by the 'tasks' * filter). Each definition contains: * 'label' - task label shown in the filter and the Task Type column * 'table' - database table probed with db_table_exists() before inclusion * 'select' - a SELECT fragment returning the common process columns in order: * pid, tasktype, taskname, taskid, timeout, started, last_update, runtime * * @return array */ function support_process_tables() : array { $poller_interval = (int) read_config_option('poller_interval'); // the full set of process tables known to Cacti. the key is the table name // for core entries so the db_table_exists() gate below matches prior behavior. $poller_label = __('Cacti Poller'); $process_label = __('Cacti Process'); $grid_label = __('RTM Process'); $automation_label = __('Automation Process'); $hmib_label = __('HMIB Process'); $mikrotik_label = __('MikroTik Process'); $webseer_label = __('WebSeer Process'); $servcheck_label = __('Service Check Process'); $mactrack_label = __('MacTrack Process'); $defaults = [ 'poller_time' => [ 'label' => $poller_label, 'table' => 'poller_time', 'select' => 'SELECT pid, ' . db_qstr($poller_label) . " AS tasktype, CONCAT('PollerID:', poller_id) AS taskname, id AS taskid, " . db_qstr((string) $poller_interval) . ' AS timeout, start_time AS started, start_time AS last_update, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(start_time) AS runtime FROM poller_time WHERE UNIX_TIMESTAMP(end_time) = 0', ], 'processes' => [ 'label' => $process_label, 'table' => 'processes', 'select' => 'SELECT pid, CONCAT(' . db_qstr($process_label) . ", ' (', tasktype, ')') AS tasktype, taskname, taskid, timeout, started, last_update, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(started) AS runtime FROM processes", ], 'grid_processes' => [ 'label' => $grid_label, 'table' => 'grid_processes', 'select' => 'SELECT pid, ' . db_qstr($grid_label) . " AS tasktype, taskname, taskid, 'N/A' AS timeout, '-' AS started, heartbeat AS last_update, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(heartbeat) AS runtime FROM grid_processes", ], 'automation_processes' => [ 'label' => $automation_label, 'table' => 'automation_processes', 'select' => 'SELECT pid, ' . db_qstr($automation_label) . ' AS tasktype, CONCAT(' . db_qstr(__('Poller:')) . ", an.poller_id) AS taskname, network_id AS taskid, 'N/A' AS timeout, an.last_started AS started, ap.heartbeat AS last_update, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(an.last_started) AS runtime FROM automation_processes AS ap INNER JOIN automation_networks AS an ON an.id = ap.network_id", ], 'plugin_hmib_processes' => [ 'label' => $hmib_label, 'table' => 'plugin_hmib_processes', 'select' => 'SELECT pid, ' . db_qstr($hmib_label) . ' AS tasktype, ' . db_qstr(__('Collector')) . " AS taskname, taskid, 'N/A' AS timeout, started, 'N/A' AS last_update, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(started) AS runtime FROM plugin_hmib_processes", ], 'plugin_mikrotik_processes' => [ 'label' => $mikrotik_label, 'table' => 'plugin_mikrotik_processes', 'select' => 'SELECT pid, ' . db_qstr($mikrotik_label) . ' AS tasktype, ' . db_qstr(__('Collector')) . " AS taskname, taskid, 'N/A' AS timeout, started, 'N/A' AS last_update, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(started) AS runtime FROM plugin_mikrotik_processes", ], 'plugin_webseer_processes' => [ 'label' => $webseer_label, 'table' => 'plugin_webseer_processes', 'select' => 'SELECT pid, ' . db_qstr($webseer_label) . ' AS tasktype, CONCAT(' . db_qstr(__('Poller:')) . ", poller_id) AS taskname, url_id AS taskid, 'N/A' AS timeout, time AS started, 'N/A' AS last_update, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(time) AS runtime FROM plugin_webseer_processes", ], 'plugin_servcheck_processes' => [ 'label' => $servcheck_label, 'table' => 'plugin_servcheck_processes', 'select' => 'SELECT pid, ' . db_qstr($servcheck_label) . ' AS tasktype, CONCAT(' . db_qstr(__('Poller:')) . ", poller_id) AS taskname, test_id AS taskid, 'N/A' AS timeout, time AS started, 'N/A' AS last_update, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(time) AS runtime FROM plugin_servcheck_processes", ], 'mac_track_processes' => [ 'label' => $mactrack_label, 'table' => 'mac_track_processes', 'select' => 'SELECT process_id AS pid, ' . db_qstr($mactrack_label) . ' AS tasktype, CONCAT(' . db_qstr(__('Device:')) . ", device_id) AS taskname, device_id AS taskid, 'N/A' AS timeout, start_date AS started, 'N/A' AS last_update, UNIX_TIMESTAMP() - UNIX_TIMESTAMP(start_date) AS runtime FROM mac_track_processes", ], ]; $definitions = api_plugin_hook_function('support_process_tables', $defaults); // a misbehaving plugin can return a non-array; fall back to the built-in set if (!is_array($definitions)) { $definitions = $defaults; } // discard anything a plugin registered that is not a well formed definition. // this only enforces stable filter keys and non-empty label/table/select // strings; it does not validate that 'select' is syntactically valid SQL or // returns the expected columns, so a malformed plugin SELECT can still break // the UNION. foreach ($definitions as $key => $definition) { if (!is_string($key) || $key === 'all' || preg_match('/^[A-Za-z0-9_]+$/', $key) !== 1 || !is_array($definition) || !isset($definition['label'], $definition['table'], $definition['select']) || !is_string($definition['label']) || !is_string($definition['table']) || !is_string($definition['select']) || trim($definition['label']) === '' || trim($definition['table']) === '' || trim($definition['select']) === '' || preg_match('/^[A-Za-z0-9_]+$/', $definition['table']) !== 1) { unset($definitions[$key]); } } return $definitions; } function show_cacti_processes() : void { global $item_rows; $definitions = support_process_tables(); // reduce the set of tables to those that actually exist, preserving the // registration order so the generated UNION is unchanged. $tables = []; foreach ($definitions as $key => $definition) { if (db_table_exists($definition['table'])) { $tables[$key] = $definition['label']; } } draw_cacti_process_filter(true, $tables); if (grv('rows') == '-1') { $rows = read_config_option('num_rows_table'); } else { $rows = grv('rows'); } $sql_where = ''; $sql_params = []; // form the 'where' clause for our main sql query if (grv('filter') != '') { $sql_where = 'WHERE (taskname LIKE ? OR tasktype LIKE ?)'; $sql_params[] = '%' . grv('filter') . '%'; $sql_params[] = '%' . grv('filter') . '%'; } // restrict to a single task table, but fail closed to 'all' if the // requested key is not one of the known/available process tables if (grv('tasks') != 'all' && array_key_exists(grv('tasks'), $tables)) { $tables = [ grv('tasks') => $tables[grv('tasks')] ]; } $sql_inner = ''; foreach (array_keys($tables) as $table) { $sql_inner .= ($sql_inner != '' ? ' UNION ' : '') . $definitions[$table]['select']; } if ($sql_inner == '') { // no process tables available; render the empty result rather than // building a broken 'FROM ()' UNION $total_rows = 0; $processes = []; } else { $total_rows = db_fetch_cell_prepared("SELECT COUNT(*) FROM ($sql_inner) AS rs $sql_where", $sql_params); $sql_order = get_order_string(); $sql_limit = ' LIMIT ' . ((int) $rows * (max(1, (int) grv('page')) - 1)) . ',' . (int) $rows; $processes = db_fetch_assoc_prepared("SELECT * FROM ($sql_inner) AS rs $sql_where $sql_order $sql_limit", $sql_params); } $display_text = [ 'tasktype' => [ 'display' => __('Task Type'), 'align' => 'left', 'sort' => 'ASC', 'tip' => __('The Type of Task. Generally represents the plugin and task within the plugin.') ], 'taskname' => [ 'display' => __('Task Name'), 'align' => 'left', 'sort' => 'ASC', 'tip' => __('The name of the Task.') ], 'taskid' => [ 'display' => __('Task ID'), 'align' => 'right', 'sort' => 'DESC', 'tip' => __('The ID of the Task which is often times the LSF clusterid or the License Service ID, but can be other metrics as well.') ], 'runtime' => [ 'display' => __('Run/Update Time'), 'align' => 'right', 'tip' => __('The Process runtime or times since last heartbeat.') ], 'pid' => [ 'display' => __('Process ID'), 'align' => 'right', 'tip' => __('The Process ID for the task.') ], 'timeout' => [ 'display' => __('Timeout'), 'align' => 'right', 'sort' => 'DESC', 'tip' => __('The background process timeout in seconds when a Cacti process. Otherwise controlled by the individual plugins.') ], 'started' => [ 'display' => __('Start Time'), 'align' => 'right', 'sort' => 'DESC', 'tip' => __('Time the background process started for a Cacti process. The start time may or may not be supported for various plugins.') ], 'last_update' => [ 'display' => __('Last Updated'), 'align' => 'right', 'sort' => 'DESC', 'tip' => __('Time the process last registered its status to the status tables.') ] ]; $nav = html_nav_bar('support.php?tab=background', MAX_DISPLAY_PAGES, grv('page'), $rows, $total_rows, 8, __('Processes'), 'page', 'main'); print $nav; html_start_box('', '100%', false, 3, 'center', ''); html_header_sort($display_text, grv('sort_column'), grv('sort_direction'), 1, 'support.php?tab=background', 'main'); if (cacti_sizeof($processes)) { foreach ($processes as $p) { // pid backs the SELECT fragments plugins register, so cast it before // using it to build the row id -- a non-numeric value could otherwise // break out of the id='...' attribute in form_alternate_row(). form_alternate_row('line' . (int) $p['pid'], false); if ($p['timeout'] != 'N/A') { $timeout_time = $p['timeout']; $timeout_date = get_daysfromtime($timeout_time, true); if (str_contains($timeout_date, 'y:')) { $timeout_date = __('> 1 Year'); } } else { $timeout_date = $p['timeout']; } // escape every column: plugins can contribute the SELECT fragments // behind these rows via the support_process_tables hook, so the // values are not trusted (taskname is already escaped by filter_value). // use form_selectable_ecell(), the file's existing escaping helper, // rather than hand-wrapping form_selectable_cell() with html_escape(). form_selectable_ecell($p['tasktype'], $p['pid']); form_selectable_cell(filter_value(cacti_strtoupper($p['taskname']), ''), $p['pid']); form_selectable_ecell($p['taskid'], $p['pid'], '', 'right'); form_selectable_ecell($p['runtime'], $p['pid'], '', 'right'); form_selectable_ecell($p['pid'], $p['pid'], '', 'right'); // form_selectable_cell($p['timeout'], $p['pid'], '', 'right'); form_selectable_ecell($timeout_date, $p['pid'], '', 'right'); form_selectable_ecell($p['started'], $p['pid'], '', 'right'); form_selectable_ecell($p['last_update'], $p['pid'], '', 'right'); form_end_row(); } } else { print "" . __('No Cacti or Plugin Background Processes Found') . ''; } html_end_box(false); if (cacti_sizeof($processes)) { print $nav; } } function show_php_modules() : void { $php_info = utilities_php_modules(); html_section_header(__('PHP Module Information'), 2); $php_info = str_replace( ['width="600"', 'th colspan="2"', ','], ['', 'th class="subHeaderColumn"', ', '], $php_info ); print "" . $php_info . ''; } function show_cacti_poller() : void { if (db_column_exists('sites', 'disabled')) { $sql_where = 'AND IFNULL(s.disabled, "") != "on"'; } else { $sql_where = ''; } $problematic = db_fetch_assoc("SELECT h.id, h.description, h.polling_time, h.avg_time FROM host h LEFT JOIN sites s ON s.id = h.site_id WHERE IFNULL(h.disabled, '') != 'on' $sql_where ORDER BY polling_time DESC LIMIT 20"); html_section_header(__('Worst 20 Polling Time Devices'), 2); form_alternate_row(); print ""; if (cacti_sizeof($problematic)) { print ""; print ''; print ""; print " '; print " '; print " '; print " '; print ''; print ''; foreach ($problematic as $host) { form_alternate_row(); print ''; print ''; print ''; print ''; form_end_row(); } print '
" . __('Description') . '" . __('ID') . '" . __('Avg Polling Time') . '" . __('Actual Polling Time') . '
' . htmle($host['description']) . '' . $host['id'] . '' . number_format_i18n($host['avg_time'],3) . '' . number_format_i18n($host['polling_time'],3) . '
'; } else { print __('No host found'); } print ''; form_end_row(); $problematic = db_fetch_assoc('SELECT h.id, h.description, h.failed_polls/h.total_polls AS ratio FROM host h LEFT JOIN sites s ON h.site_id = s.id WHERE IFNULL(h.disabled,"") != "on" AND IFNULL(s.disabled,"") != "on" ORDER BY ratio DESC LIMIT 20'); html_section_header(__('Worst 20 failed/total polls ratio'), 2); form_alternate_row(); print ""; if (cacti_sizeof($problematic)) { print ""; print ''; print ""; print " '; print " '; print " '; print ''; print ''; foreach ($problematic as $host) { form_alternate_row(); print ''; print ''; print ''; form_end_row(); } print '
" . __('Description') . '" . __('ID') . '" . __('Failed/Total polls') . '
' . htmle($host['description']) . '' . $host['id'] . '' . number_format_i18n($host['ratio'],3) . '
'; } else { print __('No host found'); } print ''; form_end_row(); } function show_database_tables() : void { global $local_db_cnn_id; // Get table status if (POLLER_ID == 1) { $tables = db_fetch_assoc('SELECT * FROM information_schema.tables WHERE table_schema = SCHEMA()'); } else { $tables = db_fetch_assoc('SELECT * FROM information_schema.tables WHERE table_schema = SCHEMA()', false, $local_db_cnn_id); } form_alternate_row(); print " "; if (cacti_sizeof($tables)) { print ""; print ''; print ""; print " '; print " '; print " '; print " '; print " '; print " '; print " '; print " '; print " '; print ''; print ''; foreach ($tables as $table) { form_alternate_row(); print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; form_end_row(); } print '
" . __('Name') . '" . __('Engine') . '" . __('Rows') . '" . __('Avg Row Length') . '" . __('Data Length') . '" . __('Index Length') . '" . __('Collation') . '" . __('Row Format') . '" . __('Comment') . '
' . htmle($table['TABLE_NAME']) . '' . htmle($table['ENGINE']) . '' . number_format_i18n($table['TABLE_ROWS'], -1) . '' . number_format_i18n($table['AVG_ROW_LENGTH'], -1) . '' . number_format_i18n($table['DATA_LENGTH'], -1) . '' . number_format_i18n($table['INDEX_LENGTH'], -1) . '' . htmle($table['TABLE_COLLATION']) . '' . htmle($table['ROW_FORMAT']) . '' . htmle($table['TABLE_COMMENT']) . '
'; } else { print __('Unable to retrieve table status'); } print ''; form_end_row(); } function show_cacti_changelog() : void { $changelog = file(CACTI_PATH_BASE . '/CHANGELOG'); foreach (($changelog ?: []) as $s) { if (trim($s) == '') { continue; } if (strlen(trim($s)) && stripos($s, 'CHANGELOG') === false) { if (!str_contains($s, '-') || str_contains($s, '-dev')) { html_section_header(__('Version %s', $s), 2); } else { form_alternate_row(); print '' . $s . ''; form_end_row(); } } } } function show_database_settings() : void { $status = db_fetch_assoc('show global variables'); print ""; print ''; print ""; print " '; print " '; print ''; print ''; foreach ($status as $s) { form_alternate_row(); print ''; if (strlen($s['Value']) > 70) { $s['Value'] = str_replace(',', ', ', $s['Value']); } print ''; form_end_row(); } } function show_database_permissions() : void { $status = db_get_permissions(true); $display_text = [ __('Permission Name'), __('Database'), __('Value'), __('Permission Name'), __('Database'), __('Value') ]; html_header($display_text); $r = 0; $j = 1; foreach ($status as $database => $perms) { $first = true; $r = 0; foreach ($perms as $key => $value) { if (($r % 2) == 0 || $first) { form_alternate_row("line$j"); } print ''; print ''; print ''; if (($r % 2) == 1) { form_end_row(); } $r++; $first = false; } if (($r % 2) == 1) { print ''; print ''; print ''; form_end_row(); $j++; } } } function show_database_status() : void { $status = db_fetch_assoc('show global status'); print "
" . __('Variable Name') . '" . __('Value') . '
' . htmle($s['Variable_name']) . '' . (is_numeric($s['Value']) ? number_format_i18n($s['Value'], -1) : htmle($s['Value'])) . '' . $key . '' . $database . '' . ($value ? __('Yes') : __('No')) . '   
"; print ''; print ""; print " '; print " '; print ''; print ''; foreach ($status as $s) { form_alternate_row(); print ''; print ''; form_end_row(); } } function show_tech_summary() : void { global $database_hostname, $poller_options, $input_types, $local_db_cnn_id; // When on, identifying values on this tab and in the copied report are masked. $redact = (grv('redact') == 1); // Get poller stats $poller_item = db_fetch_assoc('SELECT action, count(action) AS total FROM poller_item GROUP BY action'); // Get system stats $host_count = db_fetch_cell('SELECT COUNT(*) FROM host WHERE deleted = ""'); $graph_count = db_fetch_cell('SELECT COUNT(*) FROM graph_local'); $data_count = db_fetch_assoc('SELECT i.type_id, COUNT(i.type_id) AS total FROM data_template_data AS d, data_input AS i WHERE d.data_input_id = i.id AND local_data_id > 0 GROUP BY i.type_id'); // Get RRDtool version $rrdtool_version = __('Unknown'); $rrdtool_release = __('Unknown'); $storage_location = read_config_option('storage_location'); $out_array = []; if ($storage_location == 0) { if ((file_exists(read_config_option('path_rrdtool'))) && ((function_exists('is_executable')) && (is_executable(read_config_option('path_rrdtool'))))) { exec(cacti_escapeshellcmd(read_config_option('path_rrdtool')), $out_array); } } else { $rrdtool_pipe = rrd_init(); $out_array[] = rrdtool_execute('info', false, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe, 'WEBLOG'); rrd_close($rrdtool_pipe); } if (cacti_sizeof($out_array) > 0) { if (preg_match('/^RRDtool ([0-9.]+)/', $out_array[0], $m)) { preg_match('/^([0-9]+\.[0-9]+\.[0.9]+)/', $m[1], $m2); $rrdtool_release = $m[1]; $rrdtool_version = $rrdtool_release; } } // Get SNMP cli version. $snmp_installed distinguishes raw tool output // (must be escaped when printed) from the pre-built status span below // (already markup, must not be escaped). $snmpget_path = (string) read_config_option('path_snmpget'); if ($snmpget_path !== '' && !str_contains($snmpget_path, "\0") && is_file($snmpget_path) && is_executable($snmpget_path)) { $snmp_version = (string) shell_exec(cacti_escapeshellcmd($snmpget_path) . ' -V 2>&1'); $snmp_installed = true; } else { $snmp_version = "" . __('NET-SNMP Not Installed or its paths are not set. Please install if you wish to monitor SNMP enabled devices.') . ''; $snmp_installed = false; } // Check RRDtool issues $rrdtool_errors = []; if (cacti_version_compare($rrdtool_version, get_rrdtool_version(), '<')) { $rrdtool_errors[] = "" . __('ERROR: Installed RRDtool version does not exceed configured version.
Please visit the %s and select the correct RRDtool Utility Version.', "" . __('Configuration Settings') . '') . '
'; } $graph_gif_count = db_fetch_cell('SELECT COUNT(*) FROM graph_templates_graph WHERE image_format_id = 2'); if ($graph_gif_count > 0) { $rrdtool_errors[] = "" . __('ERROR: RRDtool 1.2.x+ does not support the GIF images format, but %d" graph(s) and/or templates have GIF set as the image format.', $graph_gif_count) . ''; } // Get spine version $spine_version = 'Unknown'; if ((file_exists(read_config_option('path_spine'))) && ((function_exists('is_executable')) && (is_executable(read_config_option('path_spine'))))) { $out_array = []; exec(cacti_escapeshellcmd(read_config_option('path_spine')) . ' --version', $out_array); if (cacti_sizeof($out_array) > 0) { $spine_version = $out_array[0]; } } html_section_header(__('General Information'), 2); form_alternate_row(); print ''; print ''; form_end_row(); $lockout = read_config_option('cacti_lockout_status', true); $enabled = read_config_option('auth_maint_lockout_type', true); form_alternate_row(); print ''; if ($enabled >= 0) { if ($lockout != '') { $lockout = json_decode($lockout, true); $unlock_time = $lockout['time'] + (30 * 60); $unlock_hms = date('H:i', $unlock_time); print ''; } else { print ''; } } else { print ''; } form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); api_plugin_hook_function('custom_version_info'); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); $rsa_fingerprint = read_config_option('rsa_fingerprint'); if ($redact && $rsa_fingerprint != '') { $rsa_fingerprint = substr($rsa_fingerprint, 0, 8) . '… (' . __('masked') . ')'; } form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; if (!$snmp_installed) { // Pre-built status span; already markup, render as-is. print ''; } else { print ''; } form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); if (!empty($rrdtool_errors)) { form_alternate_row(); print ''; $br = ''; print ''; form_end_row(); } form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); html_section_header(__('Poller Information'), 2); form_alternate_row(); print ''; print ''; $poller_type_name = $poller_options[read_config_option('poller_type')] ?? __('Unknown'); if (file_exists(read_config_option('path_spine')) && $poller_type_name == 'spine') { $type = $spine_version; if (!strpos($spine_version, CACTI_VERSION)) { $type .= ' (' . __('Different version of Cacti and Spine!') . ')'; } } else { $type = $poller_type_name; } form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); $processes = db_fetch_cell('SELECT GROUP_CONCAT( CONCAT("' . __('Name: ') . '", name, ", ' . __('Procs: ') . '", processes) SEPARATOR "
" ) AS poller FROM poller WHERE disabled=""'); $threads = db_fetch_cell('SELECT GROUP_CONCAT( CONCAT("' . __('Name: ') . '", name, ", ' . __('Threads: ') . '", threads) SEPARATOR "
" ) AS poller FROM poller WHERE disabled=""'); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); $script_servers = read_config_option('php_servers'); form_alternate_row(); print ''; print ''; form_end_row(); if (POLLER_ID == 1) { $max_connections = db_get_global_variable('max_connections'); $max_local_connections = 0; } elseif (CACTI_CONNECTION == 'online') { $max_connections = db_get_global_variable('max_connections'); $max_local_connections = db_get_global_variable('max_connections', $local_db_cnn_id); } else { $max_connections = 0; $max_local_connections = db_get_global_variable('max_connections'); } $total_dc_threads = db_fetch_cell("SELECT SUM((processes * threads) + (processes * $script_servers)) AS threads FROM poller WHERE disabled = ''"); $recommend_mc = $total_dc_threads + 100; if ($recommend_mc > $max_connections) { if (POLLER_ID == 1) { $db_connections = '' . __('Main Server: Current: %s, Min Required: %s', $max_connections, $recommend_mc) . ''; } elseif (CACTI_CONNECTION == 'online') { $db_connections = '' . __('Main Server: Current: %s, Min Required: %s', $max_connections, $recommend_mc) . ''; } else { $db_connections = ''; } } else { if (POLLER_ID == 1) { $db_connections = '' . __('Main Server: Current: %s, Min Required: %s', $max_connections, $recommend_mc) . ''; } elseif (CACTI_CONNECTION == 'online') { $db_connections = '' . __('Main Server: Current: %s, Min Required: %s', $max_connections, $recommend_mc) . ''; } else { $db_connections = ''; } } if (POLLER_ID > 1) { if ($recommend_mc > $max_local_connections) { $db_connections .= '
' . __('Local Server: Current: %s, Min Required: %s', $max_local_connections, $recommend_mc) . ''; } else { $db_connections .= '
' . __('Local Server: Current: %s, Min Required: %s', $max_local_connections, $recommend_mc) . ''; } } form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); // Get System Memory $memInfo = utilities_get_system_memory(); // print '
';print_r($memInfo);print '
'; $total_memory = 0; if (cacti_sizeof($memInfo)) { html_section_header(__('System Memory'), 2); foreach ($memInfo as $name => $value) { if (CACTI_SERVER_OS == 'win32') { form_alternate_row(); print ""; print ''; form_end_row(); } else { switch($name) { case 'SwapTotal': case 'SwapFree': case 'Cached': case 'MemTotal': case 'MemFree': case 'MemAvailable': case 'Buffers': case 'Active': case 'Inactive': // Convert to GBi $value /= (1000 * 1000 * 1000); form_alternate_row(); print ""; print ''; form_end_row(); if ($name == 'MemTotal') { $total_memory = $value; } } } } form_end_row(); } $mysql_info = get_mysql_info(POLLER_ID); $database = $mysql_info['database']; $version = $mysql_info['version']; $link_ver = $mysql_info['link_ver']; $variables = $mysql_info['variables']; $myhost = php_uname('n'); $dbhost = $database_hostname; if ($dbhost == 'localhost' || $myhost == $dbhost) { $local_db = true; } else { $local_db = false; } // Get Maximum Memory in GB for MySQL/MariaDB if (POLLER_ID == 1) { if ($database == 'MariaDB') { $systemMemory = db_fetch_cell('SELECT (@@GLOBAL.key_buffer_size + @@GLOBAL.query_cache_size + @@GLOBAL.tmp_table_size + @@GLOBAL.innodb_buffer_pool_size + @@GLOBAL.innodb_log_buffer_size) / 1024 / 1024 / 1024'); $maxPossibleMyMemory = db_fetch_cell('SELECT ( (@@GLOBAL.key_buffer_size + @@GLOBAL.query_cache_size + @@GLOBAL.tmp_table_size + @@GLOBAL.innodb_buffer_pool_size + @@GLOBAL.innodb_log_buffer_size + @@GLOBAL.max_connections * ( @@GLOBAL.sort_buffer_size + @@GLOBAL.read_buffer_size + @@GLOBAL.read_rnd_buffer_size + @@GLOBAL.join_buffer_size + @@GLOBAL.thread_stack + @@GLOBAL.binlog_cache_size) ) / 1024 / 1024 / 1024)'); } else { $systemMemory = db_fetch_cell('SELECT (@@GLOBAL.key_buffer_size + @@GLOBAL.tmp_table_size + @@GLOBAL.innodb_buffer_pool_size + @@GLOBAL.innodb_log_buffer_size) / 1024 / 1024 / 1024'); $maxPossibleMyMemory = db_fetch_cell('SELECT ( (@@GLOBAL.key_buffer_size + @@GLOBAL.tmp_table_size + @@GLOBAL.innodb_buffer_pool_size + @@GLOBAL.innodb_log_buffer_size + @@GLOBAL.max_connections * ( @@GLOBAL.sort_buffer_size + @@GLOBAL.read_buffer_size + @@GLOBAL.read_rnd_buffer_size + @@GLOBAL.join_buffer_size + @@GLOBAL.thread_stack + @@GLOBAL.binlog_cache_size) ) / 1024 / 1024 / 1024)'); } $clientMemory = db_fetch_cell('SELECT @@GLOBAL.max_connections * ( @@GLOBAL.sort_buffer_size + @@GLOBAL.read_buffer_size + @@GLOBAL.read_rnd_buffer_size + @@GLOBAL.join_buffer_size + @@GLOBAL.thread_stack + @@GLOBAL.binlog_cache_size) / 1024 / 1024 / 1024'); } else { if ($database == 'MariaDB') { $maxPossibleMyMemory = db_fetch_cell('SELECT ( (@@GLOBAL.key_buffer_size + @@GLOBAL.query_cache_size + @@GLOBAL.tmp_table_size + @@GLOBAL.innodb_buffer_pool_size + @@GLOBAL.innodb_log_buffer_size + @@GLOBAL.max_connections * ( @@GLOBAL.sort_buffer_size + @@GLOBAL.read_buffer_size + @@GLOBAL.read_rnd_buffer_size + @@GLOBAL.join_buffer_size + @@GLOBAL.thread_stack + @@GLOBAL.binlog_cache_size) ) / 1024 / 1024 / 1024)', '', false, $local_db_cnn_id); $systemMemory = db_fetch_cell('SELECT (@@GLOBAL.key_buffer_size + @@GLOBAL.query_cache_size + @@GLOBAL.tmp_table_size + @@GLOBAL.innodb_buffer_pool_size + @@GLOBAL.innodb_log_buffer_size) / 1024 / 1024 / 1024', '', false, $local_db_cnn_id); } else { $maxPossibleMyMemory = db_fetch_cell('SELECT ( (@@GLOBAL.key_buffer_size + @@GLOBAL.tmp_table_size + @@GLOBAL.innodb_buffer_pool_size + @@GLOBAL.innodb_log_buffer_size + @@GLOBAL.max_connections * ( @@GLOBAL.sort_buffer_size + @@GLOBAL.read_buffer_size + @@GLOBAL.read_rnd_buffer_size + @@GLOBAL.join_buffer_size + @@GLOBAL.thread_stack + @@GLOBAL.binlog_cache_size) ) / 1024 / 1024 / 1024)', '', false, $local_db_cnn_id); $systemMemory = db_fetch_cell('SELECT (@@GLOBAL.key_buffer_size + @@GLOBAL.tmp_table_size + @@GLOBAL.innodb_buffer_pool_size + @@GLOBAL.innodb_log_buffer_size) / 1024 / 1024 / 1024', '', false, $local_db_cnn_id); } $clientMemory = db_fetch_cell('SELECT @@GLOBAL.max_connections * ( @@GLOBAL.sort_buffer_size + @@GLOBAL.read_buffer_size + @@GLOBAL.read_rnd_buffer_size + @@GLOBAL.join_buffer_size + @@GLOBAL.thread_stack + @@GLOBAL.binlog_cache_size) / 1024 / 1024 / 1024', '', false, $local_db_cnn_id); } html_section_header(__('MySQL/MariaDB Memory Statistics (Source: MySQL Tuner)'), 2); if ($total_memory > 0 && $local_db) { if ($maxPossibleMyMemory > ($total_memory * 0.8)) { form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); } else { form_alternate_row(); print ''; print ''; form_end_row(); } } else { form_alternate_row(); print ''; print ''; form_end_row(); } if ($total_memory > 0 && $local_db) { if ($systemMemory > ($total_memory * 0.8)) { form_alternate_row(); print ''; print ''; form_end_row(); } else { form_alternate_row(); print ''; print ''; form_end_row(); } form_alternate_row(); print ''; print ''; form_end_row(); if ($clientMemory > ($total_memory * 0.8)) { form_alternate_row(); print ''; print ''; form_end_row(); } else { form_alternate_row(); print ''; print ''; form_end_row(); } form_alternate_row(); print ''; print ''; form_end_row(); } else { form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); } html_section_header(__('PHP Information'), 2); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); form_alternate_row(); print ''; print ''; form_end_row(); utilities_get_mysql_recommendations(); utilities_get_mysql_capabilities(); // Shareable diagnostics report (Feature: Copy Diagnostics). // When the redact toggle is on, hostnames/IPs embedded in the SNMP version // line and web server string are masked via support_redact(), and the RSA // fingerprint is truncated. The report does not include the DB host, // credentials or absolute paths. $snmp_line = $snmp_installed ? trim(explode("\n", $snmp_version)[0]) : trim(strip_tags($snmp_version)); $web_server = (string) ($_SERVER['SERVER_SOFTWARE'] ?? __('Unknown')); $poller_report = $poller_options[read_config_option('poller_type')] ?? __('Unknown'); if ($redact) { $snmp_line = support_redact($snmp_line); $web_server = support_redact($web_server); } if ($poller_report == 'spine' && $spine_version != 'Unknown') { $poller_report = $spine_version; } $rsa = read_config_option('rsa_fingerprint'); if ($rsa == '') { $rsa_report = __('N/A'); } elseif ($redact) { $rsa_report = substr($rsa, 0, 8) . '… (' . __('masked') . ')'; } else { $rsa_report = $rsa; } $report = "### Cacti Diagnostics\n"; $report .= '- ' . __('Cacti Version') . ': ' . CACTI_VERSION . "\n"; $report .= '- ' . __('Cacti OS') . ': ' . CACTI_SERVER_OS . "\n"; $report .= '- ' . __('Web Server') . ': ' . $web_server . "\n"; $report .= '- ' . __('PHP Version') . ': ' . PHP_VERSION . "\n"; $report .= '- ' . __('PHP OS') . ': ' . PHP_OS . "\n"; $report .= '- ' . __('Database') . ': ' . trim($database . ' ' . $version) . "\n"; $report .= '- ' . __('RRDtool Version Configured') . ': ' . get_rrdtool_version() . "+\n"; $report .= '- ' . __('RRDtool Version Found') . ': ' . $rrdtool_release . "\n"; $report .= '- ' . __('NET-SNMP Version') . ': ' . $snmp_line . "\n"; $report .= '- ' . __('Poller Interval') . ': ' . read_config_option('poller_interval') . "\n"; $report .= '- ' . __('Poller Type') . ': ' . $poller_report . "\n"; $report .= '- ' . __('Last Run Statistics') . ': ' . read_config_option('stats_poller') . "\n"; $report .= '- ' . 'memory_limit: ' . ini_get('memory_limit') . "\n"; $report .= '- ' . 'max_execution_time: ' . ini_get('max_execution_time') . "\n"; $report .= '- ' . __('System Memory') . ': ' . ($total_memory > 0 ? number_format_i18n($total_memory, 2, 1000) . ' GB' : __('N/A')) . "\n"; $report .= '- ' . __('RSA Fingerprint') . ': ' . $rsa_report . "\n"; form_alternate_row(); print "'; form_end_row(); } function show_tech_environment() : void { // utility_php_verify_extensions() alone can't tell whether a module is // really loaded in both contexts: it seeds CLI-only extensions such as // pcntl with 'web' => true (not required in web), so checking either the // seeded flag or a bare extension_loaded() in isolation misreports one // context or the other. Verify web in-process, shell out to cli_check.php // for the real CLI state (same path_php_binary lookup already used below // for rrdtool/snmpget), and combine with the web && cli rule from // utility_php_set_installed(), the same one lib/installer.php relies on. $extensions = false; utility_php_verify_extensions($extensions, 'web'); $php_binary = read_config_option('path_php_binary'); if ($php_binary != '' && file_exists($php_binary) && is_executable($php_binary)) { $cli_json = shell_exec(cacti_escapeshellcmd($php_binary) . ' -q ' . cacti_escapeshellarg(CACTI_PATH_INSTALL . '/cli_check.php') . ' extensions'); $cli_ext = @json_decode((string) $cli_json, true); if (is_array($cli_ext)) { foreach ($cli_ext as $name => $ext) { if (isset($extensions[$name])) { $extensions[$name]['cli'] = !empty($ext['cli']); } } } } utility_php_set_installed($extensions); html_section_header(__('Required PHP Extensions'), 2); foreach ($extensions as $name => $extension) { $loaded = !empty($extension['installed']); form_alternate_row(); print ''; print ''; form_end_row(); } // Recommended (optional) extensions such as php-snmp. $optionals = false; utility_php_verify_optionals($optionals, 'web'); html_section_header(__('Recommended PHP Extensions'), 2); foreach ($optionals as $name => $optional) { $loaded = !empty($optional['web']); form_alternate_row(); print ''; print ''; form_end_row(); } // Key php.ini values and thresholds (reuses the installer's recommendations). $recommends = false; utility_php_verify_recommends($recommends, 'web'); html_section_header(__('PHP Configuration'), 2); foreach ($recommends as $recommend) { if ($recommend['name'] == 'location' || $recommend['name'] == 'version') { continue; } form_alternate_row(); print ''; print ''; form_end_row(); } $file_uploads = (bool) ini_get('file_uploads'); form_alternate_row(); print ''; print ''; form_end_row(); // Required binaries: existence + version. $binaries = [ 'path_php_binary' => '-v', 'path_rrdtool' => '--version', 'path_snmpget' => '-V', ]; html_section_header(__('Required Binaries'), 2); foreach ($binaries as $option => $arg) { $path = read_config_option($option); form_alternate_row(); print ''; if ($path != '' && file_exists($path) && function_exists('is_executable') && is_executable($path)) { if (is_function_enabled('shell_exec')) { $out = shell_exec(cacti_escapeshellarg($path) . ' ' . $arg . ' 2>&1'); $version = ($out != '' ? trim(explode("\n", $out)[0]) : __('Unknown version')); print ''; } else { print ''; } } else { print ''; } form_end_row(); } // Writable directories. cache/rra/log must be writable; scripts/resource // are usually read-only so a non-writable result is only a warning. $directories = [ CACTI_PATH_CACHE => true, CACTI_PATH_RRA => true, CACTI_PATH_LOG => true, CACTI_PATH_SCRIPTS => false, CACTI_PATH_RESOURCE => false, ]; html_section_header(__('Writable Directories'), 2); foreach ($directories as $directory => $required) { $writable = is_writable($directory); if ($writable) { $status = DB_STATUS_SUCCESS; $text = __('Writable'); } else { $status = ($required ? DB_STATUS_ERROR : DB_STATUS_WARNING); $text = __('Not writable'); } form_alternate_row(); print ''; print ''; form_end_row(); } } /** * tech_env_status - render a status badge for the technical support report. * $text is treated as plain text and escaped here, so callers pass raw values * and must not pre-escape (doing so would double-encode). */ function tech_env_status(int $status, string $text) : string { [$class, $icon] = match ($status) { DB_STATUS_SUCCESS => ['deviceUp', 'fa-check-circle'], DB_STATUS_ERROR => ['deviceDown', 'fa-times-circle'], default => ['deviceRecovering', 'fa-exclamation-triangle'], }; return " " . html_escape($text) . ''; } /** * support_redact - mask identifying details in a display string so a support * report can be shared publicly. Display-only and deliberately conservative: * it prefers to over-mask rather than leak. Not applied to version numbers. */ function support_redact(string $value) : string { if ($value === '') { return $value; } // IPv6 before IPv4 so the longer pattern wins. The candidate pattern is // deliberately loose (it also matches "::" compression and would catch // non-addresses like MACs or "1:2:3"); filter_var() gates the replacement // so only strings that are genuinely IPv6 get masked. $value = preg_replace_callback('/(?:[0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}/', function ($m) { return filter_var($m[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false ? '' : $m[0]; }, $value); $value = preg_replace('/\b(?:\d{1,3}\.){3}\d{1,3}\b/', '', (string) $value); // FQDNs (foo.bar.example). The alphabetic TLD requirement leaves version // numbers such as 1.7.2 untouched. This must run before the node-name // replacement below: if the node name is a prefix of the FQDN (e.g. "db01" // in "db01.local"), masking the node name first would strip the prefix and // leave a bare suffix ("local") that no longer looks like an FQDN, leaking // part of the domain. $value = preg_replace('/\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}\b/', '', (string) $value); // Replace this host's own node name wherever it appears (php_uname, SNMP banner, etc.). $node = function_exists('php_uname') ? php_uname('n') : ''; if ($node != '' && strlen($node) > 1) { $value = str_ireplace($node, '', $value); } // /home// and /Users// path segments. $value = preg_replace('#/(?:home|Users)/[^/\s]+#', '/home/', (string) $value); return $value; } /** * support_tail_severity - read the last WARN/ERROR/SECURITY lines from a log * file without loading the whole file. Seeks to the end and reads at most * $cap_bytes so a multi-gigabyte log cannot exhaust memory. * * @return array */ function support_tail_severity(string $file, int $max_lines, int $cap_bytes) : array { // The path is checked by the caller, but the file can still vanish or // become unreadable between that check and here; suppress the warning // PHP would otherwise emit into the rendered page and fail closed. $size = @filesize($file); if ($size === false || $size == 0) { return []; } $fp = @fopen($file, 'rb'); if ($fp === false) { return []; } $read = ($size < $cap_bytes ? $size : $cap_bytes); if (fseek($fp, -$read, SEEK_END) !== 0) { fclose($fp); return []; } $buffer = fread($fp, $read); fclose($fp); if ($buffer === false) { return []; } $all = explode("\n", $buffer); // The byte cap can slice through the first line; drop that partial fragment, // but only when the file was actually larger than the cap. if ($size > $cap_bytes) { array_shift($all); } $matched = []; foreach ($all as $line) { $line = rtrim($line, "\r"); if ($line === '') { continue; } if (str_contains($line, 'WARN') || str_contains($line, 'ERROR') || str_contains($line, 'SECURITY')) { $matched[] = $line; } } if (cacti_sizeof($matched) > $max_lines) { $matched = array_slice($matched, -$max_lines); } return $matched; } function show_tech_log() : void { $redact = (grv('redact') == 1); $max_lines = 100; $cap_bytes = 256 * 1024; // Only ever the configured Cacti log is read. The path comes from // read_config_option('path_cactilog'); no request variable can influence it. $logfile = read_config_option('path_cactilog'); html_section_header(__('Recent Log (WARN / ERROR / SECURITY)'), 2); if ($logfile == '' || str_contains($logfile, chr(0)) || !file_exists($logfile) || !is_file($logfile) || !is_readable($logfile)) { form_alternate_row(); print "'; form_end_row(); return; } $lines = support_tail_severity($logfile, $max_lines, $cap_bytes); if (!cacti_sizeof($lines)) { form_alternate_row(); print "'; form_end_row(); return; } form_alternate_row(); print "'; form_end_row(); }
" . __('Variable Name') . '" . __('Value') . '
' . htmle($s['Variable_name']) . '' . (is_numeric($s['Value']) ? number_format_i18n($s['Value'], -1) : htmle($s['Value'])) . '' . __('Diagnostics Export') . ' ' . '' . __('Cacti Lockout Status') . '' . __('Date') . '' . date('r') . '' . __('Cacti Version') . '' . CACTI_VERSION . '' . __('Cacti OS') . '' . CACTI_SERVER_OS . '' . __('RSA Fingerprint') . '' . html_escape($rsa_fingerprint) . '' . __('NET-SNMP Version') . '' . $snmp_version . '' . ($redact ? html_escape(support_redact($snmp_version)) : html_escape($snmp_version)) . '' . __('RRDtool Version') . ' ' . __('Configured') . '' . get_rrdtool_version() . '+' . __('RRDtool Version') . ' ' . __('Found') . '' . $rrdtool_release . ' '; foreach ($rrdtool_errors as $rrdtool_error) { print $br . $rrdtool_error; $br = '
'; } print '
' . __('Devices') . '' . number_format_i18n($host_count, -1) . '' . __('Graphs') . '' . number_format_i18n($graph_count, -1) . '' . __('Data Sources') . ''; $data_total = 0; if (cacti_sizeof($data_count)) { foreach ($data_count as $item) { print $input_types[$item['type_id']] . ': ' . number_format_i18n($item['total'], -1) . '
'; $data_total += $item['total']; } print __('Total: %s', number_format_i18n($data_total, -1)); } else { print "0"; } print '
' . __('Interval') . '' . read_config_option('poller_interval') . '' . __('Type') . '' . $type . '' . __('Items') . ''; $total = 0; if (cacti_sizeof($poller_item)) { foreach ($poller_item as $item) { print __('Action[%s]', $item['action']) . ': ' . number_format_i18n($item['total'], -1) . '
'; $total += $item['total']; } print __('Total: %s', number_format_i18n($total, -1)); } else { print "" . __('No items to poll') . ''; } print '
' . __('Concurrent Processes') . '' . $processes . '' . __('Max Threads') . '' . $threads . '' . __('PHP Servers') . '' . htmle($script_servers) . '' . __('Minimum Connections:') . '' . $db_connections . '
' . __('Assumes 100 spare connections for Web page users and other various connections.') . '
' . __('The minimum required can vary greatly if there is heavy user Graph viewing activity.') . '
' . __('Each browser tab can use upto 10 connections depending on the browser.') . '
' . __('Script Timeout') . '' . read_config_option('script_timeout') . '' . __('Max OID') . '' . read_config_option('max_get_size') . '' . __('Last Run Statistics') . '' . read_config_option('stats_poller') . '$name' . number_format_i18n($value / 1000, 2) . ' MB$name' . __('%s GB', number_format_i18n($value, 2, 1000)) . '' . __('Max Total Memory Possible') . '' . __('%s GB', number_format_i18n($maxPossibleMyMemory, 2, 1000)) . '' . __('Reduce MySQL/MariaDB Memory to less than 80% of System Memory. Preserve additional Cache Memory for RRDfiles if the Database is on the same system as the RRDfiles. See Core and Client Totals below for explanation of calculation method.') . '' . __('Max Total Memory Possible') . '' . __('%s GB', number_format_i18n($maxPossibleMyMemory, 2, 1000)) . '' . __('Max Total Memory Possible') . '' . __('%s GB', number_format_i18n($maxPossibleMyMemory, 2, 1000)) . '' . __('Max Core Memory Possible') . '' . __('%s GB', number_format_i18n($systemMemory, 2, 1000)) . '  (' . __('Reduce Total Core Memory') . '' . __('Max Core Memory Possible') . '' . __('%s GB', number_format_i18n($systemMemory, 2, 1000)) . '' . __('Calculation Formula') . 'SELECT @@GLOBAL.key_buffer_size +
@@GLOBAL.query_cache_size +
@@GLOBAL.tmp_table_size +
@@GLOBAL.innodb_buffer_pool_size +
@@GLOBAL.innodb_log_buffer_size
' . __('Max Connection Memory Possible') . '' . __('%s GB', number_format_i18n($clientMemory, 2, 1000)) . '  (' . __('Reduce Total Client Memory') . ')' . __('Max Connection Memory Possible') . '' . __('%s GB', number_format_i18n($clientMemory, 2, 1000)) . '' . __('Calculation Formula') . 'SELECT @@GLOBAL.max_connections * (
@@GLOBAL.sort_buffer_size +
@@GLOBAL.read_buffer_size +
@@GLOBAL.read_rnd_buffer_size +
@@GLOBAL.join_buffer_size +
@@GLOBAL.thread_stack +
@@GLOBAL.binlog_cache_size)
' . __('Max Core Memory Possible') . '' . __('%s GB', number_format_i18n($systemMemory, 2, 1000)) . '' . __('Calculation Formula') . 'SELECT @@GLOBAL.key_buffer_size +
@@GLOBAL.query_cache_size +
@@GLOBAL.tmp_table_size +
@@GLOBAL.innodb_buffer_pool_size +
@@GLOBAL.innodb_log_buffer_size
' . __('Max Connection Memory Possible') . '' . __('%s GB', number_format_i18n($clientMemory, 2, 1000)) . '' . __('Calculation Formula') . 'SELECT @@GLOBAL.max_connections * (
@@GLOBAL.sort_buffer_size +
@@GLOBAL.read_buffer_size +
@@GLOBAL.read_rnd_buffer_size +
@@GLOBAL.join_buffer_size +
@@GLOBAL.thread_stack +
@@GLOBAL.binlog_cache_size)
' . __('PHP Version') . '' . PHP_VERSION . '' . __('PHP OS') . '' . PHP_OS . '' . __('PHP uname') . ''; if (function_exists('php_uname')) { print html_escape($redact ? support_redact(php_uname()) : php_uname()); } else { print __('N/A'); } print '' . __('PHP SNMP') . ''; if (!function_exists('snmpget')) { print __('Not Installed'); } elseif (!CACTI_PHP_SNMP) { print __('Installed, but disabled by $php_snmp_support in include/config.php. The Net-SNMP binaries are used instead.'); } else { print __('Installed.'); } print 'max_execution_time' . html_escape(ini_get('max_execution_time')) . 'memory_limit' . html_escape(ini_get('memory_limit')); // Calculate memory suggestion based off of data source count $memory_suggestion = $data_total * 32768; // Set minimum - 16M if ($memory_suggestion < 16777216) { $memory_suggestion = 16777216; } // Set maximum - 512M if ($memory_suggestion > 536870912) { $memory_suggestion = 536870912; } // Suggest values in 8M increments $memory_suggestion = round($memory_suggestion / 8388608) * 8388608; if (memory_bytes(ini_get('memory_limit')) < $memory_suggestion) { print "
"; if ((ini_get('memory_limit') == -1)) { print __("You've set memory limit to 'unlimited'.") . '
'; } print __('It is highly suggested that you alter you php.ini memory_limit to %s or higher.', memory_readable($memory_suggestion)) . '
' . __('This suggested memory value is calculated based on the number of data source present and is only to be used as a suggestion, actual values may vary system to system based on requirements.'); print '

'; } print '
' . html_escape($name) . '' . tech_env_status($loaded ? DB_STATUS_SUCCESS : DB_STATUS_ERROR, $loaded ? __('Installed') : __('Missing')) . '' . html_escape($name) . '' . tech_env_status($loaded ? DB_STATUS_SUCCESS : DB_STATUS_WARNING, $loaded ? __('Installed') : __('Not installed')) . '' . html_escape($recommend['name']) . '' . tech_env_status((int) $recommend['status'], __('Current: %s, Recommended: %s', $recommend['current'], $recommend['value'])) . 'file_uploads' . tech_env_status($file_uploads ? DB_STATUS_SUCCESS : DB_STATUS_ERROR, $file_uploads ? __('On') : __('Off')) . '' . html_escape($option) . '' . tech_env_status(DB_STATUS_SUCCESS, $version) . '' . tech_env_status(DB_STATUS_WARNING, __('Version unavailable: shell_exec() is disabled')) . '' . tech_env_status(DB_STATUS_ERROR, __('Not found or not executable: %s', $path)) . '' . html_escape($directory) . '' . tech_env_status($status, $text) . '" . __('Log not available.') . '" . __('No recent WARN, ERROR or SECURITY log entries found.') . '"; print "
";

	foreach ($lines as $line) {
		if ($redact) {
			$line = support_redact($line);
		}

		print html_escape($line) . "\n";
	}

	print '