Files
thetool/application/DeviceMonitoring/DeviceMonitoringController.php
2025-09-01 10:26:01 +00:00

350 lines
13 KiB
PHP

<?php
class DeviceMonitoringController extends mfBaseController
{
private string $ZABBIX_API_URL = ZABBIX_API_URL;
private string $ZABBIX_API_KEY = ZABBIX_API_KEY;
private ?Zabbix $zabbix = null;
protected function init(): void
{
$this->needlogin = true;
$me = new User();
$me->loadMe();
$this->layout()->set("me", $me);
$this->me = $me;
if (!$this->me->is("Admin")) {
http_response_code(403);
self::returnJson(['success' => false, 'message' => 'Permission denied']);
die();
}
if (defined('ZABBIX_API_URL') && defined('ZABBIX_API_KEY') && ZABBIX_API_URL && ZABBIX_API_KEY) {
$this->zabbix = new Zabbix($this->ZABBIX_API_URL, $this->ZABBIX_API_KEY);
} else {
http_response_code(500);
self::returnJson(['error' => 'Zabbix API is not configured on the server.']);
die();
}
$this->postData = json_decode(file_get_contents('php://input'), true) ?? [];
}
protected function listInterfacesAction()
{
$hostId = $this->request->hostId;
$items = $this->zabbix->getHostInterfaceItems($hostId);
$interfaces = [];
foreach ($items as $item) {
$baseName = preg_replace('/:\s*Bits\s*(sent|received)$/i', '', $item['name']);
$direction = str_contains(strtolower($item['name']), 'received') ? 'rx' : 'tx';
if (!isset($interfaces[$baseName])) {
$interfaces[$baseName] = ['name' => $baseName, 'rx' => null, 'tx' => null];
}
$interfaces[$baseName][$direction] = $item;
}
$sortedInterfaces = array_values($interfaces);
usort($sortedInterfaces, fn($a, $b) => strcmp($a['name'], $b['name']));
self::returnJson($sortedInterfaces);
}
protected function interfaceDataAction()
{
$itemIds = $this->postData['itemIds'] ?? [];
if (empty($itemIds)) {
self::returnJson([]);
return;
}
$timeRange = $this->postData['timeRange'] ?? '24h';
$time_from = strtotime('-' . str_replace(['m', 'h', 'd'], [' minutes', ' hours', ' days'], $timeRange));
$params = [
'itemids' => $itemIds,
'output' => 'extend',
'history' => 3, // Type of history: float
'sortfield' => 'clock',
'sortorder' => 'ASC',
'time_from' => $time_from,
];
$history = $this->zabbix->zabbixRequest('history.get', $params)['result'] ?? [];
$historyByItemId = [];
foreach ($history as $point) {
$historyByItemId[$point['itemid']][] = [
'x' => intval($point['clock']) * 1000,
'y' => round(floatval($point['value']) / 1000000, 2) // Mbps
];
}
self::returnJson($historyByItemId);
}
protected function generalDataAction() {
$hostId = $this->request->hostId;
$data = $this->zabbix->getOverviewData($hostId);
self::returnJson($data);
}
protected function getProblemsAction() {
$hostId = $this->request->hostId;
$currentProblems = $this->zabbix->zabbixRequest('problem.get', [
'hostids' => $hostId,
'output' => 'extend',
'recent' => true,
'sortfield' => ['eventid'],
'sortorder' => 'DESC'
])['result'] ?? [];
$resolvedProblems = $this->zabbix->getResolvedProblems($hostId, strtotime('-7 days'));
self::returnJson([
'current' => $currentProblems,
'resolved' => $resolvedProblems
]);
}
protected function getConfigurationDataAction() {
$hostId = $this->request->hostId;
$host = $this->zabbix->getHostWithInterfaces($hostId);
if (!$host) {
self::returnJson(['error' => 'Host not found.']);
return;
}
$snmpInterface = null;
foreach ($host['interfaces'] as $iface) {
if ($iface['type'] == '2') { // SNMP type
$snmpInterface = $iface;
break;
}
}
$opStatusItems = $this->zabbix->getInterfaceOperationalStatusItems($hostId);
$allTriggers = $this->zabbix->getTriggersForHostByDescription($hostId, "Interface ");
$triggerMap = [];
foreach ($allTriggers as $trigger) {
$triggerMap[$trigger['description']] = $trigger;
}
$interfaceAlarms = [];
foreach ($opStatusItems as $item) {
$expectedDescription = "Interface " . $item['name'] . " is down on " . $host['name'];
$trigger = $triggerMap[$expectedDescription] ?? null;
$interfaceAlarms[] = [
'itemid' => $item['itemid'],
'name' => $item['name'],
'key' => $item['key_'],
'isAlarmed' => !is_null($trigger),
'triggerId' => $trigger['triggerid'] ?? null
];
}
self::returnJson([
'snmp' => $snmpInterface,
'interfaces' => $interfaceAlarms
]);
}
protected function updateSnmpAction() {
$interfaceId = $this->postData['interfaceId'] ?? null;
$details = $this->postData['details'] ?? null;
if (!$interfaceId || !$details) {
http_response_code(400);
self::returnJson(['error' => 'Missing required parameters.']);
return;
}
$result = $this->zabbix->updateHostInterface($interfaceId, $details);
self::returnJson($result);
}
protected function updateInterfaceAlarmAction() {
$hostId = $this->postData['hostId'];
$item = $this->postData['item'];
$enabled = $this->postData['enabled'];
$host = $this->zabbix->getHostById($hostId)[0] ?? null;
if (!$host) {
self::returnJson(['error' => 'Host not found.']);
return;
}
$description = "Interface " . $item['name'] . " is down on " . $host['name'];
if ($enabled) {
$expression = "last(/".$host['host']."/".$item['key'].")=2";
$result = $this->zabbix->createInterfaceLinkDownTrigger($expression, $description);
} else {
$triggers = $this->zabbix->getTriggersForHostByDescription($hostId, $description);
$triggerIds = array_column($triggers, 'triggerid');
$result = $this->zabbix->deleteTriggers($triggerIds);
}
self::returnJson($result);
}
protected function getReportDataAction() {
$hostId = $this->request->hostId;
$timeRange = $this->request->timeRange ?? '7d';
$time_from = strtotime('-' . str_replace(['d'], [' days'], $timeRange));
// Step 1: Fetch all interface-related items (traffic and speed) in a single API call.
// We include 'value_type' to handle different history types correctly.
$items = $this->zabbix->zabbixRequest('item.get', [
'hostids' => $hostId,
'output' => ['itemid', 'name', 'key_', 'value_type'],
'search' => ['key_' => ['net.if.in', 'net.if.out', 'net.if.speed']],
'searchByAny' => true,
'sortfield' => 'name'
])['result'] ?? [];
// Step 2: Organize items and group them for efficient processing.
$interfaces = [];
$trafficItems = []; // Will hold item info for both rx and tx.
$speedItemsByType = [];
$speedItemMap = [];
foreach ($items as $item) {
$key = $item['key_'];
if (str_contains($key, 'net.if.in') || str_contains($key, 'net.if.out')) {
$baseName = preg_replace('/:\s*Bits\s*(sent|received)$/i', '', $item['name']);
$direction = str_contains($key, 'net.if.in') ? 'rx' : 'tx';
if (!isset($interfaces[$baseName])) {
$interfaces[$baseName] = ['name' => $baseName, 'rx_item' => null, 'tx_item' => null, 'speed' => null];
}
$interfaces[$baseName][$direction . '_item'] = $item;
$trafficItems[$item['itemid']] = $item;
} elseif (str_contains($key, 'net.if.speed')) {
$baseName = preg_replace('/:\s*Interface\s*|\s*speed$/i', '', $item['name']);
$value_type = (int)$item['value_type'];
$speedItemsByType[$value_type][] = $item['itemid'];
$speedItemMap[$item['itemid']] = $baseName;
}
}
// Step 3: Aggressively fetch the last known speed for all interfaces.
// We query history with a larger limit to find the value even if it's not recent.
foreach ($speedItemsByType as $type => $itemIds) {
$historyResult = $this->zabbix->zabbixRequest('history.get', [
'itemids' => $itemIds,
'history' => $type,
'output' => ['itemid', 'value'],
'sortfield' => 'clock',
'sortorder' => 'DESC',
'limit' => count($itemIds) * 5 // Increase limit to better ensure finding a value for each item
])['result'] ?? [];
$latestForType = [];
foreach ($historyResult as $point) {
if (!isset($latestForType[$point['itemid']])) {
$latestForType[$point['itemid']] = $point;
$baseName = $speedItemMap[$point['itemid']] ?? null;
if ($baseName && isset($interfaces[$baseName])) {
$interfaces[$baseName]['speed'] = (float)$point['value'];
}
}
}
}
// Step 4: Attempt to fetch trend data for all traffic items at once.
$trafficItemIds = array_keys($trafficItems);
$trends = $this->zabbix->getTrends($trafficItemIds, $time_from);
$trendsByItemId = [];
foreach ($trends as $trend) {
$trendsByItemId[$trend['itemid']][] = $trend;
}
// Step 5: Build the report, using trends first and falling back to raw history if trends are unavailable.
$report = [];
foreach ($interfaces as $iface) {
$rx_item = $iface['rx_item'];
$tx_item = $iface['tx_item'];
$speed = $iface['speed'];
// This function calculates statistics from either trend data or raw history data.
$calcStats = function($item, $speed, $trendData) use ($time_from) {
if (!$item) return ['avg' => 0, 'max' => 0, 'usage' => 0];
$values = [];
$avg = 0;
$max = 0;
if (!empty($trendData)) {
// Method 1: Use efficient trend data if available.
$avg = array_sum(array_column($trendData, 'value_avg')) / count($trendData);
$max = max(array_column($trendData, 'value_max'));
} else {
// Method 2 (Fallback): Fetch raw history if trends are missing.
$history = $this->zabbix->zabbixRequest('history.get', [
'itemids' => [$item['itemid']],
'history' => (int)$item['value_type'],
'time_from' => $time_from,
'output' => ['value']
])['result'] ?? [];
if (!empty($history)) {
$values = array_column($history, 'value');
$avg = array_sum($values) / count($values);
$max = max($values);
}
}
$usage = ($speed > 0) ? ($avg / $speed) * 100 : 0;
return [
'avg' => round($avg / 1000000, 2), // bps to Mbps
'max' => round($max / 1000000, 2), // bps to Mbps
'usage' => round($usage, 2)
];
};
$report[] = [
'name' => $iface['name'],
'speed' => $speed !== null ? round($speed / 1000000) : 'N/A', // bps to Mbps
'rx' => $calcStats($rx_item, $speed, $trendsByItemId[$rx_item['itemid']] ?? []),
'tx' => $calcStats($tx_item, $speed, $trendsByItemId[$tx_item['itemid']] ?? [])
];
}
usort($report, fn($a, $b) => strnatcmp($a['name'], $b['name']));
self::returnJson($report);
}
protected function liveDataAction() {
$itemId = $this->request->itemId;
if(empty($itemId)) {
http_response_code(400);
self::returnJson(['error' => 'Item ID is required.']);
return;
}
$this->zabbix->createTask($itemId);
sleep(3);
$history = $this->zabbix->getItemValues([$itemId], 1);
if(empty($history)) {
self::returnJson(null);
return;
}
$point = $history[0];
$formattedPoint = [
'x' => intval($point['clock']) * 1000,
'y' => round(floatval($point['value']) / 1000000, 2)
];
self::returnJson($formattedPoint);
}
public function liveGraphPageAction() {
$this->layout(false);
$this->layout()->set('API_BASE_URL', self::getUrl("DeviceMonitoring"));
$this->layout()->setTemplate('Device/LiveGraph');
}
}