350 lines
13 KiB
PHP
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');
|
|
}
|
|
} |