0))"; } else { // triggered return "(td.thold_enabled='on' AND ((td.thold_alert != 0 AND td.thold_fail_count >= td.thold_fail_trigger) OR (td.bl_alert > 0 AND td.bl_fail_count >= td.bl_fail_trigger)))"; } } /** * Get host ids currently associated with triggered/breached thresholds. * * @return array */ function checkTholds(): array { $thold_hosts = []; if (api_plugin_is_enabled('thold')) { return array_rekey( db_fetch_assoc('SELECT DISTINCT dl.host_id FROM thold_data AS td INNER JOIN data_local AS dl ON td.local_data_id=dl.id WHERE ' . getTholdWhere()), 'host_id', 'host_id' ); } return $thold_hosts; } /** * Get failing/triggered service checks for a host. * * The servcheck plugin keys its tests by hostname/ipaddress rather than by * Cacti host id, so matching is done against the device's hostname. A check * is considered failing when it is enabled, has run at least once, and is * either in a triggered down state or its last result was not successful. * * The query adapts to the installed servcheck schema: released v0.3 names the * check-time column `lastcheck` and keeps result data in plugin_servcheck_log, * while v0.4+ renamed it to `last_check` and added last_result/last_error to * the test table. When the result columns are absent the triggered/failures * counters are used to detect a failing check. * * @param array $host Host row (requires the 'hostname' key). * * @return array Failing servcheck test rows, ordered most severe first. */ function getHostTriggeredServchecks(array $host): array { if (!api_plugin_is_enabled('servcheck')) { return []; } if (!isset($host['hostname']) || $host['hostname'] == '') { return []; } if (!db_table_exists('plugin_servcheck_test')) { return []; } $check_col = db_column_exists('plugin_servcheck_test', 'last_check') ? 'last_check' : 'lastcheck'; if (db_column_exists('plugin_servcheck_test', 'last_result')) { $select = 'id, name, triggered, last_result, last_error'; $fail_predicate = "(triggered > 0 OR (last_result != 'ok' AND last_result != 'not yet'))"; } else { $select = 'id, name, triggered'; $fail_predicate = '(triggered > 0 OR failures > 0)'; } return db_fetch_assoc_prepared( "SELECT $select FROM plugin_servcheck_test WHERE enabled = 'on' AND (hostname = ? OR ipaddress = ?) AND $check_col > 0 AND $fail_predicate ORDER BY triggered DESC, $check_col DESC", [$host['hostname'], $host['hostname']] ); } /** * Get host ids that currently have a failing/triggered service check. * * Mirrors checkTholds() for the servcheck plugin: it resolves the servcheck * tests (keyed by hostname/ipaddress) that are failing into Cacti host ids so * the monitor board, list and status filters can flag them. Realm-gated so the * promoted status is only visible to users with Service Check access, and * schema-aware across servcheck v0.3 (lastcheck/failures) and v0.4+ * (last_check/last_result). * * @return array Map of host_id => host_id. */ function checkServchecks(): array { $servcheck_hosts = []; if (!api_plugin_is_enabled('servcheck') || !api_plugin_user_realm_auth('servcheck_test.php')) { return $servcheck_hosts; } if (!db_table_exists('plugin_servcheck_test')) { return $servcheck_hosts; } $check_col = db_column_exists('plugin_servcheck_test', 'last_check') ? 'last_check' : 'lastcheck'; if (db_column_exists('plugin_servcheck_test', 'last_result')) { $fail_predicate = "(sct.triggered > 0 OR (sct.last_result != 'ok' AND sct.last_result != 'not yet'))"; } else { $fail_predicate = '(sct.triggered > 0 OR sct.failures > 0)'; } return array_rekey( db_fetch_assoc("SELECT DISTINCT h.id FROM host AS h INNER JOIN plugin_servcheck_test AS sct ON (sct.hostname = h.hostname OR sct.ipaddress = h.hostname) WHERE sct.enabled = 'on' AND sct.$check_col > 0 AND $fail_predicate AND h.deleted = ''"), 'id', 'id' ); } /** * Per-host threshold counts for the list-view Thresholds pill. * * @return array Map of host_id => ['total' => int, 'triggered' => int, * 'breached' => int]. Empty when thold is unavailable. */ function monitorTholdCounts(): array { if (!api_plugin_is_enabled('thold') || !db_table_exists('thold_data')) { return []; } $rows = db_fetch_assoc("SELECT dl.host_id, COUNT(DISTINCT td.id) AS total, SUM(td.thold_enabled = 'on' AND ((td.thold_alert != 0 AND td.thold_fail_count >= td.thold_fail_trigger) OR (td.bl_alert > 0 AND td.bl_fail_count >= td.bl_fail_trigger))) AS triggered, SUM(td.thold_enabled = 'on' AND td.bl_alert > 0 AND td.bl_fail_count >= td.bl_fail_trigger) AS breached FROM thold_data AS td INNER JOIN data_local AS dl ON td.local_data_id = dl.id WHERE td.thold_enabled = 'on' GROUP BY dl.host_id"); $map = []; foreach ($rows as $row) { $map[$row['host_id']] = [ 'total' => (int) $row['total'], 'triggered' => (int) $row['triggered'], 'breached' => (int) $row['breached'], ]; } return $map; } /** * Per-host service-check counts for the list-view Service Checks pill. * * Servcheck tests are keyed by hostname/ipaddress (no host id), schema-aware * across servcheck v0.3 (lastcheck/failures) and v0.4+ (last_check/last_result), * and realm-gated like checkServchecks(). * * @return array Map of host_id => ['total' => int, 'failed' => int]. Empty when * servcheck is unavailable or not permitted. */ function monitorServcheckCounts(): array { if (!api_plugin_is_enabled('servcheck') || !api_plugin_user_realm_auth('servcheck_test.php')) { return []; } if (!db_table_exists('plugin_servcheck_test')) { return []; } $check_col = db_column_exists('plugin_servcheck_test', 'last_check') ? 'last_check' : 'lastcheck'; if (db_column_exists('plugin_servcheck_test', 'last_result')) { $fail_predicate = "(sct.triggered > 0 OR (sct.last_result != 'ok' AND sct.last_result != 'not yet'))"; } else { $fail_predicate = '(sct.triggered > 0 OR sct.failures > 0)'; } $rows = db_fetch_assoc("SELECT h.id AS host_id, COUNT(DISTINCT sct.id) AS total, SUM(sct.$check_col > 0 AND $fail_predicate) AS failed FROM host AS h INNER JOIN plugin_servcheck_test AS sct ON (sct.hostname = h.hostname OR sct.ipaddress = h.hostname) WHERE sct.enabled = 'on' AND h.deleted = '' GROUP BY h.id"); $map = []; foreach ($rows as $row) { $map[$row['host_id']] = [ 'total' => (int) $row['total'], 'failed' => (int) $row['failed'], ]; } return $map; } /** * Append an IN-clause fragment to an existing SQL where string. * * @param string $sql_where SQL where fragment, updated in place. * @param string $sql_join Join token used between predicates (e.g. AND/OR). * @param string $sql_field Field name to compare with IN list. * @param string $sql_data Comma-delimited values for the IN list. * @param string $sql_suffix Optional suffix appended inside predicate. * * @return void */ function renderGroupConcat(string &$sql_where, string $sql_join, string $sql_field, string $sql_data, string $sql_suffix = ''): void { // Remove empty entries if something was returned if (!empty($sql_data)) { $sql_data = trim(str_replace(',,', ',', $sql_data), ','); if (!empty($sql_data)) { $sql_where .= ($sql_where != '' ? $sql_join : '') . "($sql_field IN($sql_data) $sql_suffix)"; } } } /** * Build the additive (OR-combined) SQL predicate for the selected Events. * * Each event maps to a host-table predicate; a device matches when it satisfies * any selected event. Triggered/muted sets come from the precomputed thold, * servcheck and session mute id lists. * * @param array $events Validated event keys currently selected. * * @return string Parenthesised OR predicate, or '' when nothing is selected. */ function monitorEventFilterSql(array $events): string { global $thold_hosts, $servcheck_hosts; if (!cacti_sizeof($events)) { return ''; } $thold_ids = (isset($thold_hosts) && cacti_sizeof($thold_hosts)) ? implode(',', array_map('intval', $thold_hosts)) : '0'; $servcheck_ids = (isset($servcheck_hosts) && cacti_sizeof($servcheck_hosts)) ? implode(',', array_map('intval', $servcheck_hosts)) : '0'; $muted = (isset($_SESSION['monitor_muted_hosts']) && cacti_sizeof($_SESSION['monitor_muted_hosts'])) ? implode(',', array_map('intval', $_SESSION['monitor_muted_hosts'])) : '0'; $map = [ 'down' => "(h.status = 1 AND h.id NOT IN ($muted))", 'recovering' => '(h.status = 2)', 'triggered' => "(h.id IN ($thold_ids) AND h.id NOT IN ($muted))", 'down_ma' => "(h.status = 1 AND h.id IN ($muted))", 'triggered_ma' => "(h.id IN ($thold_ids) AND h.id IN ($muted))", 'warn_ping' => '(h.status = 3 AND h.monitor_warn > 0 AND h.cur_time > h.monitor_warn AND NOT (h.monitor_alert > 0 AND h.cur_time > h.monitor_alert))', 'alert_ping' => '(h.status = 3 AND h.monitor_alert > 0 AND h.cur_time > h.monitor_alert)', 'failed_svc' => "(h.id IN ($servcheck_ids))", ]; $preds = []; foreach ($events as $event) { if (isset($map[$event])) { $preds[] = $map[$event]; } } if (!cacti_sizeof($preds)) { return ''; } return '(' . implode(' OR ', $preds) . ')'; } /** * Build core monitor host query join/where fragments from current filters. * * @param string $sql_where SQL where fragment, updated in place. * @param string $sql_join SQL join fragment, updated in place. * * @return void */ function renderWhereJoin(string &$sql_where, string &$sql_join): void { global $servcheck_hosts; if (get_request_var('crit') > 0) { $awhere = 'h.monitor_criticality >= ' . get_request_var('crit'); } elseif (get_request_var('crit') == 0) { // 'Not Set' pill / None selection: devices with no criticality assigned. $awhere = 'h.monitor_criticality = 0'; } else { $awhere = ''; } if (get_request_var('grouping') == 'site') { if (get_request_var('site') > 0) { $awhere .= ($awhere == '' ? '' : ' AND ') . 'h.site_id = ' . get_request_var('site'); } elseif (get_request_var('site') == -2) { $awhere .= ($awhere == '' ? '' : ' AND ') . ' h.site_id = 0'; } } if (get_request_var('rfilter') != '') { $awhere .= ($awhere == '' ? '' : ' AND ') . " (h.description RLIKE '" . get_request_var('rfilter') . "' OR h.location RLIKE '" . get_request_var('rfilter') . "')"; } if (get_request_var('grouping') == 'tree') { if (get_request_var('tree') > 0) { $hlist = db_fetch_cell_prepared( 'SELECT GROUP_CONCAT(DISTINCT host_id) FROM graph_tree_items AS gti INNER JOIN host AS h ON h.id = gti.host_id WHERE host_id > 0 AND graph_tree_id = ? AND h.deleted = ""', [get_request_var('tree')] ); renderGroupConcat($awhere, ' AND ', 'h.id', $hlist); } elseif (get_request_var('tree') == -2) { $hlist = db_fetch_cell('SELECT GROUP_CONCAT(DISTINCT h.id) FROM host AS h LEFT JOIN (SELECT DISTINCT host_id FROM graph_tree_items WHERE host_id > 0) AS gti ON h.id = gti.host_id WHERE gti.host_id IS NULL AND h.deleted = ""'); renderGroupConcat($awhere, ' AND ', 'h.id', $hlist); } } // Multi-select Device Template filter (host_template_id IN list; 0 = // Non-Templated). Empty selection means all templates. if (get_request_var('grouping') == 'template') { $tpl_raw = get_nfilter_request_var('template'); if (is_array($tpl_raw)) { $tpls = $tpl_raw; } elseif ($tpl_raw === '' || $tpl_raw === null) { $tpls = []; } else { $tpls = explode(',', (string) $tpl_raw); } $tpls = array_map('intval', array_filter(array_map('trim', $tpls), static function ($t) { return ctype_digit((string) $t); })); if (cacti_sizeof($tpls)) { $awhere .= ($awhere == '' ? '' : ' AND ') . 'h.host_template_id IN (' . implode(',', $tpls) . ')'; } } // Additive Events filter (OR-combined) from the multi-select. $events_raw = get_nfilter_request_var('events'); if (is_array($events_raw)) { $events = $events_raw; } elseif ($events_raw === '' || $events_raw === null) { $events = []; } else { $events = array_map('trim', explode(',', (string) $events_raw)); } $event_sql = monitorEventFilterSql($events); if ($event_sql !== '') { $awhere .= ($awhere != '' ? ' AND ' : '') . $event_sql; } if (!empty($awhere)) { $awhere = ' AND ' . $awhere; } if (get_request_var('status') == -2) { // All Devices (monitored or not). $sql_join = 'LEFT JOIN thold_data AS td ON td.host_id=h.id'; $sql_where = 'WHERE h.disabled = "" AND h.deleted = ""' . $awhere; } elseif (get_request_var('status') == -3) { // Devices not Monitored. $sql_join = 'LEFT JOIN thold_data AS td ON td.host_id=h.id'; $sql_where = 'WHERE h.disabled = "" AND h.monitor = "" AND h.deleted = ""' . $awhere; } elseif (get_request_var('status') == -4) { // Devices without Thresholds. $sql_join = 'LEFT JOIN thold_data AS td ON td.host_id=h.id'; $sql_where = 'WHERE (h.disabled = "" AND h.deleted = "" AND td.id IS NULL)' . $awhere; } elseif (get_request_var('status') == -5 && api_plugin_is_enabled('servcheck') && api_plugin_user_realm_auth('servcheck_test.php') && db_table_exists('plugin_servcheck_test')) { // Devices without Service Checks (no enabled servcheck test matches the // device hostname/ipaddress). Realm-gated server-side: hiding the scope // option in monitor.php is not authorization, so a user without Service // Check access cannot submit status=-5 to probe which devices have them. $sql_join = 'LEFT JOIN thold_data AS td ON td.host_id=h.id'; $sql_where = 'WHERE h.disabled = "" AND h.deleted = "" AND NOT EXISTS ( SELECT 1 FROM plugin_servcheck_test AS sct WHERE sct.enabled = "on" AND (sct.hostname = h.hostname OR sct.ipaddress = h.hostname) )' . $awhere; } else { // All Monitored Devices (default scope, status -1). validateRequestVars() // normalizes any legacy availability-status code (0/1/2/...) or other // out-of-range value saved by an older release to -1 before we get here, // so this branch only ever serves the -1 scope rather than silently // broadening a stale status into All Monitored Devices. $sql_join = 'LEFT JOIN thold_data AS td ON td.host_id=h.id'; $sql_where = 'WHERE h.disabled = "" AND h.monitor = "on" AND h.deleted = "" AND (h.availability_method > 0 OR h.snmp_version > 0 OR ((td.thold_enabled="on" AND td.thold_alert > 0) OR td.id IS NULL) )' . $awhere; } } /** * Return host ids that are down/triggered and visible to current user. * * @param bool $prescan Whether to use prescan severity threshold. * * @return array */ function getHostsDownOrTriggeredByPermission(bool $prescan): array { global $render_style; $PreScanValue = 2; if ($prescan) { $PreScanValue = 3; } $result = []; if (get_request_var('crit') > 0) { $sql_add_where = 'monitor_criticality >= ' . get_request_var('crit'); } else { $sql_add_where = ''; } if (get_request_var('grouping') == 'tree') { if (get_request_var('tree') > 0) { $devices = db_fetch_cell_prepared( 'SELECT GROUP_CONCAT(DISTINCT host_id) AS hosts FROM graph_tree_items AS gti INNER JOIN host AS h WHERE host_id > 0 AND h.deleted = "" AND graph_tree_id = ?', [get_request_var('tree')] ); renderGroupConcat($sql_add_where, ' OR ', 'h.id', $devices, 'AND h.status < 2'); } } if (get_request_var('status') > 0) { $triggered = db_fetch_cell('SELECT GROUP_CONCAT(DISTINCT host_id) AS hosts FROM host AS h INNER JOIN thold_data AS td ON td.host_id = h.id WHERE ' . getTholdWhere() . ' AND h.deleted = ""'); renderGroupConcat($sql_add_where, ' OR ', 'h.id', $triggered, 'AND h.status > 1'); $_SESSION['monitor_triggered'] = array_rekey( db_fetch_assoc('SELECT td.host_id, COUNT(DISTINCT td.id) AS triggered FROM thold_data AS td INNER JOIN host AS h ON td.host_id = h.id WHERE ' . getTholdWhere() . ' AND h.deleted = "" GROUP BY td.host_id'), 'host_id', 'triggered' ); } $sql_where = "h.monitor = 'on' AND h.disabled = '' AND h.deleted = '' AND ((h.status < " . $PreScanValue . ' AND (h.availability_method > 0 OR h.snmp_version > 0)) ' . ($sql_add_where != '' ? ' OR (' . $sql_add_where . '))' : ')'); // do a quick loop through to pull the hosts that are down $hosts = get_allowed_devices($sql_where); if (cacti_sizeof($hosts)) { foreach ($hosts as $host) { $result[] = $host['id']; sort($result); } } return $result; } /** * Return non-tree monitor hosts after applying current filters. * * @return array */ function getHostNonTreeArray(): array { $leafs = []; $sql_where = ''; $sql_join = ''; renderWhereJoin($sql_where, $sql_join); $hierarchy = db_fetch_assoc("SELECT DISTINCT h.*, gti.title, gti.host_id, gti.host_grouping_type, gti.graph_tree_id FROM host AS h LEFT JOIN graph_tree_items AS gti ON h.id=gti.host_id $sql_join $sql_where AND gti.graph_tree_id IS NULL ORDER BY h.description"); if (cacti_sizeof($hierarchy) > 0) { $leafs = []; $branchleafs = 0; foreach ($hierarchy as $leaf) { $leafs[$branchleafs] = $leaf; $branchleafs++; } } return $leafs; } /** * Render the Monitor status legend as a row of equal-width, rounded colour * chips (one per interface/device status). Each chip is sized to the longest * label via the --monitor-chip-min custom property so the grid stays a row of * equal chips as it wraps. * * @param array $iclasses Status index => CSS status class (the chip carries "Full"). * @param array $icolorsdisplay Status index => human-readable label. * * @return void Outputs the legend HTML directly. */ function monitor_legend(array $iclasses, array $icolorsdisplay): void { $chip_min = 0; foreach ($iclasses as $index => $class) { $chip_min = max($chip_min, mb_strlen($icolorsdisplay[$index])); } print "
"; foreach ($iclasses as $index => $class) { print "
" . html_escape($icolorsdisplay[$index]) . '
'; } print '
'; }