added new device consolidation
This commit is contained in:
@@ -70,7 +70,7 @@ asort($Devices);
|
||||
<label class="col-lg-2 col-form-label" for="name">Name *</label>
|
||||
<div class="col-lg-10">
|
||||
<input type="text" class="form-control" name="name" id="name"
|
||||
value="<?= $device->name ?>" required="required" >
|
||||
value="<?= $device->name ?? $_GET['name'] ?? '' ?>" required="required">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -167,7 +167,7 @@ asort($Devices);
|
||||
<label class="col-lg-2 col-form-label" for="ip">IP-Adresse *</label>
|
||||
<div class="col-lg-10">
|
||||
<input required="required" type="text" class="form-control" name="ip" id="ip" placeholder="10.0.0.1"
|
||||
value="<?= $device->ip ?>">
|
||||
value="<?= $device->ip ?? $_GET['ip'] ?? '' ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<li class=""><a href="<?=self::getUrl("Device")?>"><i class="fad fa-fw fa-router text-info "></i> Devices</a></li>
|
||||
<?php endif; ?>
|
||||
<?php if($me->is(["Admin"])): ?>
|
||||
<li class=""><a href="<?=self::getUrl("Device", "zabbixConsolidation")?>"><i class="fad fa-fw fa-database text-info "></i> Device Konsolidierung</a></li>
|
||||
<li class="has-sub-submenu"><a href="<?=self::getUrl("User")?>"><i class="fad fa-fw fa-users text-info"></i> Benutzer</a></li>
|
||||
<li class="has-sub-submenu font-weight-bold mt-1 mobile-hide"><a>Grundstammdaten</a></li>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -349,6 +349,12 @@ class DeviceController extends mfBaseController
|
||||
case "deviceoltserviceportrefresh":
|
||||
$this->deviceoltserviceportrefresh($ip);
|
||||
break;
|
||||
case "getZabbixConsolidationData":
|
||||
$this->getZabbixConsolidationData();
|
||||
break;
|
||||
case "updateZabbixCoordinates":
|
||||
$this->updateZabbixCoordinates();
|
||||
break;
|
||||
default:
|
||||
$return = false;
|
||||
}
|
||||
@@ -512,4 +518,266 @@ class DeviceController extends mfBaseController
|
||||
|
||||
return $data ?? [];
|
||||
}
|
||||
|
||||
protected function zabbixConsolidationAction()
|
||||
{
|
||||
if (!$this->me->is(["Admin"])) {
|
||||
$this->layout()->setFlash("Keine Berechtigung", "error");
|
||||
$this->redirect("Dashboard");
|
||||
}
|
||||
|
||||
$JSGlobals = [
|
||||
"BASE_URL" => self::getUrl(""),
|
||||
"API_URL" => self::getUrl("Device", "api"),
|
||||
"DEVICE_TYPES" => DevicetypeModel::getAll(),
|
||||
"POPS" => PopModel::getAll(),
|
||||
"PAGE_TITLE" => "Zabbix Consolidation",
|
||||
"PATH" => [
|
||||
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
|
||||
["text" => "Devices", "href" => self::getUrl("Device")],
|
||||
["text" => "Zabbix Consolidation", "href" => self::getUrl("Device", "zabbixConsolidation")]
|
||||
]
|
||||
];
|
||||
|
||||
$this->layout()->set("vueViewName", "DeviceZabbixConsolidation");
|
||||
$this->layout()->set("JSGlobals", $JSGlobals);
|
||||
$this->layout()->setTemplate("VueViews/Vue");
|
||||
}
|
||||
|
||||
protected function getZabbixConsolidationData() {
|
||||
$zabbix = new Zabbix(ZABBIX_API_URL, ZABBIX_API_KEY);
|
||||
$theToolDevices = DeviceModel::getAll();
|
||||
|
||||
$excludedGroupIds = [2, 4, 6, 7];
|
||||
|
||||
$allZabbixHosts = $zabbix->getAllHostsWithDetails();
|
||||
|
||||
$zabbixHosts = array_filter($allZabbixHosts, function($host) use ($excludedGroupIds) {
|
||||
$hostGroupIds = array_column($host['hostgroups'] ?? [], 'groupid');
|
||||
if (empty($hostGroupIds)) {
|
||||
return true;
|
||||
}
|
||||
return empty(array_intersect($hostGroupIds, $excludedGroupIds));
|
||||
});
|
||||
|
||||
$theToolDevicesByName = [];
|
||||
$theToolDevicesByIp = [];
|
||||
foreach ($theToolDevices as $device) {
|
||||
if ($device->ip === '127.0.0.1') continue;
|
||||
$theToolDevicesByName[$device->name] = $device;
|
||||
if (!empty($device->ip)) {
|
||||
$theToolDevicesByIp[$device->ip] = $device;
|
||||
}
|
||||
}
|
||||
|
||||
$zabbixHostsByVisibleName = [];
|
||||
$zabbixHostsByIp = [];
|
||||
foreach ($zabbixHosts as $host) {
|
||||
if (!empty($host['name'])) {
|
||||
$zabbixHostsByVisibleName[$host['name']] = $host;
|
||||
}
|
||||
if (!empty($host['host'])) {
|
||||
$zabbixHostsByIp[$host['host']] = $host;
|
||||
}
|
||||
}
|
||||
|
||||
$results = [
|
||||
'mismatchedNames' => [],
|
||||
'inTheToolOnly' => [],
|
||||
'inZabbixOnly' => [],
|
||||
'noSnmp' => [],
|
||||
'noCoordsInZabbix' => [],
|
||||
'mismatchedCoords' => [],
|
||||
];
|
||||
|
||||
foreach ($theToolDevices as $ttDevice) {
|
||||
if ($ttDevice->ip === '127.0.0.1') continue;
|
||||
|
||||
$deviceDetails = new Device($ttDevice->id);
|
||||
$theToolData = ['id' => $deviceDetails->id, 'data' => (array) $deviceDetails->data];
|
||||
if ($deviceDetails->pop && $deviceDetails->pop->id) {
|
||||
$theToolData['data']['pop'] = (array) $deviceDetails->pop->data;
|
||||
}
|
||||
|
||||
if (!isset($zabbixHostsByVisibleName[$ttDevice->name]) && !isset($zabbixHostsByIp[$ttDevice->ip])) {
|
||||
$results['inTheToolOnly'][] = $theToolData;
|
||||
} else {
|
||||
if (isset($zabbixHostsByVisibleName[$ttDevice->name])) {
|
||||
$zbxHost = $zabbixHostsByVisibleName[$ttDevice->name];
|
||||
$ttLat = (float)($theToolData['data']['pop']['gps_lat'] ?? $theToolData['data']['gps_lat'] ?? 0);
|
||||
$ttLon = (float)($theToolData['data']['pop']['gps_long'] ?? $theToolData['data']['gps_long'] ?? 0);
|
||||
$zbxLat = (float)($zbxHost['inventory']['location_lat'] ?? 0);
|
||||
$zbxLon = (float)($zbxHost['inventory']['location_lon'] ?? 0);
|
||||
|
||||
if (($ttLat || $ttLon || $zbxLat || $zbxLon) && (abs($ttLat - $zbxLat) > 0.0001 || abs($ttLon - $zbxLon) > 0.0001)) {
|
||||
$results['mismatchedCoords'][] = ['theTool' => $theToolData, 'zabbix' => $zbxHost];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($zabbixHosts as $zbxHost) {
|
||||
if (empty($zbxHost['name'])) continue;
|
||||
|
||||
if (!isset($theToolDevicesByName[$zbxHost['name']])) {
|
||||
$results['inZabbixOnly'][] = $zbxHost;
|
||||
}
|
||||
|
||||
$hasSnmp = false;
|
||||
if (!empty($zbxHost['parentTemplates'])) {
|
||||
foreach ($zbxHost['parentTemplates'] as $template) {
|
||||
if (stripos($template['name'], 'icmp ping') === false) {
|
||||
$hasSnmp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$hasSnmp) {
|
||||
$results['noSnmp'][] = $zbxHost;
|
||||
}
|
||||
|
||||
$zbxLat = (float)($zbxHost['inventory']['location_lat'] ?? 0);
|
||||
$zbxLon = (float)($zbxHost['inventory']['location_lon'] ?? 0);
|
||||
|
||||
$theToolDeviceForCoords = $theToolDevicesByName[$zbxHost['name']] ?? null;
|
||||
$theToolDataForCoords = null;
|
||||
if($theToolDeviceForCoords){
|
||||
$deviceDetails = new Device($theToolDeviceForCoords->id);
|
||||
$theToolDataForCoords = ['id' => $deviceDetails->id, 'data' => (array) $deviceDetails->data];
|
||||
if ($deviceDetails->pop && $deviceDetails->pop->id) {
|
||||
$theToolDataForCoords['data']['pop'] = (array) $deviceDetails->pop->data;
|
||||
}
|
||||
}
|
||||
|
||||
if ($zbxLat == 0 && $zbxLon == 0) {
|
||||
$results['noCoordsInZabbix'][] = [
|
||||
'zabbix' => $zbxHost,
|
||||
'theTool' => $theToolDataForCoords
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($zabbixHosts as $zbxHost) {
|
||||
if(isset($theToolDevicesByIp[$zbxHost['host']])) {
|
||||
$ttDeviceFromIp = new Device($theToolDevicesByIp[$zbxHost['host']]->id);
|
||||
$theToolData = ['id' => $ttDeviceFromIp->id, 'data' => $ttDeviceFromIp->data];
|
||||
if ($ttDeviceFromIp->pop && $ttDeviceFromIp->pop->id) {
|
||||
$theToolData['data']['pop'] = $ttDeviceFromIp->pop->data;
|
||||
}
|
||||
|
||||
if($ttDeviceFromIp->name !== $zbxHost['name']){
|
||||
$results['mismatchedNames'][] = ['theTool' => $theToolData, 'zabbix' => $zbxHost];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self::returnJson($results);
|
||||
}
|
||||
|
||||
protected function getZabbixTemplatesAction() {
|
||||
$zabbix = new Zabbix(ZABBIX_API_URL, ZABBIX_API_KEY);
|
||||
$templateNames = [
|
||||
"ICMP Ping",
|
||||
"1_Default XINON Template",
|
||||
"1_XINON Extreme EXOS by SNMP",
|
||||
"1_MIKROTIK_TEMPLATE"
|
||||
];
|
||||
$templates = $zabbix->getTemplatesByNames($templateNames);
|
||||
$formattedTemplates = array_map(function($template) {
|
||||
return ['value' => $template['templateid'], 'text' => $template['name']];
|
||||
}, $templates);
|
||||
|
||||
self::returnJson($formattedTemplates);
|
||||
}
|
||||
|
||||
protected function createZabbixHostAction() {
|
||||
$this->postData = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
$deviceId = $this->postData['deviceId'] ?? null;
|
||||
$templateId = $this->postData['templateId'] ?? null;
|
||||
if (!$deviceId || !$templateId) {
|
||||
self::sendError("Device ID or Template ID is missing.");
|
||||
}
|
||||
|
||||
$device = DeviceModel::getOne($deviceId);
|
||||
if (!$device || !$device->id) {
|
||||
self::sendError("Device not found in TheTool.");
|
||||
}
|
||||
|
||||
$zabbix = new Zabbix(ZABBIX_API_URL, ZABBIX_API_KEY);
|
||||
$groupName = "Discovered hosts";
|
||||
|
||||
$existingHosts = $zabbix->getHosts($device->name, $device->ip);
|
||||
if (!empty($existingHosts)) {
|
||||
self::sendError("Host with this name or IP already exists in Zabbix.");
|
||||
}
|
||||
|
||||
$groupId = $zabbix->getHostGroupIdByName($groupName);
|
||||
if (!$groupId) {
|
||||
self::sendError("Host group '$groupName' not found in Zabbix.");
|
||||
}
|
||||
|
||||
$result = $zabbix->createHost($device->name, $device->ip, $groupId, $templateId);
|
||||
|
||||
if (isset($result['hostids'])) {
|
||||
$device->zabbix_host_id = $result['hostids'][0];
|
||||
$device->save();
|
||||
self::returnJson(['success' => true, 'message' => 'Host successfully created in Zabbix and linked in TheTool.']);
|
||||
} else {
|
||||
self::sendError("Failed to create host in Zabbix: " . json_encode($result['error'] ?? 'Unknown error'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function updateTheToolCoordinatesAction() {
|
||||
$deviceId = $this->postData['deviceId'] ?? null;
|
||||
$lat = $this->postData['lat'] ?? null;
|
||||
$lon = $this->postData['lon'] ?? null;
|
||||
|
||||
if (!$deviceId || $lat === null || $lon === null) {
|
||||
self::sendError("Device ID or coordinates are missing.");
|
||||
}
|
||||
|
||||
$device = new Device($deviceId);
|
||||
if (!$device->id) {
|
||||
self::sendError("Device not found.");
|
||||
}
|
||||
|
||||
if ($device->pop_id) {
|
||||
self::sendError("Koordinaten können nicht geändert werden, da sie vom POP-Standort geerbt werden.");
|
||||
}
|
||||
|
||||
$device->update([
|
||||
'gps_lat' => $lat,
|
||||
'gps_long' => $lon
|
||||
]);
|
||||
$device->save();
|
||||
|
||||
self::returnJson(['success' => true, 'message' => 'TheTool coordinates updated successfully.']);
|
||||
}
|
||||
|
||||
protected function updateZabbixCoordinates() {
|
||||
$this->postData = json_decode(file_get_contents('php://input'), true);
|
||||
$hostId = $this->postData['hostId'] ?? null;
|
||||
$lat = $this->postData['lat'] ?? null;
|
||||
$lon = $this->postData['lon'] ?? null;
|
||||
|
||||
if (!$hostId || $lat === null || $lon === null) {
|
||||
self::sendError("Host ID or coordinates are missing.");
|
||||
}
|
||||
|
||||
$zabbix = new Zabbix(ZABBIX_API_URL, ZABBIX_API_KEY);
|
||||
$inventoryData = [
|
||||
'location_lat' => (string)$lat,
|
||||
'location_lon' => (string)$lon,
|
||||
];
|
||||
|
||||
$result = $zabbix->updateHostInventory($hostId, $inventoryData);
|
||||
|
||||
if (isset($result['hostids'])) {
|
||||
self::returnJson(['success' => true, 'message' => 'Coordinates updated successfully in Zabbix.']);
|
||||
} else {
|
||||
self::sendError("Failed to update coordinates in Zabbix: " . json_encode($result['error'] ?? 'Unknown error'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -113,4 +113,83 @@ class Zabbix {
|
||||
));
|
||||
return $response['result'];
|
||||
}
|
||||
|
||||
public function getAllHostsWithDetails() {
|
||||
$response = $this->zabbixRequest('host.get', [
|
||||
'output' => ['hostid', 'host', 'name', 'status'],
|
||||
'selectInventory' => ['location_lat', 'location_lon'],
|
||||
'selectParentTemplates' => ['templateid', 'name'],
|
||||
'selectHostGroups' => 'extend' // This is the new line
|
||||
]);
|
||||
return $response['result'] ?? [];
|
||||
}
|
||||
|
||||
public function updateHostInventory($hostId, $inventoryData) {
|
||||
// First, get the current inventory to avoid overwriting existing fields
|
||||
$hostResponse = $this->zabbixRequest('host.get', [
|
||||
'hostids' => $hostId,
|
||||
'selectInventory' => 'extend'
|
||||
]);
|
||||
|
||||
$currentInventory = $hostResponse['result'][0]['inventory'] ?? [];
|
||||
|
||||
// Merge new coordinates into the existing inventory
|
||||
$newInventory = array_merge($currentInventory, $inventoryData);
|
||||
|
||||
$params = [
|
||||
'hostid' => $hostId,
|
||||
'inventory_mode' => 0, // Set to manual mode
|
||||
'inventory' => $newInventory
|
||||
];
|
||||
|
||||
$response = $this->zabbixRequest('host.update', $params);
|
||||
return $response['result'] ?? ['error' => $response['error'] ?? 'Unknown error'];
|
||||
}
|
||||
public function getTemplateIdByName($templateName) {
|
||||
$response = $this->zabbixRequest('template.get', [
|
||||
'output' => ['templateid'],
|
||||
'filter' => ['host' => [$templateName]]
|
||||
]);
|
||||
return $response['result'][0]['templateid'] ?? null;
|
||||
}
|
||||
|
||||
public function getTemplatesByNames(array $templateNames) {
|
||||
$response = $this->zabbixRequest('template.get', [
|
||||
'output' => ['templateid', 'name'],
|
||||
'filter' => ['host' => $templateNames]
|
||||
]);
|
||||
return $response['result'] ?? [];
|
||||
}
|
||||
|
||||
|
||||
public function createHost($visibleName, $ip, $groupId, $templateId) {
|
||||
$params = [
|
||||
'host' => $ip, // Technical name is the IP
|
||||
'name' => $visibleName, // Visible name
|
||||
'interfaces' => [
|
||||
[
|
||||
'type' => 1, // Agent interface
|
||||
'main' => 1,
|
||||
'useip' => 1,
|
||||
'ip' => $ip,
|
||||
'dns' => '',
|
||||
'port' => '10050'
|
||||
]
|
||||
],
|
||||
'groups' => [['groupid' => $groupId]],
|
||||
'templates' => [['templateid' => $templateId]]
|
||||
];
|
||||
|
||||
$response = $this->zabbixRequest('host.create', $params);
|
||||
return $response['result'] ?? ['error' => $response['error'] ?? 'Unknown error'];
|
||||
}
|
||||
|
||||
public function getHostGroupIdByName($groupName) {
|
||||
$response = $this->zabbixRequest('hostgroup.get', [
|
||||
'output' => ['groupid'],
|
||||
'filter' => ['name' => [$groupName]]
|
||||
]);
|
||||
return $response['result'][0]['groupid'] ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
.consolidation-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.consolidation-section .card-header {
|
||||
font-weight: bold;
|
||||
padding: .75rem 1.25rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.consolidation-section .card-header .fas {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.consolidation-section .card-header.collapsed .fa-chevron-down {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.consolidation-section .card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.consolidation-section .list-group-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.consolidation-section .device-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.consolidation-section .actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#map-modal .modal-body {
|
||||
height: 60vh;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#map-modal .modal-body > .form-group {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50px;
|
||||
z-index: 1001;
|
||||
background-color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#leaflet-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Leaflet GeoSearch styles */
|
||||
#map-modal .leaflet-control-geosearch a.glass,
|
||||
#map-modal .leaflet-control-geosearch .results a.glass {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
#map-modal .leaflet-control-geosearch form input {
|
||||
padding: 0 10px;
|
||||
height: 30px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
#map-modal .leaflet-control-geosearch .results {
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
.coords-display {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
background: rgba(255,255,255,0.8);
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
z-index: 1000;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.bg-purple {
|
||||
background-color: #6f42c1 !important;
|
||||
}
|
||||
|
||||
.device-info a {
|
||||
margin-left: 8px;
|
||||
color: #6c757d;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.device-info a:hover {
|
||||
color: #343a40;
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
Vue.component('device-zabbix-consolidation', {
|
||||
template: `
|
||||
<tt-card>
|
||||
<div v-if="loading" class="text-center p-5">
|
||||
<div class="spinner-border" role="status"></div>
|
||||
<p class="mt-2">Lade und vergleiche Daten von TheTool und Zabbix...</p>
|
||||
</div>
|
||||
<div v-else class="consolidation-container">
|
||||
|
||||
<template v-for="(section, key) in sections">
|
||||
<div class="consolidation-section card" :key="key">
|
||||
<div :class="[section.headerClass, {'collapsed': !section.isOpen}]" class="card-header" @click="toggleSection(key)">
|
||||
<span>{{ section.title }} ({{ results[key] ? results[key].length : 0 }})</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div v-show="section.isOpen" class="card-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li v-if="!results[key] || !results[key].length" class="list-group-item">Keine Abweichungen gefunden.</li>
|
||||
|
||||
<li v-if="key === 'inZabbixOnly'" v-for="host in results.inZabbixOnly" :key="host.hostid" class="list-group-item">
|
||||
<div class="device-info">
|
||||
<strong>{{ host.name }}</strong> ({{ host.host }})
|
||||
<a :href="zabbixUrl + '/zabbix.php?action=latest.view&hostids%5B%5D=' + host.hostid" class="zabbix-link" target="_blank" title="In Zabbix anzeigen"><i class="fas fa-external-link-alt"></i></a>
|
||||
</div>
|
||||
<div class="actions"><tt-button sm text="Gerät anlegen" icon="fas fa-plus" additional-class="btn-primary" @click="createDeviceFromZabbix(host)"/></div>
|
||||
</li>
|
||||
|
||||
<li v-if="key === 'inTheToolOnly'" v-for="item in results.inTheToolOnly" :key="item.id" class="list-group-item">
|
||||
<div class="device-info">
|
||||
<strong>{{ item.data.name }}</strong> ({{ item.data.ip }})
|
||||
<a :href="window.TT_CONFIG.BASE_URL + '/Device/Detail?id=' + item.id" class="thetool-link" target="_blank" title="In TheTool anzeigen"><i class="fas fa-eye"></i></a>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<tt-button sm text="In Zabbix anlegen" icon="fas fa-server" additional-class="btn-success" @click="openCreateZabbixHostModal(item.id)" :loading="item.id === zabbixCreateModal.loadingId"/>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li v-if="key === 'mismatchedNames'" v-for="item in results.mismatchedNames" :key="item.zabbix.hostid" class="list-group-item">
|
||||
<div class="device-info">
|
||||
<div><strong>IP:</strong> {{ item.zabbix.host }}</div>
|
||||
<div>
|
||||
<strong>TheTool Name:</strong> {{ item.theTool.data.name }}
|
||||
<a :href="window.TT_CONFIG.BASE_URL + '/Device/Detail?id=' + item.theTool.id" class="thetool-link" target="_blank" title="In TheTool anzeigen"><i class="fas fa-eye"></i></a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Zabbix Name:</strong> {{ item.zabbix.name }}
|
||||
<a :href="zabbixUrl + '/zabbix.php?action=latest.view&hostids%5B%5D=' + item.zabbix.hostid" class="zabbix-link" target="_blank" title="In Zabbix anzeigen"><i class="fas fa-external-link-alt"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li v-if="key === 'noSnmp'" v-for="host in results.noSnmp" :key="host.hostid" class="list-group-item">
|
||||
<div class="device-info">
|
||||
<strong>{{ host.name }}</strong> ({{ host.host }})
|
||||
<a :href="zabbixUrl + '/zabbix.php?action=latest.view&hostids%5B%5D=' + host.hostid" class="zabbix-link" target="_blank" title="In Zabbix anzeigen"><i class="fas fa-external-link-alt"></i></a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li v-if="key === 'mismatchedCoords'" v-for="item in results.mismatchedCoords" :key="item.zabbix.hostid" class="list-group-item">
|
||||
<div class="device-info">
|
||||
<div>
|
||||
<strong>{{ item.theTool.data.name }}</strong>
|
||||
<a :href="window.TT_CONFIG.BASE_URL + '/Device/Detail?id=' + item.theTool.id" class="thetool-link" target="_blank" title="In TheTool anzeigen"><i class="fas fa-eye"></i></a>
|
||||
<a :href="zabbixUrl + '/zabbix.php?action=latest.view&hostids%5B%5D=' + item.zabbix.hostid" class="zabbix-link" target="_blank" title="In Zabbix anzeigen"><i class="fas fa-external-link-alt"></i></a>
|
||||
</div>
|
||||
<div><small><strong>TheTool:</strong> <span>Lat: {{ item.theTool.data.pop ? item.theTool.data.pop.gps_lat : item.theTool.data.gps_lat || 'N/A' }}, Lon: {{ item.theTool.data.pop ? item.theTool.data.pop.gps_long : item.theTool.data.gps_long || 'N/A' }}</span></small></div>
|
||||
<div><small><strong>Zabbix:</strong> <span>Lat: {{ item.zabbix.inventory.location_lat || 'N/A' }}, Lon: {{ item.zabbix.inventory.location_lon || 'N/A' }}</span></small></div>
|
||||
</div>
|
||||
<div class="actions"><tt-button sm text="Koordinaten setzen" icon="fas fa-map-marker-alt" additional-class="btn-warning" @click="openMapModal(item.zabbix, item.theTool)"/></div>
|
||||
</li>
|
||||
|
||||
<li v-if="key === 'noCoordsInZabbix'" v-for="item in results.noCoordsInZabbix" :key="item.zabbix.hostid" class="list-group-item">
|
||||
<div class="device-info">
|
||||
<strong>{{ item.zabbix.name }}</strong> ({{ item.zabbix.host }})
|
||||
<span v-if="item.theTool" class="text-success ml-2">(Gerät in TheTool gefunden)</span>
|
||||
<a v-if="item.theTool" :href="window.TT_CONFIG.BASE_URL + '/Device/Detail?id=' + item.theTool.id" class="thetool-link" target="_blank" title="In TheTool anzeigen"><i class="fas fa-eye"></i></a>
|
||||
<a :href="zabbixUrl + '/zabbix.php?action=latest.view&hostids%5B%5D=' + item.zabbix.hostid" class="zabbix-link" target="_blank" title="In Zabbix anzeigen"><i class="fas fa-external-link-alt"></i></a>
|
||||
<div v-if="item.theTool && item.theTool.data.pop_id && item.theTool.data.pop && item.theTool.data.pop.gps_lat" class="text-danger small mt-1">
|
||||
Koordinaten vom POP-Standort geerbt. Bitte im POP-Modul bearbeiten.
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions" v-if="!(item.theTool && item.theTool.data.pop_id && item.theTool.data.pop && item.theTool.data.pop.gps_lat)">
|
||||
<tt-button sm text="Koordinaten setzen" icon="fas fa-map-marker-alt" additional-class="btn-warning" @click="openMapModal(item.zabbix, item.theTool)"/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<tt-modal v-if="mapModal.show" id="map-modal" :show.sync="mapModal.show" :title="'Koordinaten für ' + mapModal.host.name" @submit="saveCoordinates" :save-loading="mapModal.loading" :delete="false">
|
||||
<div id="leaflet-map" ref="leafletMap"></div>
|
||||
<div class="coords-display">Lat: {{ mapModal.markerCoords ? mapModal.markerCoords.lat.toFixed(6) : 'N/A' }}, Lng: {{ mapModal.markerCoords ? mapModal.markerCoords.lng.toFixed(6) : 'N/A' }}</div>
|
||||
<div v-if="mapModal.theToolDevice" style="padding: 1rem 1rem 0 1rem;">
|
||||
<tt-checkbox label="Koordinaten in TheTool überschreiben" v-model="mapModal.updateTheToolCoords" :disabled="mapModal.deviceHasPop" :hint="mapModal.deviceHasPop ? 'Koordinaten werden vom POP-Standort geerbt.' : ''" sm/>
|
||||
</div>
|
||||
</tt-modal>
|
||||
|
||||
<tt-modal v-if="zabbixCreateModal.show" :show.sync="zabbixCreateModal.show" title="Gerät in Zabbix anlegen" @submit="submitCreateZabbixHost" :save-loading="zabbixCreateModal.loading">
|
||||
<p>Bitte wählen Sie ein Template für das Gerät <strong>{{ zabbixCreateModal.deviceName }}</strong>.</p>
|
||||
<tt-select label="Template" :options="zabbixCreateModal.templates" v-model="zabbixCreateModal.selectedTemplateId" sm row required/>
|
||||
</tt-modal>
|
||||
</div>
|
||||
</tt-card>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
window: window,
|
||||
zabbixUrl: 'https://monitoring.xinon.at',
|
||||
results: {},
|
||||
sections: {
|
||||
inZabbixOnly: { title: 'Geräte in Zabbix, aber nicht in TheTool', isOpen: false, headerClass: 'bg-warning' },
|
||||
inTheToolOnly: { title: 'Geräte in TheTool, aber nicht in Zabbix', isOpen: false, headerClass: 'bg-info' },
|
||||
mismatchedNames: { title: 'Abweichende Namen (IP-basiert)', isOpen: false, headerClass: 'bg-secondary text-white' },
|
||||
noSnmp: { title: 'Geräte in Zabbix ohne SNMP Template', isOpen: false, headerClass: 'bg-light' },
|
||||
mismatchedCoords: { title: 'Abweichende Koordinaten', isOpen: false, headerClass: 'bg-danger text-white' },
|
||||
noCoordsInZabbix: { title: 'Geräte in Zabbix ohne Koordinaten', isOpen: false, headerClass: 'bg-purple text-white' }
|
||||
},
|
||||
mapModal: {
|
||||
show: false,
|
||||
loading: false,
|
||||
host: null,
|
||||
theToolDevice: null,
|
||||
map: null,
|
||||
marker: null,
|
||||
markerCoords: null,
|
||||
updateTheToolCoords: false,
|
||||
deviceHasPop: false
|
||||
},
|
||||
zabbixCreateModal: {
|
||||
show: false,
|
||||
loading: false,
|
||||
loadingId: null,
|
||||
deviceId: null,
|
||||
deviceName: '',
|
||||
templates: [],
|
||||
selectedTemplateId: null
|
||||
}
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.refreshData();
|
||||
},
|
||||
methods: {
|
||||
toggleSection(key) {
|
||||
this.sections[key].isOpen = !this.sections[key].isOpen;
|
||||
},
|
||||
createDeviceFromZabbix(host) {
|
||||
const params = new URLSearchParams();
|
||||
params.append('name', host.name);
|
||||
params.append('ip', host.host);
|
||||
window.open(`${window.TT_CONFIG.BASE_URL}/Device/add?${params.toString()}`, '_blank');
|
||||
},
|
||||
async openCreateZabbixHostModal(deviceId) {
|
||||
const device = this.results.inTheToolOnly.find(d => d.id === deviceId);
|
||||
if (!device) return;
|
||||
|
||||
this.zabbixCreateModal.deviceId = deviceId;
|
||||
this.zabbixCreateModal.deviceName = device.data.name;
|
||||
this.zabbixCreateModal.loading = true;
|
||||
this.zabbixCreateModal.show = true;
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${window.TT_CONFIG.API_URL}?do=getZabbixTemplates`);
|
||||
this.zabbixCreateModal.templates = response.data;
|
||||
} catch (e) {
|
||||
window.notify('error', 'Fehler beim Laden der Zabbix Templates.');
|
||||
this.zabbixCreateModal.show = false;
|
||||
} finally {
|
||||
this.zabbixCreateModal.loading = false;
|
||||
}
|
||||
},
|
||||
async submitCreateZabbixHost() {
|
||||
if (!this.zabbixCreateModal.selectedTemplateId) {
|
||||
return window.notify('error', 'Bitte wählen Sie ein Template aus.');
|
||||
}
|
||||
this.zabbixCreateModal.loading = true;
|
||||
this.zabbixCreateModal.loadingId = this.zabbixCreateModal.deviceId;
|
||||
try {
|
||||
const response = await axios.post(`${window.TT_CONFIG.API_URL}?do=createZabbixHost`, {
|
||||
deviceId: this.zabbixCreateModal.deviceId,
|
||||
templateId: this.zabbixCreateModal.selectedTemplateId
|
||||
});
|
||||
if (response.data.success) {
|
||||
window.notify('success', response.data.message);
|
||||
this.zabbixCreateModal.show = false;
|
||||
await this.refreshData();
|
||||
} else {
|
||||
window.notify('error', response.data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
window.notify('error', 'Ein Fehler ist aufgetreten.');
|
||||
} finally {
|
||||
this.zabbixCreateModal.loading = false;
|
||||
this.zabbixCreateModal.loadingId = null;
|
||||
this.zabbixCreateModal.deviceId = null;
|
||||
this.zabbixCreateModal.selectedTemplateId = null;
|
||||
}
|
||||
},
|
||||
openMapModal(host, theToolDevice = null) {
|
||||
this.mapModal.host = host;
|
||||
this.mapModal.theToolDevice = theToolDevice;
|
||||
this.mapModal.deviceHasPop = !!(theToolDevice && theToolDevice.data && theToolDevice.data.pop_id);
|
||||
this.mapModal.updateTheToolCoords = false;
|
||||
this.mapModal.show = true;
|
||||
this.$nextTick(this.initMap);
|
||||
},
|
||||
async initMap() {
|
||||
if (typeof L === 'undefined' || typeof window.GeoSearch === 'undefined') {
|
||||
await this.loadScripts();
|
||||
}
|
||||
if (this.mapModal.map) {
|
||||
this.mapModal.map.remove();
|
||||
}
|
||||
|
||||
const ttDevice = this.mapModal.theToolDevice;
|
||||
const zbxHost = this.mapModal.host;
|
||||
let initialLat = 47.0707; // Default to Graz
|
||||
let initialLon = 15.4395;
|
||||
let zoom = 13;
|
||||
|
||||
const ttDeviceLat = ttDevice?.data?.pop?.gps_lat ?? ttDevice?.data?.gps_lat;
|
||||
const ttDeviceLon = ttDevice?.data?.pop?.gps_long ?? ttDevice?.data?.gps_long;
|
||||
|
||||
if (ttDevice && parseFloat(ttDeviceLat) && parseFloat(ttDeviceLon)) {
|
||||
initialLat = parseFloat(ttDeviceLat);
|
||||
initialLon = parseFloat(ttDeviceLon);
|
||||
zoom = 16;
|
||||
} else if (zbxHost.inventory && parseFloat(zbxHost.inventory.location_lat) && parseFloat(zbxHost.inventory.location_lon)) {
|
||||
initialLat = parseFloat(zbxHost.inventory.location_lat);
|
||||
initialLon = parseFloat(zbxHost.inventory.location_lon);
|
||||
zoom = 16;
|
||||
}
|
||||
|
||||
this.mapModal.map = L.map(this.$refs.leafletMap).setView([initialLat, initialLon], zoom);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(this.mapModal.map);
|
||||
|
||||
const searchControl = new window.GeoSearch.GeoSearchControl({
|
||||
provider: new window.GeoSearch.OpenStreetMapProvider(),
|
||||
style: 'bar',
|
||||
showMarker: false,
|
||||
autoClose: true,
|
||||
});
|
||||
this.mapModal.map.addControl(searchControl);
|
||||
|
||||
this.mapModal.marker = L.marker([initialLat, initialLon], { draggable: true }).addTo(this.mapModal.map);
|
||||
this.mapModal.markerCoords = this.mapModal.marker.getLatLng();
|
||||
|
||||
this.mapModal.marker.on('dragend', (event) => this.mapModal.markerCoords = event.target.getLatLng());
|
||||
this.mapModal.map.on('click', (e) => {
|
||||
this.mapModal.marker.setLatLng(e.latlng);
|
||||
this.mapModal.markerCoords = e.latlng;
|
||||
});
|
||||
this.mapModal.map.on('geosearch/showlocation', (result) => {
|
||||
const latLng = L.latLng(result.location.y, result.location.x);
|
||||
this.mapModal.marker.setLatLng(latLng);
|
||||
this.mapModal.markerCoords = latLng;
|
||||
});
|
||||
|
||||
setTimeout(() => this.mapModal.map.invalidateSize(), 100);
|
||||
},
|
||||
async saveCoordinates() {
|
||||
this.mapModal.loading = true;
|
||||
const coords = this.mapModal.marker.getLatLng();
|
||||
let allSuccess = true;
|
||||
|
||||
try {
|
||||
const zabbixResponse = await axios.post(`${window.TT_CONFIG.API_URL}?do=updateZabbixCoordinates`, {
|
||||
hostId: this.mapModal.host.hostid,
|
||||
lat: coords.lat,
|
||||
lon: coords.lng
|
||||
});
|
||||
if (!zabbixResponse.data.success) {
|
||||
allSuccess = false;
|
||||
window.notify('error', 'Fehler beim Speichern der Zabbix-Koordinaten: ' + zabbixResponse.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
allSuccess = false;
|
||||
window.notify('error', 'Fehler beim Speichern der Zabbix-Koordinaten.');
|
||||
}
|
||||
|
||||
if (this.mapModal.updateTheToolCoords && this.mapModal.theToolDevice && allSuccess) {
|
||||
try {
|
||||
const theToolResponse = await axios.post(`${window.TT_CONFIG.API_URL}?do=updateTheToolCoordinates`, {
|
||||
deviceId: this.mapModal.theToolDevice.id,
|
||||
lat: coords.lat,
|
||||
lon: coords.lng
|
||||
});
|
||||
if (!theToolResponse.data.success) {
|
||||
allSuccess = false;
|
||||
window.notify('error', 'Fehler beim Speichern der TheTool-Koordinaten: ' + theToolResponse.data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
allSuccess = false;
|
||||
window.notify('error', 'Fehler beim Speichern der TheTool-Koordinaten.');
|
||||
}
|
||||
}
|
||||
|
||||
if(allSuccess) {
|
||||
window.notify('success', 'Koordinaten erfolgreich gespeichert.');
|
||||
this.mapModal.show = false;
|
||||
await this.refreshData();
|
||||
}
|
||||
|
||||
this.mapModal.loading = false;
|
||||
},
|
||||
async refreshData() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await axios.post(`${window.TT_CONFIG.API_URL}?do=getZabbixConsolidationData`);
|
||||
this.results = response.data;
|
||||
} catch (error) {
|
||||
window.notify('error', 'Fehler beim Aktualisieren der Daten.');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
loadScripts() {
|
||||
const scripts = [
|
||||
{ type: 'link', url: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' },
|
||||
{ type: 'link', url: 'https://unpkg.com/leaflet-geosearch@3.0.0/dist/geosearch.css' },
|
||||
{ type: 'script', url: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js' },
|
||||
{ type: 'script', url: 'https://unpkg.com/leaflet-geosearch@latest/dist/bundle.min.js' },
|
||||
];
|
||||
|
||||
const promises = scripts.map(s => new Promise((resolve, reject) => {
|
||||
let el;
|
||||
if (document.querySelector(`[href="${s.url}"], [src="${s.url}"]`)) {
|
||||
resolve(); return;
|
||||
}
|
||||
if (s.type === 'script') {
|
||||
el = document.createElement('script');
|
||||
el.src = s.url; el.async = false; el.onload = resolve; el.onerror = reject;
|
||||
} else {
|
||||
el = document.createElement('link');
|
||||
el.rel = 'stylesheet'; el.href = s.url; resolve();
|
||||
}
|
||||
if (el) document.head.appendChild(el); else reject();
|
||||
}));
|
||||
return Promise.all(promises);
|
||||
},
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user